前言

很久以前其实就看过几期吾爱破解的逆向教学课程,(破壳之类的),但是最后由于懒惰也没有坚持下去,这里顺便放一下吾爱的教学链接吧,《[公告] 吾爱破解论坛官方入门教学培训第一期开始啦!【已更新到第十课】》

但是,这次之所以发这一篇,是因为看到了youtube的一个教学视频,O(∩_∩)O,于是乎,想复现一下,也顺便浅学一下逆向工程,来自《everything is open source if you can reverse engineer (try it RIGHT NOW!)》

正文

1. 必要文件下载

首先,下载两个文件吧,一个是CrackMe文件,一个是用于读二进制码的编译器IDA,下载地址分别为

  1. babys-first-crackme
  2. ida-free

1.1 Baby First CrackMe 下载

直接Github clone命令就可以了。

git clone https://github.com/lowlevellearning/babys-first-crackme.git

image.png

关于Baby First CrackMe下载的一点补充:由于这个CrackMe运行文件是基于C基于Linux系统编译的,所以MacOS和Windows下都很难打开这个文件,于是乎我又在Linux虚拟机上重新Github clone了这个文件,运行图如下:

image.png

先来简单解释一下这个CrackMe文件,其实就是一个if的功能,运行文件显示两行分别是Welcome to your first crackme problem!What is the password?:

  1. 当输入正确的Password时,会显示That is correct!
  2. 当输入错误的Password时,什么都不会显示。

1.2 IDA Free下载

直接官网下载即可,然后对一下SHA256

image.png

2. 运行IDA

双击运行,IDA
image.png

2.1 选择一个New file

image.png

2.2 打开binary文件

当打开文件的时候,请确保选择的是ELF64

image.png

当我们成功打开界面的时候,我们可以看到如下画面:

image.png

3. 逆向工程

3.1 Start point

Start point是一个程序开始运行的地方。我们可以看出来这里有很多的assembly language。在main以前,大部分都是C的库的加载,可以看出来还有int main的参数argc。

我们主要关注的是其中有一条lea rdi, main; main。 关于什么是MOV 什么是 LEA, 今天在stack overflow看到一个很好的解释。将放在Section 4里面说。

image.png

对于rdi,这里要介绍一个逆向工程非常基础的知识,ABI (Application Binary Interface),可以浅读一下《什么是应用程序二进制接口ABI》。基本来说,这是计算机中的一个规范,就是如果你要call 一个函数foo(1,2);的话,我们会将1的值给rdi,2的值给rsi,(我盲猜是,因为我也在看着这篇YouTube在学习,是mov rdi, 1; mov rsi, 2,但是这个地方的1和2应该会随着数据类型的不一样有改变?更正确的答案不是今天的目的 :D),我们要记住的就是rdi是第一个,rsi是第二个。同时如果有return value的话,他会到rax。总结就是:

  1. 第一个参数 rdi
  2. 第二个参数 rsi
  3. return value为 rax

3.2 main

然后我们继续,先从main函数入手,双击main。

image.png

我们进入main函数后,我们可以看见4个字符串

  1. Welcome to your first crackme problem!
  2. What is the password?:
  3. %64s
  4. That is correct!

我们可以很明显看出,答案可以从jz (jump zero)这个判断入手jz short loc_1318。而我们想要得到This is correct!的话,我们使jz 这个不为zero,也就是底下的红线。

image.png

那我们要怎么得到这个jz的值呢?我们就要看上一条assembly函数了test eax, eax

test指令操作是目的操作数和源操作数按位逻辑“与“操作,这个地方的test eax, eax 是对两个eax进行按位逻辑“与“操作,基本的意图就是判断。

由于and也是按位逻辑“与“操作,这里介绍一下and和test的区别:基本上and 和 test的功能类似,但是and eax,eax会改变eax的结果,test eax, eax 不改变eax的结果,参考来自《test eax,eax》

那么这个eax,来自于上面的call sub_11A9,(这个eax是rax的一半,具体更详细的看下图,下图来自《rax,eax,ax,ah,al 关系》,所以这个地方只是单纯对比eax 和 eax的按位逻辑与操作,然后根据结果设置标志位,一些后续的操作我们暂时就不细谈了,因为我也不知道= =第一次接触还在学习,但是,无论如何这个地方test eax, eax 和 jz short loc_1318的意思就是如果结果为0就跳转。那么,据此推论我们的答案肯定在call sub_11A9之中,因为这个函数的结果rax/eax,会决定我们后面的跳转,那么我们跳过去看看是什么情况。

image.png

3.3 sub_11A9

我觉得,当sub_11A9 这个函数打开的一瞬间,不需要ASSEMBLY语言估计也能看得清楚一个大概了,我们这里可以直接盲猜一下答案,我们这里有无数个字符型数据:

  1. c
  2. a
  3. n
  4. _
  5. y
  6. a
  7. _
  8. d
  9. i
  10. g
  11. _
  12. i
  13. t
  14. ?

我们组合起来就是can_ya_dig_it?

但是我们还是得要解释一下这个地方发生了什么,具体的ASSEMBLY如下:

push rbp
mov rbp, rsp
mov [rbp+var_8], rdi
mov rax, [rbp+var_8]
movzx eax, byte ptr [rax]
cmp al, 63h
jnz loc_129E

这个地方

  1. 首先push rbp; mov rbp, rsp;,创建了stack栈的相关操作,
  2. 然后mov [rbp+var_8], rdi,将我们的第一个参数rdi的值给了[rbp+var_8]
  3. 然后mov rax, [rbp+var_8],将这个rdi的值给了rbp+var_8之后给了rax,
  4. 接下来movzx eax, byte ptr [rax],将这个rax的第一个字节的指针的值,给了eax,
  5. 接着cmp al, 63h,对比al63h的值,这里al是rax的更低字节的版本,他们都存在同一个寄存器里面,我们可以见之前的寄存器相关的图,这个时候我们也可以看见IDE提示我们63h代表了c
  6. 最后jnz loc_129E,jnz(jump not zero),如果走红线,那么我们就会到下一个a的地方,这也是我们想要的,也就是jnz不成立,也就是jump not zero 是false,也就是jump 是zero,(比较绕口);如果成立,那么就绕走了。

然后我们到了a所在的地方,这里的ASSEMBLY是

mov rax, [rbp+var_8]
add rax, 1
movzx eax, byte ptr [rax]
cmp al, 61h
jnz loc_129E

这个地方

  1. 由于rdi已经入栈了,所以不需要考虑上面入栈push rbp;之类的相关操作,我们直接继续mov rax, [rbp+buffer],输入原来的值,
  2. 但是这个时候,我们要将rax的地址加上一个字节,因为我们已经对比完了第一个字符,也就是c,具体ASSEMBLY为add rax, 1
  3. 紧接着的操作是一样的,我们将rax的指针指向eax,movzx eax, byte ptr [rax]其实我不太明白,既然都在一个寄存器,为什么非要rax 指向他自己的32位也就是eax
  4. 然后进行对比,注意这个时候rax的地址已经+1了,所以其实是我们rdi的第二个字符和61h进行对比,cmp al, 61h
  5. 然后就是jnz loc_129E的判断,同样的我们希望走红线,去看下一个n的地方

以此类推...

最后可以得到答案can_ya_dig_it?,where和我们的猜想一样。O(∩_∩)O

image.png

3.4 check是否成功CrackMe

在Ubuntu中,尝试运行./babys-first,并且输入can_ya_dig_it?

1690632494704.png

显示That is correct!,很明显,我们成功了~!

Yeah! 撒花!

3.5 实际的C源码

#include <stdio.h>

int getPass(char *b)
{

	if (b[0] == 'c') {
	if (b[1] == 'a') {
	if (b[2] == 'n') {
	if (b[3] == '_') {
	if (b[4] == 'y') {
	if (b[5] == 'a') {
	if (b[6] == '_') {
	if (b[7] == 'd') {
	if (b[8] == 'i') {
	if (b[9] == 'g') {
	if (b[10] == '_') {
	if (b[11] == 'i') {
	if (b[12] == 't') {
	if (b[13] == '?') {
		return 1;
	}
	}
	}
	}
	}
	}
	}
	}
	}
	}
	}
	}
	}
	}
	return 0;
}
int main(int argc, char **argv)
{
	char buffer[64];

	printf("Welcome to your first crackme problem!\n");
	printf("What is the password?: ");
	scanf("%64s", buffer);

	if (getPass(buffer))
	{
		printf("That is correct!\n");
	}
}

4. LEA vs MOV

今天看见一个对LEA和MOV很好的解释,来自于《Stack overflow-What's the purpose of the LEA instruction?》其实就是一个传递地址,一个传递地址的值

我们假设有一个C数据结构如下:

struct Point
{
     int xcoord;
     int ycoord;
};

我们想要执行一个这样的命令

int y = points[i].ycoord;

假设此处:

  1. points[] 是一个指针数组
  2. 此points[]数组已经在EBX中了
  3. 变量i已经在EAX中了
  4. xcoord 和 ycoord都是int类型,也就是32bits,4bytes
  5. 变量y在EDX中

那么这一条C命令就可以被用ASSEMBLE 写为,这就是将points[i].ycoord这个地方地址的值,传给int型的y。

MOVE EDX, [EBX + 8*EAX + 4]; right side is "effective address"

如果,我们想要执行一个这样的命令,此处左为指针int *p,右为取地址符号$

int *p = &points[i].ycoord;

这个时候,我们并不想知道points[i].ycoord的值是什么,而是只想知道其地址是什么,于是乎我们用LEA (load effective address),如下(此时变量*p在ESI中):

LEA ESI, [EBX + 8*EAX + 4]

5. Baby First Crack的后续

5.1 举一反三

这时候我们可以举一反三的思考一下,是否能够,直接不输入密码,就能够得到这个"That is correct!"这条信息呢。

答案是肯定的,而且方法有无数种,这里我选择了最简单的一种。

我们直接看到 函数sub_11A9

5.2 sub_11A9的更改

可以顺便看了一下,这个时候我已经给我们的sub_11A9函数改了名字,更方便查看,由于它里面的功能是检查密码是否正确,所以我们这个时候给它取了一个名字就叫checkPassword

image.png

进入函数checkPassword,这里其实我已经更改了,这是更改之后的结果,直接将原本的 cmp al, 63h 改为了 cmp al, al,这就会导致,永远成立,也就是直接将我们本来的条件语句完全绕过了。这样一来我们也不用再输入任何正确的语句了,我们随便输入一个语句应该就能得到That is correct!,而且也任意字数都可以,因为即使指针超过了变量的大小,自己和自己比,也肯定是条件成立的。

image.png

5.3 结果

很明显,我们成功了 :D

1690642860182.png

5.4 新的解法

我们发现在checkPassword函数的最下方,有一个loc_129E,当密码错误的时候都会跳转到loc_129E,然后这个地方会返回0,然后跳转到之前的main里面,有一个test eax,eax,如果是0的话,那么结果必然是0,所以无法触发That is correct!;但是如果不跳转到loc_129E这个地方的话,那么就会返回1,这个时候如果跳转到main里面的,test eax,eax的话,那么结果就是1,就会触发That is correct!

所以这个时候,我们有个很简单的办法,不让loc_129E返回0,或者换句话说,无论我们的输入是什么,checkPassword始终返回1,如下图:

image.png

结果,显而易见,成功!

1690666254631.png

总结

第一次的逆向工程。结果看来,还是需要认真地学习计算机操作系统 和 汇编语言等计算机基础知识。

都是很重要的!

但同时第一次逆向工程也很开心~~!

而且自己还进行了进一步的探索,更加开心~~~!

参考

[1] [公告] 吾爱破解论坛官方入门教学培训第一期开始啦!【已更新到第十课】
[2] everything is open source if you can reverse engineer (try it RIGHT NOW!)
[3] 什么是应用程序二进制接口ABI
[4] test eax,eax
[5] rax,eax,ax,ah,al 关系
[6] Stack overflow-What's the purpose of the LEA instruction?

Q.E.D.


立志做一个有趣的碳水化合物