某網絡監視器完整逆向

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

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

✎逆向分析目錄:

1、注入代碼分析 2、shellCode調試方法 3、shellCode動態分析
--------★ ★ ★------- -----------------★ ★ ★------------------- -----------★ ★ ★ ★-----------

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

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

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

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

<br>
客戶端:

#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不熟悉,請看以前寫的博客http://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獲取了當前的地址,注意使用這種方式容易被敏感操作截斷,繼續看:
某網絡監視器完整逆向
                  圖片二十二:hook send執行流程2
 利用CreateFile在\.\Pipe\下面帶開了文件句柄(圖三中的文件路徑),格式化輸出的是什麼?
某網絡監視器完整逆向
                  圖片二十三:wvsprintfA函數
 格式化輸出,我們看到了一些關鍵的數據,如上圖中PID,TID等等,爲了傳送給網絡監控工具顯示數據而準備。
某網絡監視器完整逆向
                  圖片二十四:截獲的send消息寫入文件句柄
 其實是ASCII截獲的數據則是第二個參數,也就是緩衝區中的內容加上PID一些附加信息數據,寫入大小是第三個參數加上PID等附加大小。注入程序去讀取文件句柄內容,把捕獲的消息數據通過ListView控件(MFC)顯示到界面中。
 簡單來說,就幹了這麼一件事,利用inlinehook技術,hook send 和 recv兩個函數,截獲第二個參數中的緩衝區,顯示到三環。所以使用windows SDK網絡編程,或者說使用這兩個函數,你發送與響應的消息會被截獲。
 到此你應該知道軟件實現原理及過程,可以自己寫一個更適用的網絡軟件,還可以做過濾,對一些敏感的數據操作,從而實現三環的網絡監控功能、過濾功能等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章