某網絡監視器完整逆向

✎引子:
  早些時候想去研究Windows Filter Platform (WFP),參考資料少且不齊全。貼吧、論壇蒐集一些關於網絡過濾、網絡監聽的工具。開始琢磨別人是怎樣寫,怎樣實現的。然而沒有去研究驅動層(不少原理性的東西須要時間),本身寫用戶層前一直琢磨,三環如何去實現這些網絡監聽?用什麼API能夠實現對數據包的捕獲呢?怎樣把這些數據進行處理?
  當我看到其中的項目的時候,單純的.exe文件,運行後也沒有釋放dll之類的動態資源,腦海中出現一個念頭shellCode(這裏就先叫shellCode了,其實準確說是機器碼)。這個程序是好多年前的,比較單一,注入任意進程,捕獲網絡響應數據,兼容性也還不錯,用360瀏覽器作測試,windows7~windwos10網絡響應捕獲正常。
 這是給你們提供一些逆向的思路,並非教程系列,有必定逆向基礎才能夠(對彙編、網絡編程、OD等工具瞭解)。當遇到相似的程序或者問題,對他們的實現原理作到心中有度。ios

  • 以下圖所示:
    某網絡監視器完整逆向
                      圖片一:網絡監控exe

✎逆向分析目錄:shell

一、注入代碼分析 二、shellCode調試方法 三、shellCode動態分析
--------★ ★ ★------- -----------------★ ★ ★------------------- -----------★ ★ ★ ★-----------

☂草稿示意圖:
某網絡監視器完整逆向
                  圖片二:程序流程草圖編程

1、☛注入代碼分析:
 ➊ 用IDA先簡略的瀏覽一下彙編指令,發現反彙編代碼不算多,瞭解了基本的程序結構,拖到OD開始動態調試。
 ➋ 如圖二中第一步所示,獲取被注入的數據,須要獲取選中的目標進程Id等,而且OpenProcess打開目標進程,獲取句柄才能夠完成注入,以下圖所示(圖中關鍵代碼已給出解釋):
某網絡監視器完整逆向
某網絡監視器完整逆向
                  圖片三:獲取目標進程信息及獲取句柄
 ➌ 目標進程申請虛擬內存,以下圖所示:
某網絡監視器完整逆向
                  圖片四:申請虛擬內存空間
 ➍ 目標進程虛擬內存申請以後,寫入shellCode,且5次寫入目標程序申請的虛擬內存空間,這個地方咱們無需關係寫入shellCode的內容及做用,後面會詳細介紹,咱們只須要經過反彙編簡單看一下便可。
某網絡監視器完整逆向
                  圖片五:第一次寫入shellCode
某網絡監視器完整逆向
                  圖片六:第二次寫入shellCode
某網絡監視器完整逆向
                  圖片七:第三次寫入shellCode
某網絡監視器完整逆向
                  圖片八:第四次寫入shellCode
在第四次寫入以後,又作了一些事情,如建立了事件(保證如下操做在多線程環境下安全),建立了一個全局句柄,以下圖所示:
某網絡監視器完整逆向
                  圖片九:事件及新句柄建立
✍注意第五次寫入的是函數地址圖片中的註釋是第一次分析時候註釋,並非IAT,也不是修復重定位,只是爲了方便shellCode調用而寫入的地址,在目標程序中shellCode會用到的函數地址,做爲一個格外的附加項寫入到了目標程序,以下圖所示:
某網絡監視器完整逆向windows

                  圖片十:第五次寫入shellCode
 ➎ 建立遠程線程及且把第五次寫入的shellCode做爲參數執行:
某網絡監視器完整逆向
                  圖片十一:建立遠程線程
 以上就是整個目標程序注入的過程,發現並不複雜,這時候又要考慮,注入到目標進程shellCode,如何去分析這些代碼呢?瀏覽器

2、☛shellCode調試方法:
 第一次用的是dump,dump下來的是丟失的、不是完整的代碼,思路很阻塞...... 後來找朋友請教了一些問題,思考後大致有如下兩種辦法供參考:
  一、手動構建pe文件,修改shellCode或者寫入到目標進程中shellCode,在虛擬內存空間二進制複製出來。二進制複製的代碼拖入IDA中,咱們須要手動去找些函數名稱(根據第五次寫入的函數),這樣雖然能達到靜態分析的過程,可是相對比較麻煩。下面是在010中打開的複製的shellCode,咱們能夠看到與第5次寫入的函數徹底一致,以下圖所示:
某網絡監視器完整逆向
                  圖片十二:010中查看數據
  二、雙進程動態調試,在目標程序中分析觀察(動態)。簡單點來講,被注入的進程是你可以附加並且能夠調試的程序(有網絡響應)。就能動態的觀察虛擬內存的申請、寫入的過程。能下內存訪問斷點,可以動態的調試,並且是真實的應用環境下進行的,更爲精準。
 第三部分的內容將採用這種方式進行解析,分析代碼都幹了什麼事情?是怎樣捕獲這些網絡數據?下面咱們一塊兒來看。
3、☛shellCode動態分析:
 一、雙進程調試,注入程序與被注入程序。當注入程序(也就是圖一軟件),在目標進程中建立虛擬內存空間後,EAX會返回建立成功的地址咱們要到目標進程中找到地址,注意是目標進程中!
 二、通常會遇到這種問題:在目標進程中Ctrl+G查找地址的時候會找不到注入程序申請的虛擬內存?明明申請都成功了爲什麼還找不到?不慌!,咱們在OD中Alt+M,而後拉到最下面(通常都在最下面),就會發現申請的虛擬內存空間。
 三、當注入的程序調用WriteProcessMemory,5次寫入代碼的時候,咱們就能夠在目標程序的數據窗口跟隨,動態的觀察寫入的數據,直到5次寫入完成。
 四、在建立遠程線程以前,這時候目標程序中的虛擬內存應該是有數據的,由於寫入已經完成。不要反彙編而後在申請的虛擬內存中F2,好像也沒辦法F2下斷點。保險起見直接下內存訪問斷點便可,而後注入程序建立遠程線程成功,咱們就可讓目標程序跑起來,直接會在申請的虛擬內存中斷下來,剩下的就好辦了。安全

✎咱們開始動態調試shellCode,這段代碼先幹了些什麼?以下圖所示:
某網絡監視器完整逆向
                  圖片十三:獲取send、recv函數地址
 這段代碼先來了個獲取send、recv的函數地址,居然這樣咱們科普一下這兩個函數,爲了讓你們更容易理解,下面寫了一段簡單的網絡編程,來看以如何進行網絡通信。
 先來看函數原型,send與recv兩個函數,分別是發送與響應,函數原型以下:服務器

int WSAAPI recv(
    _In_ SOCKET s,
    _Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf,
    _In_ int len,
    _In_ int flags
    );

int WSAAPI send(
    _In_ SOCKET s,
    _In_reads_bytes_(len) const char FAR * buf,
    _In_ int len,
    _In_ int flags
    );

參數基本相同,第二個參數是指向char* 類型的緩衝區,這第三個參數是緩衝區大小,這兩個很關鍵。

服務器端:網絡

#include "pch.h"
#include <WinSock2.h>
#include <iostream>
#pragma comment(lib, "WS2_32.lib")

using namespace std;

/*
    Socket網絡編程服務器端
*/

// 用於接受客戶端發來的消息 強轉後查看是否數據一致(精準)
typedef struct _Message
{
    int Code;

    char Number;
}Message, *pMessage;

