在前一篇博客 中,我们对一个二进制程序进行逆向工程分析,并从中提取了口令;但该口令是以明文形式存放在二进制程序中的。对新手来说这是很好的开始,但在当前的现实环境中你不可能找到这样的二进制程序。实际上,口令通常会经过混淆或加密处理。大多数情况下,口令甚至不会存储在二进制文件中,而是存储在一些远程服务器上,所有的序列号处理和检验过程都会在服务器上完成。
在本文中,我编写了一段简单的C/C++(二者兼容)程序代码。在代码中,我们并没有将任何口令以明文的形式硬编码写入二进制文件中。事实上,主口令是利用XOR(异或)的方法进行加密的。异或是一种最基础的加密方法,它在很多年前就开始投入应用,目前仍然十分有效,而且直到现在还有很多攻击者正在使用。然而,这种方法并不用于实际的加密过程,而是更多地用于对存储文本进行混淆操作。如果想要了解XOR、AND、OR的具体原理,你可以查阅以下链接:https://en.wikipedia.org/wiki/XOR_cipher
以下是我们二进制程序的源码(相关网址:https://github.com/paranoidninja/ScriptDotSh-Reverse-Engineering/tree/master/part_2-2)。你也可以直接下载并运行Git目录下的可执行文件。
你可以以如下命令格式,在x64dbg加载二进制程序的过程中,在powershell或cmd中向程序传递其参数和绝对路径:
在程序加载之后,需要记住的一点是,在windows系统中调试器不会直接跳转到二进制程序的反汇编代码处,这与linux系统中的情况不同。调试器首先需要加载二进制程序运行所必需的Windows DLL模块。你可以在调试器的日志选项卡中查看加载的DLL模块。
如上图所示,程序调用了ntdll.dll, kernel32.dll, KernelBase.dll和msvcrt.dll。之所以特别提到这些DLL模块,是因为我们当前并不需要对它们进行深入研究。使用‘stepi’命令进行单步步入操作会执行DLL模块的每条语句,并且进入目前我们并不需要了解的模块内部;当前我们只需要对这个二进制程序本身进行逆向分析。因此和上次一样,我们还是先查看字符串引用,并在这个二进制程序中寻找能够设置断点的位置。
下图是字符串引用的情况;我们将在反汇编器中追踪‘Serial key must be…’字符串的位置,并查看所对应的反汇编代码。
如上图所示,我在0x0000000000401681地址处设置了断点。下面介绍一下选择该字符串引用以及在此处设置断点的原因。在之前运行上述代码时,我们发现它进行了一系列的条件检查。例如,第一个条件检查是如果没有输入参数,程序会输出帮助语句;第二个检查是如果输入了长度小于或大于10的密码,程序会打印错误信息,提示需要输入正好10个字符的密码。这意味着,只有在我们提供了正确的参数之后,程序才会继续检查口令,而这就是我们选择这个字符串引用的原因。
如果对0x0000000000401668和0x000000000040166E地址处的代码进行观察你会发现,两处都会进行条件检查,并在随后跳转到0x0000000000401681地址处。如果任意一处的条件检查失败,将会打印‘Serial key error’信息并退出。现在,让我们运行一下二进制程序,来验证我们的猜测是否正确。
开始运行二进制程序之后,你会发现0x00000000004016A9到0x00000000004016EF地址处的代码将某些内容加载到RBP寄存器指向的多个DWORD区域中。
目前还不清楚这些内容是什么,因此我们选择先把它们记录保存下来,以备将来不时之需。这10个字符分别是U, V, W, X, Y, Z, Q, R, S, T。还要注意的是, 你在这些字符的左边所看到的数字,是字符所对应的十六进制数值。比方说,如果将0x55转换为十进制, 你会得到
我们都知道, 85 是字符U 的 ascii 值。同样, 如果将所有值转换为 ascii值, 我们就会得到
让我们先把这张表格放在一边。记住, 我们的口令是一个10字符的序列, 和这个集合的长度相等。还有在进行异或操作时, 某值与0的异或的结果是该值自身。这就是在进行异或操作时,待加密的字符串长度要与密钥序列长度相同的原因。
在通过使用‘stepi’命令进行一系列的单步步入操作从而返回程序代码之后,我们执行到了0x00000000004016FA地址处。你会发现“jg crackme_xor. 40172F”语句进行了另一次条件判断。通过查看RBX寄存器,你会发现对应位置放置的是数字10。
jg指令的意思是大于则跳转,因此这段代码的功能是,不断循环执行jg指令到0x000000000040172D地址之间的代码段,直至循环变量值递增到10则结束循环。你会发现,0x000000000040172D处的语句跳转返回到了0x00000000004016F6地址处,而在这里程序使用cmp dword ptr指令对值进行比较,若大于10,则直接跳转到0x000000000040172F地址处,否则程序会一直循环,直到寄存器的值等于10.
现在,如果继续使用‘stepi’命令对程序进行单步步入操作,你将会发现在0x0000000000401704地址处,程序把我们输入的口令存储到了rax寄存器中,然后在0x000000000040170F地址处将其放入了eax寄存器中。
在0x0000000000401712地址处,AL寄存器的值(如‘p’)被放入EDX寄存器中,而另一个值在0x 0000000000401715地址处被放入eax寄存器中;这两个值在0x000000000040171E地址处进行异或。
现在,使用‘stepi’命令对语句进行单步步入,你会发现rdx寄存器中存放的‘p’就是我们所输入口令’password12‘中的首字母;而另一个用于异或操作的值(存放在Eax寄存器中),就是大写字母’U’。
接下来,我们逐步执行整个循环过程。当前我们的增量计数器值为1;要记住,这个计数器将会运行10次,这意味着其他9个值也将会被异或。下图展示了下个循环过程会载入的内容。
可以看到,程序将我们所输入口令‘password12’中的‘a’加载到rdx寄存器中,而与‘a’异或的是另一个值’V’。同样,如果运行整个循环,你将会发现所输入的每一个字母都会通过以下方式进行异或,直到在0x00000000004016FA地址处计数器的值增至10。简而言之,与所输入口令进行异或的值正是之前所载入的U, V, W, X, Y, Z, Q, R, S, T。通过将我们的口令与上述密钥序列进行异或操作,我们可以得到
将以上两组二进制数字进行异或,我们得到以下异或二进制结果:
喔太棒了!我们可以看到在0x000000000040156C地址处,另一组字符被加载到寄存器中。把这些字符从十六进制转换成十进制,我们得到以下结果:
让我们先把这个结果放到一边,继续执行代码。在0x 00000000004015BD地址处,程序再次使用cmp指令对值进行比较,并且RBX的值是10。之后程序中再次出现了jg指令;在计数器的值大于10的情况下,该语句跳转到0x00000000004015F2地址处。现在,我们执行到0x00000000004015E2处的代码,程序在该位置比较了‘&’和‘%’的值。我们知道‘&’就是上面所找到的字符,其十六进制值为0x26,ascii码等于38。但是‘%’是什么值,为什么要和‘&’进行比较呢?
‘%’的ascii码是37;向前查找一下,你会发现‘%’是‘P’和’U’进行异或的结果。现在,继续循环执行这段反汇编代码,你会发现程序不会进行下一次的比较;原因就藏在0x00000000004015E2地址处的代码中。在这里,程序对EDX(%)和EAX(&)进行比较,如果不相等则CF位(Carry Flag,进位标志)置1[j1] 。下一条指令在0x00000000004015E4地址处,它将检查上一条指令的值是否是0。je指令在值为0的情况下将发生跳转,但是由于两个值不相等,所以我们将值设为1,因而它不会跳转,并且[j2] 将继续执行下一条指令。
更新【2018-5-29】:je指令是相等则跳转,即ZF位被设置时跳转到指定指令,在本例中该标志位没有被设置。
随后在0x 00000000004015F6地址处,程序会输出‘Incorrect Password’信息,并结束循环退出程序。
因此,我们还不能恢复程序口令。但是,我们已经有了所需的素材。我们拥有了[U, V, W, X, Y, Z, Q, R, S, T]这一组密钥序列,还有用于比较的一组值[26, 22, 25, 37, 37, 3D, 21, 33, 20, 27]。请记住,用于比较的值不是口令,而是实际口令与密钥序列进行异或的结果值。因此,我们需要对这些用于比较的值和密钥序列再次进行异或,才能得到实际的口令。
现在,让我们把以上两组值都转换成十进制格式并且将其异或。下表即为最终结果:
把最后异或得到的十进制值转换成文本,我们得到:
所以,如果我们的推理基本正确的话,口令应该是‘strongpass’。接下来在二进制程序中检验一下:
原文链接:https://scriptdotsh.com/index.php/2018/05/09/ground-zero-part-2-2-reverse-engineering-xor-encryption-windows-x64/
编译:看雪翻译小组 欢歌笑语
校对:看雪翻译小组 木无聊偶
在前一篇博客 中,我们对一个二进制程序进行逆向工程分析,并从中提取了口令;但该口令是以明文形式存放在二进制程序中的。对新手来说这是很好的开始,但在当前的现实环境中你不可能找到这样的二进制程序。实际上,口令通常会经过混淆或加密处理。大多数情况下,口令甚至不会存储在二进制文件中,而是存储在一些远程服务器上,所有的序列号处理和检验过程都会在服务器上完成。
在本文中,我编写了一段简单的C/C++(二者兼容)程序代码。在代码中,我们并没有将任何口令以明文的形式硬编码写入二进制文件中。事实上,主口令是利用XOR(异或)的方法进行加密的。异或是一种最基础的加密方法,它在很多年前就开始投入应用,目前仍然十分有效,而且直到现在还有很多攻击者正在使用。然而,这种方法并不用于实际的加密过程,而是更多地用于对存储文本进行混淆操作。如果想要了解XOR、AND、OR的具体原理,你可以查阅以下链接:https://en.wikipedia.org/wiki/XOR_cipher
以下是我们二进制程序的源码(相关网址:https://github.com/paranoidninja/ScriptDotSh-Reverse-Engineering/tree/master/part_2-2)。你也可以直接下载并运行Git目录下的可执行文件。
#include <stdio.h>
#include <string.h>
void CheckPass(int *XoredPassword) {
int PArray[10] = {38, 34, 37, 55, 55, 61, 33, 51, 32, 39};
bool is_equal = true;
for (int i=0; i<10; i++) {
if (XoredPassword[i] != PArray[i]) {
is_equal = false;
break;
}
}
if (is_equal == true) {
printf ("[+] Correct Password");
}
else {
printf ("[-] Incorrect Password");
}
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Help:\n%s <10 character serial key>\n", argv[0]);
}
else {
int stringLength = strlen(argv[1]);
if ((stringLength > 10) || (stringLength < 10)) {
printf("[-] Serial key must be of 10 characters. Please recheck your key\n");
}
else {
int XoredDecimal[10] = {};
int keyStore[10] = {85, 86, 87, 88, 89, 90, 81, 82, 83, 84};
for (int i=0; i<10; i++) {
XoredDecimal[i] = ((int)(argv[1][i]))^(keyStore[i]);
}
CheckPass(XoredDecimal);
}
}
return 0;
}
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2018-6-5 19:12
被欢歌笑语编辑
,原因: 图片尺寸问题