没想到居然写了这么多……写完后打算去blogcn抒发一下高兴的心情,却发现blogcn被黑了……不过那个Hacker算是手下留情的,把主页备份了。
=================================================================================
为什么我在ItemDraw里用了String后没有达到预想的效果呢?从反汇编的结果
看,问题有点复杂。按道理说,过程/函数的局部简单变量是放在堆栈里的,但是
这里似乎有点例外情况。还是应该跟踪一下。
用od跟,首先找到GetWindowText的调用,前面有个push 0ah,下个HWBP,运
行。下面是IDA在那里的disasm结果:
CODE:00013D06 mov eax, [ebp+var_4]
CODE:00013D09 call sub_12DA4
//这里是检查eax是否为0,如是,则eax就被设为var_4的地址
CODE:00013D0E mov esi, eax
CODE:00013D10 push esi ; lpString
CODE:00013D11 mov eax, [ebx+14h]
CODE:00013D14 push eax ; hWnd
CODE:00013D15 call GetWindowTextA
继续,在00013D0E处,eax=12da9,但是这好象是CODE段。run。Exception!
这时程序中断在User32里。很明显,Delphi在处理String时有点小小的例外。那么,
来分析一下。
一、PChar
PChar是Delphi为和C兼容而引入的数据类型,对应于char*。对程序做如下改动:
Procedure ItemDraw(lpDIS:PDrawItemStruct);
Var
Str:PChar;
Begin
...
GetWindowText(lpDIS^.hwndItem,Str,10);
运行结果是,按钮文字乱码,od报告Exception。反汇编结果如下:
CODE:00013CE5 push 0Ah ; nMaxCount
CODE:00013CE7 push esi ; lpString
CODE:00013CE8 mov eax, [ebx+14h]
CODE:00013CEB push eax ; hWnd
CODE:00013CEC call GetWindowTextA
前面没有给esi赋值的地方,这也是意料之中。跟踪发现,esi=00013FB8,
指向LicenseProc。那么,看到的乱码就是LicenseProc的开始部分。大概是
User32里有SEH,直接运行程序没有Exception。
注意,Str没有在堆栈里分配,哪怕是个指针。但是这个可以理解。
改改代码:
Procedure ItemDraw(lpDIS:PDrawItemStruct);
Var
Str:PChar;
Begin
...
GetWindowText(lpDIS^.hwndItem,@Str,10);
可以正常运行。反汇编:
CODE:00013CA4 String = byte ptr -4
CODE:00013CA4
CODE:00013CA4 push ebx
CODE:00013CA5 push ecx
CODE:00013CA6 mov ebx, eax
CODE:00013CA8 mov eax, ds:hbr
...
CODE:00013CE5 push 0Ah ; nMaxCount
CODE:00013CE7 lea eax, [esp+8+String]
CODE:00013CEB push eax ; lpString
CODE:00013CEC mov eax, [ebx+14h]
CODE:00013CEF push eax ; hWnd
CODE:00013CF0 call GetWindowTextA
...
CODE:00013D4B pop edx
CODE:00013D4C pop ebx
CODE:00013D4D retn
有一个局部变量!跟踪到00013CA4,发现esi指向LicenseProc,esp=0013FA7C。
到00013CA6时,esp=0013FA74,没问题。到00013CE7,esp=0013FA70。向下,
eax=0013FA74,指向push的ecx。好了,得到的字符串覆盖了堆栈里的ecx和ebx。
那么,“正常运行”的结果应该是一个巧合。但是这个地方非常有趣,ebx当然要保
存,但是ecx完全不必保存的。这从过程的首尾可以看到。怎么回事呢?很简单,
PChar只是一个Point,push ecx就是分配空间。但是ebx呢?Delphi的HLP里明确说明
必须保护ebx。很巧,程序是SDK写的,没有用VCL。所以ebx坏了没事,而且最长的文
字才6字节。否则肯定出问题了。
玩个花样:
Var
Str:PChar;
Begin
Str:='123';
...
GetWindowText(lpDIS^.hwndItem,Str,10);
按钮的字全部是123。很容易就想到,123是常量,很可能放在CODE里。异常,写
不进。反汇编也证实了这点:
CODE:00013CA4 sub_13CA4 proc near
CODE:00013CA4 push ebx
CODE:00013CA5 push esi
CODE:00013CA6 mov ebx, eax
CODE:00013CA8 mov esi, offset loc_13D48
...
CODE:00013CEA push 0Ah ; nMaxCount
CODE:00013CEC push esi ; lpString
CODE:00013CED mov eax, [ebx+14h]
CODE:00013CF0 push eax ; hWnd
CODE:00013CF1 call GetWindowTextA
...
CODE:00013D46 retn
CODE:00013D46 sub_13CA4 endp
CODE:00013D46
CODE:00013D47 align 4
CODE:00013D48
CODE:00013D48 loc_13D48:
CODE:00013D48 xor [edx], esi
CODE:00013D4A xor eax, [eax]
但是用Aspack2.12加壳后,运行程序,可以正常显示文字。很明显,代码段可
写。这个结果很有趣,在加密上会有用的。顺便说一句,此时由于部分代码被破坏,
窗口重画时就异常退出了。
二、String
1、直接用String
Var
Str:String;
Begin
GetWindowText(lpDIS^.hwndItem,PChar(Str),10);
没有文字。
CODE:00013CB0 sub_13CB0 proc near
CODE:00013CB0
CODE:00013CB0 var_4 = dword ptr -4
CODE:00013CB0
CODE:00013CB0 push ebp
CODE:00013CB1 mov ebp, esp
CODE:00013CB3 push 0
CODE:00013CB5 push ebx
CODE:00013CB6 push esi
CODE:00013CB7 mov ebx, eax
CODE:00013CB9 xor eax, eax
CODE:00013CBB push ebp
CODE:00013CBC push offset loc_13D7E
CODE:00013CC1 push dword ptr fs:[eax]
CODE:00013CC4 mov fs:[eax], esp
...
CODE:00013D04 push 0Ah ; nMaxCount
CODE:00013D06 mov eax, [ebp+var_4]
CODE:00013D09 call sub_12DA4
CODE:00013D0E mov esi, eax
CODE:00013D10 push esi ; lpString
CODE:00013D11 mov eax, [ebx+14h]
CODE:00013D14 push eax ; hWnd
CODE:00013D15 call GetWindowTextA
...
CODE:00013D68
CODE:00013D68 loc_13D68:
CODE:00013D68 xor eax, eax
CODE:00013D6A pop edx
CODE:00013D6B pop ecx
CODE:00013D6C pop ecx
CODE:00013D6D mov fs:[eax], edx
CODE:00013D70 push offset loc_13D85
CODE:00013D75
CODE:00013D75 loc_13D75:
CODE:00013D75 lea eax, [ebp+var_4]
CODE:00013D78 call sub_12D80
CODE:00013D7D retn
CODE:00013D7D sub_13CB0 endp ; sp = -14h
CODE:00012DA4 sub_12DA4 proc near
CODE:00012DA4 test eax, eax
CODE:00012DA6 jz short loc_12DAA
CODE:00012DA8 retn
CODE:00012DA9 byte_12DA9 db 0
CODE:00012DAA
CODE:00012DAA loc_12DAA:
CODE:00012DAA mov eax, offset byte_12DA9
CODE:00012DAF retn
CODE:00012DAF sub_12DA4 endp
00013CB0处,esp=0013FA7C。00013D06处,eax=0。那么,看来局部的String是
分配一个指针,和PChar一样。这是在{H+}条件下的结果。注意,这里用了SEH。加
上Str:='123';一句,结果和PChar的情况一样。但是这里有个额外的call,上面没有
列出。
改一下:
Var
Str:String;
Begin
...
GetWindowText(lpDIS^.hwndItem,@Str,10);
程序异常了。经过跟踪可以得知,异常点在12D8C:
CODE:00012D80 sub_12D80 proc near
CODE:00012D80 mov edx, [eax]
CODE:00012D82 test edx, edx
CODE:00012D84 jz short locret_12DA2
CODE:00012D86 mov dword ptr [eax], 0
CODE:00012D8C mov ecx, [edx-8]
CODE:00012D8F dec ecx
CODE:00012D90 jl short locret_12DA2
CODE:00012D92 lock dec dword ptr [edx-8]
CODE:00012D96 jnz short locret_12DA2
CODE:00012D98 push eax
CODE:00012D99 lea eax, [edx-8]
CODE:00012D9C call sub_1249C
CODE:00012DA1 pop eax
CODE:00012DA2
CODE:00012DA2 locret_12DA2:
CODE:00012DA2 retn
CODE:00012DA2 sub_12D80 endp
说明一下,00012D80处,eax就指向堆栈里的那个指针。那么,edx应该就是
String的实际地址了,而edx-8很可能是一个引用计数。很明显,对@Str的写入破坏
了指针(正常应该是0)。
但是这里有个情况,如果对Str初始化一下
Var
Str:String;
Begin
Str:='1423423';
那么,如果使用PChar(Str),按钮的文字显示为1423423。而使用@Str[1]则正
常。
用PChar转换时,部分反汇编结果如下
CODE:00013CDC sub_13CDC proc near
CODE:00013CDC
CODE:00013CDC var_4 = dword ptr -4
CODE:00013CDC
...
CODE:00013CF3 lea eax, [ebp+var_4]
CODE:00013CF6 mov edx, offset a1423423 ; "1423423"
CODE:00013CFB call sub_12DA4
执行完sub_12DA4后,var_4被放上了位于CODE里的a1423423的地址。
而用后一种形式时,'1423423'仍然在CODE里,但是在00013DA9的call以后,eax
变了!
CODE:00013DA4 push 0Ah ; nMaxCount
CODE:00013DA6 lea eax, [ebp+var_4]
CODE:00013DA9 call sub_12E0C
CODE:00013DAE push eax ; lpString
CODE:00013DAF mov eax, [ebx+14h]
CODE:00013DB2 push eax ; hWnd
CODE:00013DB3 call GetWindowTextA
看来是新分配了一段空间。应该是堆空间。
2、用String[xx]形式:
Var
Str:String[20];
Begin
...
GetWindowText(lpDIS^.hwndItem,@Str[1],10);
正常运行。这时局部变量已经变了
CODE:00013CA4 sub_13CA4 proc near
CODE:00013CA4
CODE:00013CA4 String = byte ptr -17h
CODE:00013CA4
CODE:00013CA4 push ebx
CODE:00013CA5 add esp, 0FFFFFFE8h
...
CODE:00013CE7 push 0Ah ; nMaxCount
CODE:00013CE9 lea eax, [esp+1Ch+String]
CODE:00013CED push eax ; lpString
CODE:00013CEE mov eax, [ebx+14h]
CODE:00013CF1 push eax ; hWnd
CODE:00013CF2 call GetWindowTextA
...
CODE:00013D4D
CODE:00013D4D loc_13D4D:
CODE:00013D4D add esp, 18h
CODE:00013D50 pop ebx
CODE:00013D51 retn
CODE:00013D51 sub_13CA4 endp
直接看分析代码可以看到,对于20字节长的ShortStr,分配了24个堆栈字节。加
一下偏移就看得到,长度标志还是1字节。4个字节的差别是为了对齐。
结论是:在{H+}条件下,对于String[xx]形式,就是ObjectPascal的String,直
接在堆栈分配。PChar是一个放在堆栈里的指针,内容视情况而定。String则是在堆
栈里分配一个指针,指向一个结构体。限于时间,我没有对这个结构体做进一步探
索。但是,如果用PChar转换,则指针一般是指向CODE的。
那么,为什么开始的程序没有出问题也就很明白了。也是巧合,我用了PChar强
制转换,所以指针实际是指向CODE的。由于User32很可能有SEH什么的,结果是什么
都显示不出。加上对Message不熟悉,这样我以为按钮的文字可能在WM_PAINT里,否
则肯定早就发现原因了。
================================================================
如果你看到这里觉得我没有讲什么新东西,别打我……
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)