网络游戏逆向分析-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里面:
芜湖搞定啦,我们做到了!!!
需要注意的是,由于久了没动游戏会退出,而重新加载会导致地址改变,但是其中的逻辑是没有变的
总结: