请查看附件
- 标 题:2008看雪论坛读书月第二题的注册机
- 作 者:mstwugui
- 时 间:2008-07-21 07:40
- 链 接:http://bbs.pediy.com/showthread.php?t=68954
请查看附件
输入用户名和密码点regist按钮后,用户名和注册码将分别被保存到HKEY_CURRENT_USER\Software\MegaX键下的name和key中
重新运行CrackMe.net.exe后,开始读入上述注册表键值并校验用户名和注册码
首先注册码由4组字符串组成,每组5个字符,中间由"-"号分开(这里我们分别以AAAAA, BBBBB, CCCCC, DDDDD表示四组注册码,USERNAME表示用户名)
其次由固定字符串"CrackMe","MegaX",第一,第二组注册码以及用户名合成一个字符串
CrackMeAAAAABBBBBMegaXUSERNAMEMegaX,并计算这个字符串的md5
然后将计算出后的md5每个字节都转换为10进制数并合成一个字符串,然后取最前面的5个字符
然后在一个固定字符串“8x3p5BeabcdfghijklmnoqrstuvwyzACDEFGHIJKLMNOPQRSTUVWXYZ1246790”中依次查找上一步得到的字符串的每个字符,并将返回的偏移地址减去第三组注册码CCCCC中相对应的每个字节在该固定字符串中的偏移地址,如果为负数则加上该固定字符串的长度,最后在该固定字符串中查找上面计算出来的偏移地址的字符,并合成最后一组注册码
忘记提到了,这道题还有一个特点是如果注册格式不正确则根本就不会继续校验下去,因此在注意到注册码格式之前很容易就跟飞了
这里先说一声对不起哈,我的文字表达能力不太好,如果有文笔不通之处还请多多包涵了
【文章标题】: 《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”分别是怎么计算出来的
第一个问题比较容易,分析一下最后这一步很容易就得出了
CString CalcFinalKey(CString sKey) { DWORD i; CString sRet = ""; CString sMegaX = "MegaX"; CString sFinalKeys = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; BYTE c; for (i=0; i<5; i++) { c = sKey.GetAt(i); c += sMegaX.GetAt(i); c += 0x23; sRet += sFinalKeys.GetAt(c%0x24); } return sRet; }
char GetRandByte() { char c = rand()%36; c += (c<10)?0x30:0x37; return c; } CString GenerateRandKey() { CString sRandKey = ""; for (int i=0;i<5;i++) sRandKey += GetRandByte(); return sRandKey; } CString CalcFinalKey(CString sKey) { DWORD i; CString sRet = ""; CString sMegaX = "MegaX"; CString sFinalKeys = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; BYTE c; for (i=0; i<5; i++) { c = sKey.GetAt(i); c += sMegaX.GetAt(i); c += 0x23; sRet += sFinalKeys.GetAt(c%0x24); } return sRet; } CString CalcKey(CString sName, CString sPassword1, CString sPassword2, CString sPassword3) { CString sTemp = "CrackMe" + sPassword1 + sPassword2 + "MegaX" + sName + "MegaX"; CString sTemp1 = ""; CString sRet = ""; CString sKeys = "8x3p5BeabcdfghijklmnoqrstuvwyzACDEFGHIJKLMNOPQRSTUVWXYZ1246790"; HCRYPTPROV hProv; HCRYPTHASH hHash; if (CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, 0xF0000000)) { if (CryptCreateHash(hProv, CALG_MD5, NULL, 0, &hHash)) { if (CryptHashData(hHash, (const BYTE*)sTemp.GetBuffer(), sTemp.GetLength(), 0)) { DWORD size = 16; BYTE code[16]; int offset; if (CryptGetHashParam(hHash, HP_HASHVAL, (BYTE*)code, &size, 0)) { DWORD i, j; for (i=0; i<size; i++) { sTemp.Format("%d", code[i]); sTemp1 += sTemp; } for (i=0; i< 5; i++) { j = sKeys.Find(sPassword3.GetAt(i)); offset = sKeys.Find(sTemp1.GetAt(i)) - j; if (offset < 0) offset += sKeys.GetLength(); sRet += sKeys.GetAt(offset); } } } CryptDestroyHash(hHash); } CryptReleaseContext(hProv,0); } if (sRet.GetLength() == 0) { MessageBox(0, "Failed to invoke cryptographic api!", "ERROR", MB_ICONSTOP); ExitProcess(0); } sRet.MakeUpper(); return CalcFinalKey(sRet); } void CpediykgDlg::OnEnChangeEdit1() { // TODO: If this is a RICHEDIT control, the control will not // send this notification unless you override the CDialog::OnInitDialog() // function and call CRichEditCtrl().SetEventMask() // with the ENM_CHANGE flag ORed into the mask. // TODO: Add your control notification handler code here OnBnClickedGenerate(); } void CpediykgDlg::OnBnClickedGenerate() { // TODO: Add your control notification handler code here CString sName; int nNameLength; CString sPassword, sPassword1, sPassword2, sPassword3, sPassword4; m_edtPassword.SetWindowText(""); m_edtName.GetWindowText(sName); nNameLength = sName.GetLength(); if (nNameLength == 0) return; srand(GetTickCount()); sPassword1 = GenerateRandKey(); sPassword2 = GenerateRandKey(); sPassword3 = GenerateRandKey(); sPassword4 = CalcKey(sName, sPassword1, sPassword2, sPassword3); sPassword = sPassword1; sPassword += "-"; sPassword += sPassword2; sPassword += "-"; sPassword += sPassword3; sPassword += "-"; sPassword += sPassword4; m_edtPassword.SetWindowText(sPassword); }
这一题应该还有很多不同的解法,我的办法肯定不是最正统的,或许可以先脱壳再常规反编译.NET程序,但因为不熟悉.NET环境所以一时我也只能这样了,希望熟悉的朋友不吝指教