一转眼,又发现很久没有写点什么东西了。几年来,诸事皆惰,不知所欲,着实令人颓丧。
好吧,总要写点东西向别人证明一下我还喘气。
这个成果没有什么商业价值,嘿嘿,所以可以公布一下,顺便打一打我们“鑫勇士工作室”的名号。
讲的过程中省略了很多逆向基础知识,我假设看官都知道了。否则你需要好好补习补习去了。
言归正传。
在QQ华夏这个游戏中(其实华夏II也一样),当前选中的对象,只能看见血条蓝条,却不知道具体量多少,100血与10000血都是一样长的条条。在打高级怪的时候特别郁闷,没打不知道怪有多皮实,打不动的时候才知道,原来怪血这么厚!
于是,我们有必要知道当前对象有多少血嘛。但是游戏公司愣是不这么做,也不知道是出去什么目的。
OK,那我们自己来打造一个补丁,显示当前对象的红与蓝。
关注到:鼠标移到自己的血条与蓝条上面,会有相应的Tooltips显示“生命值:XXX”“法术值:XXX”,而鼠标移动对方的血条蓝条上没有一点动静。
那么,究竟这两个类型相同的事件在触发上有什么区别?
通过跟踪发现,区别就在于,鼠标移动到对方的血条蓝条上触发的函数是一个空函数,所以你什么都看不到(这个函数下面会被展示)。
而显示自身的血量与蓝量的函数哩?显然,这个函数做了不少事。使,我们看到了想要的结果。
怎么跟到这个函数?关键字符串定位喽--“生命值”,定位到WndMgr.dll中的函数如下(我们这里暂且把它叫做func1):
IDA =
.text: 10045965 push ebp
.text: 10045966 mov ebp , esp
.text: 10045968 sub esp , 64h
.text:1004596B mov [ ebp+var_64 ] , ecx
.text:1004596E mov ecx , [ ebp+var_64 ]
.text: 10045971 call ds:XWindow::GetDesktop(void)
.text: 10045977 mov [ ebp+var_14 ] , eax
.text:1004597A cmp [ ebp+var_14 ] , 0
.text:1004597E jnz short loc_10045985
.text:1004597E
.text: 10045980 jmp loc_10045A4D
.text: 10045980
....................................................
.text:100459AA mov edx , [ ebp+var_64 ]
.text:100459AD mov ecx , [ edx+0ECh ]
.text:100459B3 call sub_1003017A
.text:100459B3
.text:100459B8 push eax
.text:100459B9 mov eax , [ ebp+var_64 ]
.text:100459BC mov ecx , [ eax+0ECh ]
.text:100459C2 call sub_100300D8
.text:100459C2
.text:100459C7 push eax
.text:100459C8 push offset s_DD_0 ; "生命值: %d / %d"
.text:100459CD lea ecx , [ ebp+var_54 ]
.text:100459D0 push ecx ; char *
.text:100459D1 call _sprintf
.text:100459D1
.text:100459D6 add esp , 10h
.text:100459D9 jmp short loc_10045A18
....................................................
而另外一个触发函数却在哪里哩?定位的思想:他们是同一个类型的对象,至少,他们的父类是相同的,那么这个触发的成员函数应该都是从父类派生的。
自己对象的成员函数被重构了,而对方对象的成员函数却没有被重构或被重构成空函数。这说明,他们的成员函数调用应该是一样的。
断点自身对象显示触发:+45965h,函数返回,可以看见一个call dword ptr [edx+XX]的指令,断点取消,在这条指令下断。
我们可以猜测这条指令其实就是调用鼠标触发显示Tooltips的。事实证明的确如此。
现在可以鼠标移动到当前对象的血条去触发当前对象的成员函数。(注意小心触发别的Tooltips的显示而干扰定位的准确)
中断跟进,得到成员函数如下(我们这里暂且把它叫做func2):
07B3B72A /. 55 push ebp
07B3B72B |. 8BEC mov ebp , esp
07B3B72D |. 51 push ecx
07B3B72E |. 894D FC mov [ local.1 ] , ecx
07B3B731 |. 8BE5 mov esp , ebp
07B3B733 |. 5D pop ebp
07B3B734 . C2 1000 retn 10
[ /code ]
看到吧,什么事都不做。当然不能显示当前对象的血量与蓝量喽。
改造它!怎么改?我们能不能利用现有的函数?
由于两个成员函数都是从父类派生的(当然是我们自己想当然的啦,不过也许事实就是如此呢?嘿嘿),它们的调用格式(参数、类型等)是一致的。故而,我们可以在+3B72Ah处来个jmp,直接跳到自身对象的成员函数去嘛!
然而,既然是作为两个类众父类派生,这类之间总是有点区别的嘛,不能一个jmp就完事啊,还得看看jmp后能不能如你所愿地干活。
自然不行,一个jmp就能搞定,你也太小看逆向事业了。
为什么不行?只能看见一个空的tooltip被显示出来了。这只说明,成功了一半。
为什么?动态跟啦。跟踪过程略。
首先,传递进来的this指针有差异。自身触发时,自身对象存在于this+88h处。而对方触发时,对方对象存在于this+0E0h处。
看来,func1的取值部分要改写了。
分析func1的流程,发现对鼠标位置有判断,然后在对方触发时,这个判断会失效。
怎么办?折衷。干脆,把这两个判断都去掉,不管是指向血条还是蓝条,血量与蓝量都显示不就结了?嘿嘿。
那么血量和蓝值哪里去取?这里点一下:[this]+5Ch处的成员函数可以给你一些帮助哦。压参7.8.9.10分别返回血量、血量上限,蓝量,蓝量上限。(其它参数自己琢磨去,不透露,嘿嘿)
取值也准备好了,那格式化字符串的事呢?这事还是让程序原来的代码来做这事吧。
但是问题又出来了:生命与法术是两个字符串啊。--这是一个很小的问题啦,这两个字符串不是紧挨着嘛,第一串的null换掉不就结了。。。
但是换什么好呢?空格?这样显示的结果太长了。分行?0Dh与0Ah在sprintfA中都被忽略。想要分行还得给一个"/ n"。但是它有两个byte啊。。。
变通一下老大,那么我们就把字符串合并且缩减嘛。于是把字符串改造成:“生命:%d / %d /n法术:%d / %d”后面补0。
改造代码如下:
0766040F > 55 push ebp
07660410 . 8BEC mov ebp , esp
07660412 . 83EC 64 sub esp , 64
07660415 . 894D 9C mov dword ptr [ ebp-64 ] , ecx
07660418 . 8B4D 9C mov ecx , dword ptr [ ebp-64 ]
0766041B . FF15 18C46A07 call dword ptr [ <&WndSys.XWindow::GetDesktop> ] ; WndSys.XWindow::GetDesktop
07660421 . 8945 EC mov dword ptr [ ebp-14 ] , eax
07660424 . 837D EC 00 cmp dword ptr [ ebp-14 ] , 0
07660428 . 75 05 jnz short 0766042F
0766042A . E9 C8000000 jmp 076604F7
0766042F > 8B4D EC mov ecx , dword ptr [ ebp-14 ]
07660432 . FF15 28C46A07 call dword ptr [ <&WndSys.XDesktop::GetSysToolTip> ] ; WndSys.XEdit::GetValidHeight
07660438 . 8945 A8 mov dword ptr [ ebp-58 ] , eax
0766043B . 837D A8 00 cmp dword ptr [ ebp-58 ] , 0
0766043F . 75 05 jnz short 07660446
07660441 . E9 B1000000 jmp 076604F7 ; 从这个后面开始修改
07660446 > 8B4D 9C mov ecx , dword ptr [ ebp-64 ] ; 取到前面保存的this
07660449 . 8B81 88000000 mov eax , dword ptr [ ecx+88 ] ; 先到+88处取值
0766044F 3D FFFFFF00 cmp eax , 0FFFFFF ; 增加代码稳定性,因为如果是当前对象触发,+88取到的值不定,也不一定为0
07660454 7F 0A jg short 07660460
07660456 8B81 E0000000 mov eax , dword ptr [ ecx+E0 ] ; 如果+88取不到值,就往+E0处取
0766045C 85C0 test eax , eax
0766045E 74 65 je short 076604C5 ; 如果+E0处也取不到值,那就全跳过吧
07660460 8BC8 mov ecx , eax
07660462 894D A0 mov dword ptr [ ebp-60 ] , ecx ; 保存取到的对象
07660465 8B4D A0 mov ecx , dword ptr [ ebp-60 ]
07660468 6A 0A push 0A
0766046A 8B01 mov eax , dword ptr [ ecx ]
0766046C FF50 64 call dword ptr [ eax+64 ] ; 取蓝上限
0766046F 50 push eax ; 蓝上限入栈,__cdel调用格式,参数反入栈
07660470 EB 05 jmp short 07660477 ; 后面五个字节不能用。因为这里原来是:push offset s_DD_0 ; "生命值: %d / %d"
07660472 90 db 90 ; 因为是全局变量,PE加载器会在DLL加载的时候进行重定位,跟踪的时候不会错,但修改PE文件的时候,
07660473 . 9090F287 dd 87F29090 ; 会因为代码改变而使程序崩溃,为了懒得去动DLL的重定位表,我们干脆就放弃使用这5字节,爱咋咋地
07660477 > 8B4D A0 mov ecx , dword ptr [ ebp-60 ]
0766047A . 6A 09 push 9
0766047C . 8B01 mov eax , dword ptr [ ecx ] ; 取蓝值
0766047E . FF50 64 call dword ptr [ eax+64 ]
07660481 . 50 push eax
07660482 . 8B4D A0 mov ecx , dword ptr [ ebp-60 ]
07660485 . 6A 08 push 8
07660487 . 8B01 mov eax , dword ptr [ ecx ]
07660489 . 8B01 mov eax , dword ptr [ ecx ]
0766048B . FF50 64 call dword ptr [ eax+64 ] ; 取血值上限
0766048E . 50 push eax
0766048F . 8B4D A0 mov ecx , dword ptr [ ebp-60 ]
07660492 . 6A 07 push 7
07660494 . 8B01 mov eax , dword ptr [ ecx ]
07660496 . FF50 64 call dword ptr [ eax+64 ] ; 取血值
07660499 . 50 push eax
0766049A . EB 15 jmp short 076604B1 ; 四个参数压完后直接跳到后面让原来的代码给我们字符串格式化了
0766049C . A0 6A078B01 mov al , byte ptr [ 18B076A ]
076604A1 . FF50 64 call dword ptr [ eax+64 ]
076604A4 . 50 push eax
076604A5 . EB 0A jmp short 076604B1
076604A7 D0 db D0
076604A8 00 db 00
076604A9 00 db 00
076604AA 00 db 00
076604AB . E8 F8BAFEFF call 0764BFA8
076604B0 . 50 push eax
076604B1 > 68 78616C07 push 076C6178 ; 生命:%d / %d 法术:%d / %d
076604B6 . 8D45 AC lea eax , dword ptr [ ebp-54 ] ; 注意,前面的全局变量的相对偏移要改了吧。原来它是指向“法术值...”
076604B9 . 50 push eax
076604BA . E8 0DBF0300 call 0769C3CC
076604BF . 83C4 10 add esp , 10
076604C2 > 8D4D AC lea ecx , dword ptr [ ebp-54 ]
076604C5 . 51 push ecx
076604C6 . 8B4D A8 mov ecx , dword ptr [ ebp-58 ]
076604C9 . FF15 A8C46A07 call dword ptr [ <&WndSys.XToolTip::SetText> ] ; WndSys.XToolTip::SetText
好了,累人,总结一下:
[code]
一:主函数修改+37h
0123456789abcdef0123456789abcdef //一行16个字节,共5行又6字节
8B4D9C8B81880000003DFFFFFF007F0A
8B81E000000085C074658BC8894DA08B
4DA06A0A8B01FF506450EB0590909090
908B4DA06A098B01FF5064508B4DA06A
088B018B01FF5064508B4DA06A078B01
FF506450EB15
二、数据修改
076C6178 C9 FA C3 FC A3 BA 25 64 20 2F 20 25 64 5C 6E B7 生命:%d / %d/n
076C6188 A8 CA F5 A3 BA 25 64 20 2F 20 25 64 00 00 00 00 ㄊ酰?d / %d....
三、跳转修改
07657ADE /E9 2C890000 jmp 0766040F
07657AE3 |90 nop
07657AE4 |90 nop
07657AE5 |. |8BE5 mov esp, ebp
四、原始数据偏移修改 -10h
076604B1 > 68 78616C07 push 076C6178 ; 生命:%d / %d/n法术:%d / %d
[/code]
先在OD中改一遍,跑一下成不成功,成功后把数据直接拿去改PE。RVA搞不清楚??那你还能看到这里?好好学习,天天向上去!
修改前把原版的DLL保存一下,免得你改乱了游戏都得重新下载。
这里不放出修改好的版本,免得不劳而获的人太高兴,而且,或有人问:“别是木马吧”,这句话噎死人。好了,只给代码,自己鉴别去。
省略了很多,看得明白共同进步,看不明白你当我在聒噪也行。
另外,这是2008.5.1前的版本分析,5.1后改版了,相对偏移自己跟踪去,以上代码都是从我随笔日志中cpy过来的,所以我这里懒得更新,嘿嘿。
补丁选择一个对象,鼠标过去,看看,Tooltips显示了么,嘿嘿,有点爽吧。要知道,5.1活动,怪“藏毒”有80000的血啊!!
结束,欢迎拍砖,谢谢!