int main()
{
    cout << "服務端:" << endl;

    WSADATA str_Data = { 0, };

    int SockAddSize = sizeof(sockaddr_in);

    int nResult = 0;

    // 1. 初始化
    nResult = WSAStartup(MAKEWORD(2, 2), &str_Data);

    if (nResult == SOCKET_ERROR)
    {

        cout << "WSAStartup() ErrorCode = " << GetLastError() << endl;

        system("pause");

        return -1;
    }

    // 2. 建立套接字
    SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    // 3. 初始化Ip及端口信息
    sockaddr_in str_Addrs = { 0, };

    str_Addrs.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

    str_Addrs.sin_family = AF_INET;

    str_Addrs.sin_port = htons(8888);

    // 4. 綁定socket
    nResult = bind(sock, (sockaddr*)&str_Addrs, SockAddSize);

    if (SOCKET_ERROR == nResult)
    {
        closesocket(sock);

        WSACleanup();

        cout << "bind() failuer ErrorCode = " << GetLastError() << endl;

        return -1;
    }

    // 5. 監聽(失敗概率與種500w有一拼,因此不作判斷)
    try
    {
        listen(sock, SOMAXCONN);
    }
    catch (const std::exception&)
    {
        return -1;
    }

    sockaddr_in str_Client = { 0, };

    // 6. 鏈接響應(若是不設置異步 會阻塞等待 tcp),知道有客戶端去鏈接
    SOCKET ClientSock = accept(sock, (sockaddr *)&str_Client, &SockAddSize);

    if (ClientSock == INVALID_SOCKET)
    {
        closesocket(sock);

        WSACleanup();

        cout << "bind() failuer ErrorCode = " << GetLastError() << endl;

    }

    char nBuf[] = "消息已收到!";

    int BufSize = sizeof(nBuf);

    Message str_Msg = {0,};

    // 7. 等待鏈接(這是一個死循環)

    // 若是有客戶端鏈接成功,發送一條消息看是否成功
    if (SOCKET_ERROR == recv(ClientSock, (char*)&str_Msg, sizeof(Message), 0))
        cout << "recvError Code =  " << GetLastError() << endl;

    cout << "客戶端發來消息:  Code = " << str_Msg.Code << endl;

    cout << "客戶端發來消息:  Code = " << str_Msg.Number << endl;

    // 回覆客戶端一條消息
    send(ClientSock, nBuf, BufSize, 0);

    system("pause");

    return 0;
}

客戶端:多線程

#include "pch.h"
#include <iostream>
#include <WinSock2.h>

#pragma comment (lib, "WS2_32.lib")

using namespace std;

/*
    Socket客戶端
*/

// 使用結構體 更直觀表示經過send能夠傳送大量的數據
typedef struct _Message
{
    int Code;

    char Number;
}Message, *pMessage;

int main()
{
    cout << "客戶端:" << endl;

    WSADATA str_Data = { 0, };

    int nRet = 0;

    // 1. 初始化
    nRet = WSAStartup(MAKEWORD(2, 2), &str_Data);

    if (SOCKET_ERROR == nRet)
    {

        cout << "WSAStartup() ErrorCode = " << GetLastError() << endl;

        return -1;
    }

    // 2. Socket初始化
    SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    sockaddr_in str_sockAdd = { 0, };

    str_sockAdd.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

    str_sockAdd.sin_family = AF_INET;

    str_sockAdd.sin_port = htons(8888);

    int socketSize = sizeof(sockaddr_in);

    nRet = connect(sock, (sockaddr *)&str_sockAdd, socketSize);

    if (SOCKET_ERROR == nRet)
    {
        cout << "connect failuer ErrorCode = " << GetLastError() << endl;

        closesocket(sock);

        WSACleanup();

        return -1;
    }

    Message str_Msg = { 0, };

    str_Msg.Code = 1;

    str_Msg.Number = 'a';

    // 成功消息到服務器端
    send(sock, (char*)&str_Msg, sizeof(Message), 0);

    char nBuf[20] = {0,};
    // 響應服務器發來的消息
    recv(sock, nBuf, sizeof(nBuf), 0);

    cout << "服務器端發來消息:" << nBuf << endl;

    system("pause");

    return 0;
}

 若是對網絡編程不熟悉,請把上面代碼學習一下,由於下面是對這兩個函數的inlinehook,因此掌握函數使用與實現很重要。
 若是對hook不熟悉,請看之前寫的博客https://blog.51cto.com/13352079/2342776異步

上面咱們分析了shellCode第一段代碼,獲取了recv與send函數,下面接着上圖:

某網絡監視器完整逆向
                  圖片十三:讀取函數前5個字節
某網絡監視器完整逆向
                  圖片十四:inlinehook的offset計算
某網絡監視器完整逆向
                  圖片十五:替換原函數前5個字節
簡單的打個比方:
 先讀取原函數send的前5個字節,而後計算偏移: 中轉地址 - 原函數地址 - 5。爲何-5?如圖十五所示,原函數前5個字節hook後變爲jmp,運行後被響應而後跳轉,若是你不-5,那不是又到了jmp,應該jmp執行以後,該執行jmp下一條指令,因此-5。
 若是還不太清楚,咱們來作一個對比 hook前與hook後發生了哪些變化,以下圖所示:
某網絡監視器完整逆向
                  圖片十六:hook以前的函數
某網絡監視器完整逆向
                  圖片十七:hook以後的函數
 因此破壞了原函數前5個字節,一開始先讀取是爲了保存前5個字節的內容,執行JMP之後,跳轉到JMP下一條指令以前(SUB ESP,0X20以前)仍是會執行保存的5個字節機器碼,在跳轉到SUB ESP,0X20繼續執行原函數。
inlinehook的recv函數幹些什麼事??
某網絡監視器完整逆向
                  圖片十八:hook recv執行過程
✎注意!如上圖所示,上述圖片中缺乏少一個步驟,上面圖關聯到一塊兒只是爲了讓你們好理解,可是缺乏了執行原函數棧頂的操做,其實CALL DWORD PTR DS:[ESI + 0XA90C]是跳轉到本身的shellCode中,而後執行原函數的前5個字節,如圖十二所示,到底CALL的是什麼內容?以下圖所示:
某網絡監視器完整逆向
                  圖片十九:執行原函數棧頂
 如上圖所示,CALL過來以後,執行機器指令8BFF558BEC(原函數的前5個字節),後面則是JMP ws2_32.74BF5FF5,其實就是 :中轉地址 - (send或者recv函數地址) - 5,上面介紹計算的偏移的做用就體現出來了,正好跳轉到原函數的JMP下一條指令。
inlinehook的send函數幹些什麼事??
某網絡監視器完整逆向
                  圖片二十:截獲send函數
某網絡監視器完整逆向
                  圖片二十一:hook send執行流程1
 根據截獲跳轉到BaseAddress + 0x400的地方,Getpc獲取了當前的地址,注意GetPC這種方式,如E8 00000000是敏感操做,有時候這樣使用當前地址如下的彙編指令將被截斷,繼續看:
某網絡監視器完整逆向
                  圖片二十二:hook send執行流程2
 利用CreateFile在\.\Pipe\下面帶開了文件句柄(圖三中的文件路徑),格式化輸出的是什麼?
某網絡監視器完整逆向
                  圖片二十三:wvsprintfA函數
 格式化輸出,咱們看到了一些關鍵的數據,如上圖中PID,TID等等,爲了傳送給網絡監控工具顯示數據而準備。
某網絡監視器完整逆向
                  圖片二十四:截獲的send消息寫入文件句柄
 實際上是ASCII截獲的數據則是第二個參數,也就是緩衝區中的內容加上PID一些附加信息數據,寫入大小是第三個參數加上PID等附加大小。注入程序去讀取文件句柄內容,把捕獲的消息數據經過ListView控件(MFC)顯示到界面中。
 簡單來講,就幹了這麼一件事,利用inlinehook技術,hook send 和 recv兩個函數,截獲第二個參數中的緩衝區,顯示到三環。因此使用windows SDK網絡編程,或者說使用這兩個函數,你發送與響應的消息會被截獲。 到此你應該知道軟件實現原理及過程,能夠本身寫一個更適用的網絡軟件,還能夠作過濾,對一些敏感的數據操做,從而實現三環的網絡監控功能、過濾功能等。

相關文章
相關標籤/搜索