此系列是本人一个字一个字码出来的,包括示例和实验截图。可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏逆向指引——序 ,方便学习本教程。
本次示例使用C++
,也就是编译型语言,为了方便查看汇编代码和编码故使用VS 2022
,其他IDE
即可,代码如下:
#include <iostream> using namespace std; int main(int argc, char* argv[]) { int x = 0; cout << "请输入密钥:" << endl; cin >> x; if (x == 1234) { cout << "成功,By.寂静的羽夏,CNBLOG Only!!!" << endl; } else { cout << "失败,By.寂静的羽夏,CNBLOG Only!!!" << endl; } system("pause"); return 0; }
是不是代码很简单?判断你输入的是不是1234
,是的话就是成功,反之失败。这是最简单的校验示例了。
代码的流程十分简单,我们可以用下面的流程图进行表示:
graph TD; A("程序开始")-->B("显示信息,提示输入")-->C[/"获取输入"/]-->D{"判断是否和 1234 相等"}; D--YES-->E("输出正确信息")-->F("程序结束"); D-.NO.->G("输出错误信息")-->F; 由于是初次分析编译型程序,我们直接在IDE
先看看它的反汇编:
#include <iostream> using namespace std; int main(int argc, char* argv[]) { 00E42550 push ebp 00E42551 mov ebp,esp 00E42553 sub esp,0D0h 00E42559 push ebx 00E4255A push esi 00E4255B push edi 00E4255C lea edi,[ebp-10h] 00E4255F mov ecx,4 00E42564 mov eax,0CCCCCCCCh 00E42569 rep stos dword ptr es:[edi] 00E4256B mov eax,dword ptr [__security_cookie (0E4C004h)] 00E42570 xor eax,ebp 00E42572 mov dword ptr [ebp-4],eax 00E42575 mov ecx,offset _3226632D_ConsoleApplication3@cpp (0E4F066h) 00E4257A call @__CheckForDebuggerJustMyCode@4 (0E41384h) int x = 0; 00E4257F mov dword ptr [x],0 cout << "请输入密钥:" << endl; 00E42586 mov esi,esp 00E42588 push offset std::endl<char,std::char_traits<char> > (0E4103Ch) 00E4258D push offset string "\xc7\xeb\xca\xe4\xc8\xeb\xc3\xdc\xd4\xbf\xa3\xba" (0E49B30h) 00E42592 mov eax,dword ptr [__imp_std::cout (0E4D0DCh)] 00E42597 push eax 00E42598 call std::operator<<<std::char_traits<char> > (0E411A9h) 00E4259D add esp,8 00E425A0 mov ecx,eax 00E425A2 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E4D0A8h)] 00E425A8 cmp esi,esp 00E425AA call __RTC_CheckEsp (0E4128Fh) cin >> x; 00E425AF mov esi,esp 00E425B1 lea eax,[x] 00E425B4 push eax 00E425B5 mov ecx,dword ptr [__imp_std::cin (0E4D098h)] 00E425BB call dword ptr [__imp_std::basic_istream<char,std::char_traits<char> >::operator>> (0E4D09Ch)] 00E425C1 cmp esi,esp 00E425C3 call __RTC_CheckEsp (0E4128Fh) if (x == 1234) 00E425C8 cmp dword ptr [x],4D2h 00E425CF jne __$EncStackInitStart+0A0h (0E425FCh) { cout << "成功,By.寂静的羽夏,CNBLOG Only!!!" << endl; 00E425D1 mov esi,esp 00E425D3 push offset std::endl<char,std::char_traits<char> > (0E4103Ch) 00E425D8 push offset string "\xb3\xc9\xb9\xa6\xa3\xacBy.\xbc\xc5\xbe\xb2\xb5\xc4\xd3\xf0\xcf\xc4\xa3\xacCNBLOG Onl@"... (0E49B64h) 00E425DD mov eax,dword ptr [__imp_std::cout (0E4D0DCh)] 00E425E2 push eax 00E425E3 call std::operator<<<std::char_traits<char> > (0E411A9h) 00E425E8 add esp,8 00E425EB mov ecx,eax 00E425ED call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E4D0A8h)] 00E425F3 cmp esi,esp 00E425F5 call __RTC_CheckEsp (0E4128Fh) } 00E425FA jmp __$EncStackInitStart+0C9h (0E42625h) else { cout << "失败,By.寂静的羽夏,CNBLOG Only!!!" << endl; 00E425FC mov esi,esp 00E425FE push offset std::endl<char,std::char_traits<char> > (0E4103Ch) 00E42603 push offset string "\xca\xa7\xb0\xdc\xa3\xacBy.\xbc\xc5\xbe\xb2\xb5\xc4\xd3\xf0\xcf\xc4\xa3\xacCNBLOG Onl@"... (0E49D40h) 00E42608 mov eax,dword ptr [__imp_std::cout (0E4D0DCh)] 00E4260D push eax 00E4260E call std::operator<<<std::char_traits<char> > (0E411A9h) 00E42613 add esp,8 00E42616 mov ecx,eax 00E42618 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0E4D0A8h)] 00E4261E cmp esi,esp 00E42620 call __RTC_CheckEsp (0E4128Fh) } system("pause"); 00E42625 mov esi,esp 00E42627 push offset string "pause" (0E49B40h) 00E4262C call dword ptr [__imp__system (0E4D1D4h)] 00E42632 add esp,4 00E42635 cmp esi,esp 00E42637 call __RTC_CheckEsp (0E4128Fh) return 0; 00E4263C xor eax,eax }
看不懂汇编的意思的,请自学汇编。看不懂整体汇编语言的作用,请学习我的教程 羽夏看C语言 即可看懂。
输出信息最关键的当然是判断了,也就是下面这个部分:
if (x == 1234) 00E425C8 cmp dword ptr [x],4D2h 00E425CF jne __$EncStackInitStart+0A0h (0E425FCh)
如果我把jne
给干掉,无论我输入什么,都会输出正确。也就是我们把判断失败的链条给砍断了,只能走这条路。
上面只改汇编代码的,通过修改汇编以修改代码执行流程实现自己的目的,就是我们所谓的暴力破解,也就是patch
,俗称打补丁。如果我逆向分析得到密钥就是1234
,直接输入,这东西也就是所谓的key
。当然,现实中验证不可能这么简单,它们往往是通过某种算法,通过获取计算机独特的信息生成注册码(如果我们能够知道算法是什么,根据算法来写出算key
的软件,俗称注册机
),然后根据校验函数进行校验,大大增大了分析难度。这就是说为什么爆破比注册机简单很多。
但是爆破也不是那么轻松的,如果在校验函数中生成了大量的中间结果,如果它再拿来这东西校验的话,被破解的难度就会提高一个层次。发现被破解,直接退出程序,这往往是付费软件防破解的一个手段。
当然,我们破解软件的时候不可能是拿到别人的源码按照本篇文章开头那样进行分析。我们来真枪实战一下。假设这个软件是我们未知的,如何分析这个程序。
本小节是按照常规套路进行分析,分析不同类型的软件可能流程不太一样,比如分析病毒你肯定不能直接在真机上跑,有些病毒还有反虚拟机的模块,甚至变形多态;编译型代码软件分析和解释型代码软件分析也不太一样;只是分支操作有所不同。接下来是编译型软件的常规操作:
查壳,看看是啥软件编写的:
直接拖到IDA
瞅一瞅,如果没报错,说明软件可以直接分析。如果报错,说明软件被加壳或者加密。当然这软件没有加壳,所以正常,由于软件短小精悍,我们很快定位到主函数部分,下面是它的流程:
看到汇编代码后,IDA
会有一些注释,看下图:
这个明明是字符串,但IDA
没有识别出来。这个是经常会遇到的情况。有时候IDA
识别不出Unicode
字符串,甚至识别错误以为是函数偏移,不要仅依赖这个软件。那么我们如何处理呢?把光标放到上面字符串的首地址,按下ALT + A
,将会弹出下面的窗体:
这个就是将数据转化为字符串,由于这个是普通的ASCII
字符串,直接选C-style
即可。
转换成功后结果如下:
听说IDA
有一个F5
的功能,的确,它是IDA
的一个插件。如果没有选择调试器的情况下,它会默认调用Hex-ray
这个插件翻译成伪C代码
。但是不要过度依赖这个插件,因为翻译出来的东西很多是不准确的,甚至是错误的。比如函数调用的参数个数、多个变量其实就是一个变量、类型不对等等。这些东西都需要依赖自身的开发经验和分析进行调整。经过分析和重命名后,如下所示:
通过分析,我们既得到了Key
,也分析透了函数流程。
IDA
其实可以patch
软件的,怎么搞自行学习,比较麻烦,一旦patch
就会导致原来的分析流程变化,自认为不太适用,如果用于去除花指令的话另当别论。在动态调试器里修改是最快捷的,最舒服的方式。如下的动图进行演示:
学习本篇指引,如果IDA
不会使用的话,请自行找教程进行练习。本系列教程只是个指引,故不提供IDA
使用教程。IDA
有如下基本操作:函数和变量的重命名、跳转、类型转化、搜索字符串、查找引用是最常用的操作了,请自行学习。
羽夏逆向指引——补丁