-
-
[原创]开局一场直播,我踏上了逆向还原的征程 一
-
发表于: 2025-3-16 17:40 1666
-
起点
最近在哔哩哔哩观看了钱老师(逆向老钱)关于逆向还原程序的直播,这场直播让我深受启发,仿佛打开了一扇通往新世界的大门。通过观看钱老师的讲解以及他分析的历史录屏,我对逆向还原这一领域产生了浓厚的兴趣,并决心深入学习和实践。作为一名初学者,我深知这条路上还有许多需要探索的地方,因此希望能得到各位前辈的指导与帮助,期待与大家共同进步!
话不多说,由此开始吧。
NO.1
以下代码是此次逆向还原程序的源代码。
1 2 3 4 5 6 7 8 9 | #include <stdio.h> int main( int argc, char * argv[]) { int a = 20 ; int b = 30 ; int c = b - a; printf( "%d\r\n" ,c); return 0 ; } |
静态分析(编译器选用VC6以及VS2010)
注意:分析时需要删除pdb符号文件
Debug
用IDA打开VC6 Debug版本的程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | .text: 0040B750 55 push ebp .text: 0040B751 8B EC mov ebp, esp .text: 0040B753 83 EC 4C sub esp, 4Ch 开辟栈空间 .text: 0040B756 53 push ebx .text: 0040B757 56 push esi .text: 0040B758 57 push edi .text: 0040B759 8D 7D B4 lea edi, [ebp + var_4C] .text: 0040B75C B9 13 00 00 00 mov ecx, 13h .text: 0040B761 B8 CC CC CC CC mov eax, 0CCCCCCCCh .text: 0040B766 F3 AB rep stosd 用IDA打开 VS2010 Debug版本的程序 保存异变寄存器以及将栈空间全部初始化为C .text: 0040B768 C7 45 FC 14 00 00 00 mov [ebp + var_4], 14h .text: 0040B76F C7 45 F8 1E 00 00 00 mov [ebp + var_8], 1Eh .text: 0040B776 8B 45 F8 mov eax, [ebp + var_8] .text: 0040B779 2B 45 FC sub eax, [ebp + var_4] .text: 0040B77C 89 45 F4 mov [ebp + var_C], eax .text: 0040B77F 8B 4D F4 mov ecx, [ebp + var_C] .text: 0040B782 51 push ecx .text: 0040B783 68 1C 00 42 00 push offset Format ; "%d\r\n" .text: 0040B788 E8 D3 58 FF FF call _printf 栈操作 .text: 0040B788 .text: 0040B78D 83 C4 08 add esp, 8 .text: 0040B790 33 C0 xor eax, eax .text: 0040B792 5F pop edi .text: 0040B793 5E pop esi .text: 0040B794 5B pop ebx .text: 0040B795 83 C4 4C add esp, 4Ch .text: 0040B798 3B EC cmp ebp, esp .text: 0040B79A E8 41 59 FF FF call __chkesp .text: 0040B79A .text: 0040B79F 8B E5 mov esp, ebp .text: 0040B7A1 5D pop ebp .text: 0040B7A2 C3 retn 恢复栈并返回 |
用IDA打开 VS2010 Debug版本的程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | .text: 00411380 55 push ebp .text: 00411381 8B EC mov ebp, esp .text: 00411383 81 EC E4 00 00 00 sub esp, 0E4h 开辟栈空间 .text: 00411389 53 push ebx .text: 0041138A 56 push esi .text: 0041138B 57 push edi .text: 0041138C 8D BD 1C FF FF FF lea edi, [ebp + var_E4] .text: 00411392 B9 39 00 00 00 mov ecx, 39h .text: 00411397 B8 CC CC CC CC mov eax, 0CCCCCCCCh .text: 0041139C F3 AB rep stosd 保存异变寄存器以及将栈空间填充为C .text: 0041139E C7 45 F8 14 00 00 00 mov [ebp + var_8], 14h .text: 004113A5 C7 45 EC 1E 00 00 00 mov [ebp + var_14], 1Eh .text: 004113AC 8B 45 EC mov eax, [ebp + var_14] .text: 004113AF 2B 45 F8 sub eax, [ebp + var_8] .text: 004113B2 89 45 E0 mov [ebp + var_20], eax .text: 004113B5 8B F4 mov esi, esp .text: 004113B7 8B 45 E0 mov eax, [ebp + var_20] .text: 004113BA 50 push eax .text: 004113BB 68 3C 57 41 00 push offset Format ; "%d\r\n" .text: 004113C0 FF 15 B0 82 41 00 call ds:printf 栈操作 .text: 004113C0 .text: 004113C6 83 C4 08 add esp, 8 .text: 004113C9 3B F4 cmp esi, esp .text: 004113CB E8 5C FD FF FF call 栈内操作解释 二者相同,就只解释一种了。 j___RTC_CheckEsp .text: 004113CB .text: 004113D0 33 C0 xor eax, eax .text: 004113D2 5F pop edi .text: 004113D3 5E pop esi .text: 004113D4 5B pop ebx .text: 004113D5 81 C4 E4 00 00 00 add esp, 0E4h .text: 004113DB 3B EC cmp ebp, esp .text: 004113DD E8 4A FD FF FF call j___RTC_CheckEsp .text: 004113DD .text: 004113E2 8B E5 mov esp, ebp .text: 004113E4 5D pop ebp .text: 004113E5 C3 retn .text: 004113E5 恢复栈并返回 |
栈内操作解释
二者相同,就只解释一种了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | .text: 0041139E C7 45 F8 14 00 00 00 mov [ebp + var_8], 14h 将 14h ( 20 )赋给局部变量a .text: 004113A5 C7 45 EC 1E 00 00 00 mov [ebp + var_14], 1Eh 将 1Eh ( 30 )赋给局部变量B .text: 004113AC 8B 45 EC mov eax, [ebp + var_14] 将局部变量B的值赋给eax寄存器 .text: 004113AF 2B 45 F8 sub eax, [ebp + var_8] 使用减法指令,使得变量B - A,将结果存在eax寄存器中 .text: 004113B2 89 45 E0 mov [ebp + var_20], eax 将eax的值赋给局部变量C 比较 从Debug版本的代码来看,VC6和VS2010在栈内操作上差别不大。 主要差别在于恢复栈并返回时使用了运行时检查函数的次数不同。 Release 用IDA打开VC6 Release版本的程序 .text: 004113B5 8B F4 mov esi, esp 将当前 ESP 的值复制到 ESI。这通常用来保存当前栈的位置,以便后续操作中使 用,并不会改变栈帧的结构(栈帧依旧由 EBP 固定)。 .text: 004113B7 8B 45 E0 mov eax, [ebp + var_20] .text: 004113BA 50 push eax 取出C的值存入栈中 .text: 004113BB 68 3C 57 41 00 push offset Format ; "%d\r\n" 取出格式符号存入栈中 .text: 004113C0 FF 15 B0 82 41 00 call ds:printf 调用printf函数输出 |
比较
从Debug版本的代码来看,VC6和VS2010在栈内操作上差别不大。
主要差别在于恢复栈并返回时使用了运行时检查函数的次数不同。
Release
用IDA打开VC6 Release版本的程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | .text: 004113B5 8B F4 mov esi, esp 将当前 ESP 的值复制到 ESI。这通常用来保存当前栈的位置,以便后续操作中使 用,并不会改变栈帧的结构(栈帧依旧由 EBP 固定)。 .text: 004113B7 8B 45 E0 mov eax, [ebp + var_20] .text: 004113BA 50 push eax 取出C的值存入栈中 .text: 004113BB 68 3C 57 41 00 push offset Format ; "%d\r\n" 取出格式符号存入栈中 .text: 004113C0 FF 15 B0 82 41 00 call ds:printf 调用printf函数输出 在VC6代码块中,只在恢复局部变量后、 cmp ebp, esp 后调用一次 __chkesp 来检查栈指针是否恢复正确。 VS2010代码块则在两个阶段进行了检查: 先在 add esp, 8 后通过 cmp esi, esp 并调用 j___RTC_CheckEsp,这可能用于验证某个保存的寄存器(如 esi) 或调用者参数的正确性; 然后,在恢复完局部空间后,通过 cmp ebp, esp 再次调用 j___RTC_CheckEsp 检查栈帧基址与栈指针是否一致。 |
Release
用IDA打开VC6 Release版本的程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | .text: 00401000 6A 0A push 0Ah .text: 00401002 68 30 60 40 00 push offset Format ; "%d\r\n" 用IDA打开VS2010 Release版本的程序 比较 VC6以及VS2010直接优化了程序的计算、赋值过程。直接将计算好的常量直 接作为参数传递 拓展 以下为如今编译器在release模式下所会采用的优化方式 .text: 00401007 E8 14 00 00 00 call _printf .text: 00401007 .text: 0040100C 83 C4 08 add esp, 8 .text: 0040100F 33 C0 xor eax, eax .text: 00401011 C3 retn |
用IDA打开VS2010 Release版本的程序
1 2 3 4 5 6 7 8 | .text: 00401000 6A 0A push 0Ah .text: 00401002 68 F4 20 40 00 push offset Format ; "%d\r\n" .text: 00401007 FF 15 A0 20 40 00 call ds:printf .text: 00401007 .text: 0040100D 83 C4 08 add esp, 8 .text: 00401010 33 C0 xor eax, eax .text: 00401012 C3 retn |
比较
VC6以及VS2010直接优化了程序的计算、赋值过程。直接将计算好的常量直
接作为参数传递
拓展一 编译器优化方式
以下为如今编译器在release模式下所会采用的优化方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 常量折叠(Constant Folding)与传播(Constant Propagation): 在编译期间计算所有可以确定的常量表达式,将常量值直接替换到代码中。 死代码消除(Dead Code Elimination): 移除那些永远不会被执行或其结果不会被使用的代码片段。 函数内联(Function Inlining): 将小函数的代码嵌入到调用处,减少函数调用开销。 WinDBG 循环优化: 包括循环展开(Loop Unrolling)、循环不变代码外提(Loop Invariant Code Motion)和循环融合等,以减少循环控制带来的开销。 寄存器分配优化(Register Allocation): 将变量尽可能分配到寄存器中,减少内存访问次数,提高运行速度。 公共子表达式消除(Common Subexpression Elimination): 合并重复计算的表达式,避免重复求值。 尾递归优化(Tail - Call Optimization): 对尾递归进行优化,转化为迭代,从而节省栈空间。 指令调度与短视优化(Peephole Optimization): 对小范围内的指令序列进行重排和合并,生成更高效的机器码。 矢量化(Vectorization): 利用 SIMD 指令集,把标量运算转换为并行的矢量运算(如果硬件支持)。 跨过程优化(Interprocedural Optimization, IPO / LTO): 在函数边界内进行优化,甚至整个程序级别的优化(如链接时优化 Link Time Optimization)。 配置导向优化(Profile Guided Optimization, PGO): 基于实际运行时的数据,优化热路径和分支预测(需要额外的运行数据)。 |
拓展二 WinDBG
常用命令(后续每次更新会加一些拓展内容)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | - 列出当前寄存器环境 0 : 000 > r eax = 00000000 ebx = 00000003 ecx = 10e70000 edx = 00000000 esi = 004000d8 edi = 003ec000 eip = 7717ceac esp = 0019fa44 ebp = 0019fa70 iopl = 0 nv up ei pl zr na pe nc cs = 0023 ss = 002b ds = 002b es = 002b fs = 0053 gs = 002b efl = 00000246 ntdll!LdrInitShimEngineDynamic + 0x6dc : 7717ceac cc int 3 k - 查看栈列表(kb 显示栈函数以及函数参数) 0 : 000 > k # ChildEBP RetAddr WARNING: Stack unwind information not available. Following frames may be wrong. 00 0019fa70 7714303f ntdll!LdrInitShimEngineDynamic + 0x6dc 01 0019fca8 77134556 ntdll!WinSqmAddToStreamEx + 0x1bdf 02 0019fcf8 7713446b ntdll!LdrInitializeThunk + 0x136 03 0019fd08 77134430 ntdll!LdrInitializeThunk + 0x4b 04 0019fd10 00000000 ntdll!LdrInitializeThunk + 0x10 p - 单步步过 0 : 000 > p eax = 00000000 ebx = 00000003 ecx = 10e70000 edx = 00000000 esi = 004000d8 edi = 003ec000 eip = 7717cead esp = 0019fa44 ebp = 0019fa70 iopl = 0 nv up ei pl zr na pe nc cs = 0023 ss = 002b ds = 002b es = 002b fs = 0053 gs = 002b efl = 00000244 ntdll!LdrInitShimEngineDynamic + 0x6dd : 7717cead eb07 jmp ntdll!LdrInitShimEngineDynamic + 0x6e6 ( 7717ceb6 ) t - 单步步入 0 : 000 > t eax = 00000000 ebx = 00000003 ecx = 10e70000 edx = 00000000 esi = 004000d8 edi = 003ec000 eip = 7717ceb6 esp = 0019fa44 ebp = 0019fa70 iopl = 0 nv up ei pl zr na pe nc Windows知识拓展 用户态和内核态 用户态(User Mode)和内核态(Kernel Mode)是两种不同的 CPU 运行 模式,它们主要用于区分普通应用程序和操作系统核心功能的执行权限。 简而言之: 用户态运行普通应用程序,权限受限,不能直接访问硬件和关键系统资源。 内核态运行操作系统核心代码,具有最高权限,可以直接控制硬件和管理系 统资源。 cs = 0023 ss = 002b ds = 002b es = 002b fs = 0053 gs = 002b efl = 00000246 ntdll!LdrInitShimEngineDynamic + 0x6e6 : 7717ceb6 c745fcfeffffff mov dword ptr [ebp - 4 ], 0FFFFFFFEh ss: 002b : 0019fa6c = 00000000 . cls - 清屏 |
今天就先写到这里吧!这是我的第一篇文章,但绝不会是最后一篇。作为一名初学者,我深知自己还有许多不足,因此非常期待能与各位前辈和同好交流学习。如果文章中有任何疏漏或不妥之处,恳请大家不吝指正。愿我们共同进步,在技术的道路上越走越远!
赞赏
赞赏
雪币:
留言: