国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

使用Hook攔截socket函數(shù)解決虛擬局域網(wǎng)部分游戲聯(lián)機(jī)找不到房間的問題——以文明6為例

這篇具有很好參考價值的文章主要介紹了使用Hook攔截socket函數(shù)解決虛擬局域網(wǎng)部分游戲聯(lián)機(jī)找不到房間的問題——以文明6為例。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點(diǎn)擊"舉報違法"按鈕提交疑問。

前言

許多單機(jī)游戲都有局域網(wǎng)聯(lián)機(jī)功能,盡管有些也提供了互聯(lián)網(wǎng)聯(lián)機(jī)功能,但是一般這些游戲的土豆服務(wù)器讓玩家非常惱火,于是諸如游俠等對戰(zhàn)平臺則是其中一種選擇。使用這些平臺提供的局域網(wǎng)聯(lián)機(jī)功能就可以獲得比較穩(wěn)定的聯(lián)機(jī)體驗(yàn)。還有一種方法就是搭建虛擬局域網(wǎng)(VLAN)了,比如使用N2N就可以搭建一個(需要自備服務(wù)器),或者用ZeroTier、Radmin LAN這類工具。

但是許多人在搭建好一個自己的局域網(wǎng)之后,卻發(fā)現(xiàn)進(jìn)了游戲怎么也找不到房間。

局域網(wǎng)聯(lián)機(jī)的游戲基本上是通過向 255.255.255.255 發(fā)送 UDP 廣播數(shù)據(jù)包來傳播游戲房間信息,但是 Windows 只會在首選的網(wǎng)絡(luò)接口(網(wǎng)卡)上發(fā)送全局 IP 廣播數(shù)據(jù)包,也就是說局域網(wǎng)游戲的信息沒有被 Windows 在虛擬局域網(wǎng)接口上廣播

以上摘自https://bugxia.com/3128.html,原出處https://www.bilibili.com/read/cv14633088。

本文以文明6這款游戲的找房機(jī)制為例,采用Hook技術(shù),攔截游戲?qū)DP廣播的發(fā)送、接收等操作,并用一系列的方法使得游戲的廣播能夠成功發(fā)送到所有網(wǎng)卡,并成功接收到來自其他客戶端的回復(fù)。

本文會詳細(xì)說明如何分析文明6的局域網(wǎng)聯(lián)機(jī)找房機(jī)制,以及Hook相關(guān)函數(shù)的細(xì)節(jié),所以內(nèi)容可能比較冗長,希望你能耐心看完。

在開始之前,希望你能對文本涉及到的Windows編程、socket編程、Hook技術(shù)、網(wǎng)絡(luò)抓包、反匯編調(diào)試等內(nèi)容有所了解,這樣可以便于理解。否則對于初學(xué)者來說可能會有一定的難度。

還有一點(diǎn),本文所提出的方法,可能并不適用除文明6以外的其他游戲,因?yàn)槊總€游戲的具體機(jī)制有所不同,需要通過分析得出。如果你熟悉分析方法,可以很容易地對不同的游戲找到適用的解決方案。而且,本文的方法僅適用與真實(shí)的或者虛擬的局域網(wǎng),而不適用于游俠這類同樣使用Hook的對戰(zhàn)平臺。

如果有IPv6地址,可以考慮使用該項(xiàng)目提供的工具:xaxys/injciv6: 文明6聯(lián)機(jī) - 基于IP的游戲發(fā)現(xiàn) (IPv4/IPv6) (github.com),相關(guān)視頻:https://www.bilibili.com/video/BV1qV411X7FP,其在本文基礎(chǔ)上添加了IPv6直連的功能,可以不需要組網(wǎng)。不過這里提醒一下,IPv6的入站比較復(fù)雜,目前絕大多數(shù)的光貓和路由器都是默認(rèn)開啟了防火墻的,需要依次到光貓和路由器關(guān)閉IPv6的防火墻(大部分都可以關(guān)),同時在系統(tǒng)放行游戲的網(wǎng)絡(luò)包,為了安全,不建議關(guān)閉系統(tǒng)的防火墻。至于移動數(shù)據(jù)的IPv6上網(wǎng),不確定能不能成功入站。

正文

本文代碼及編譯好的二進(jìn)制文件可以在這個倉庫找到。

https://gitcode.net/PeaZomboss/miscellaneous

源代碼在文件夾230130-hookgamesendto
若要下載二進(jìn)制,請到https://gitcode.net/PeaZomboss/miscellaneous/-/releases/civ6-hook-binary。

未來有關(guān)的代碼均可在上面的倉庫找到。

起因

去年的時候我和哥們一起玩起了文明6,為了能夠一起聯(lián)機(jī)游玩,就找了許多方法,后來找到了由Bug俠基于N2N開發(fā)的EasyN2N(在這里感謝作者免費(fèi)提供服務(wù)器供我們使用)。但是經(jīng)常遇到找不到對方房間的問題,后來逐漸排查出了原因,就是UDP廣播沒有發(fā)出去的問題,也發(fā)現(xiàn)EasyN2N有一定的解決辦法。

比如其集成了WinIPBroadcast這款廣播轉(zhuǎn)發(fā)工具,不過在實(shí)際使用中發(fā)現(xiàn)有的時候沒有作用。但是手動把虛擬網(wǎng)卡的躍點(diǎn)改小,提高其優(yōu)先級是一個有效的方案。但不知是哪邊的bug,這個躍點(diǎn)會時不時的自動變回去,很是惱火,就想找一個方便的方法。

由于N2N這類技術(shù)是使用虛擬網(wǎng)卡來實(shí)現(xiàn)虛擬局域網(wǎng)的搭建,所以我們只要想辦法把游戲的廣播發(fā)到虛擬網(wǎng)卡就可以解決問題了。不過實(shí)際上把廣播轉(zhuǎn)發(fā)到每張網(wǎng)卡就可以了,因?yàn)檫@樣比較方便實(shí)現(xiàn)。

于是我就想到了使用Hook技術(shù),把游戲發(fā)的廣播內(nèi)容攔截了,再給他轉(zhuǎn)發(fā)豈不是就能解決問題了?

于是就上網(wǎng)查了一下資料,花了不少時間上手做了一個demo,自己用著感覺不錯(其實(shí)有bug,能不能成功還是看玄學(xué)),然后幾個月沒怎么玩了,后來看到文明6正在更新領(lǐng)袖包,打算等更新完了再快樂聯(lián)機(jī),又想到之前寫過的代碼,就想著把原來的代碼梳理一下,然后重新寫個新的,順便溫習(xí)一下相關(guān)知識。

好了事情的起因就是這樣子了。

經(jīng)過

正當(dāng)我以為一切順利的時候,我發(fā)現(xiàn)事情沒有那么簡單,實(shí)際上不是簡單Hook并轉(zhuǎn)發(fā)一下廣播就能實(shí)現(xiàn)的。這不僅涉及到sendto函數(shù)的基本用法,還有一些比較復(fù)雜的細(xì)節(jié),包括端口復(fù)用如何收發(fā)包的問題。于是又要涉及到對于收包函數(shù)recvfrom,以及select函數(shù)的攔截與使用。

后來我根據(jù)抓包的結(jié)果,推測文明6游戲的邏輯,仿制了一個類似的測試工具,并在此基礎(chǔ)上不斷調(diào)試。后來為了確保能實(shí)現(xiàn)一個比較準(zhǔn)確的結(jié)果,拿出了x64dbg這一神器對游戲進(jìn)程進(jìn)行了一些調(diào)試,最終得出了游戲找房過程的具體方法,并復(fù)刻了一個在原理上幾乎是完全一致的工具。

有興趣可以閱讀其源碼。關(guān)于游戲的調(diào)試過程,我會在后文進(jìn)行一個介紹。

總之,在一段時間的努力之后,我終于從sendto到recvfrom和select三位一體進(jìn)行精準(zhǔn)Hook,拿下了最終的成功。

技術(shù)介紹

本文的代碼實(shí)現(xiàn)部分使用Hook技術(shù)(確切說是Inline Hook)以及socket技術(shù),由于還要涉及對其他進(jìn)程的Hook,所以還要用到注入技術(shù)。

本文的分析過程涉及到網(wǎng)絡(luò)抓包及軟件調(diào)試技術(shù),這個會在游戲分析部分進(jìn)行具體說明。

Hook介紹

Hook技術(shù)一般翻譯為鉤子技術(shù),就是提前在特定事件或消息處掛上鉤子,等執(zhí)行到此處就會觸發(fā)鉤子,執(zhí)行鉤子的代碼。Windows系統(tǒng)提供了SetWindowsHookEx等一系列函數(shù)實(shí)現(xiàn)Hook的功能,不過這和我們實(shí)際用到的不太一樣。

本文所用的Inline Hook就是把任何函數(shù)調(diào)用的前幾個字節(jié)改成一句跳轉(zhuǎn)指令,跳轉(zhuǎn)到自己的地方執(zhí)行,然后返回到原來的主調(diào)函數(shù),此時就獲得了函數(shù)的參數(shù)等一系列信息。許多人做的微信Hook就是這么搞的,不過缺點(diǎn)就是一旦函數(shù)地址或者參數(shù)變了,就得重新編寫相應(yīng)的代碼。

有關(guān)Inline Hook的具體介紹,請看本人寫的這篇文章。本文所用的方法就是以此為基礎(chǔ)的。

當(dāng)然對于游戲來說,其發(fā)送廣播必然離不開系統(tǒng)調(diào)用sendto或者WSASendTo,而接收包多是通過recvfrom,所以我們只要hook相應(yīng)的系統(tǒng)調(diào)用就可以了,而大部分系統(tǒng)函數(shù)的地址都是完全公開的。

當(dāng)然如果只是這樣還只能hook自己的進(jìn)程,想要hook其他進(jìn)程就得先把hook代碼編進(jìn)一個dll,再想辦法讓目標(biāo)進(jìn)程加載這個dll,這個過程叫注入(inject)。

當(dāng)我們的dll打入敵人內(nèi)部,就可以窺探其全部的虛擬地址空間,這樣我們就可以大施拳腳,為所欲為了??。

socket介紹

所謂套接字(Socket),就是對網(wǎng)絡(luò)中不同主機(jī)上的應(yīng)用進(jìn)程之間進(jìn)行雙向通信的端點(diǎn)的抽象。一個套接字就是網(wǎng)絡(luò)上進(jìn)程通信的一端,提供了應(yīng)用層進(jìn)程利用網(wǎng)絡(luò)協(xié)議交換數(shù)據(jù)的機(jī)制。從所處的地位來講,套接字上聯(lián)應(yīng)用進(jìn)程,下聯(lián)網(wǎng)絡(luò)協(xié)議棧,是應(yīng)用程序通過網(wǎng)絡(luò)協(xié)議進(jìn)行通信的接口,是應(yīng)用程序與網(wǎng)絡(luò)協(xié)議棧進(jìn)行交互的接口。

摘自百度百科

socket最早是伯克利在Unix引入的一套API,后來的Linux以及Windows都兼容了這套API,盡管Windows的有些許不同,不過大體上是相似的。

游戲發(fā)送UDP廣播一般是調(diào)用了winsock的sendto函數(shù),而現(xiàn)在新的winsock2兼容舊的winsock,所以對于絕大多數(shù)新老游戲,使用的sendto函數(shù)都可以被攔截。

本文要涉及到的socket函數(shù)有以下三個:sendto、select、recvfrom


關(guān)于sendto函數(shù),微軟官方介紹如下:
https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-sendto

函數(shù)原型如下:

int sendto(
  [in] SOCKET         s,     // socket描述符
  [in] const char     *buf,  // 發(fā)送的數(shù)據(jù)
  [in] int            len,   // 發(fā)送數(shù)據(jù)的長度
  [in] int            flags, // 發(fā)送的選項(xiàng),一般為0
  [in] const sockaddr *to,   // 目標(biāo)地址信息
  [in] int            tolen  // 目標(biāo)地址信息長度
);

關(guān)于recvfrom函數(shù),文檔如下:
https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-recvfrom

函數(shù)原型如下:

int recvfrom(
  [in]                SOCKET   s,       // socket描述符
  [out]               char     *buf,    // 接收數(shù)據(jù)的緩沖區(qū)
  [in]                int      len,     // 要接收數(shù)據(jù)的長度
  [in]                int      flags,   // 接收的選項(xiàng),一般為0
  [out]               sockaddr *from,   // 發(fā)送方的地址信息
  [in, out, optional] int      *fromlen // 地址信息長度
);

有關(guān)這兩個函數(shù)的用法介紹,網(wǎng)上不計(jì)其數(shù),就不再展開,重點(diǎn)說一下select函數(shù)。


select函數(shù)文檔如下:
https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-select

函數(shù)原型:

int WSAAPI select(
  [in]      int           nfds,       // 用于兼容,winsock不關(guān)心
  [in, out] fd_set        *readfds,   // 輸入一組socket集合,留下可讀的socket
  [in, out] fd_set        *writefds,  // 輸入一組socket集合,留下可寫的socket
  [in, out] fd_set        *exceptfds, // 輸入一組socket集合,留下異常的socket
  [in]      const timeval *timeout    // 最大等待時間
);

返回值有3種情況:

  1. 返回-1,說明函數(shù)出錯,原因通過WSAGetLastError獲取。
  2. 返回0,說明超過最大等待時間沒有可用的socket。
  3. 返回n,n>0,說明有集合中有n個準(zhǔn)備就緒的socket。

其中fd_set結(jié)構(gòu)定義如下:

typedef struct fd_set {
  u_int  fd_count;             // 集合內(nèi)socket的數(shù)量
  SOCKET fd_array[FD_SETSIZE]; // socket集合,最大容量為FD_SETSIZE,默認(rèn)64
} fd_set, FD_SET, *PFD_SET, *LPFD_SET;

timeval結(jié)構(gòu)定義如下:

typedef struct timeval {
  long tv_sec;  // 秒
  long tv_usec; // 微秒
} TIMEVAL, *PTIMEVAL, *LPTIMEVAL;

為了便于操作,winsock2提供了一系列的宏,具體就不多說了。

值得注意到是,readfds、writefds、exceptfds都是可選的,只要不全是NULL就可以了,一般對于接收,只需設(shè)置readfds就行了。而這個timeout參數(shù)的時間則是由timeval類型的秒和微秒加起來的時間,如果timeout參數(shù)為NULL,則會一直等待。

由于輸入的集合會在調(diào)用select之后被改變,所以需要每次調(diào)用select之前重新設(shè)置集合。實(shí)際上fd_set的fd_count表明了可用的socket數(shù)量,而fd_array則依次保存了fd_count個socket。

這大概就是Windows下select的一些細(xì)節(jié)了。

注入介紹

寫好一個dll,想要加載一般有兩種方法,靜態(tài)鏈接和動態(tài)鏈接。而注入就是想辦法讓目標(biāo)進(jìn)程自己調(diào)用LoadLibrary函數(shù)來動態(tài)加載這個dll。而這個方法也就是最為簡單的直接注入法。當(dāng)然復(fù)雜的還有反射式注入和鏤空注入等方法,這些方法就不是直接調(diào)用LoadLibrary了,一般是用一段shellcode,不過這個就有點(diǎn)做病毒的意味了。

關(guān)于一些詳細(xì)的介紹可以看這個https://bbs.kanxue.com/thread-274131.htm。

這里我們選擇使用簡單方便的直接注入,具體的方法可以參考上面的鏈接,不過由于當(dāng)時寫程序的時候并沒有看到此文,所以我并沒有文中所說“給進(jìn)程提權(quán)”的方法,但也可以實(shí)現(xiàn)效果,原因暫不清楚。

直接注入一般是獲取目標(biāo)進(jìn)程,使用OpenProcess打開進(jìn)程,然后使用*遠(yuǎn)程線程(Remote Thread)*執(zhí)行LoadLibraryA,具體代碼會在后面說明。

游戲分析

這一部分主要是說如何分析文明6這款游戲局域網(wǎng)聯(lián)機(jī)的方式。

工具

使用Wiresharkx64dbg軟件。

對于Wireshark,我們需要找對網(wǎng)卡然后捕獲一段時間內(nèi)的數(shù)據(jù)進(jìn)行分析,具體只要熟悉工具欄前四個按鈕的功能就行了,其他更高級的操作也不需要。

對于x64dbg,我們需要在游戲運(yùn)行后附加(Attach)到進(jìn)程,然后對關(guān)鍵函數(shù)下斷點(diǎn)并進(jìn)行單步調(diào)試,需要熟悉一些基本的操作方法,同時建議裝上反調(diào)試插件減少可能的麻煩。

抓包分析

分析的時候最好不要有太多其他的網(wǎng)絡(luò)活動,避免抓包的時候干擾太多。

首先打開Wireshark,選擇Adapter for loopback traffic capture這個選項(xiàng),然后打開游戲點(diǎn)刷新,過一會就停止捕獲,從這些記錄里找目的是255.255.255.255的UDP包,這樣就可以發(fā)現(xiàn)這個包是從那個網(wǎng)卡發(fā)出來的了。

文明6局域網(wǎng)刷不出房間,雜項(xiàng),游戲,網(wǎng)絡(luò),udp,c++,windows

比如上圖,我抓了一下包,發(fā)現(xiàn)了大量從10.31.23.127:49190發(fā)出的UDP廣播,內(nèi)容長度都是4字節(jié),用ipconfig查一下發(fā)現(xiàn)這個IP是來自PPP撥號上網(wǎng)的網(wǎng)卡,其子網(wǎng)掩碼為255.255.255.255,即不可能發(fā)出廣播。在這種情況下,即使你使用虛擬局域網(wǎng)組網(wǎng)方案,但游戲UDP廣播依然不走這個網(wǎng)卡,而是被系統(tǒng)安排到了默認(rèn)網(wǎng)卡,所以根本找不到房間。

當(dāng)然還有一種情況我也遇到過,就是通過局域網(wǎng)上網(wǎng),IP地址為192.168.1.1,但是廣播既不從這個地址發(fā)出,也不從虛擬網(wǎng)卡發(fā)出,而是從192.168.56.1發(fā)出,而查了發(fā)現(xiàn)此IP竟然是VirtualBox的網(wǎng)卡。這種情況下,不管是同一個物理局域網(wǎng)還是虛擬局域網(wǎng),都不可能找到房間。

總之你會發(fā)現(xiàn),不論你以何種方式上網(wǎng),似乎都可能會遇到這種問題。我在網(wǎng)上看到過一個解決方法,就是把無關(guān)的網(wǎng)卡都禁用了,這確實(shí)可以,但是正如我前面測試的,假如你是PPP上網(wǎng),那么就算你關(guān)了所有多余的網(wǎng)卡,只留下虛擬網(wǎng)卡,廣播依然可能會從PPP的網(wǎng)卡發(fā)出去,而你不可能禁用這張上網(wǎng)的網(wǎng)卡。

當(dāng)然還有一個方法,就是修改網(wǎng)卡躍點(diǎn)。把一張網(wǎng)卡的躍點(diǎn)改小了,那么其優(yōu)先級就會提高,這樣廣播就會從指定的網(wǎng)卡發(fā)出去了。不過你也不知道躍點(diǎn)到底什么時候會不會自動改回去,這點(diǎn)有時候比較惱火。

回到前面那張抓包的截圖,如果你自己抓幾次包,然后分析一下每次抓到的數(shù)據(jù)包,就可以得出一個結(jié)論:游戲每次發(fā)包,都是用同一個端口向62900-62999這100個端口發(fā)送4字節(jié)的實(shí)際內(nèi)容。

很容易猜想到,游戲每次找房,都是用同一個端口發(fā)送廣播,而收到廣播的一端則向這個端口發(fā)送回信,提供房間信息。如果你有可以進(jìn)行聯(lián)機(jī)測試的條件的話,那就可以試試游戲是怎么接收房間信息的。如果可以成功聯(lián)機(jī)的話,就先確定能聯(lián)機(jī)的網(wǎng)卡,然后對該網(wǎng)卡抓包。

假如你現(xiàn)在在搜索房間,點(diǎn)一下刷新,如果看到了房間的話停止抓包,分析一下包的內(nèi)容,可以看到有定向的UDP包(通常有2個,因?yàn)閿?shù)據(jù)比較大)來自對方的IP,內(nèi)容是JSON格式的房間信息。再看一下端口,可以看到對方用62900-62999之間的某一個端口給你發(fā)包的那個端口發(fā)送給了這些房間信息。

這個時候反過來讓對方來找你的房間,你會收到來自對方的IP發(fā)送的那些包,找到最早收到的那幾個包,記住端口,你的客戶端一般就是用第一個收到的端口回復(fù)了房間信息出去。

然后當(dāng)雙方在同一個房間的時候,就可以通過抓包看到看到雙方都在用62056端口瘋狂的你一句我一句,好是熱鬧。當(dāng)然我也不清楚為什么到這里又是用固定端口通信了,我沒試過占用這個端口會怎么樣,也沒試過作為主機(jī)的時候和多個客戶機(jī)的通信過程,如果你條件允許的話可以試試看,如果測出結(jié)果了可以補(bǔ)充一下。

基于以上內(nèi)容,我們可以猜想游戲是用一個循環(huán)向這100個端口發(fā)送了廣播數(shù)據(jù),因?yàn)橛昧送粋€端口,所以可以基本確定用了類似如下代碼發(fā)送的數(shù)據(jù):

SOCKET s = socket(AF_INET, SOCK_DGRAM, 0);
BOOL opt = TRUE;
setsockopt(s, SOL_SOCKET, SO_BROADCAST, (char *)&opt, sizeof(BOOL)); // 允許廣播
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(BOOL)); // 允許地址(端口)復(fù)用
sockaddr_in to;
to.sin_family = AF_INET;
to.sin_addr.S_addr = INADDR_BROADCAST;
for (int i=62900;i<63000;i++){
    to.sin_port = htons(i);
    sendto(s,databuffer,4,flags,(sockaddr *)to,sizeof(to));
}

這里補(bǔ)充一些知識,一個socket想要獲得相關(guān)功能必須用setsockopt,這里主要說一下SO_REUSEADDR(地址復(fù)用),其實(shí)在Linux還有一個SO_REUSEPORT(端口復(fù)用),而Windows只有SO_REUSEADDR,不過兼具SO_REUSEPORT功能。設(shè)置這個以后呢,就可以用另一個socket(也要設(shè)置SO_REUSEADDR)再次bind這個端口了,否則是不行的。

其次,獲取一個socket的端口和地址可以用getsockname這個函數(shù)獲取,這樣就可以用一個新的socket來綁定這個端口監(jiān)聽了;但是由于一個沒有綁定過的socket是沒有端口信息的,所以要先執(zhí)行sendto,然后系統(tǒng)就會給這個socket分配一個端口(其實(shí)是隱式綁定),這樣就可以實(shí)現(xiàn)每次刷新都用系統(tǒng)提供的不同端口來發(fā)送和接收了。而我們就可以利用這個來捕獲這個端口,甚至強(qiáng)行設(shè)定一個端口。

由于涉及到100個端口,所以接收方(創(chuàng)建了房間的客戶端)就用100個socket分別監(jiān)聽來自這100個端口的信息(好處是防止端口占用,畢竟100個端口同時被占的情況幾乎不存在),然后用select函數(shù)來篩選第一個收到消息的端口,然后用這個端口發(fā)送房間信息給對方。

那我們的目標(biāo)就很明確了,就是hook住這個sendto函數(shù),在循環(huán)發(fā)送100個包的時候,把這100個包發(fā)到每一張網(wǎng)卡去,當(dāng)然也可以指定一張或幾張網(wǎng)卡。

hook之后程序的流程就變成了如下這樣:

// ...
for (int i=62900;i<63000;i++){
    to.sin_port = htons(i);
    sendto(s,databuffer,4,flags,(sockaddr *)to,sizeof(to)); // 會調(diào)用下面的fake_sendto
}

Hook替換的偽代碼:

int fake_sendto(/*參數(shù)列表*/)
{
    取消hook;
    if (不是廣播)
        sendto();
    else {
        for (每一個地址) {
            創(chuàng)建socket;
            綁定地址;
            設(shè)置屬性;
            sendto();
            關(guān)閉socket;
        }
    }
    重新hook;
}

提前透露一下,這個方法是有問題的,原因如下:

  1. 在調(diào)用sendto后立即關(guān)閉socket,極有可能根本發(fā)送不出去,這個可以用socket隊(duì)列解決。
  2. 在調(diào)用sendto發(fā)送出去后,必須用同一個socket才能接收到回復(fù),即使復(fù)用了端口。
  3. 取消hook再重新hook并不適用于多線程的情況

所以我們要用固定的socket來實(shí)現(xiàn)這個效果,而不是會變的socket。

調(diào)試分析

前面的網(wǎng)絡(luò)抓包的結(jié)果只能猜測,實(shí)際的方法還得要調(diào)試才能知道。

打開游戲,待游戲加載完成后,打開x64dbg,選擇文件-附加,找到游戲進(jìn)程附加上去。之后就可以選擇下斷點(diǎn)的位置了。

首先可以確定的是,sendto函數(shù)大概率是會被調(diào)用的。而且在點(diǎn)擊局域網(wǎng)游戲的時候就會發(fā)送廣播,之后每次刷新都會發(fā)送。所以我們可以在x64dbg下方的命令框輸入bp sendto,就可以在sendto函數(shù)處下一個斷點(diǎn)。

這樣當(dāng)我們點(diǎn)擊局域網(wǎng)或者刷新的一瞬間,游戲應(yīng)該被阻塞,此時x64dbg已經(jīng)成功攔截到了sendto函數(shù)并將RIP指向第一句。這時我們已經(jīng)進(jìn)入到sendto函數(shù)內(nèi)部了,而我們對這個不感興趣,只需在調(diào)試菜單選擇運(yùn)行到用戶代碼即可回到游戲調(diào)用sendto之后的一句了。

文明6局域網(wǎng)刷不出房間,雜項(xiàng),游戲,網(wǎng)絡(luò),udp,c++,windows

注意這是x64的匯編,結(jié)合動態(tài)調(diào)試結(jié)果,稍微解釋一下:

這是一個for循環(huán),其中di寄存器為循環(huán)變量,si是循環(huán)的最大值,當(dāng)di小于si就接著循環(huán),di的初始值為62900,而si則為固定值63000。看140826160這一句mov ecx, di,把端口號作為參數(shù)傳給了htons函數(shù),將其轉(zhuǎn)換到大端。接著就是給后面調(diào)用sendto函數(shù)傳參的幾句代碼,就沒什么好說了。這說明之前猜測的發(fā)送代碼邏輯上基本沒什么問題。

此時注意觀察一下14082616D這一句mov rcx, qword ptr ds:[rbx+68],可以看一下rbx+68處的內(nèi)容,就是發(fā)送時所用的那個socket描述符了。調(diào)試的時候可以記一下這個數(shù)值,一會還會出現(xiàn)。

之后游戲一定會等待這個socket收到的信息,于是你可以試著bp recvfrom,可是卻發(fā)現(xiàn)根本沒有被斷下!

參考前面說的,實(shí)際上游戲用了select函數(shù)來判斷這個socket能否讀取數(shù)據(jù),而不是直接調(diào)用recvfrom,因?yàn)閞ecvfrom是一個阻塞函數(shù),這肯定是不能直接用的。

不妨用bp select斷下,再進(jìn)行一次調(diào)試,在調(diào)用完sendto函數(shù)之后,按下F9讓程序運(yùn)行,果然到了select內(nèi)部被斷了下來。此時可以看一下調(diào)用參數(shù)的內(nèi)容:

文明6局域網(wǎng)刷不出房間,雜項(xiàng),游戲,網(wǎng)絡(luò),udp,c++,windows

對照之前的函數(shù)原型,就可以知道第二個參數(shù)1476B0處的是可讀取socket集合,到內(nèi)存窗口看一下,內(nèi)容如下:

文明6局域網(wǎng)刷不出房間,雜項(xiàng),游戲,網(wǎng)絡(luò),udp,c++,windows

這個就是fd_set結(jié)構(gòu)體了,第一個01說明只有一個socket,第二個E8 0F就是之前那個socket(每次都不一樣)。

我們再看看調(diào)用完socket之后的邏輯:

文明6局域網(wǎng)刷不出房間,雜項(xiàng),游戲,網(wǎng)絡(luò),udp,c++,windows

可以看到在140827AB0處進(jìn)行了判斷,如果返回值為0,則140827AB2處的跳轉(zhuǎn)就會執(zhí)行。接著再調(diào)用__WSAFDIsSet來判斷目標(biāo)socket是否還在集合中,不在的話同樣跳轉(zhuǎn)。這樣之后,就開始執(zhí)行memset來清空一片內(nèi)存并用recvfrom來接收數(shù)據(jù)了。

根據(jù)上述調(diào)試,簡單概括一下的話,就是在你點(diǎn)擊進(jìn)入局域網(wǎng)或者在局域網(wǎng)內(nèi)進(jìn)行刷新的時候,游戲就會先新建一個socket然后循環(huán)調(diào)用sendto發(fā)送數(shù)據(jù),然后在一個線程里用select函數(shù)來等待一段時間,判斷是否收到數(shù)據(jù),確認(rèn)收到后才會調(diào)用recvfrom進(jìn)行真正的接收。

我們可以Hook上述三個函數(shù),依次用fake_sendto、fake_selectfake_recvfrom替換。

在第一次執(zhí)行fake_sendto時枚舉當(dāng)前所有網(wǎng)卡,然后建立幾個socket分別綁定這些網(wǎng)卡,之后所有的發(fā)送都通過這幾個socket進(jìn)行。

fake_select中執(zhí)行真正的select,但是檢查的集合卻是我們在fake_sendto中建立的那幾個socket,在檢測到有數(shù)據(jù)后,就記錄可以獲取數(shù)據(jù)的那個socket,并返回另一個集合的數(shù)量欺騙原來的程序,讓其以為收到了回復(fù)。

然后我們還在fake_recvfrom中調(diào)用真正的recvfrom來接收數(shù)據(jù),但第一個參數(shù)被替換成了在之前fake_select記錄下來的那個有數(shù)據(jù)的socket。

這樣一套組合拳,形象的解釋就是游戲用廣播喊話,但是麥克風(fēng)沒有連接到廣播,而是連到了我們這,我們根據(jù)游戲喊話的內(nèi)容再喊了一遍,當(dāng)然熱線電話就改成了我們自己的;游戲要等電話,實(shí)際是我們相當(dāng)于做了一個接線員,把聽眾打過來的電話轉(zhuǎn)接給了游戲;這樣下來,游戲以為自己的廣播喊出去了,也接到了聽眾的電話。

所以游戲還是按照它自己的邏輯進(jìn)行收發(fā)數(shù)據(jù)包,但是整個過程是我們幫它進(jìn)行了代理轉(zhuǎn)發(fā)。

代碼說明

這部分主要解釋一下實(shí)際使用的代碼邏輯和原理。

這部分踩了很多坑,重點(diǎn)說明一下大坑。

注入程序

先說注入程序,因?yàn)橹挥谐晒ψ⑷肓瞬拍軠y試dll有沒有問題。

直接上代碼,具體有注釋說明:

// 參數(shù)1:進(jìn)程的pid,參數(shù)2:dll絕對路徑,返回值:成功true,失敗false
bool inject_dll(DWORD pid, const char *dll_path)
{
    int path_len = strlen(dll_path) + 1; // 獲取長度包括'\0'
    HANDLE hproc = 0;   // 目標(biāo)進(jìn)程句柄
    LPVOID pmem = NULL; // 目標(biāo)進(jìn)程的虛擬內(nèi)存指針
    HANDLE hthread = 0; // 遠(yuǎn)程線程句柄
    bool result = false;
    hproc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); // 打開進(jìn)程
    if (hproc == 0) goto finally;
    pmem = VirtualAllocEx(hproc, NULL, path_len, MEM_COMMIT, PAGE_READWRITE); // 申請內(nèi)存
    if (pmem == NULL) goto finally;
    WriteProcessMemory(hproc, pmem, dll_path, path_len, NULL); // 把dll路徑寫進(jìn)去
    hthread = CreateRemoteThread(hproc, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, pmem, 0, NULL); // 創(chuàng)建遠(yuǎn)程線程注入
    if (hthread == 0) goto finally;
    WaitForSingleObject(hthread, INFINITE); // 等待線程執(zhí)行
    DWORD threadres;
    GetExitCodeThread(hthread, &threadres); // 獲取返回值
    result = threadres != 0; // LoadLibraryA錯誤返回0
finally: // 安全釋放相應(yīng)資源
    if (pmem)
        VirtualFreeEx(hproc, pmem, 0, MEM_RELEASE);
    if (hthread != 0)
        CloseHandle(hthread);
    if (hproc != 0)
        CloseHandle(hproc);
    return result;
}

根據(jù)進(jìn)程名獲取pid的方法(需要頭文件tlhelp32.h):

// 參數(shù):目標(biāo)進(jìn)程名,返回值:進(jìn)程pid,失敗為0
DWORD find_pid_by_name(const char *name)
{
    HANDLE procsnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 procentry;
    procentry.dwSize = sizeof(PROCESSENTRY32);
    Process32First(procsnapshot, &procentry);
    if (strcmp(procentry.szExeFile, name) == 0) {
        CloseHandle(procsnapshot);
        return procentry.th32ProcessID;
    }
    while (Process32Next(procsnapshot, &procentry)) {
        if (strcmp(procentry.szExeFile, name) == 0) {
            CloseHandle(procsnapshot);
            return procentry.th32ProcessID;
        }
    }
    CloseHandle(procsnapshot);
    return 0;
}

這段代碼比較清晰,建立快照,簡單循環(huán)查找,使用字符串比較,無需過多解釋。唯一要注意的是這里用的是char *,所以不要用UNICODE模式,如果用VS的話要加上#undef UNICODE再include頭文件。

上面兩段代碼是注入的關(guān)鍵內(nèi)容,剩下的就自行查看源代碼(都是一些基礎(chǔ)的邏輯),需要注意的是,如果被inject函數(shù)注入的進(jìn)程有管理員權(quán)限,那么調(diào)用inject的進(jìn)程也要有管理員權(quán)限,否則注入會失敗。

Hook DLL

這里就簡單貼一下關(guān)鍵代碼和相應(yīng)的注釋,詳細(xì)了解請看前面的說明。

重要提醒:這里的Hook代碼僅供參考,由于存在兼容性問題,不適合直接使用,新版已經(jīng)進(jìn)行了修改

Hook類
#include <windows.h>

#define HOOK_JUMP_LEN 5

class InlineHook
{
private:
    void *old_entry; // 存放原來的代碼和跳轉(zhuǎn)回去的代碼
    char hook_entry[HOOK_JUMP_LEN]; // hook跳轉(zhuǎn)的代碼
    void *func_ptr;  // 被hook函數(shù)的地址
public:
    InlineHook(HMODULE hmodule, const char *name, void *fake_func, int entry_len);
    ~InlineHook();
    void hook();
    void unhook();
    void *get_old_entry();
};

#ifdef _CPU_X64
static void *FindModuleTextBlankAlign(HMODULE hmodule)
{
    BYTE *p = (BYTE *)hmodule;
    p += ((IMAGE_DOS_HEADER *)p)->e_lfanew + 4; // 根據(jù)DOS頭獲取PE信息偏移量
    p += sizeof(IMAGE_FILE_HEADER) + ((IMAGE_FILE_HEADER *)p)->SizeOfOptionalHeader; // 跳過可選頭
    WORD sections = ((IMAGE_FILE_HEADER *)p)->NumberOfSections; // 獲取區(qū)段長度
    for (int i = 0; i < sections; i++) {
        IMAGE_SECTION_HEADER *psec = (IMAGE_SECTION_HEADER *)p;
        p += sizeof(IMAGE_SECTION_HEADER);
        if (memcmp(psec->Name, ".text", 5) == 0) { // 是否.text段
            BYTE *offset = (BYTE *)hmodule + psec->VirtualAddress + psec->Misc.VirtualSize; // 計(jì)算空白區(qū)域偏移量
            offset += 16 - (INT_PTR)offset % 16; // 對齊16字節(jié)
            UINT64 *buf = (UINT64 *)offset;
            while (buf[0] != 0 || buf[1] != 0) // 找到一塊全是0的區(qū)域
                buf += 16;
            return (void *)buf;
        }
    }
    return 0;
}
#endif

InlineHook::InlineHook(HMODULE hmodule, const char *name, void *fake_func, int entry_len)
{
    func_ptr = (void *)GetProcAddress(hmodule, name);
    // 范圍檢查
    if (entry_len < HOOK_JUMP_LEN)
        entry_len = HOOK_JUMP_LEN;
    if (entry_len > HOOK_PATCH_MAX)
        entry_len = HOOK_PATCH_MAX;
    // 允許func_ptr處最前面的5字節(jié)內(nèi)存可讀可寫可執(zhí)行
    VirtualProtect(func_ptr, HOOK_JUMP_LEN, PAGE_EXECUTE_READWRITE, NULL);
    // 使用VirtualAlloc申請內(nèi)存,使其可讀可寫可執(zhí)行
    old_entry = VirtualAlloc(NULL, 32, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
#ifdef _CPU_X64
    union
    {
        void *ptr;
        struct
        {
            DWORD32 lo;
            DWORD32 hi;
        };
    } ptr64;
    void *blank = FindModuleTextBlankAlign(hmodule); // 找到第一處空白區(qū)域
    VirtualProtect(blank, 14, PAGE_EXECUTE_READWRITE, NULL); // 可讀寫
    hook_entry[0] = 0xE9; // 跳轉(zhuǎn)代碼
    *(DWORD32 *)&hook_entry[1] = (BYTE *)blank - (BYTE *)func_ptr - 5; // 跳轉(zhuǎn)到空白區(qū)域
    ptr64.ptr = fake_func;
    BYTE blank_jump[14];
    blank_jump[0] = 0x68; // push xxx
    *(DWORD32 *)&blank_jump[1] = ptr64.lo; // xxx,即地址的低4位
    blank_jump[5] = 0xC7;
    blank_jump[6] = 0x44;
    blank_jump[7] = 0x24;
    blank_jump[8] = 0x04; // mov dword [rsp+4], yyy
    *(DWORD32 *)&blank_jump[9] = ptr64.hi; // yyy,即地址的高4位
    blank_jump[13] = 0xC3; // ret
    // 寫入真正的跳轉(zhuǎn)代碼到空白區(qū)域
    WriteProcessMemory(GetCurrentProcess(), blank, &blank_jump, 14, NULL);
    // 保存原來的入口代碼
    memcpy(old_entry, func_ptr, entry_len);
    ptr64.ptr = (BYTE *)func_ptr + entry_len;
    // 設(shè)置新的跳轉(zhuǎn)代碼
    BYTE *new_jump = (BYTE *)old_entry + entry_len;
    new_jump[0] = 0x68;
    *(DWORD32 *)(new_jump + 1) = ptr64.lo;
    new_jump[5] = 0xC7;
    new_jump[6] = 0x44;
    new_jump[7] = 0x24;
    new_jump[8] = 0x04;
    *(DWORD32 *)(new_jump + 9) = ptr64.hi;
    new_jump[13] = 0xC3;
#endif
#ifdef _CPU_X86
    hook_entry[0] = 0xE9; // 跳轉(zhuǎn)代碼
    *(DWORD32 *)&hook_entry[1] = (BYTE *)fake_func - (BYTE *)func_ptr - 5; // 直接到hook的代碼
    memcpy(old_entry, func_ptr, entry_len); // 保存入口
    BYTE *new_jump = (BYTE *)old_entry + entry_len;
    *new_jump = 0xE9; // 跳回去的代碼
    *(DWORD32 *)(new_jump + 1) = (BYTE *)func_ptr + entry_len - new_jump - 5;
#endif
}
fake_xxx

結(jié)合之前所述,我們需要以下全局變量來存放:

static SOCKET origin_sock = 0, fake_sock = 0;
static std::vector<SOCKET> socks;

其中origin_sock是sendto函數(shù)的那個socket,游戲是通過這個socket來實(shí)現(xiàn)數(shù)據(jù)的收發(fā)的。
fake_sock則是用來在fake_selectfake_recvfrom中給游戲接收數(shù)據(jù)用的。
vector容器socks則是存放每一個網(wǎng)卡對應(yīng)的socket,實(shí)現(xiàn)接收回復(fù)用的。


函數(shù)fake_sendto是被hook的sendto跳轉(zhuǎn)過來的執(zhí)行的函數(shù),這是完成廣播轉(zhuǎn)發(fā)的核心。

static int WINAPI fake_sendto(SOCKET s, const char *buf, int len, int flags, const sockaddr *to, int tolen)
{
    sockaddr_in *toaddr = (sockaddr_in *)to;
    if (toaddr->sin_addr.S_un.S_addr != INADDR_BROADCAST)
        return _sendto(s, buf, len, flags, to, tolen); // 非廣播直接原樣發(fā)送
    int result = -1;
    origin_sock = s; // 暫存這個socket
    const std::vector<in_addr> &list = enum_addr(); // 枚舉網(wǎng)卡地址
    if (socks.size() != list.size()) { // 數(shù)量不同則重新綁定socket
        sockaddr_in addr_self;
        addr_self.sin_family = AF_INET;
        int namelen = sizeof(sockaddr_in);
        getsockname(s, (sockaddr *)&addr_self, &namelen); // 獲取原sockaddr
        if (addr_self.sin_port == 0) {
            // 如果沒有端口號,先原樣發(fā)送,這樣系統(tǒng)才會分配一個端口號
            result = _sendto(s, buf, len, flags, to, tolen);
            getsockname(s, (sockaddr *)&addr_self, &namelen); // 重新獲取
        }
        for (int i = 0; i < socks.size(); i++)
            closesocket(socks[i]); // 關(guān)閉之前的socket
        socks.clear(); // 清空
        for (int i = 0; i < list.size(); i++) {
            addr_self.sin_addr = list[i]; // 把新的地址換上去,然后綁定
            SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
            BOOL opt = TRUE;
            setsockopt(sock, SOL_SOCKET, SO_BROADCAST, (char *)&opt, sizeof(BOOL)); // 廣播
            setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(BOOL)); // 重用地址端口
            bind(sock, (sockaddr *)&addr_self, sizeof(sockaddr)); // 綁定到地址端口
            socks.push_back(sock); // 存入socket
        }
    }
    // 向列表中的每一個地址轉(zhuǎn)發(fā)廣播
    for (int i = 0; i < socks.size(); i++)
        result = _sendto(socks[i], buf, len, flags, to, tolen);
    return result;
}

注意在枚舉網(wǎng)卡數(shù)量后進(jìn)行判斷,如果網(wǎng)卡數(shù)量和socket數(shù)量不一致,則關(guān)閉并清空所有socket,然后重新綁定。這個情況一般只會發(fā)生在第一次Hook的時候,偶爾也會在網(wǎng)卡數(shù)量更改的時候。

枚舉網(wǎng)卡的代碼如下:

static const std::vector<in_addr> &enum_addr()
{
    static std::vector<in_addr> list;
    hostent *phost = gethostbyname(""); // 獲取本機(jī)網(wǎng)卡
    if (phost) {
        char **ppc = phost->h_addr_list; // 獲取地址列表
        int addr_num = 0;
        while (*ppc)
            addr_num++, ppc++; // 獲取地址個數(shù)
        if (addr_num == list.size()) // 數(shù)量相同直接返回
            return list;
        ppc = phost->h_addr_list;
        list.clear();
        // 遍歷列表添加到容器
        while (*ppc) {
            in_addr addr;
            memcpy(&addr, *ppc, sizeof(in_addr));
            list.push_back(addr);
            ppc++;
        }
    }
    return list;
}

函數(shù)fake_select相對簡單,主要是注意不能誤傷其他正常調(diào)用的select就可以了。

static int WINAPI fake_select(int n, fd_set *rd, fd_set *wr, fd_set *ex, const TIMEVAL *timeout)
{
    // 這里通過判斷數(shù)量和內(nèi)容,確保是我們需要修改的情況
    if (rd && rd->fd_count == 1 && origin_sock == rd->fd_array[0]) {
        fd_set fds;
        FD_ZERO(&fds);
        for (int i = 0; i < socks.size(); i++)
            FD_SET(socks[i], &fds); // 把用于發(fā)送的socket放入集合
        int r = _select(0, &fds, NULL, NULL, timeout);
        if (r > 0) {
            fake_sock = fds.fd_array[0]; // 暫存第一個有數(shù)據(jù)的socket
            return fds.fd_count; // 只要大于0都可以
        }
        // 否則全部置為0即可
        fake_sock = 0;
        rd->fd_count = 0;
        return 0;
    }
    return _select(n, rd, wr, ex, timeout);
}

同理,對于fake_recvfrom,我們只要判斷是不是之前暫存的兩個socket就行了。

static int WINAPI fake_recvfrom(SOCKET s, char *buf, int len, int flags, sockaddr *from, int *fromlen)
{
    // 要接收的socket是我們暫存的且存在有數(shù)據(jù)的socket
    if (s == origin_sock && fake_sock != 0)
        return _recvfrom(fake_sock, buf, len, flags, from, fromlen);
    return _recvfrom(s, buf, len, flags, from, fromlen);
}

移除注入的DLL

和注入程序類似,使用CreateRemoteThread,不過運(yùn)行的函數(shù)是FreeLibrary,參數(shù)是DLL的地址。

不過需要先獲取指定進(jìn)程加載的DLL,然后找到Hook的DLL是否已經(jīng)被加載了。

和枚舉進(jìn)程查找PID類似,使用CreateToolhelp32Snapshot獲取指定進(jìn)程模塊快照,然后挨個字符串匹配找到指定模塊。

HMODULE find_module_handle_from_pid(DWORD pid, const char *module_name)
{
    HMODULE h_result = 0;
    HANDLE hsnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid);
    MODULEENTRY32 module_entry;
    module_entry.dwSize = sizeof(MODULEENTRY32);
    Module32First(hsnap, &module_entry);
    do {
        if (strcmp(module_entry.szModule, module_name) == 0) {
            h_result = module_entry.hModule;
            break;
        }
    } while (Module32Next(hsnap, &module_entry));
    CloseHandle(hsnap);
    return h_result;
}

然后使用以下代碼移除:

bool remove_module(DWORD pid, HMODULE module_handle)
{
    HANDLE hproc = 0;
    HANDLE hthread = 0;
    bool result = false;
    hproc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if (hproc == 0) goto finally;
    hthread = CreateRemoteThread(hproc, NULL, 0, (LPTHREAD_START_ROUTINE)FreeLibrary, module_handle, 0, NULL);
    if (hthread == 0) goto finally;
    WaitForSingleObject(hthread, INFINITE);
    DWORD threadres;
    GetExitCodeThread(hthread, &threadres);
    result = threadres != 0;
finally:
    if (hthread != 0)
        CloseHandle(hthread);
    if (hproc != 0)
        CloseHandle(hproc);
    return result;
}

參數(shù)module_handle就是上一個函數(shù)獲取到的模塊基址。

調(diào)試方法

這類注入到其他進(jìn)程的DLL調(diào)試起來難度較大,這里給出幾種方案供參考:

  1. 使用文件IO操作,使用固定路徑如D:\debug.txt,將所有需要寫入的調(diào)試信息寫入這個文件,供后續(xù)分析。
  2. 使用TCP或UDP協(xié)議,編寫一個程序用來接收調(diào)試信息,把需要的內(nèi)容直接發(fā)送出來。
  3. 使用x64dbg進(jìn)行跟蹤調(diào)試,因?yàn)镠ook了函數(shù),所以可以跟進(jìn)DLL實(shí)現(xiàn)動態(tài)調(diào)試。

注意調(diào)試的時候最好全程使用Wireshark抓包,從而方便確定問題。

這里推薦方法1,因?yàn)楸容^簡單且可以配合Wireshark抓包結(jié)果;當(dāng)然能力夠強(qiáng)的話用方法3,配合抓包這個是最直觀明了的。

成品測試

打開游戲,雙擊injciv6.exe注入,打開Wireshark抓包。在游戲里進(jìn)入局域網(wǎng),刷新,可以看到Wireshark一瞬間出現(xiàn)了好多包,看到每個可用的地址都通過同一個端口向62900-62999這100個端口發(fā)送了廣播。

文明6局域網(wǎng)刷不出房間,雜項(xiàng),游戲,網(wǎng)絡(luò),udp,c++,windows

然后可以找人聯(lián)機(jī)測試,基本上是不會失敗的,尤其是在抓到了含房間信息的包之后,如果游戲房間列表還是沒有顯示,那一般是游戲的問題,這個時候再刷新幾次,如果不行就返回到主菜單重新進(jìn)去。

關(guān)于游戲穩(wěn)定性,這個幾乎是不會有影響的,畢竟單機(jī)的時候都經(jīng)常崩潰,在不用這個方法之前聯(lián)機(jī)也總是崩潰,而且聯(lián)機(jī)的時候網(wǎng)絡(luò)的穩(wěn)定性更加重要。

其他游戲

目前我沒有試過其他游戲是個什么樣的情況,因?yàn)椴煌挠螒蚩赡苡胁灰粯拥臋C(jī)制,但是如果找房間的機(jī)制是用的UDP廣播,且使用了select進(jìn)行判斷,那么理論上此方法是可以使用的。

否則需要針對性進(jìn)行一些修改,具體就要通過抓包+反匯編調(diào)試了。但是萬變不離其宗,最終都是讓游戲能成功發(fā)出信息并收到回復(fù)。

目前研究發(fā)現(xiàn)饑荒聯(lián)機(jī)版的局域網(wǎng)聯(lián)機(jī)和文明6使用類似機(jī)制,但不完全一樣,饑荒使用的是RakNet網(wǎng)絡(luò)庫,但是好巧不巧啊,廣播都是只能發(fā)一個網(wǎng)卡的。

總結(jié)

之前我對這塊知識的了解還是不夠,把一些問題想簡單了,所以提出了不正確的方法,而且在bug的作用下居然將錯就錯跑出了結(jié)果。不過好在基本原理沒有出錯,所以在進(jìn)一步的研究之后,終于還是成功解決了問題。這也讓我對相應(yīng)的知識有了更進(jìn)一步的認(rèn)識。

此次重寫(2023-03)花費(fèi)不少時間,尤其是中途事情比較多,所以進(jìn)展緩慢。而且工程量比較大,不亞于寫一篇新的文章。但是本著認(rèn)真負(fù)責(zé)的原則,對出現(xiàn)問題的內(nèi)容必須加以改正,才能不誤導(dǎo)他人。

本文未來應(yīng)該不會再有比較大的變動了,不過代碼倉庫還是會有更新的,有興趣了解更多東西可以到開頭的倉庫看一看。文章來源地址http://www.zghlxwxcb.cn/news/detail-567979.html

更新記錄

  • 2023-01-19:修正一些錯別字,優(yōu)化部分表述問題,新增更多的解釋
  • 2023-01-31:優(yōu)化部分代碼,添加新的源代碼鏈接,新增更新預(yù)告
  • 2023-03-05:完成了對大部分內(nèi)容的重寫
  • 2023-09-01:修正錯別字,優(yōu)化部分內(nèi)容,代碼優(yōu)化
  • 2023-09-24:新增移除注入的DLL說明
  • 2024-01-31:修正部分內(nèi)容,添加相關(guān)項(xiàng)目的簡要介紹

到了這里,關(guān)于使用Hook攔截socket函數(shù)解決虛擬局域網(wǎng)部分游戲聯(lián)機(jī)找不到房間的問題——以文明6為例的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請點(diǎn)擊違法舉報進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 【Unity】(Socket)TCP 實(shí)現(xiàn)同一局域網(wǎng) 一臺主機(jī)控制多臺主機(jī)

    【Unity】(Socket)TCP 實(shí)現(xiàn)同一局域網(wǎng) 一臺主機(jī)控制多臺主機(jī)

    前幾天博主接到一個任務(wù):5臺HTCVIVEPro設(shè)備,其中一臺設(shè)備進(jìn)行演示,另外四臺設(shè)備畫面同步。 在設(shè)備沒到之前,博主進(jìn)行了一下前期準(zhǔn)本工作: 同一局域網(wǎng) 一臺主機(jī)控制多臺主機(jī) PS:博主參考了其它博主大大的文章,感覺很有用?。。。。?! 如果需要其它的一些TCP操作流程

    2024年02月10日
    瀏覽(38)
  • VLAN---虛擬局域網(wǎng)

    VLAN---虛擬局域網(wǎng)

    VLAN— 虛擬局域網(wǎng) LAN—局域網(wǎng) MAN—城域網(wǎng) WAN—廣域網(wǎng) 1.一個VLAN相當(dāng)于是一個廣播域 VLAN—通過路由器和交換機(jī)協(xié)同工作后,將原本的一個廣播域邏輯上,拆 分為多個虛擬的廣播域。 VLAN配置: 1.創(chuàng)建VLAN VID—VLAN ID------用來區(qū)分和標(biāo)識不同的VLAN(廣播域) 二進(jìn)制構(gòu)成,12位二

    2024年02月15日
    瀏覽(22)
  • 虛擬局域網(wǎng)

    ?? 作者:小劉在這里 ?? 每天分享云計(jì)算網(wǎng)絡(luò)運(yùn)維課堂筆記,疫情之下,你我素未謀面,但你一定要平平安安,一? 起努力,共赴美好人生! ?? 夕陽下,是最美的,綻放,愿所有的美好,再疫情結(jié)束后如約而至。 目錄 一.IPv6 地址的表示 ? ?二. ??? 本地單播地址

    2024年02月20日
    瀏覽(24)
  • TincVPN:組建虛擬局域網(wǎng)

    TincVPN是一個P2PVPN,即兩端可以直接通信,虛擬局域網(wǎng)內(nèi)只需要一臺機(jī)器(VPS)能夠有外網(wǎng)訪問的能力即可,一旦兩端建立起連接,流量就不會再經(jīng)過VPS。 其實(shí)TincVPN可以代替ZeroTier,雖然ZeroTier免費(fèi)版也足夠用,但用著ZeroTier的公共Moon/WEB面板,總感覺差了點(diǎn)意思。而TincVPN就是

    2024年02月11日
    瀏覽(28)
  • zerotier虛擬網(wǎng)絡(luò)配置,局域網(wǎng)與外網(wǎng)如同局域網(wǎng)一樣訪問。

    zerotier虛擬網(wǎng)絡(luò)配置,局域網(wǎng)與外網(wǎng)如同局域網(wǎng)一樣訪問。

    WireGuard相比于傳統(tǒng)VPN的核心優(yōu)勢是沒有VPN網(wǎng)關(guān),所有節(jié)點(diǎn)之間都可以點(diǎn)對點(diǎn)(P2P)連接,效率高,速度快,成本低。但WireGuard目前最大的痛點(diǎn)就是上層應(yīng)用的功能不夠健全,WireGuard本身只是一個內(nèi)核級別的模塊,它只是一個數(shù)據(jù)平面,Tailscale就是基于Wireguard的一個聯(lián)網(wǎng)工具,

    2024年02月05日
    瀏覽(23)
  • 【網(wǎng)絡(luò)】· VTP虛擬局域網(wǎng)中繼

    【網(wǎng)絡(luò)】· VTP虛擬局域網(wǎng)中繼

    目錄 ??VTP技術(shù) ??以太網(wǎng)通道配置 ??sw1配置 ??sw2配置 ??VTP工作原理 ??VTP模式 ??VTP通告 ??VTP的版本 ??VTP修改編號 ??VTP通告類型 ??VTP修剪 ??VTP前置 ??VTP配置命令 ??VTP實(shí)例 ??TCP/IP總結(jié) ?? ? ??博客主頁:大蝦好吃嗎的博客 ? ? ???專欄地址:網(wǎng)絡(luò)專欄 ?????

    2024年02月08日
    瀏覽(17)
  • 虛擬局域網(wǎng)VLAN的實(shí)現(xiàn)機(jī)制

    虛擬局域網(wǎng)VLAN的實(shí)現(xiàn)機(jī)制

    IEEE802.1Q幀(也稱Dot One Q幀)對以太網(wǎng)的MAC幀格式進(jìn)行了擴(kuò)展,插入了4字節(jié)的 VLAN 標(biāo)記。 Access Trunk Hybrid Access端口一般用于連接用戶計(jì)算機(jī) Access端口只能屬于一個VLAN Access端口的PVID值與端口所屬VLAN的ID相同(默認(rèn)為1) Access端口接收處理方法: 一般只接受“未打標(biāo)簽”的普通

    2024年02月10日
    瀏覽(22)
  • 數(shù)據(jù)鏈路層:虛擬局域網(wǎng)(VLAN)

    數(shù)據(jù)鏈路層:虛擬局域網(wǎng)(VLAN)

    筆記來源: 湖科大教書匠:虛擬局域網(wǎng)(VLAN)概述 湖科大教書匠:虛擬局域網(wǎng)(VLAN)實(shí)現(xiàn)機(jī)制 聲明:該學(xué)習(xí)筆記來自湖科大教書匠,筆記僅做學(xué)習(xí)參考 使用一個或多個以太網(wǎng)交換機(jī)互連起來的交換式以太網(wǎng),其所有站點(diǎn)都屬于同一個廣播域。 巨大廣播域帶來許多問題:廣

    2024年02月07日
    瀏覽(27)
  • 計(jì)算機(jī)網(wǎng)絡(luò) Cisco虛擬局域網(wǎng)劃分

    計(jì)算機(jī)網(wǎng)絡(luò) Cisco虛擬局域網(wǎng)劃分

    1、分別把交換機(jī)命名為SWA、SWB 2、劃分虛擬局域網(wǎng) valn ,并將端口靜態(tài)劃分到 vlan 中 劃分vlan 方法一:在全局模式下劃分vlan,在SWA交換機(jī)上創(chuàng)建三個vlan,分別為vlan2,vlan3,vlan4。 方法二:進(jìn)入vlan database劃分vlan,在SWB交換機(jī)上創(chuàng)建三個vlan,分別為vlan2,vlan3,vlan4。 把端口靜

    2024年04月15日
    瀏覽(27)
  • 局域網(wǎng)內(nèi)其他主機(jī)對VMWare虛擬機(jī)訪問

    局域網(wǎng)內(nèi)其他主機(jī)對VMWare虛擬機(jī)訪問

    右鍵點(diǎn)擊虛擬機(jī)-點(diǎn)擊設(shè)置進(jìn)入設(shè)置頁面 此時用另一臺電腦Ping此IP地址,就可以Ping通了。 將網(wǎng)絡(luò)適配器由橋接改為NAT模式。然后按照以下圖片步驟實(shí)現(xiàn)橋接。 此處的虛擬機(jī)IP地址是獲取的當(dāng)前的虛擬機(jī)IP地址。 按照教程下載Xshell并安裝,打開Xshell。 新建一個會話如上圖所以

    2024年02月05日
    瀏覽(25)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包