網路遊戲逆向分析-5-執行緒發包函數

網路遊戲逆向分析-5-執行緒發包函數

非執行緒發包執行流程:

 

 

 

執行緒發包執行流程:

 

 

 

多執行緒可能是執行緒A把數據給執行緒B,然後執行緒B再把數據給伺服器進行交互。

之前的可能就一個執行緒就搞定了,這次就需要複雜一點,兩個執行緒協同合作來交互數據。

執行緒A把封包數據寫到某個地方,然後執行緒B一直讀該地方如果有值就發送,沒有就繼續一直讀。

實戰:

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

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

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

那麼,如何通過執行緒B走到執行緒A拿到發包函數呢?

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

 

實操:

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

 

 

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

 

 

這次我發了一個 「1111」的字元串

 

 

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

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

再查看第二次斷下來的情況:

 

 

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

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

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

 

 

 

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

如何判斷是否是執行緒發包

有一個很簡單的方式:

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

 

 

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

 

如何跳出執行緒發包

如何跳出這個發包的執行緒,來到真實處理程式碼邏輯的內容?

其實前面也說了思路,因為兩個執行緒,執行緒B是要一直訪問一個內容,而執行緒A判斷了程式碼邏輯後會給某個地址寫內容,這樣找到該地址,打上一個寫入斷點就可以回到執行緒A的內容里了。

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

 

 

因為函數的原型是:

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

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

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

所有就很有可能,這個地址的內容是由執行緒A來修改了,然後再由執行緒B來讀取發包。

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

 

斷下來之後是在這樣的一個模組裡面:

 

 

這個一看就是系統的模組,還有msvcr這種API,而且用黃色標註了的,所以我們先跳出這個函數:Ctrl+F9然後F8,是這樣的內容:

 

 

這裡如果跟我們想的差不多的話,應該就是跳到了執行緒A,然後我們繼續往上面找會找到我們之前通過喊話call的第一次斷下來一直往上分析喊話函數的內容,我們是有打注釋的,所以往上的話可以找到我們注釋的內容。相關內容:網路遊戲逆向分析-3-通過發包函數找功能call – Sna1lGo – 部落格園 (cnblogs.com)

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

 

 

這表明了我們還在執行緒B裡面,還是在這裡沒有出去。

那麼很有可能是執行緒B把兩個用來交互的數據的內容,給拷貝到了某個地址,然後再來讀取:

 

 

因為肯定是有一個給執行緒A和B來交互的地址數據。所以可能是這樣樣子的,中間加了一層內容,將封包數據,讀取到了封包數據B裡面,然後再從B裡面拿數據。

那麼我們可以繼續追蹤剛剛找到的地址內容,來查看到底是怎麼一回事。

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

 

 

 

 

這裡通過暫存器可以看到,是調用了一個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進行交互的地址。

 

 

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

 

 

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

 

 

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

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

 

 

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

 

 

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

然後執行緒A對這個指針對應的地址的內容進行修改,修改後,執行緒B把這個地址的內容複製到了自己的一個地址裡面,然後再把自己的地址的內容拿去發包。

//執行緒A
char *buffer;
*buffer = "123456"

//執行緒B
memmove(seftAddress,buffer,count)
send(seftAddress)

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

 

 

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

 

 

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

 

需要注意的是,由於久了沒動遊戲會退出,而重新載入會導致地址改變,但是其中的邏輯是沒有變的

 

總結:

首先判斷出是不是執行緒發包,前面講了辦法,然後跟蹤發包函數的地址,通過地址來一步一步探索,直到回到另一個執行緒。因為執行緒是肯定會有交互的,除非兩個執行緒的獨立開來的,只要有交互就會有緩衝區啊,或者執行緒同步之類的東西存在,就可以順藤摸瓜。