網絡游戲逆向分析-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里面:

 

 

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

 

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

 

總結:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM