这里先说一声对不起哈,我的文字表达能力不太好,如果有文笔不通之处还请多多包涵了
【文章标题】:
《2008
看雪论坛读书月第二题》分析随笔
【文章作者】: mstwugui
【软件名称】: 2008
看雪论坛读书月第二题CrackMe02
【文件MD5
】: 62c5e33505563dbd8a597dbaf5331f8e
【下载地址】:
http://bbs.pediy.com/showthread.php?t=68795
【加壳方式】:
不知道具体是什么壳,但很明显被混淆保护了
【开发环境】: Microsoft Visual Studio .NET 2008
【使用工具】: OllyDbg, regshot, ApiMonitor
【操作平台】: XP SP3 + .NET Framework 2.0 SP1
【软件介绍】: 2008
看雪论坛读书月第二题
刚看到第二题时有些意外也有些开心,正好没有分析过.NET
程序这下有机会熟悉熟悉练习一下了,:)于是立刻下载下来运行一下
倒。。。电脑上安装好的.NET 1.1
看来没用,再下个2.0 SP1
吧
咣当。。。万事开头难,坚持。。。坚持。。。看了看论坛,似乎其他朋友也遇到了这样那样的问题,但也有成功运行的,看来是环境问题。
重启电脑。。。开机后立刻运行CrackMe.net.exe
,哈,运气不错,正常启动了
可是。。。接下来我该从哪开始呢?
。。。
嗯,还是先打开google
搜索了一下.NET
的反编译吧
Reflector…Dis#...Salamander…
看雪的题目果然不一般,想想也对,这道题肯定是混淆保护过的,如果从网上随便下个工具就能反编译出源代码那还玩个什么劲啊,算了吧,还是不浪费时间偷懒找反编译了,老老实实自己动手一定也可以丰衣足食,:)
于是习惯性的IDA
了一眼。。。倒。。。。一堆问号不知所云。。。再换OD
看看。。。哈,终于看到些认识的蝌蚪文了
jmp dword ptr [<&mscoree._CorExeMain>]
很明显如果跟进去也只是系统调用里瞎转悠,既然不熟悉.NET
也不知道该再哪下断点,还是暂且搁置一下先
再来运行一下CrackMe.net.exe
,随手输入usernmae
和key
然后点regist
再
倒。。。又出错退出了,可是我也没有多干啥呀,和刚才刚启动好的时候也就多了几个进程,难道是内存不够?嗯,还是先把那几个新启动的进程都给关了再试试。汗。。。又正常启动了,可能是保护机制有BUG
吧看来是程序启动时校验username
和key,
这种情况下通常都是将注册信息保存在注册表或是文件系统中。嗯,用regshot
对文件系统和注册表做一个snapshot
再regist
然后比较前后区别。。。果然不出所料,注册信息存放在注册表中
HKEY_CURRENT_USER\Software\MegaX\name: "masterwugui"
HKEY_CURRENT_USER\Software\MegaX\key: "testkey"
嗯,回OD
以前还需要确认一下具体是哪个API
读取的注册信息,打开ApiMonitor
设置监视RegQueryValueA, RegQueryValueW, RegQueryValueExA, RegQueryValueExW
,然后重新启动CrackMe.net.exe
,哈,注册表访问还不少,在接近最尾部的一处调用看到了是通过RegQueryValueExW
读取的注册信息。
是时候回到OD
了,开始调试CrackMe.net.exe
,停在入口点后在ADVAPI32.RegQueryValueExW
设一个断点,并设置条件为:
((([[esp+8]] == 0x0061006E) || ([[esp+8]] == 0x0065006B)) && ([esp+0x14] != 0))
为什么要设置这个条件?呵呵,RegQueryValueExW
可不会只被调用一次,我们只需要在读取name
和key
的时候中断
按下F9
。。。开跑。。。停下来了,先看一眼[esp+8]
,嗯,指向的是name
,在[esp+0x14]
设一个单字节硬件访问断点A
,继续跑。。。
先是停在了ntdll.memmove
里面,向缓冲写入刚刚从注册表里读取出来的用户名,继续跑。。。
接下来停在mscorwks
里面,看代码很明显是计算字符串长度,继续跑。。。
mov si, word ptr [eax]
inc eax
inc eax
cmp si, bx
jnz $-8
再接下来又停在了msvcr80.memcpy
里面,对目标地址再设一个单字节硬件访问断点B
,继续跑。。。
又停在mscorwks
,不过这次是清空断点A
的字符串,看来断点A
指向的是个临时缓冲,删除了断点A
,继续跑。。。
add word ptr [edi+eax], 0
哈,回到了RegQueryValueExW
,赶快看看[esp+8]
,嗯,现在指向的是key
了,重复上面的步骤直到设置单字节硬件访问断点C
好了,现在有了两个单字节硬件访问断点B
和C
,分别指向name
和key
。有了这些基本信息后我们可以先F9
简单看一下流程
连续在mscorlib
里面同一处停下来两次,一次是因为name
,一次是key
,但没有什么特别的,系统调用,跳过。。。
接下来还是停在了mscorlib,
只不过这次似乎有些文章,请注意下图中加红框的部分,此时AX
中存放了key
指向的字符,而EDX
指向的是一个字符’-’
看来key
中必须有字符’-’
,而根据蓝色的这一句看来,应该需要两个或更多的’-’
,不过我们暂时先忽略这个问题,继续F9
。。。哈,没有再遇到其他的断点直接跑出来了
很明显我们要想继续跟踪下去必须先修改key
,如果key
中没有’-‘
字符就会直接注册校验失败。先把key
改为1234-5678
,然后再重新调试。
嗯,现在又走近了一步,停在了mscorlib
中,很明显这次是把key
中‘-’
字符之前的子字符串复制到目标地址,因此在目标地址下个单字节硬件访问断点D
,继续F9
。。。
什么?又是畅通无阻直接跑出来了?看来key
的格式还有文章,先重新调试回到上一步。但这次我们不要F9
直接跑出来,重复用Ctrl+F9
执行到返回查看一下究竟有什么文章。直到返回到这一层
在这里继续单步F8连续几个跳转之后,直到如下代码
这个跳转如果没有进入很快就会从这个函数中返回,所以这里肯定也是一道关卡。原来[eax+4]
中存放的是key
分割以后的数量,因此我们需要四组由’-‘
分开的key
。现在将key
改为1234-5678-ABCD-EFGH
后,重新调试
在经过上一步数量校验之后,我们继续单步直到如下代码:
此时,ecx
指向的是分割后的字符串对象(注意这里指向的是个对象而不是字符串本身),而[B1659C]
的作用是取该字符串的长度,因此key
不仅仅需要3
个’-‘
分割,而且每组分割后的字符串长度必须为5
。将key
修改为01234-56789-ABCDE-FGHIJ
,重新调试
先F9
连续跑一次试试,哈,看来没有更多的格式校验了,我们终于重新停在了指向name
的断点B
上。确定好格式没有问题之后,是时候该改变跟踪策略了。
因为上一步校验每组密码字符串长度所处的代码在临时分配空间,所以无法直接在此处设置断点重新启动后自动中断下来。而且每次因为太多硬件断点停留也烦人,所以可以先删除所有其他的断点只保留指向name
的断点B
。当断点B
在RegQueryValueExW
取得name
以后,下一次中断时这个分组字符串长度校验所处的内存空间已经正常分配,这时候就可以在cmp eax, 5
这条指令上下一个断点,然后F9
直接跑到这里。连续四次校验完四组密钥之后,一路F8
(此时还会路过4
次分割key
,但我们暂时忽略),直到看到如下代码
此时压入堆栈的eax
和[ebp-dc]
分别指向两个字符串对象”MegaX”
和用户名”masterwugui”
,而ecx
和edx
分别指向密钥中的第一”01234”
和第二组字符串”56789”
。
现在在下一个调用”call dword ptr [B165B0]”
下个断点E
,继续F9
。。。
我们会因为指向name
的断点B
中断两次,直接跳过,直到回到刚刚添加的断点E
,这时候查看一下ecx
和edx
的值,原来前面那个调用是把那四个字符串合成起来,ecx
因此指向了一个字符串对象”0123456789MegaXmasterwugui”
,而edx
指向的是第三组密钥字符串”ABCDE”
呵呵,黎明越来越近了,很明显下一个调用”call dword ptr [B165B0]”
非常关键,立刻F7
单步进入。这里会连续两次动态解析加载代码,直接跳过
在第二次从上述代码返回后,程序开始对之前传入的两个字符串对象”0123456789MegaXmasterwugui”
以及”ABCDE”
进行一连串的加密动作,这里我们暂时先忽略,继续运行到下图中红框处
此时eax
指向一个字符串对象”uVuyv”
,也就是上面两个字符串计算出来的结果。因此对该地址下一个单字节硬件访问断点,F9
继续跑
第一次停留没有什么特别之处,直接跳过,但第二次停留时。。。
字符串”uVuyv”
被转换成了大写并在被保存到[edi+eax]
中,继续在[edi+eax]
下一个单字节硬件访问断点F
,继续F9
。。。
看来是对上面计算出来的字符串”UVUYV”
再次加密,此时红框里edx
指向我们输入的第四组密钥字符串,而ecx
指向的则是正确的第四组密钥字符串,终于得到了第一组有效的username
和key
Username: masterwugui
Key: 01234-56789-ABCDE-RGHF3
庆祝一下?别急别急,还有两个问题没有解决”RGHF3”
和”UVUYV”
分别是怎么计算出来的
第一个问题比较容易,分析一下最后这一步很容易就得出了
[FONT=新宋体]CString CalcFinalKey(CString sKey)[/FONT]
[FONT=新宋体]{[/FONT]
[FONT=新宋体] DWORD i;[/FONT]
[FONT=新宋体] CString sRet = [COLOR=#a31515]""[/COLOR];[/FONT]
[FONT=新宋体] CString sMegaX = [COLOR=#a31515]"MegaX"[/COLOR];[/FONT]
[FONT=新宋体] CString sFinalKeys = [COLOR=#a31515]"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[/COLOR];[/FONT]
[FONT=新宋体] BYTE c;[/FONT]
[FONT=新宋体] [COLOR=blue]for[/COLOR] (i=0; i<5; i++)[/FONT]
[FONT=新宋体] {[/FONT]
[FONT=新宋体] c = sKey.GetAt(i);[/FONT]
[FONT=新宋体] c += sMegaX.GetAt(i);[/FONT]
[FONT=新宋体] c += 0x23;[/FONT]
[FONT=新宋体] sRet += sFinalKeys.GetAt(c%0x24);[/FONT]
[FONT=新宋体] }[/FONT]
[FONT=新宋体] [COLOR=blue]return[/COLOR] sRet;[/FONT]
[FONT=新宋体]}[/FONT]
问题是”uVuyv”
是从哪里来的。。。现在我们如果回到前面去慢慢单步分析多半会头晕眼花,还是对”uVuyv”
设置单字节硬件写入断点反着追吧
接下来并没有多少难点也就不多费口舌了,在连续的设置单字节写入断点反追之后,发现”uVuyv”
来自与一个字符串"1129720123519959835898227196100347012562"
的计算,而这个字符串则是由16
个字节分别转为十进字数后合成的。
又多了一个问题,这16
个字节哪来的?同上面一样,继续反追之后发现最后对这16
个字节进行写操作是ADVAPI32.CryptGetHashParam
,看到这里是不是突然明白了?立刻在ADVAPI32.CryptCreateHash
下一个断点重新调试,果然不出所料,在最后写入那关键的16
个字节之前,CryptCreateHash
用的参数是CALC_MD5
现在大家应该都明白了,原来这16
个字节就是之前用”0123456789MegaXmasterwugui”
和第三组密钥字符串”ABCDE”
合成的字符串"CrackMe0123456789MegaXmasterwuguiMegaX"
计算出来的MD5
,好了,现在可以完成最后的步骤,写注册机了。
[COLOR=blue][FONT=新宋体]char[/FONT][/COLOR][FONT=新宋体] GetRandByte()[/FONT]
[FONT=新宋体]{[/FONT]
[FONT=新宋体] [COLOR=blue]char[/COLOR] c = rand()%36;[/FONT]
[FONT=新宋体] c += (c<10)?0x30:0x37;[/FONT]
[FONT=新宋体] [COLOR=blue]return[/COLOR] c;[/FONT]
[FONT=新宋体]}[/FONT]
[FONT=新宋体]CString GenerateRandKey()[/FONT]
[FONT=新宋体]{[/FONT]
[FONT=新宋体] CString sRandKey = [COLOR=#a31515]""[/COLOR];[/FONT]
[FONT=新宋体] [COLOR=blue]for[/COLOR] ([COLOR=blue]int[/COLOR] i=0;i<5;i++)[/FONT]
[FONT=新宋体] sRandKey += GetRandByte();[/FONT]
[FONT=新宋体] [COLOR=blue]return[/COLOR] sRandKey;[/FONT]
[FONT=新宋体]}[/FONT]
[FONT=新宋体]CString CalcFinalKey(CString sKey)[/FONT]
[FONT=新宋体]{[/FONT]
[FONT=新宋体] DWORD i;[/FONT]
[FONT=新宋体] CString sRet = [COLOR=#a31515]""[/COLOR];[/FONT]
[FONT=新宋体] CString sMegaX = [COLOR=#a31515]"MegaX"[/COLOR];[/FONT]
[FONT=新宋体] CString sFinalKeys = [COLOR=#a31515]"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[/COLOR];[/FONT]
[FONT=新宋体] BYTE c;[/FONT]
[FONT=新宋体] [COLOR=blue]for[/COLOR] (i=0; i<5; i++)[/FONT]
[FONT=新宋体] {[/FONT]
[FONT=新宋体] c = sKey.GetAt(i);[/FONT]
[FONT=新宋体] c += sMegaX.GetAt(i);[/FONT]
[FONT=新宋体] c += 0x23;[/FONT]
[FONT=新宋体] sRet += sFinalKeys.GetAt(c%0x24);[/FONT]
[FONT=新宋体] }[/FONT]
[FONT=新宋体] [COLOR=blue]return[/COLOR] sRet;[/FONT]
[FONT=新宋体]}[/FONT]
[FONT=新宋体]CString CalcKey(CString sName, CString sPassword1, CString sPassword2, CString sPassword3)[/FONT]
[FONT=新宋体]{[/FONT]
[FONT=新宋体] CString sTemp = [COLOR=#a31515]"CrackMe"[/COLOR] + sPassword1 + sPassword2 + [COLOR=#a31515]"MegaX"[/COLOR] + sName + [COLOR=#a31515]"MegaX"[/COLOR];[/FONT]
[FONT=新宋体] CString sTemp1 = [COLOR=#a31515]""[/COLOR];[/FONT]
[FONT=新宋体] CString sRet = [COLOR=#a31515]""[/COLOR];[/FONT]
[FONT=新宋体] CString sKeys = [COLOR=#a31515]"8x3p5BeabcdfghijklmnoqrstuvwyzACDEFGHIJKLMNOPQRSTUVWXYZ1246790"[/COLOR];[/FONT]
[FONT=新宋体] HCRYPTPROV hProv;[/FONT]
[FONT=新宋体] HCRYPTHASH hHash;[/FONT]
[FONT=新宋体] [COLOR=blue]if[/COLOR] (CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, 0xF0000000))[/FONT]
[FONT=新宋体] {[/FONT]
[FONT=新宋体] [COLOR=blue]if[/COLOR] (CryptCreateHash(hProv, CALG_MD5, NULL, 0, &hHash))[/FONT]
[FONT=新宋体] {[/FONT]
[FONT=新宋体] [COLOR=blue]if[/COLOR] (CryptHashData(hHash, ([COLOR=blue]const[/COLOR] BYTE*)sTemp.GetBuffer(), sTemp.GetLength(), 0))[/FONT]
[FONT=新宋体] {[/FONT]
[FONT=新宋体] DWORD size = 16;[/FONT]
[FONT=新宋体] BYTE code[16];[/FONT]
[FONT=新宋体] [COLOR=blue]int[/COLOR] offset;[/FONT]
[FONT=新宋体] [COLOR=blue]if[/COLOR] (CryptGetHashParam(hHash, HP_HASHVAL, (BYTE*)code, &size, 0))[/FONT]
[FONT=新宋体] {[/FONT]
[FONT=新宋体] DWORD i, j;[/FONT]
[FONT=新宋体] [COLOR=blue]for[/COLOR] (i=0; i<size; i++)[/FONT]
[FONT=新宋体] {[/FONT]
[FONT=新宋体] sTemp.Format([COLOR=#a31515]"%d"[/COLOR], code[i]);[/FONT]
[FONT=新宋体] sTemp1 += sTemp;[/FONT]
[FONT=新宋体] }[/FONT]
[FONT=新宋体] [COLOR=blue]for[/COLOR] (i=0; i< 5; i++)[/FONT]
[FONT=新宋体] {[/FONT]
[FONT=新宋体] j = sKeys.Find(sPassword3.GetAt(i));[/FONT]
[FONT=新宋体] offset = sKeys.Find(sTemp1.GetAt(i)) - j;[/FONT]
[FONT=新宋体] [COLOR=blue]if[/COLOR] (offset < 0)[/FONT]
[FONT=新宋体] offset += sKeys.GetLength();[/FONT]
[FONT=新宋体] sRet += sKeys.GetAt(offset);[/FONT]
[FONT=新宋体] }[/FONT]
[FONT=新宋体] }[/FONT]
[FONT=新宋体] }[/FONT]
[FONT=新宋体] CryptDestroyHash(hHash);[/FONT]
[FONT=新宋体] }[/FONT]
[FONT=新宋体] CryptReleaseContext(hProv,0);[/FONT]
[FONT=新宋体] }[/FONT]
[FONT=新宋体] [COLOR=blue]if[/COLOR] (sRet.GetLength() == 0)[/FONT]
[FONT=新宋体] {
[/FONT] [FONT=新宋体] MessageBox(0, [COLOR=#a31515]"Failed to invoke cryptographic api!"[/COLOR], [COLOR=#a31515]"ERROR"[/COLOR], MB_ICONSTOP);[/FONT]
[FONT=新宋体] ExitProcess(0);[/FONT]
[FONT=新宋体] }[/FONT]
[FONT=新宋体] sRet.MakeUpper();[/FONT]
[FONT=新宋体] [COLOR=blue]return[/COLOR] CalcFinalKey(sRet);[/FONT]
[FONT=新宋体]}[/FONT]
[COLOR=blue][FONT=新宋体]void[/FONT][/COLOR][FONT=新宋体] CpediykgDlg::OnEnChangeEdit1()[/FONT]
[FONT=新宋体]{[/FONT]
[FONT=新宋体] [COLOR=green]// TODO: If this is a RICHEDIT control, the control will not[/COLOR][/FONT]
[FONT=新宋体] [COLOR=green]// send this notification unless you override the CDialog::OnInitDialog()[/COLOR][/FONT]
[FONT=新宋体] [COLOR=green]// function and call CRichEditCtrl().SetEventMask()[/COLOR][/FONT]
[FONT=新宋体] [COLOR=green]// with the ENM_CHANGE flag ORed into the mask.[/COLOR][/FONT]
[FONT=新宋体] [COLOR=green]// TODO: Add your control notification handler code here[/COLOR][/FONT]
[FONT=新宋体] OnBnClickedGenerate();[/FONT]
[FONT=新宋体]}[/FONT]
[COLOR=blue][FONT=新宋体]void[/FONT][/COLOR][FONT=新宋体] CpediykgDlg::OnBnClickedGenerate()[/FONT]
[FONT=新宋体]{[/FONT]
[FONT=新宋体] [COLOR=green]// TODO: Add your control notification handler code here[/COLOR][/FONT]
[FONT=新宋体] CString sName;[/FONT]
[FONT=新宋体] [COLOR=blue]int[/COLOR] nNameLength;[/FONT]
[FONT=新宋体] CString sPassword, sPassword1, sPassword2, sPassword3, sPassword4;[/FONT]
[FONT=新宋体] m_edtPassword.SetWindowText([COLOR=#a31515]""[/COLOR]);[/FONT]
[FONT=新宋体] m_edtName.GetWindowText(sName);[/FONT]
[FONT=新宋体] nNameLength = sName.GetLength();[/FONT]
[FONT=新宋体] [COLOR=blue]if[/COLOR] (nNameLength == 0)[/FONT]
[FONT=新宋体] [COLOR=blue]return[/COLOR];[/FONT]
[FONT=新宋体] srand(GetTickCount());[/FONT]
[FONT=新宋体] sPassword1 = GenerateRandKey();[/FONT]
[FONT=新宋体] sPassword2 = GenerateRandKey();[/FONT]
[FONT=新宋体] sPassword3 = GenerateRandKey();[/FONT]
[FONT=新宋体] sPassword4 = CalcKey(sName, sPassword1, sPassword2, sPassword3);[/FONT]
[FONT=新宋体] sPassword = sPassword1;[/FONT]
[FONT=新宋体] sPassword += [COLOR=#a31515]"-"[/COLOR];[/FONT]
[FONT=新宋体] sPassword += sPassword2;[/FONT]
[FONT=新宋体] sPassword += [COLOR=#a31515]"-"[/COLOR];[/FONT]
[FONT=新宋体] sPassword += sPassword3;[/FONT]
[FONT=新宋体] sPassword += [COLOR=#a31515]"-"[/COLOR];[/FONT]
[FONT=新宋体] sPassword += sPassword4;[/FONT]
[FONT=新宋体] m_edtPassword.SetWindowText(sPassword);[/FONT]
[FONT=新宋体]}[/FONT]
注意,密钥中的前三组字符串其实也可以有小写字符的,但我写的注册机只用了大写字符和数字,纯粹是为了让密钥整齐一些看起来比较顺眼
最后再唠叨两句吧。技术日新月异,而且不会像赛跑有个理论极限,保不准明天又会出现一门新学问来,所以有时间的时候还是多练练手,有新东西的时候才会比较容易有感觉入手,而培养这个感觉基本上没有什么捷径,大多都是日积月累的经验堆出来的。
以这一题为例,第一步是需要明确入手点,然后以合适的方式切入,中间的过程虽然有些磕磕碰碰,但坚持下去并不断的总结经验,最后总会看到光明的。
破解注册算法其实只是逆向工程很小的一部分,而且最好不要太过依赖于或是期望某个强大的自动化工具或脚本,在逆向的过程中可以学习到很多技巧或机制,这些才是最值得我们学习的。
熟练的逆向=经验+毅力+信心+灵感+。。。呵呵,当然还需要一部分运气
好了,写到这里也该告个段落了,谢谢大家花这么多时间看到这里,也感谢一下MegaX和看雪论坛提供了这个练习机会