我想,凡是来这儿的人都有一种潜意识,那就是软件的加密只是被软件作者用于保护他们的利益,和真正的软件的功能没有太大关系;我们解密只不过是让这些和我们作对的保护失去作用。当我们有了一套“有权”使用的软件后,如果它运行出错,我们通常要么将错误报告给作者,要么停止使用它。我们通常不会想到去改进软件,毕竟,我们没有源代码呀。
其实,没有源代码一样也可以改进。我们不妨将软件的保护看成一种对作者有用的Bug,我们可以去掉它,对于运行错误的Bug,我们不是一样可以去掉吗?前不久,有朋友想看以前的聊天记录,可是他忘了QQ密码,怎么也登陆不上,他的唯一选择就是用聊天记录察看器来看了。看着看着,突然程序弹出一个严重错误对话框,说发生严重错误,访问了非法地址。反复试了几遍都是一样。他问我有没有什么办法。我说报告作者吧,他说,作者似乎停止了此软件的开发。我抱着试试看的想法,用IDA 反汇编了这个小程序,然后在IDA中以调试的方式运行它,重复着朋友的操作。果然,错误再次出现,但是被IDA的调试器所捕获,它将我带到如下的函数和地址:
CODE:004855D8 _TQQMsgView_ShowOneMsg proc near ; CODE XREF: _TQQMsgView_ShowMsgPage+148p
CODE:004855D8 ; DATA XREF: CODE:00483BC8o
CODE:004855D8
CODE:004855D8 var_8A4 = dword ptr -8A4h
CODE:004855D8 var_8A0 = dword ptr -8A0h
CODE:004855D8 var_89C = dword ptr -89Ch
CODE:004855D8 var_898 = dword ptr -898h
CODE:004855D8 var_894 = dword ptr -894h
CODE:004855D8 var_890 = dword ptr -890h
CODE:004855D8 var_886 = byte ptr -886h
CODE:004855D8 var_85 = byte ptr -85h
CODE:004855D8 var_20 = dword ptr -20h
CODE:004855D8 var_1C = dword ptr -1Ch
CODE:004855D8 var_18 = dword ptr -18h
CODE:004855D8 var_14 = dword ptr -14h
CODE:004855D8 var_10 = dword ptr -10h
CODE:004855D8 var_C = dword ptr -0Ch
CODE:004855D8 var_8 = dword ptr -8
CODE:004855D8 var_4 = dword ptr -4
CODE:004855D8
CODE:004855D8 push ebp
CODE:004855D9 mov ebp, esp
CODE:004855DB add esp, -8A4h
CODE:004855E1 xor ecx, ecx
CODE:004855E3 mov [ebp+var_8A4], ecx
CODE:004855E9 mov [ebp+var_8A0], ecx
CODE:004855EF mov [ebp+var_89C], ecx
CODE:004855F5 mov [ebp+var_898], ecx
CODE:004855FB mov [ebp+var_894], ecx
CODE:00485601 mov [ebp+var_890], ecx
CODE:00485607 mov [ebp+var_20], ecx
CODE:0048560A mov [ebp+var_C], ecx
CODE:0048560D mov [ebp+var_10], ecx
CODE:00485610 mov [ebp+var_8], edx
CODE:00485613 mov [ebp+var_4], eax
CODE:00485616 xor eax, eax
CODE:00485618 push ebp
CODE:00485619 push offset loc_485946
CODE:0048561E push dword ptr fs:[eax]
CODE:00485621 mov fs:[eax], esp
CODE:00485624 lea eax, [ebp+var_85]
CODE:0048562A xor ecx, ecx
CODE:0048562C mov edx, 65h
CODE:00485631 call @Windows@FillMemory$qqrpvuiuc ; Windows::FillMemory(void *,uint,uchar)
CODE:00485636 lea eax, [ebp+var_886]
CODE:0048563C xor ecx, ecx
CODE:0048563E mov edx, 65h
CODE:00485643 call @Windows@FillMemory$qqrpvuiuc ; Windows::FillMemory(void *,uint,uchar)
CODE:00485648 lea ecx, [ebp+var_C]
CODE:0048564B lea edx, [ebp+var_10]
CODE:0048564E mov eax, [ebp+var_8]
CODE:00485651 mov eax, [eax]
CODE:00485653 call sub_485150
CODE:00485658 cmp ds:dword_488850, 0
CODE:0048565F jz short loc_485677
CODE:00485661 cmp ds:dword_488850, 1
CODE:00485668 jz short loc_485677
CODE:0048566A cmp ds:dword_488850, 4
CODE:00485671 jnz loc_48574D
CODE:00485677
CODE:00485677 loc_485677: ; CODE XREF: _TQQMsgView_ShowOneMsg+87j
CODE:00485677 ; _TQQMsgView_ShowOneMsg+90j
CODE:00485677 mov eax, [ebp+var_8]
CODE:0048567A add eax, 5
CODE:0048567D mov [ebp+var_14], eax
CODE:00485680 mov eax, [ebp+var_14]
CODE:00485683 mov eax, [eax]
CODE:00485685 mov [ebp+var_18], eax
CODE:00485688 add [ebp+var_14], 4
CODE:0048568C lea eax, [ebp+var_85]
CODE:00485692 mov ecx, [ebp+var_18]
CODE:00485695 mov edx, [ebp+var_14]
CODE:00485698 call sub_407160 <---其实是内存复制函数,将其改成MemCopy
CODE:0048569D mov eax, [ebp+var_18]
CODE:004856A0 add [ebp+var_14], eax
CODE:004856A3 mov eax, [ebp+var_14]
CODE:004856A6 mov eax, [eax]
CODE:004856A8 mov [ebp+var_18], eax
CODE:004856AB add [ebp+var_14], 4
CODE:004856AF lea eax, [ebp+var_886]
CODE:004856B5 mov ecx, [ebp+var_18]
CODE:004856B8 mov edx, [ebp+var_14]
CODE:004856BB call sub_407160 <---其实是内存复制函数,将其改成MemCopy
CODE:004856C0 mov eax, [ebp+var_4] <---在这里得到内存指针
CODE:004856C3 mov eax, [eax+318h] <---在这条指令处出错!!!
程序在4856C3处出错。很显然,在4856C0得到的eax应该是一个内存指针,可是当我在[ebp+var_4]双击时,IDA将我带到的地方竟然是一堆聊天数据,极有可能是存放指针的地方被聊天数据覆盖了。但是如何证明这一点呢?往上看,有两个地方调用了同一个函数sub_407160,让我们进去看看,双击进去一看是这样的:
CODE:00407160 sub_407160 proc near ; CODE XREF: sub_4596DC+3Cp
CODE:00407160 ; sub_4596DC+59p ...
CODE:00407160 xchg eax, edx
CODE:00407161 call @System@Move$qqrpxvpvi ; System::Move(void *,void *,int)
CODE:00407166 retn
CODE:00407166 sub_407160 endp
很明显,这是一个内存复制函数,IDA的库函数识别可帮了我的大忙,这正是我一直坚持使用IDA作为调试器的原因之一。为了更直观,我将sub_407160改成MemCopy。(能将函数动态改名,多么酷的功能)。稍加分析,可以看出这个函数是以ecx作为要复制内存大小,而以eax作为复制目的地的。因此在CODE:004856BB处要将数据复制到var_886这个堆栈变量地址,虽然这个变量的大小从堆栈布局可以看出是0x801,2K多一字节,会不会是这个变量还是嫌小呢?我们在即将出错前在CODE:004856B8设断点,监视ecx是否超过0x801。果然,用不了几下,就发现ecx的值为0x8c0。哇,错误找到了--用于接受聊天数据的堆栈缓冲区太小,接下来我们要做的就是如何修改程序从而避免这个错误。
很容易想到,我们必须将var_886这个变量扩大,但是如何做到这一点呢?
在函数开始,有如下指令:
CODE:004855DB add esp, -8A4h
它是用来为函数分配堆栈变量空间的,可以看出局部变量的总大小大约是2K多一点,只要我们在这里修改一下,改成add esp, -28a4h,就可以将函数的堆栈变量大小扩大8K,但仅仅这样改还是于事无补,所扩大的8K不会被函数使用到,var_886能用的空间还是原来那么大。于是我想到了下面的办法:
在CODE:004855DB处将指令改成add esp, -28a4h,从而将函数的堆栈变量大小扩大8K后,将var_886变量地址也向上移8K,这样做既可以保证var_886还是在可使用的堆栈空间内,又保证了它大约有7K大小的空间,比原先的2K大多了。这样改后,函数中所有对var_886的引用也要往上移动8K。这看起来好像不好改,其实用以下的办法大约1到2分钟就可以改好。
1. 用UltraEdit打开要改的可执行文件,从IDA中可以知道要改的指令在文件中的位置,先跳到那个位置。
2. 到了要改的地方后,你往往可以看到指令中出现的立即数,如改堆栈变量大小时,你可以看到5c f7 ff ff,这正是-8A4h。
在IDA中,按Shift-?,弹出简易计算器,输入-0x28a4,可以知道它的16进制表示为5c d7 ff ff,因此只要将那个f7改成d7就可以了。
3.将所有对var_886的引用向上挪动8K。在IDA中找到对var_886使用的地方,并在UltraEditor中跳到相应的地方,只要将指令处的立即数作类似的修改就可以了。所有这些改动是很快的。
作完这些修改后,程序就正常了。这是我发现,原来有一条很长的聊天记录,长度超过2K。
这篇短文对程序的修改只是个个案,我觉得真正的意义在于说明,对于没有源代码的程序出现崩溃的情况,我们决不是只能望而兴叹,我们可以自己动手。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)