封包 “初体验” 嘿!大家好啊~ 又是一个被床封印的早晨,被闹钟吵醒的时候才8:35,闭上眼睛睁开眼睛 我就穿越到了 11:07 有没有谁拥有和我一样的超能力? 前些日子的几篇笔记,分享了如何快乐Ctrl+F9定位游戏功能Call,和处理游戏在这方面设置的障碍,有没有学废了呢? 假设...啊 我们假设 张三学废了,然后.... 然后.... 张三就开始快乐的测试功能Call了,嗯 正当张三同学最快乐的时候,客户端弹出了一个小窗口...第三方检测了..张三同学被封号了。 (PS:初次了解封包,最好先看完之前的三篇笔记) 上面的故事真是闻者伤心听者落泪啊,不管是重写发包函数还是线程发包,虽然技法巧妙 但是只要清楚原理 细细分析,总会有漏网之鱼。 所以龙门前不会只有这些困难,后面也是布防甚多.. 比如,功能Call的检测有什么变量检测 堆栈检测,恐怕是层层套娃 MV众多,解决起来更是烦躁无比啊!(欲扬先抑,先夸张一下) 那我们学废之前的技巧有啥子用罗?和上面那些花里胡哨的东西 中路对线? 当然不是,所谓黄河之水宜疏不宜堵,之前的三篇笔记都是铺垫 我们一开始的根本目的完全不是为了快乐的Ctrl+F9。 在揭晓谜底之前我们来观察一个珍贵无比的示例: 捕捉用户键盘消息——》技能相关的几个函数——》告诉服务器我要释放某某技能 一款网络游戏角色释放一个技能,大概就是上面这种流程。那比如我们找到了游戏的技能Call 调用它释放技能,会是什么样子? 会变成这样: 技能相关的几个函数——》告诉服务器我要释放某某技能 同学们,有没有看出来什么?哦,张三同学看出来了,同时它还有一个大胆的想法。 其实归根结底就是告诉服务器我干了啥,那我们能不能中间砍一刀 直接告诉服务我要释放啥啥技能,然后达到释放技能的目的? 当然可以,游戏实际上就是这样释放技能嘛,我们自然也可以这样做,以至于80%的功能 我们基本都能直接自己发送封包实现它。 哇哦!那这样做有什么具体的好处? 通过自己发包,试探变态功能这里就不讲了,我想 通常大家有一个很关心的点(小声:嘿~~兄弟gua稳吗?) 如果把游戏检测比作地雷,那么游戏代码游戏领空就是雷区,想调用游戏功能call免不了遇到有埋雷的情况,而就算是排雷老手也免不了会踩上几颗。所谓究极的排雷方法,就是不走雷区到目的地,你那有雷我绕着走不就是了么。 这里也是同样的道理,我们走自己的代码通过send()系统发包函数发送封包实现功能,这样就避开了游戏领空的地雷,所以只要是W美的封包 90%稳稳的很安心。 既然说到了“W美”二字,当然就有不那么W美的。毕竟自己发包实习功能,可不是说说就完成了。 让我们来观察一个珍贵无比的示例: 组包--》加密--》发包 游戏要发送一个封包,通常都要走这么一个流程。组包:把你做的坏事打包成小报告;加密:这是你和服务器的小秘密不能让别人知道;发包:把小报告邮寄给服务器。 明显的我们要想W美的达到目的主要有三大难点: 1. 分析封包 2. 寻找明文封包 3. 寻找加密函数 分析封包:客户端告诉服务端我释放了某某技能,总不可能写得汉字吧,所以我们需要分析封包,知道释放技能应该以什么形式描述。 找明文包:系统发包函数上下断确实可以获得封包内容,但是多半是加密的,而且加密的密钥可能随时会发生变化,加密后的内容基本就是一团乱码无从下手没法分析,所以我们得找到内容还未被加密的地方。 加密函数:我们找到明文包了,也分析了放什么技能该发什么包。但是服务器可是要被加密后的封包啊,直接发送一个明文过去 服务器不认事小,第三方就不好了。我们要学客户端,把明文包加密一下,当然 得按游戏的加密方式,那么我们就得找到游戏的加密函数。 找明文包:
(一)理论 明文包的寻找是我们的首要任务,以此为跳板才能分析封包 和 定位 加密函数,如何寻找?不如再来欣赏一下这个珍贵无比的示例: 组包--》加密--》发包 我们可以观察到,从组包完成到加密的这段过程,封包内容是明文的未加密的。通过面对对象的编程原则,我们可以猜测这几个步骤在代码中的表现形式,就是几个对应的函数。 或者说组包完成后,封包数据会通过函数一层一层传递,函数怎么传递数据到另外一个函数?没错,当然是通过参数。那么我们是不是能够在发包处下断,使用传统手艺Ctrl+F9逐层返回所有经过的Call(函数),通过分析他们的参数内容,来找到明文的封包内容? 正确!不过在准备撸起袖子加油干之前,我们还有一个问题需要解决,我们怎么分辨出明文封包内容?要知道这些东西可不会写着I is 明文包,它们在内存之中的形式就是一串数字,而堆栈中所有函数的任何参数也都是一堆数字,怎么办? 想象一下你在茫茫人海中,光凭一个模糊的照片去寻找一个人,这该有多么困难~但是如果那个人的长相特立独行...比如头上长个角?再顶两只猫耳朵..是不是就简单多了?我相信眼尖的你肯定一眼就认出他了。 道理是一样的,那封包里面会不会有这种引起围观的奇异种?我想大多数游戏都是有的,也就是喊话功能 或者 说聊天功能,你想想看在游戏中你和队友聊天,客户端是不是一定需要发送一个装有聊天内容的封包给服务器,这样服务器才能更新到其他玩家的客户端上。我想大家应该都知道吧,实际上电脑只能储存数字...而字符是通过一张表把一些数字定义为字符,如图: 通过表图我们可以看到,字符 1 的对应数字是49,以十六进制表示 0x31。那么我们喊话一串“11111111111111”断下,它在封包内容里面就是一串 31 31 31 31 31 31.... Nice~ 纸上谈兵不如真刀真枪的干一架,请出我们的老伙计 版的幻想神域2 带来一段珍贵无比的简单实例。 (二)实战 先在上面笔记说到的跳出线程发包代码段下断,可以当发包下断可以断到功能函数,当然我们并是要Ctrl+F9去找Call,而是返回上一层寻找未加密的封包内容。 喊话“1111111111”断下. Ctrl+F9返回上一层调用。 这个call看起来好像只有一个参数,具体还是需要观察堆栈变化才能确定。Call指令下断,断下后F8单步步过 观察堆栈esp增加了多少。 增加了0x4 看来确实只有一个参数,Call指令下断 喊话“11111”断下,分析一下这个参数。 好大的值,像这种值一般是一个地址,这说明这个参数实际上是一个结构体之类的东西,我们数据窗口中查看分析一下。 这个结构体里面的成员也像是地址,我们挨个进去看一下,第一个: 这...显然是虚表函数之类的,反正没有31 31 31 不管他...再看看第二个: 哇,哦~ 一串的31 31 31 31看来我们运气不错只返回了一层我们就找到了明文的封包,如果要确认的话也简单。我们再次再次喊话断下,把这个明文内容修改一下会怎样呢? 放开断点。 file:///C:\Users\ADMINI~1\AppData\Local\Temp\ksohtml4300\wps23.jpg 我们输入的是 “1111111111111111”,居然喊出了 “1111155555111111”这就是逆向的魅力,这也就说明我们确实找到了明文封包的位置了。 我们不如发散一下思维,如果我们调用这个Call是不是就可以直接发送明文内容实现功能了,而不必再费力寻找和分析加密函数了? 的确,从实现功能的角度讲,只要分析透了这个函数是能够实现所有的功能,但是这样还是过了游戏代码 并非“W美”的,我们从学习角度出发接着分析。 找加密Call: (一)理论 加密call在哪?想想我们跳出线程发包代码段那,封包内容应该已经被加密了,因为下面的代码再走就是把封包内容复制到全局变量里面,然后被发包线程发送出去了。游戏不可能让发包线程里面有明文内容,不然我们是不是就不用幸苦跳出发包线程了,直接分析明文的封包内容 岂不美哉? 然后我们返回一层,在传入参数里面发现了明文包,哪说明加密函数就应该在 上层之下,跳出线程发包代码段之上。哪我们的范围就小太多了,我们可以在函数头部跟踪明文内容,加密call必然会需要这个参数! (二)实战 我们来到函数头部追踪一下明文内容。 在头部发现一个堆栈的值,直接赋值给寄存器。一般来说这种[esp+0xXX]要么是局部变量,要么是函数调用者传入的参数。而这里是函数头部,上面没有对[esp+0x14]初始化的代码,这说明它不是一个局部变量而是一个被传入的参数。 原因很简单,因为局部变量的空间都是在堆栈划分的,而堆栈被反复使用里面有非常多的垃圾数据,所有如果局部变量不初始化谁也不知道它的值是啥。 当然如果看见[esp+0xXX]而自己没有啥把握的话,最好去堆栈中观察确认一下,这是最稳妥的。 那么现在edi是参数首地址了,我们再往下看。[edi+0x4]赋值给了ebp,还记得我们上面寻找明文封包的分析么,这个参数是一个结构体,他的第二个成员就是封包内容首地址。 [edi]是第一个成员,[edi+0x4]是第二个成员,那么现在ebp是明文封包内容首地址了。 再往下看,ebp和[edi+0x8]做比较,[edi+0x8]是第三个成员,这时候我想到了数组,存放数组首地址的地址+0x4通常是数组的尾地址,edi+0x8和edi+0x4 应该是这种情况。那么这个操作就很好理解了,应该是判断数组的长度..或者说里面有没有东西。 接着分析,顶住ebp这个封包内容,看有没有那个Call把它作为参数。 最后发现在范围内只有这个call把封包内容当作参数传入了,基本上可以断定它就是加密Call了。 很明显它有4个push的参数,前两个都是来源[edi+0x4] 至于为什么传2个一模一样的我们不管他,而第3个push 往前面翻一翻可以发现来自于 [edi+0x8]-[edi+0x4] 前面说过了这是尾地址减去首地址,应该是封包长度。 我相信你也看见了,它先把封包首地址+0x2 封包长度-0x2, 也就是说封包内容前2个字节是不加密的,来看看之前的封包截图。 我想前2个字节应该是代表这个封包前2字节之后的有效长度,之所以不加密应该是游戏服务器可以更好的读取和区分。 第4个push下断一看它是不断变化的,也不难猜 估计是密钥 加密就像是用密钥上锁,服务器拿到之前再用密钥打开。密钥应该是服务器隔一段时间更新下发一次,我们要想获得它得往上回溯得到它的偏移表达式和基地址,从里面读取就没问题了,基本功内容就不写出来了 这里没几个偏移就找到基地址了。 摸清楚了函数的参数,我们已经可以直接调用它来加密我们自己组装的明文包了,这样就可以用send()发给服务器了。同样的,这么做也并不“W美”,我们先进加密call里面去看看代码。 (代码太多就不放老长的图了,假装一下它是那种长图~) 我们发现这是一Call到底,里面没有调用任何其他的call,下断F8执行一遍之后也没有发现什么花里胡哨的跳转。那么我们另一条路就好走多了------偷功能! 顾名思义,我们要偷游戏加密call的功能。 怎么偷?其实也很简单,C++ 是支持内联汇编的,正好OD工具直接帮我们把汇编显示了出来,直接复制到我们的代码里面,只不过需要稍稍修改一下——JCC指令。 OD反汇编显示出来的JCC 跳转指令后面都是直接跟得立即数,内联汇编中并不支持这种写法,所以我们全部改成标签跳转就可以了,没什么技巧和技术 就是需要一点汇编基础和细心耐心。 如图:建议写一个裸函数,__asm 内联汇编把汇编复制进去就得勒。 (假装...一下..放个N长得图或者代码也没意思..) 这样就可以调用我们偷出来的函数加密了,由于是直接复制出来的,所以效果和原版一模一样。不过在我们用参数最少最简单的发包函数send()夹带私货之前,我们还需要打通最后一公里,先看看send()函数的定义。 第2 第 3 个参数也就是封包内容和长度我们有,第4个参数 常量0 我们不用管他,但是第一个参数需要解决一下,这代表东西要送到那里去。 好在三大发包函数第一个参数都是SOCKET套接字描述符,游戏里面使用的WSASend 函数当然是的,所以咱们可以直接Ctrl+G到WSASend里面下断点Ctrl+F9到调用处回溯第一个参数。 ECX看来就是我们要找的SOCKET了,都帮我们标注出来了,暖暖的很贴心~! 随便往上翻一个偏移,来到了这,如图: 游戏为了获取SOCKET调用了这个函数,不过OD已经帮我们标注出来了,那我们肯定没必要进Call追数据了,OD认识的肯定是系统的API函数,我们直接上网查查这个函数是咋回事,然后学着游戏的样子调用它就ok了。 第二个参数游戏直接传入了常量0x15 所以我们不用关心它,第一个参数是一个窗口句柄...我们怎么才能方便的获取它呢? 既然它是一个窗口句柄那么可以使用 FindWindow() 系统API函数获得,但是这个函数需要一个窗口类名,或者窗口标题....嗯,这就不得不借用Visual Studio自带的工具Spy++了。 Spy++根据窗口句柄的数值查询到窗口的类名或者标题名,而窗口句柄的数值我们可以在游戏调用GetWindowLongW的地方下断复制出来(可惜下次打开游戏会变化,不然也不用这么麻烦了),然后扔到Spy++查询一下就OK了,如图: 这下就得到类名拉,只需要通过FindWindow()传入这个类名就可以获得我们想要的那个窗口句柄了~! 接下来只需要把我们上面讲的内容有机的结合一下,就可以封装成我们自己的明文发包函数拉~! (三)代码示例 bool F发送封包(byte *p, DWORD ndIndex,char *szStr) { byte *data = p; DWORD nd包长 = ndIndex; DWORD nd加密地址 = (DWORD)data + 0x2; DWORD nd加密长度 = nd包长 - 0x2; DWORD nd密钥地址 = NULL; __try { // [[[[[0x0F84BA4]]+0x4]+0x0C+0x8]]+0x54 密钥公 0019FB90 00000B4C |Socket = 0xB4C nd密钥地址 = *(DWORD*)0x0F84BA4; nd密钥地址 = *(DWORD*)nd密钥地址; nd密钥地址 = *(DWORD*)(nd密钥地址 + 0x4); nd密钥地址 = *(DWORD*)(nd密钥地址 + 0x0C + 0x8); nd密钥地址 = *(DWORD*)nd密钥地址 + 0x54; F加密call(nd密钥地址, nd加密长度, nd加密地址, nd加密地址); HWND hWnd = (HWND)FindWindowA("Lapis Network Class", NULL); DWORD ndData = GetWindowLongW(hWnd, -21); DWORD Socket = *(DWORD*)(ndData + 0x38); send(Socket, (const char*)data, nd包长, 0); } __except (1) { F输出调试信息("幻想神域 %s\n\r", szStr); return false; } return true; } HOOK拦截输出明文包内容: (一)理论 完事具备,只欠东风。我们现在已经封装好了自己明文发包函数,就差内容了。那我们怎么才能知道,什么功能要传什么样的封包呢?比如吃药...难道直接传个“吃药”? 呃...其实说来也简单,我们不是找到明文封包了么,吃药断下我们把内容复制下来,多复制一些案例,细细分析总能猜出来。但是不同的动作会产生不同的封包,比如我们要实习一个自动打怪挂机、跑主线等,那工作量可就大了,如果都OD下断点复制,未免太麻烦了,所以我们需要一个更好的方式——HOOK。 HOOK简单来说其实就是想方设法改变程序原来的执行流程,我们这里的目标很简单,让cpu在运行加密函数之前,先运行我们的函数 我们函数里面输出明文包的内容 然后跳转回原来的地方让程序继续正常执行。 具体操作也很简单, 其实所谓的代码和指令也数字,图上可知最左边是代码在内存中的地址,中间是代码在内存中真正的模样(数字),右边是OD帮我们把数字翻译成人能看懂的汇编代码。 那么既然我们知道了代码的本质(数值)和它的地址,所以我们可以通过地址修改数值来修改代码,从而改变代码流程。 比如把红框中的2句汇编代码修改成:0xE8XXXXXXXX —— E8XXXXXXXX 对应Call指令上图也可以看到,后面的数值是算出来的 等于 要跳转地址 - (当前地址+0x5)。要跳转的地址肯定是我们的函数啊,当前地址上图就有 舒服~ 当然我们的函数必须是一个裸函数,而且开头要使用 pushad 指令来保存现场环境,干完坏事后 使用 popad指令还原现场,最后一定要记得一字不差的写上被我们破坏的那2条指令,在使用 ret 指令跳转回去 不然程序肯定崩溃。(篇幅有限这里只是讲个大概,有兴趣具体学习百度一搜就有) 这样在我们的裸函数里面,pushad popad中间就可以写代码输出明文的封包内容。 (二)参考代码 //HOOK void HXSYDialog::OnBnClickedButton3() { // TODO: 在此添加控件通知处理程序代码 /*00B92C82 8B46 08 mov eax, dword ptr ds : [esi + 0x8] 00B92C85 2BC1 sub eax, ecx 00B92C87 83C0 FE add eax, -0x2*/ DWORD ndHOOKAddress = 0x00B92C82; DWORD ndHOOK函数指针 = (DWORD)FHOOK明文发包; DWORD ndHOOK跳转值 = ndHOOK函数指针 - ndHOOKAddress - 5; DWORD old = 0; //改变内存页属性 VirtualProtect((PVOID)ndHOOKAddress, 0x30, PAGE_EXECUTE_READWRITE, &old); //修改代码 *(byte*)ndHOOKAddress = 0xE8; //Call *(DWORD*)(ndHOOKAddress + 1) = ndHOOK跳转值; //还原内存页属性 VirtualProtect((PVOID)ndHOOKAddress, 0x30, old, &old); } //还原HOOK,一样的道理修改回去 void HXSYDialog::OnBnClickedButton4() { // TODO: 在此添加控件通知处理程序代码 /*00B92C82 8B46 08 mov eax, dword ptr ds : [esi + 0x8] 00B92C85 2BC1 sub eax, ecx 00B92C87 83C0 FE add eax, -0x2*/ DWORD ndHOOKAddress = 0x00B92C82; DWORD old = 0; VirtualProtect((PVOID)ndHOOKAddress, 0x30, PAGE_EXECUTE_READWRITE, &old); *(byte*)ndHOOKAddress = 0x8B; *(DWORD*)(ndHOOKAddress + 1) = 0xC12B0846; VirtualProtect((PVOID)ndHOOKAddress, 0x30, old, &old); } //我们的函数 DWORD g_nd包长 = 0; DWORD g_nd包Address = 0; DWORD g_ndPid = NULL; HANDLE g_hProcess = 0; byte *g_byP = nullptr; char g_szObj[0x1000]; char g_szStr[0x1000]; void __declspec(naked) FHOOK明文发包() { __asm { //保存寄存器 提升堆栈 pushad //得到包地址 包长 mov eax, dword ptr[esi + 4] mov g_nd包Address, eax mov ecx, dword ptr[esi + 8] sub ecx, eax mov g_nd包长, ecx } //提升权限 F提升权限(TRUE); //获得进程ID GetWindowThreadProcessId(F获取游戏主窗口句柄(), &g_ndPid);//获得进程ID //打开进程 g_hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, g_ndPid);//打开进程 g_byP = new byte[g_nd包长]; //读出包内容 ReadProcessMemory(g_hProcess, (LPCVOID)g_nd包Address, g_byP, g_nd包长, 0);// p 封包字节集 *(WORD*)g_byP = g_nd包长 - 2; for (int i = 0; i < (int)g_nd包长; i++) { sprintf_s(g_szObj, "%02X", g_byP); strcat_s(g_szStr, g_szObj); } //自己封装 支持多线程的printf() F输出调试信息("幻想神域:明文发包 包长:%x 包内容:%s\r\n", g_nd包长, g_szStr); //清空缓冲区 sprintf_s(g_szStr, "%s", ""); delete[] g_byP; __asm { popad //还原两句 被HOOK的游戏代码 mov eax, dword ptr ds : [esi + 0x8] sub eax, ecx retn } } //自己封装 支持多线程的printf() void F输出调试信息(char * pszFormat, ...) { #ifdef _DEBUG char szbufFormat[0x1000]; char szbufFormat_Game[0x1100] = ""; va_list argList; va_start(argList, pszFormat); vsprintf_s(szbufFormat, pszFormat, argList); strcat_s(szbufFormat_Game, szbufFormat); OutputDebugStringA(szbufFormat_Game); va_end(argList); #endif } (三) 实战 我们上面的HOOK代码会把封包内容输出出来,但是由于是DLL注入形式的代码,所以我们并没有控制台窗口,故不能直接看到输出。这时候我们就需要借助一个输出捕捉工具了——Dbgview: 只需要输入关键字,就可以捕捉所有进程带关键子的输出,非常好用。 我们试着嗑下如图位置的药品,看看会游戏会发送什么样的封包。 包长:16 包内容:14004F00000000000100000000000000000000000000 包长:16 包内容:14004F00000000000A00000000000000000000000000 很明显,”1400”是封包2字节后的有效长度 我们之前已经分析过了。”4F”应该是代表吃药这个动作,那么”1” 和 “A”根据药品的位置来看 应该就是药品在背包表格中的下标,其他都为0我们可以暂时无视它们。 基本摸清了吃药封包的结构,我们可以尝试封装一个通过药品下标吃药的函数了: void F吃药Call(int i) { byte Data[0x16] = { 0x14,00,0x4F,00,00,00,00,0x00,00,00,00,00,00,00,00,00,00,00,00,00,00,00}; *(WORD*)(Data + 0x8) = i; F发送封包(Data, 0x16, ""); } 调用一下试试: file:///C:\Users\ADMINI~1\AppData\Local\Temp\ksohtml4300\wps43.jpg Nice~! 封包吃药成功~!(使用药水这几个字可不是我P上去的啊,游戏嗑药就这特性) 麻雀虽小,五脏俱全 ; 经历千辛万苦我们终于达成了目标!虽然只有一个简单的嗑药功能,但是这却是一个货真价实的封包 !!! 总结: 封包 也并不多神秘和神奇,只是省去了中间的过程,直接联系服务器。 优点: 不走游戏代码,相当于避开了所有的本地检测,极其稳定。 缺点: 需要做很多前期工作,实现麻烦、分析封包费时费力。 屈尊调用游戏功能函数“内存 ”研究速度快,认真逆向分析封包内容“封包 ”快乐而稳定!
逆向交流qun:453769015
1063838324
联系我时,请说是在 挂海论坛 上看到的,谢谢! |