用实例讲解如何读懂DELPHI程序的反编译
本帖最后由 肥牛 于 2017-6-9 21:53 编辑最近分析了几个CM以及商品程序,都是DELPHI程序写的。我发出来的文章,有很多新手也问了好多问题。我发现很多都是属于比较基础的,所以我觉得有必要把一些基础性的东西讲出来给新手。这里的新手指的是新学编程的,新学破解的。
用DELPHI写了一个非常简单的程序,两个输入框,输入用户名,输入注册码,点击注册按钮会检验用户名与注册码是否正确,错误的话提示注册失败。
是不是就像一个CM?这就是一个非常简单非常简单的CM。但是这次我们不是讲怎么破解它,我们要讲它反编译以后是什么样子的。
首先,我们先看一下它的DELPHI源码是什么样的吧。因为我们关注的是注册按钮事件,所以只看这个事件的代码:
var
strTemp : string;
begin
if Trim(edtName.Text)='' then
begin
Application.MessageBox('请输入用户名!', '错误', MB_OK + MB_ICONWARNING);
edtName.SetFocus;
Exit;
end;
if Trim(edtCode.Text)='' then
begin
Application.MessageBox('请输入注册码!', '错误', MB_OK + MB_ICONWARNING);
edtCode.SetFocus;
Exit;
end;
strTemp :=Trim(edtName.Text);
if GetCode(strTemp)=Trim(edtCode.Text) then
Application.MessageBox('注册成功!', '提示', MB_OK + MB_ICONINFORMATION)
else
Application.MessageBox('注册失败!', '提示', MB_OK + MB_ICONINFORMATION);
有一点编程经验的朋友一定可以读懂这段代码,非常非常简单。即使你没学过编程,猜也能猜出个大概。
因为这里我需要演示一个CALL调用,所以,注册码的验证部分,我单独写了一个函数GetCode,源码如下:
function GetCode(strName : string) : string;
var
i, iLen, iSum : integer;
begin
iLen := Length(strTemp);
iSum :=0;
for i:=1 to iLen do
iSum :=iSum *10 + Ord(strTemp);
Result := IntToStr(iSum);
end;
GetCode的意思我简单说明一下,就是把输入的字符串,按顺序每个字节取ASCII码(DELPHI语言中取字符的ASCII码用Ord函数,VB里应该是ASC函数),乘以10以后加上下一个字节的ASCII码,和再乘以10再加下一个,以此类推。最后的和转为字符串返回。
OK,到这里,相信大家都能看懂这个程序的意思了。用户名按照上面写的算法计算,得出来的就是注册码,然后与输入的注册码比较,给出相应的提示。
那么我们下面就看反编译的结果。DELPHI程序的反编译,首选是DeDe,它反编译的代码,结构清晰,内部函数都标注出来了。但它的缺点就是不支持高版本的DELPHI。对付高版本的DELPHI,就得用IDR了。IDR反编译的结果和DeDe其实一样,只是对于一些内部的函数及变量,标识的没有DeDe那样清晰。对于熟悉DELPHI的人或者经验比较丰富的,用IDR可能更快捷。对于初学者,还是从DeDe入手为好。
使用DeDe加载这个程序,可以非常容易看到这个程序只有两个事件,那我们要处理的就是注册按钮点击事件。
鼠标双击“btnRegisterClick”,会弹出一个新窗口,这个窗口中的代码,就是我们要分析的反编译后的代码,我把它贴出来。
004537F4 55 push ebp
004537F5 8BEC mov ebp, esp
004537F7 B904000000 mov ecx, $00000004
004537FC 6A00 push $00
004537FE 6A00 push $00
00453800 49 dec ecx
00453801 75F9 jnz 004537FC
00453803 51 push ecx
00453804 53 push ebx
00453805 8BD8 mov ebx, eax
00453807 33C0 xor eax, eax
00453809 55 push ebp
0045380A 6871394500 push $00453971
***** TRY //一般来说,DeDe中的代码, TRY这一行以上,以及后面FINALLY以下的,都可以不用看了
//DELPHI程序的特点,在CALL之前,EAX中往往是要处理的数据,EDX中是返回值,知道这个对我们看懂程序有很大帮助
|
0045380F 64FF30 push dword ptr fs:
00453812 648920 mov fs:, esp
00453815 8D55F4 lea edx, //刚才说了,EDX中是返回值,而LEA的意思就是传送的是一个地址。所以这句话的意思其实就是把返回值放到这个地址中
* Reference to control edtName : TEdit
|
00453818 8B83FC020000 mov eax, //中的值放到eax中,很明显,指向的应该是edtName这个输入框
* Reference to: Controls.TControl.GetText(TControl):TCaption;
|
0045381E E8ADEAFDFF call 004322D0 //GetText是单参数的,输入是个TControl,输出是TCaption,从edtName输入框中获取值,保存到EDX中
00453823 8B45F4 mov eax, //上面这个CALL调用,返回结果也就是edtName中的内容保存到中了,现在再把这个值赋给EAX
00453826 8D55F8 lea edx, //同上,意思是返回值放在中
* Reference to: SysUtils.Trim(AnsiString):AnsiString;overload;
|
00453829 E8FE46FBFF call 00407F2C //Trim是单参数的,输入ANSIString, 输出也是AnsiString。把EAX的字符串做TRIM操作,返回值放到EDX(也就是)中
0045382E 837DF800 cmp dword ptr , +$00 //比较中是不是空
00453832 752B jnz 0045385F
00453834 6A30 push $30
* Possible String Reference to: '错误'
|
00453836 B980394500 mov ecx, $00453980
* Possible String Reference to: '请输入用户名!'
|
0045383B BA88394500 mov edx, $00453988
00453840 A16C504500 mov eax, dword ptr [$0045506C]
00453845 8B00 mov eax,
* Reference to: Forms.TApplication.MessageBox(TApplication;PChar;PChar;Longint):Integer;
|
00453847 E8E4E8FFFF call 00452130 //MessageBox是四个参数,输入EAX是TApplication,ECX是标题,EDX是提示信息,$30是对话框类型。估计返回值也是EDX,只是这里没用上返回值。
* Reference to control edtName : TEdit
|
0045384C 8B83FC020000 mov eax,
00453852 8B10 mov edx,
00453854 FF92C4000000 call dword ptr //猜测,这里应该是SetFocus的意思
0045385A E9C5000000 jmp 00453924
0045385F 8D55EC lea edx, //这里开始处理edtCode了,和上面的分析一样,我就不再写了。
* Reference to control edtCode : TEdit
|
00453862 8B83F8020000 mov eax,
* Reference to: Controls.TControl.GetText(TControl):TCaption;
|
00453868 E863EAFDFF call 004322D0
0045386D 8B45EC mov eax,
00453870 8D55F0 lea edx,
* Reference to: SysUtils.Trim(AnsiString):AnsiString;overload;
|
00453873 E8B446FBFF call 00407F2C
00453878 837DF000 cmp dword ptr , +$00
0045387C 7528 jnz 004538A6
0045387E 6A30 push $30
* Possible String Reference to: '错误'
|
00453880 B980394500 mov ecx, $00453980
* Possible String Reference to: '请输入注册码!'
|
00453885 BA98394500 mov edx, $00453998
0045388A A16C504500 mov eax, dword ptr [$0045506C]
0045388F 8B00 mov eax,
* Reference to: Forms.TApplication.MessageBox(TApplication;PChar;PChar;Longint):Integer;
|
00453891 E89AE8FFFF call 00452130
* Reference to control edtCode : TEdit
|
00453896 8B83F8020000 mov eax,
0045389C 8B10 mov edx,
0045389E FF92C4000000 call dword ptr
004538A4 EB7E jmp 00453924
004538A6 8D55E8 lea edx, //返回值放到
* Reference to control edtName : TEdit
|
004538A9 8B83FC020000 mov eax, //还是对edtName输入框操作
* Reference to: Controls.TControl.GetText(TControl):TCaption;
|
004538AF E81CEAFDFF call 004322D0 //取输入框内容
004538B4 8B45E8 mov eax, //取出的内容放到eax中
004538B7 8D55FC lea edx, //放返回值
* Reference to: SysUtils.Trim(AnsiString):AnsiString;overload;
|
004538BA E86D46FBFF call 00407F2C //做Trim操作。这里大家应该能知道了,Trim后的结果保存到中了
004538BF 8D55E4 lea edx, //这里又放另一个返回值,哪个的返回值呢?
004538C2 8B45FC mov eax, //Trim后的结果进行操作
|
004538C5 E8B6FEFFFF call 00453780 //函数A。函数A去处理Trim(edtName),处理后的结果保存到里
004538CA 8B45E4 mov eax, //结果放到EAX里
004538CD 50 push eax //压栈,保存起来
004538CE 8D55DC lea edx, //返回值放
* Reference to control edtCode : TEdit
|
004538D1 8B83F8020000 mov eax, //对edtCode输入框进行处理
* Reference to: Controls.TControl.GetText(TControl):TCaption;
|
004538D7 E8F4E9FDFF call 004322D0 //取edtCode输入框的内容保存到中
004538DC 8B45DC mov eax, //……,上面都写那么多了,下面应该能读懂了
004538DF 8D55E0 lea edx,
* Reference to: SysUtils.Trim(AnsiString):AnsiString;overload;
|
004538E2 E84546FBFF call 00407F2C
004538E7 8B55E0 mov edx, //OK,到这里,Trim(edtCode)的值保存到edx中了
004538EA 58 pop eax //出栈,也就是把刚才保存的FunA(Trim(edtName))的值保存到EAX中
* Reference to: System.@LStrCmp;
|
004538EB E8FC0CFBFF call 004045EC //EAX和EDX的值进行比较
004538F0 751A jnz 0045390C //OK,后面不写了,综合上面写的,就是判断Trim(edtCode)是不是等于FunA(Trim(edtName)),于是需要去判断FunA是什么
004538F2 6A40 push $40
* Possible String Reference to: '提示'
|
004538F4 B9A8394500 mov ecx, $004539A8
* Possible String Reference to: '注册成功!'
|
004538F9 BAB0394500 mov edx, $004539B0
004538FE A16C504500 mov eax, dword ptr [$0045506C]
00453903 8B00 mov eax,
* Reference to: Forms.TApplication.MessageBox(TApplication;PChar;PChar;Longint):Integer;
|
00453905 E826E8FFFF call 00452130
0045390A EB18 jmp 00453924
0045390C 6A40 push $40
* Possible String Reference to: '提示'
|
0045390E B9A8394500 mov ecx, $004539A8
* Possible String Reference to: '注册失败!'
|
00453913 BABC394500 mov edx, $004539BC
00453918 A16C504500 mov eax, dword ptr [$0045506C]
0045391D 8B00 mov eax,
* Reference to: Forms.TApplication.MessageBox(TApplication;PChar;PChar;Longint):Integer;
|
0045391F E80CE8FFFF call 00452130
00453924 33C0 xor eax, eax
00453926 5A pop edx
00453927 59 pop ecx
00453928 59 pop ecx
00453929 648910 mov fs:, edx
****** FINALLY
|
* Possible String Reference to: '[嬪]?
|
0045392C 6878394500 push $00453978
00453931 8D45DC lea eax,
* Reference to: System.@LStrClr(void;void);
|
00453934 E8A708FBFF call 004041E0
00453939 8D45E0 lea eax,
0045393C BA02000000 mov edx, $00000002
* Reference to: System.@LStrArrayClr(void;void;Integer);
|
00453941 E8BE08FBFF call 00404204
00453946 8D45E8 lea eax,
00453949 BA02000000 mov edx, $00000002
* Reference to: System.@LStrArrayClr(void;void;Integer);
|
0045394E E8B108FBFF call 00404204
00453953 8D45F0 lea eax,
* Reference to: System.@LStrClr(void;void);
|
00453956 E88508FBFF call 004041E0
0045395B 8D45F4 lea eax,
* Reference to: System.@LStrClr(void;void);
|
0045395E E87D08FBFF call 004041E0
00453963 8D45F8 lea eax,
00453966 BA02000000 mov edx, $00000002
* Reference to: System.@LStrArrayClr(void;void;Integer);
|
0045396B E89408FBFF call 00404204
00453970 C3 ret
* Reference to: System.@HandleFinally;
|
00453971 E94602FBFF jmp 00403BBC
00453976 EBB9 jmp 00453931
****** END
|
00453978 5B pop ebx
00453979 8BE5 mov esp, ebp
0045397B 5D pop ebp
0045397C C3 ret
我在这段代码里也写了注释,大家可以结合上面的DELPHI源码,对照看,就更容易理解。这里需要特别强调的是,DELPHI程序的每个动作,其实都是通过CALL实现的,也就是调用WINDOWS的API函数,或者调用DELPHI自己封装好的函数,或者调用自己程序的内部函数。那么CALL函数的参数,基本上就是EAX,返回值也多为EDX(不是绝对啊,这个要看具体情况)。所以在分析DELPHI反编译的代码时就会发现,翻来覆去倒腾EAX,EDX。
而这段代码中,有一个关键的地方就是
004538C5 E8B6FEFFFF call 00453780 //函数A。函数A去处理Trim(edtName),处理后的结果保存到里
这个CALL调用的函数A,就是我在DELPHI程序里写的GetCode函数。这个反编译后的代码是这样的:
00453780 55 push ebp
00453781 8BEC mov ebp, esp
00453783 51 push ecx
00453784 53 push ebx
00453785 56 push esi
00453786 8BF2 mov esi, edx //一般函数调用的返回值是通过EDX传递的,入口部分先把这个值保存到ESI中
00453788 8945FC mov , eax //传入的参数
0045378B 8B45FC mov eax,
* Reference to: System.@LStrAddRef(void;void):Pointer;
|
0045378E E8FD0EFBFF call 00404690
00453793 33C0 xor eax, eax
00453795 55 push ebp
00453796 68E8374500 push $004537E8
***** TRY
|
0045379B 64FF30 push dword ptr fs:
0045379E 648920 mov fs:, esp
004537A1 8B45FC mov eax,
* Reference to: System.@LStrLen(String):Integer;
| or: System.@DynArrayLength;
| or: System.DynArraySize(Pointer):Integer;
| or: Variants.DynArraySize(Pointer):Integer;
|
004537A4 E8F70CFBFF call 004044A0 //取字符串长度,保存到EAX中(按道理应该是保存到EDX中的,可是按照这里代码的理解却是保存到EAX中了,请高手指点)
004537A9 33DB xor ebx, ebx //求和值EBX=0
004537AB 85C0 test eax, eax //长度为0就跳转
004537AD 7E1A jle 004537C9
004537AF BA01000000 mov edx, $00000001 //EDX=1,循环初值
004537B4 8B4DFC mov ecx, //ECX=要处理的字符串
004537B7 0FB64C11FF movzx ecx, byte ptr //按顺序取字符串的一个字节
004537BC 03DB add ebx, ebx //EBX=EBX*2;
004537BE 8D1C9B lea ebx, //EBX=EBX*5。原来的代码写的是EBX=EBX*10,计算机将*10转化为*2*(4+1),因为第一个自身相加,只用一个操作数,处理速度快,*4相当于左移两位。总之,这样处理是为了加快计算速度。
004537C1 03CB add ecx, ebx //ECX=ECX+EBX
004537C3 8BD9 mov ebx, ecx //EBX=ECX。综上,这段代码的结果就是EBX=EBX*10+ECX
004537C5 42 inc edx //EDX+1,准备下一个循环
004537C6 48 dec eax //字符串长度计数
004537C7 75EB jnz 004537B4
004537C9 8BD6 mov edx, esi //把函数入口时保存的EDX取出来
004537CB 8BC3 mov eax, ebx //把求和结果EBX赋值给EAX
* Reference to: SysUtils.IntToStr(Integer):AnsiString;overload;
|
004537CD E85A48FBFF call 0040802C //整型转字符串,结果保存到EDX中
004537D2 33C0 xor eax, eax
004537D4 5A pop edx
004537D5 59 pop ecx
004537D6 59 pop ecx
004537D7 648910 mov fs:, edx
****** FINALLY
|
004537DA 68EF374500 push $004537EF
004537DF 8D45FC lea eax,
* Reference to: System.@LStrClr(void;void);
|
004537E2 E8F909FBFF call 004041E0
004537E7 C3 ret
* Reference to: System.@HandleFinally;
|
004537E8 E9CF03FBFF jmp 00403BBC
004537ED EBF0 jmp 004537DF
****** END
|
004537EF 5E pop esi
004537F0 5B pop ebx
004537F1 59 pop ecx
004537F2 5D pop ebp
004537F3 C3 ret
这段代码也结合着前面的DELPHI代码,对比着看,相信大家很容易就理解了。
这里有个小技巧,我们看反编译的代码的地址段是00453XXX,而程序里的CALL调用,很多都是CALL 0040XXXX,这说明调用的是DELPHI封装的函数,这个知道功能就行了,不需要跟进去。而我刚才说的那个函数,是CALL 00453780,与程序是同一个地址段,说明这个是一个用户函数,对于这样的函数,在调试的时候有必要跟进去查看。
好了,对于这个反编译的讲解就到这里,有什么不明白的欢迎留言。
例子程序也附上,方便大家调试学习。
基础东西先收藏一下。精彩文档谢谢分享。{:301_1003:} Delphi 的调用约定是 fastcall,前三个参数用 edx、eax 与 ecx 代替,之后的用堆栈。“返回值一般是 edx” 并不准确,应该说是使用 lea 计算地址的指令更可能是储存返回值的内存区域。 不错的讲解,帮顶一下,写这么多,真是辛苦了 真的很基础,收藏一下 感谢分享 学习了啊 很强大,拜读一下 fgffffffffffffffffffffffff 谢谢分享
好多看前学过点DELPHI,现在全还给老师了 :wwqwq分析得好透彻,我什么时候才打到你这种境界啊 学习了解!!