逆向初学者CrakeMe(2)

这次下手的是一个用 MFC 写的小 CrakeMe,是几天前一个朋友写的。和上一次那个比较,这次的难度要高一点,刚拿到手的时候搞了好久都没有什么进展,我真是菜的不行~还好,过了这一两天,算是小小进步了一点,拼了一个下午和傍晚终于给搞明白了。像上次一样,这次主力依然是 OD,不是我。


首先执行一次看看


输入密码错误,弹出个「false」,大概也就这样。直接 OD 载入(当然我也用 IDA 看了一下,不过也没看出个什么大概),先来个全图


当然从这里什么也看不出,我们直接搜字符串,找到的一些有用的字符串如下



很自然可以想到,输入正确密码会弹出「good」。挑一个双击跳过去



跳过来后直接就看到了关键跳,先下个断再说,虽然离这里还老远。然后往上翻,在地址 003F7800 行下个断,然后就可以直接 F9 运行了。F9 后,随便输入一个假码,然后程序果然断在了设断处


往下 F8 粗跟一下,同时观察信息窗口和寄存器。过了地址 003F7871 行的一个 call 时,发现 ECX 寄存器中携带了我们输入的密码的地址,那想是来这个 call 读取了我们输入的密码。Ctrl+F2 重来一遍,在这个 call 跟进去,果然发现了 GetWindowTextW 调用。



同时要注意,这里调用的是 GetWindowTextW 函数,不是 GetWindowTextA 函数,这说明程序是把我们输入的字符当做宽字符读入的。在 ECX 寄存器上右键,「数据窗口中跟随」,然后观察数据窗口


OK,注释完继续往下跟,注意此时数据窗口还显示我们输入的密码,我们一边跟一边观察这个密码是否有变化。在过了地址 0037787A 行的 call 之后,我们发现输入的密码改变了。

在这个 call 上下个断,Ctrl+F2 重来,在这个 call 跟进去。然后先不单步执行,我们先往下看看有没有什么比较可疑的指令。果然,在地址 003F7FC6 行,我们看见一条异或指令,「 xor ecx,edx 」,这个应该很关键,下个断。然后找那几个跳转指令点着看看,有往后跳的也有往前跳的,这样的框架有点经验的人就应该知道了,这个一个循环。好了,获取了足够的信息,我们就可以单步分析了。

不过这里我就不详细分析了,大概说一下就好。这个循环的作用是,把输入的用户名的每一个字符与其对应的下标做一次异或运算,要注意的是,下标是从 1 开始算,不是从 0 开始算的,就这么简单。循环计数器从 0 开始,+ 1 后用作每一次异或运算时的下标。当计数器大于等于密码长度时,跳出循环。

还有,在循环中地址 003F7F95 行有一个 call,这个 call 的作用是取得一个字符串的长度。这里的字符串自然是我们输入的密码了



在地址 003F7FB0 的 call 的作用是从字符串中取得即将运算的字符



在地址 003F7FD2 的 call 的作用是将运算的结果替换到字符串中


这个循环就这样子。对了,想要快速找到字符串被修改的那一部分,可以试试「内存写入断点】,很好用。从这个 call 出去,回到外面的指令,继续往下分析。


在地址 003F7884 行,在信息窗口中发现了一个字符串,"helloeveryone"



继续往下分析,在地址 003F78A1 行有一个 call,这个 call 的作用和上面那个循环是一样的,将 "helloeveryone" 与其对应的下标做异或运算,生成另一个字符串 "igohjcqm{sdbh" 。有点不同的是,这里将 "igohjcqm{sdbh" 存放到内存中其它地方中,而不是覆盖原字符串了。


接着往下分析,一直到地址 003F7901 行,这里的一个 call 出来之后就直接到了我们的关键跳那里,所以在这个 call 里面肯定进行了密码正确与否的判断。很关键,下个断


然后我们跟进去。进去之后就看见一大堆长长的指令,一瞬间差点想放弃~算了,还是慢慢分析吧

单步执行,在地址 003F5109 行,在信息窗口发现一个字符串,"hellotheworld"。不知道有什么用,留个心。继续往下,碰到一个跳转,这个跳转判断我们输入的密码(异或运算后生成的那个)和 ”igohjcam{sdbh" 这两个字符串的长度是否相等,不相等就跳走,跳走就死定了


我们输入的密码是:Plus_RE,长度当然是不想等的,所以我们反转 ZF 标志位,使得我们可以继续分析下去。

接下来,从地址 003F5157 行开始到 003F51C4,我们又进入了一个循环。还是先找找可疑指令。在地址 003F51AB 行,我们看到一条 and 指令,「 and esi,edx 」,留个心,然后单步分析循环。分析结束之后,我们可以知道,这个循环的作用是:把密码与 "igohjcqm{sdbh" 逐一字符做 and 运算,并且把结果存放到 "hellotheworld" 处。也就是把运算结果替换掉 "hellotheworld" 这个字符串,它就是用来迷惑人的。

但是这里我们要注意,在判断长度是否相等那里,我们修改了标志位使得我们可以继续分析。但在循环中,因为要一个一个字符地比较,而我们两个参与运算的字符串长度不相等,这样子较短的那一个字符串迟早会越界,然后程序会产生错误。既然我们已经分析到这里了,为了更方便一点,我们重新调试程序,并输入一个长度为 13 个字节长度的密码,这一次我们输入 「 Plus_RE_Hello 」,然后执行到这里继续往下分析。

因此,循环结束后,我们得到一个全新的字符串,这个字符串是我们输入的密码经过异或运算后产生的密码和另一个经过异或运算产生的字符串再做 and 运算得来的,它与我们最初输入的密码是一一对应的。接下来,我们把这个全新的字符串称作密码。经过运算后产生的与最初的密码「 Plus_RE_Hello 」对应的全新的密码如下:


那这个密码到底是不是真正的密码呢?很明显,关键点的那一个真假比较还没出现,我们继续往下分析。


在地址 00E151E8 行的这个 call,跟进去。进去后立即就能发现一大堆很可疑的 mov 指令,看起来很像是在存放真码。数一数,地址相连的刚好有 13 个,简直不能再美妙。


不能犹豫,往下分析,又碰到了一个循环,那么熟悉的框架。一口气把循环分析完,果然验证了猜想,上面的 13 个值就是真码。把我们运算后产生的密码与这里出现的真码逐一比较,如果全部相等,那么将 al 置 1,意味着密码正确,否则 al 置 0,前功尽亏。看上面那张图片,[ebp-0x4D] 是一个旗帜,初始值为 1,如果比较过程中出现不相等,那么将它置 0,跳出循环后再赋值给 al。如果一直相等,那么循环后 al 的值就是 1。再看 [ebp-0x5C],它是循环计数器,同时用作下标。


好啦,最关键的部分都已经分析完毕,接下来的指令都是没什么用的。执行回到我们的关键跳那里,这时候 EAX 存放着真假码比较的结果,EAX 为 1,程序弹出 「good!」,EAX 为 0,程序弹出「false!」。弄明白程序的算法,我们就可以写程序求解这个程序的正确密码了。在贴求解代码前,再回顾一下这个程序判断输入的密码是否正确的算法:

        首先,输入的密码必须为 13 个字节长。然后输入的密码先和下标做一次异或运算,再和另一个字符串(这个字符串由 "helloeveryone" 与其下标异或而来)逐字符做 and 运算,从而得到一个全新的密码。这个密码最后与程序内部的真码做比较,相同即输入密码正确。


先贴输入密码正确的界面:


以下为求解密码的 C++ 程序:

#include <iostream>
#include <string>


using std::string;				using std::cout;
using std::endl;


int main(void)
{
	string inputPsw[13];
	// 由 "helloeveryone" 与其下标做异或运算得到的字符串
	string strA = { "igohjcqm{sdbh" };
	// 最后生成的密码如果和 realPsw 相同,则输入的密码正确
	// realPsw 在程序内部常量存在
	string realPsw = { 0x40, 0x67, 0x22, 0x60, 0x20, 0x63,
					0x60, 0x45, 0x59, 0x32, 0x44, 0x42, 0x68 };

	for (string::size_type i = 0; i < strA.size(); ++i)
	{
		// ASCII码值 32 到 126 所对应的字符为可见字符,可输入
		// 但通过此范围寻找答案,应该会遗漏少部分正确密码
		for (int j = 32; j <= 126; ++j)
		{
			if ((j & strA[i]) == realPsw[i])
			{
				inputPsw[i] += char(j);
			}
		}
	}

	cout << "输入的密码必须是 13 个字节长,并且:" << endl << endl;

	for (int i = 0; i < 13; ++i)
	{
		cout << "第 " << i + 1 << " 个字符可以是(空格隔开): " << endl;

		for (string::size_type index = 0; index < inputPsw[i].size(); ++index)
		{
			// 下标从 1 开始,而不是从 0 开始
			inputPsw[i][index] ^= i + 1;
			cout << inputPsw[i][index] << " ";
		}

		cout << endl << endl;
	}

	cout << "注意!以上输出可组合出大部分正确密码,但应该仍有少部分遗漏!" << endl << endl;

	return 0;
}

输出的正确的密码就不贴出了,读者可以自己写程序求解,或者直接复制我的源码执行一次。那这一次就到这里吧。


END.



程序下载:http://pan.baidu.com/s/1qXOMmw0 密码:lz06

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值