網路遊戲逆向分析-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的第一次斷下來一直往上分析喊話函數的內容,我們是有打注釋的,所以往上的話可以找到我們注釋的內容。相關內容:
但是我們不斷往上找後,又發現了問題又回到了之前的循環哪裡:
這表明了我們還在執行緒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裡面:
蕪湖搞定啦,我們做到了!!!
需要注意的是,由於久了沒動遊戲會退出,而重新載入會導致地址改變,但是其中的邏輯是沒有變的
總結: