天天看點

網絡遊戲逆向分析-5-線程發包函數

網絡遊戲逆向分析-5-線程發包函數

非線程發包執行流程:

網絡遊戲逆向分析-5-線程發包函數
線程發包執行流程:
網絡遊戲逆向分析-5-線程發包函數

多線程可能是線程A把資料給線程B,然後線程B再把資料給伺服器進行互動。

之前的可能就一個線程就搞定了,這次就需要複雜一點,兩個線程協同合作來互動資料。

線程A把封包資料寫到某個地方,然後線程B一直讀該地方如果有值就發送,沒有就繼續一直讀。

實戰:

還是采用笑傲江湖遊戲裡的喊話call來處理。

之前采用的辦法是通過給send函數下條件斷點,然後一層一層往上追溯直到看到明文的時候再來分析。

但是線程發包就不行了,因為你一直往上找可能隻是把這個線程找到頭了,但是這個線程的内部邏輯是來讀取某個内容再來發包,用前面的技巧已經不能處理了。

那麼,如何通過線程B走到線程A拿到發包函數呢?

前面的圖其實很明顯的展示了一下思路,思路就是兩個線程有個互動的地方,通過分析誰修改了這個互動的地方就可以知道線程A了。

實操:

還是優先給send函數打斷點:

網絡遊戲逆向分析-5-線程發包函數

這裡其實多試幾次就會發現,隻要給send函數打了斷點,不管是走路還是說話還是幹别的,其實都會斷下來兩次,而且不管喊話喊了多少話,封包的長度永遠為1,而第二次斷下來就和喊話的内容長度有關系了。(前面找到的内容并不是無效,也是對的,隻是可能隻是我運氣比較好。)

網絡遊戲逆向分析-5-線程發包函數

這次我發了一個 “1111”的字元串

網絡遊戲逆向分析-5-線程發包函數

第一次斷下來的時候封包長度竟然是1,不由得很奇怪了。因為這個函數的原型是這樣的:

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

再檢視第二次斷下來的情況:

網絡遊戲逆向分析-5-線程發包函數

這個就有點對味了,因為4個int就是16,用16進制來表示就是 0x10。

是以這裡我們采用對第二次斷下來的情況往上找:

一直采用Ctrl+F9 和F8也就是運作到函數傳回位址,然後再運作,就是一直跳出函數看看是什麼情況,這個在前面也有講到:

網絡遊戲逆向分析-5-線程發包函數

一直往上找會發現,突然到了這個jmp之後,程式自己運作起來了,說明這裡是一個死循環,是不是跟前面說的内容很像,一個死循環一直讀資料。

如何判斷是否是線程發包

有一個很簡單的方式:

由于線程A會來判斷,人物的動作,而線程B隻需要把内容傳輸給伺服器,那麼對于線程B來說調用邏輯始終唯一,始終是把内容發出去,它的函數調用堆棧是不會變的,是以觀察斷點的調用堆棧就可以判斷了:

網絡遊戲逆向分析-5-線程發包函數

可以看到我兩次停止在這裡,調用的堆棧都是一模一樣的。這個大家自己測試一下就可以看到了。

如何跳出線程發包

如何跳出這個發包的線程,來到真實處理代碼邏輯的内容?

其實前面也說了思路,因為兩個線程,線程B是要一直通路一個内容,而線程A判斷了代碼邏輯後會給某個位址寫内容,這樣找到該位址,打上一個寫入斷點就可以回到線程A的内容裡了。

這裡我們先找到是如何調用send函數的:

網絡遊戲逆向分析-5-線程發包函數

因為函數的原型是:

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

而且這是一個WindowsAPi,它的調用約定是__stdcall ,參數從右往左入棧,那麼倒數第二個也就是25DD108就是一個緩沖區位址.

多次嘗試後,可以很明顯的看出來,這個緩沖區位址是沒有改變的。

所有就很有可能,這個位址的内容是由線程A來修改了,然後再由線程B來讀取發包。

所有這裡我們就給這個位址下一個寫入斷點,但是這裡有一個小技巧,往後面一點寫,然後我們輸出喊話的時候多寫一點,這樣就可以防止有别的内容寫入來幹擾我們,因為這是個緩沖區的首位址,我們寫的内容越多,那麼就會順着這裡位址往裡面填充内容,是以這樣是一個很好的小技巧。

斷下來之後是在這樣的一個子產品裡面:

網絡遊戲逆向分析-5-線程發包函數

這個一看就是系統的子產品,還有msvcr這種API,而且用黃色标注了的,是以我們先跳出這個函數:Ctrl+F9然後F8,是這樣的内容:

網絡遊戲逆向分析-5-線程發包函數

這裡如果跟我們想的差不多的話,應該就是跳到了線程A,然後我們繼續往上面找會找到我們之前通過喊話call的第一次斷下來一直往上分析喊話函數的内容,我們是有打注釋的,是以往上的話可以找到我們注釋的内容。相關内容:網絡遊戲逆向分析-3-通過發包函數找功能call - Sna1lGo

但是我們不斷往上找後,又發現了問題又回到了之前的循環哪裡:

網絡遊戲逆向分析-5-線程發包函數

這表明了我們還線上程B裡面,還是在這裡沒有出去。

那麼很有可能是線程B把兩個用來互動的資料的内容,給拷貝到了某個位址,然後再來讀取:

網絡遊戲逆向分析-5-線程發包函數

因為肯定是有一個給線程A和B來互動的位址資料。是以可能是這樣樣子的,中間加了一層内容,将封包資料,讀取到了封包資料B裡面,然後再從B裡面拿資料。

那麼我們可以繼續追蹤剛剛找到的位址内容,來檢視到底是怎麼一回事。

這裡還是繼續給位址空間打硬體通路斷點,然後跳出第一次系統函數:

網絡遊戲逆向分析-5-線程發包函數
網絡遊戲逆向分析-5-線程發包函數

這裡通過寄存器可以看到,是調用了一個memmove函數,這個函數:

void *memmove(
   void *dest,
   const void *src,
   size_t count
);
wchar_t *wmemmove(
   wchar_t *dest,
   const wchar_t *src,
   size_t count
);      

這個函數簡直和memcpy是一模一樣,就是把一個緩沖區的内容複制到另一個緩沖區。dest是目标位址,src是拿來複制的位址,這個函數也是一個WINDOWS API,肯定也是從右往左入參,是以關注第二個彙編指令push edi就好了,但是這裡的斷點我們要注意,因為很多情況下會斷下來,不便于我們分析,是以我們打一個條件斷點,條件是edi==之前找到的緩沖區首位址,這樣就可以鎖定到隻有喊話的時候會斷下來了。

經過我的測試,完美支援剛剛的假設。

那麼這裡我們往上找ecx的内容,因為push ecx對應的是參數const void *src的内容,而src是源位址,dest的目标位址,目标位址我們已經找到了,接下來看源位址是不是線程A和線程B進行互動的位址。

網絡遊戲逆向分析-5-線程發包函數

這裡從下往上看ecx,被mov ecx,dword ptr ss:[esp+24]給修改了,牽扯到esp和ebp的内容多半都是函數參數,或者臨時變量,這裡我們打一個條件斷點看就知道了。

網絡遊戲逆向分析-5-線程發包函數

這裡的esp+24是這個位址,而上面就是函數的傳回位址,那麼很有可能這個是個函數的參數,因為Windows函數是,把參數入棧後,再把傳回位址入棧,然後再跳轉。是以這裡我們直接跳出這個函數,并且觀察該值有沒有改變:

網絡遊戲逆向分析-5-線程發包函數

可以看到這裡并沒有改變,是以剛剛我們的猜想是成立的。

然後給這個函數打上斷點後:

網絡遊戲逆向分析-5-線程發包函數

可以看到eax是我們要的值,是以再往上追蹤eax,發現是esi+4的内容來修改了,這裡我們加上一個條件斷點來觀察:

網絡遊戲逆向分析-5-線程發包函數

這裡我們觀察到,esi+4的位址是永遠不變的,也就是說往上沒人改變它,但是它對應的内容是一直在改變的。是以很有可能就是,esi+4是一個指針,

然後線程A對這個指針對應的位址的内容進行修改,修改後,線程B把這個位址的内容複制到了自己的一個位址裡面,然後再把自己的位址的内容拿去發包。

//線程A
char *buffer;
*buffer = "123456"

      
//線程B
memmove(seftAddress,buffer,count)
send(seftAddress)      

大概是這個邏輯,因為我們通過send拿到緩沖區位址,但是緩沖區位址調用了一個memove來改寫,然後memmove函中的有一個指針,一直沒改變,但是裡面的内容再改變。那麼這麼我們針對這個位址下一個寫入斷點,應該就可以傳回到線程A裡面了:

網絡遊戲逆向分析-5-線程發包函數

停在了這裡,我們繼續往下跳出函數看看能不能進入到線程A裡面:

網絡遊戲逆向分析-5-線程發包函數

蕪湖搞定啦,我們做到了!!!

總結: