--- title: 逆向工程与软件破解 date: 2019-03-28 15:25:04 tags: - 逆向 - 破解 categories: 二进制 --- # 软件保护方式 1. 功能限制 2. 时间限制 - 运行时长限制 - 使用日期限制 - 使用次数限制 3. 警告窗口 ![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1553759246/%E8%BD%AF%E4%BB%B6%E7%A0%B4%E8%A7%A3/%E5%9B%BE%E7%89%871.png) -------------------- # 分析工具 1. 静态分析工具 - IDA - W32Dasm - lordPE - Resource Hacker 2. 动态分析工具 - OllyDbg - WinDbg ----------------------- # 对抗分析技术 1. 反静态分析技术 - 花指令 - 自修改代码技术 - 多态技术 - 变形技术 - 虚拟机保护技术 2. 反动态分析技术 - 检测调试状态 - 检测用户态调试器 - 检测内核态调试器 - 其他方法:父进程检测;StartupInfo 结构;时间差;通过Trap Flag检测 3. 发现调试器后的处理 - 程序自身退出 - 向调试器窗口发送消息使调试器退出 - 使调试器窗口不可用 - 终止调试器进程 ---------------------- # PE文件格式基础 ------------ # 加壳脱壳 ------------------ # 反调试技术 反调试技术,程序用它来识别是否被调试,或者让调试器失效。为了阻止调试器的分析,当程序意识到自己被调试时,它们可能改变正常的执行路径或者修改自身程序让自己崩溃,从而增加调试时间和复杂度。 ## 探测windows调试器 1. 使用windows API 使用Windows API函数探测调试器是否存在是最简单的反调试技术。 通常,防止使用API进行反调试的方法有在程序运行期间修改恶意代码,使其不能调用API函数,或修改返回值,确保执行合适的路径,还有挂钩这些函数。 常用来探测调试器的API函数有:`IsDebuggerPresent` `CheckRemoteDebuggerPresent` `NtQueryInformationProcess` `OutputDebuggString` 2. 手动检测数据结构 程序编写者经常手动执行与这些API功能相同的操作 - 检查BeingDebugged属性 - 检测ProcessHeap属性 - 检测NTGlobalFlag 3. 系统痕迹检测 通常,我们使用调试工具来分析程序,但这些工具会在系统中驻留一些痕迹。程序通过搜索这种系统的痕迹,来确定你是否试图分析它。例如,查找调试器引用的注册表项。同时,程序也可以查找系统的文件和目录,查找当前内存的痕迹,或者查看当前进程列表,更普遍的做法是通过FindWindows来查找调试器。 ## 识别调试器的行为 在逆向工程中,可以使用断点或单步调试来帮助分析,但执行这些操作时,会修改进程中的代码。因此可以使用几种反调试技术探测INT扫描、完整性校验以及时钟检测等几种类型的调试器行为。 1. INT扫描 调试器设置断点的基本机制是用软件中断INT 3,机器码为0xCC,临时替换程序中的一条指令。因此可以通过扫描INT 3修改来检测。 2. 执行代码校验和检查 与INT扫描目的相同,但仅执行机器码的CRC或MD5校验和检查。 3. 时钟检测 被调试时,进程的运行速度大大降低,常用指令有:`rdstc` `QueryPerformanceCounter` `GetTickCount`,有如下两种方式探测时钟: - 记录执行一段操作前后的时间戳 - 记录触发一个异常前后的时间戳 ## 干扰调试器的功能 本地存储(TLS)回调:TLS回调被用来在程序入口点执行之前运行代码,这发生在程序刚被加载到调试器时 使用异常:使用SEH链可以实现异常,程序可以使用异常来破坏或探测调试器,调试器捕获异常后,并不会将处理权立即返回给被调试进程。 插入中断:插入INT 3、INT 2D、ICE ## 调试器漏洞 PE头漏洞、OutputDebugString漏洞 ------------------------------ # 实验一:软件破解 ## 对象 [crack.exe](https://res.cloudinary.com/dozyfkbg3/raw/upload/v1553761280/%E8%BD%AF%E4%BB%B6%E7%A0%B4%E8%A7%A3/crack.exe1),28.0 KB - 无保护措施:无壳、未加密、无反调试措施 - 用户名至少要5个字节 - 输入错误验证码时输出:“Bad Boy!” ## 爆破 ### 查找显示注册结果相关代码 当输入错误验证码时,程序会输出“Bad Boy”,因此我们将程序拖入IDA,以流程图显示函数内部的跳转。查找“Bad Boy”字符串,我们可以定位到显示注册结果的相关代码: ![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1553772615/%E8%BD%AF%E4%BB%B6%E7%A0%B4%E8%A7%A3/%E6%8D%95%E8%8E%B71.png) ### 查找注册码验证相关代码 用鼠标选中程序分支点,按空格切换回汇编指令界面 ![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1553773066/%E8%BD%AF%E4%BB%B6%E7%A0%B4%E8%A7%A3/%E6%8D%95%E8%8E%B72.png) 可以看到,这条指令位于PE文件的.text节,并且IDA已经自动将地址转换为运行时的内存地址`VA:004010F9` ### 修改程序跳转 - 现在关闭IDA,换用OllyDbg进行动态调试来看看程序时如何分支跳转的`Ctrl+G`直接跳到由IDA得到的`VA:004010F9`处查看那条引起程序分支的关键指令 - 选中这条指令,按F2设置断点,再按F9运行程序,这时候控制权会回到程序,OllyDbg暂时挂起。到程序提示输入名字和序列号,随意输入(名字大于五个字节),点击ok后,OllyDbg会重新中断程序,收回控制权,如图: ![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1553775053/%E8%BD%AF%E4%BB%B6%E7%A0%B4%E8%A7%A3/%E6%8D%95%E8%8E%B74.png) - 验证函数的返回值存于EAX寄存器中,if语句通过以下两条指令执行 ``` cmp eax,ecx jnz xxxxxxx ``` - 也就是说,当序列号输入错误时,EAX中的值为0,跳转将被执行。 如果我们把`jnz`这条指令修改为`jz`,那么整个程序的逻辑就会反过来。 双击`jnz`这条指令,将其改为`jz`,单击"汇编"将其写入内存 ![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1553775817/%E8%BD%AF%E4%BB%B6%E7%A0%B4%E8%A7%A3/%E6%8D%95%E8%8E%B75.png) 可以看到此时程序执行了相反的路径 - 上面只是在内存中修改程序,我们还需要在二进制文件中也修改相应的字节,这里考察VA与文件地址之间的关系 - 用LordPE打开.exe文件,查看PE文件的节信息 ![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1553776239/%E8%BD%AF%E4%BB%B6%E7%A0%B4%E8%A7%A3/%E6%8D%95%E8%8E%B76.png) 根据VA与文件地址的换算公式: ``` 文件偏移地址 = VA - Image Base - 节偏移 = 0x004010F9 - 0x00400000 - 0 = 0x10F9 ``` 也就是说,这条指令在PE文件中位于`10F9`字节处,使用010Editer打开crack.exe,将这一字节的`75(JNZ)``改为`74(JZ)``,保存后重新执行,破解成功! ## 编写注册机 ### 查找显示注册结果相关代码 通过查找字符串“good boy”等,我们可以找到显示注册结果的相关代码 ### 查找注册码验证相关代码 因为检测密钥是否正确时会将结果返回到EAX寄存器中,因此,在检测密钥前必然会对EAX寄存器清空,由此我们可以找到注册码验证的相关代码。 ![](https://res.cloudinary.com/dozyfkbg3/image/upload/v1553858953/%E8%BD%AF%E4%BB%B6%E7%A0%B4%E8%A7%A3/%E6%8D%95%E8%8E%B77.png) ### 根据注册码验证代码编写注册机 分析上图算法,按tab键转换为高级语言 ``` for ( i = 0; i < v6; v12 = v10 ) v10 = (v6 + v12) * lpStringa[i++]; if ( (v12 ^ 0xA9F9FA) == atoi(v15) ) MessageBoxA(hDlg, aTerimaKasihKer, aGoodBoy, 0); ``` 可以看出,生成注册码主要在for循环中完成,之后将生成的注册码与输入相比较,判断是否正确。 所以,只要能弄明白`v6,v12,v10,v15`的含义,我们就可以轻松的编写注册机。 打开ollybdg,在进入循环之前设下断点,动态调试程序 ``` 004010CC |> /8B4D 10 |mov ecx,[arg.3] //此时ecx为name 004010CF |. 8B55 0C |mov edx,[arg.2] //edx为0x1908 004010D2 |. 03D3 |add edx,ebx //edx加上name的长度(ebx) 004010D4 |. 0FBE0C08 |movsx ecx,byte ptr ds:[eax+ecx] //ecx=61h 004010D8 |. 0FAFCA |imul ecx,edx //61h(a) * edx 004010DB |. 40 |inc eax //eax加1(初始为0) 004010DC |. 894D 0C |mov [arg.2],ecx 004010DF |. 3BC3 |cmp eax,ebx //循环是否结束 ``` `arg.3`为输入的`name`,`arg.2`初始为`0x1908`,`ebx`为`name`的长度,`eax`每次循环加1直到等于长度 因此,我们可以对参数的含义进行解释如下 ``` v12 = 6408; //0x1908 v10 = 6408; //0x1908 v6 = len(name); v12 = input_serial; for ( i = 0; i < v6; i++ ){ v12 = v10; v10 = (v6 + v12) * lpStringa[i]; } if ((v12 ^ 0xA9F9FA) == atoi(v15)){ MessageBoxA(hDlg, aTerimaKasihKer, aGoodBoy, 0); } ``` 可见,`v12^0xA9F9FA`的结果即是正确的注册码,我们编写一个[简单的程序](https://res.cloudinary.com/dozyfkbg3/raw/upload/v1553937750/%E8%BD%AF%E4%BB%B6%E7%A0%B4%E8%A7%A3/reg.cpp)帮助我们生成注册码: ``` #include #include using namespace::std; int main(){ int v12; int v10 = 6408; //0x1908 string name; cout << "请输入name: "; cin >> name; int len = name.size(); for(int i = 0; i < len+1; i++ ){ v12 = v10; v10 = (len + v12) * name[i]; } cout<<"\n"<<"注册码为: "<<(v12 ^ 0xA9F9FA)<