前言
很久以前其实就看过几期吾爱破解的逆向教学课程,(破壳之类的),但是最后由于懒惰也没有坚持下去,这里顺便放一下吾爱的教学链接吧,《[公告] 吾爱破解论坛官方入门教学培训第一期开始啦!【已更新到第十课】》。
但是,这次之所以发这一篇,是因为看到了youtube的一个教学视频,O(∩_∩)O,于是乎,想复现一下,也顺便浅学一下逆向工程,来自《everything is open source if you can reverse engineer (try it RIGHT NOW!)》。
正文
1. 必要文件下载
首先,下载两个文件吧,一个是CrackMe文件,一个是用于读二进制码的编译器IDA,下载地址分别为
1.1 Baby First CrackMe 下载
直接Github clone命令就可以了。
git clone https://github.com/lowlevellearning/babys-first-crackme.git
关于Baby First CrackMe下载的一点补充:由于这个CrackMe运行文件是基于C基于Linux系统编译的,所以MacOS和Windows下都很难打开这个文件,于是乎我又在Linux虚拟机上重新Github clone了这个文件,运行图如下:
先来简单解释一下这个CrackMe文件,其实就是一个if的功能,运行文件显示两行分别是Welcome to your first crackme problem!
和 What is the password?:
- 当输入正确的Password时,会显示
That is correct!
- 当输入错误的Password时,什么都不会显示。
1.2 IDA Free下载
直接官网下载即可,然后对一下SHA256
2. 运行IDA
双击运行,IDA
2.1 选择一个New file
2.2 打开binary文件
当打开文件的时候,请确保选择的是ELF64
当我们成功打开界面的时候,我们可以看到如下画面:
3. 逆向工程
3.1 Start point
Start point是一个程序开始运行的地方。我们可以看出来这里有很多的assembly language。在main以前,大部分都是C的库的加载,可以看出来还有int main的参数argc。
我们主要关注的是其中有一条lea rdi, main; main
。 关于什么是MOV 什么是 LEA, 今天在stack overflow看到一个很好的解释。将放在Section 4里面说。
对于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。总结就是:
- 第一个参数 rdi
- 第二个参数 rsi
- return value为 rax
3.2 main
然后我们继续,先从main函数入手,双击main。
我们进入main函数后,我们可以看见4个字符串
Welcome to your first crackme problem!
What is the password?:
%64s
That is correct!
我们可以很明显看出,答案可以从jz (jump zero)这个判断入手jz short loc_1318
。而我们想要得到This is correct!
的话,我们使jz 这个不为zero,也就是底下的红线。
那我们要怎么得到这个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,会决定我们后面的跳转,那么我们跳过去看看是什么情况。
3.3 sub_11A9
我觉得,当sub_11A9 这个函数打开的一瞬间,不需要ASSEMBLY语言估计也能看得清楚一个大概了,我们这里可以直接盲猜一下答案,我们这里有无数个字符型数据:
- c
- a
- n
- _
- y
- a
- _
- d
- i
- g
- _
- i
- t
- ?
我们组合起来就是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
这个地方
- 首先
push rbp; mov rbp, rsp;
,创建了stack栈的相关操作, - 然后
mov [rbp+var_8], rdi
,将我们的第一个参数rdi
的值给了[rbp+var_8]
, - 然后
mov rax, [rbp+var_8]
,将这个rdi的值给了rbp+var_8之后给了rax, - 接下来
movzx eax, byte ptr [rax]
,将这个rax的第一个字节的指针的值,给了eax, - 接着
cmp al, 63h
,对比al
和63h
的值,这里al
是rax的更低字节的版本,他们都存在同一个寄存器里面,我们可以见之前的寄存器相关的图,这个时候我们也可以看见IDE提示我们63h
代表了c
- 最后
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
这个地方
- 由于rdi已经入栈了,所以不需要考虑上面入栈
push rbp;
之类的相关操作,我们直接继续mov rax, [rbp+buffer]
,输入原来的值, - 但是这个时候,我们要将rax的地址加上一个字节,因为我们已经对比完了第一个字符,也就是
c
,具体ASSEMBLY为add rax, 1
- 紧接着的操作是一样的,我们将rax的指针指向eax,
movzx eax, byte ptr [rax]
(其实我不太明白,既然都在一个寄存器,为什么非要rax 指向他自己的32位也就是eax) - 然后进行对比,注意这个时候rax的地址已经+1了,所以其实是我们rdi的第二个字符和
61h
进行对比,cmp al, 61h
- 然后就是
jnz loc_129E
的判断,同样的我们希望走红线,去看下一个n
的地方
以此类推...
最后可以得到答案can_ya_dig_it?
,where和我们的猜想一样。O(∩_∩)O
3.4 check是否成功CrackMe
在Ubuntu中,尝试运行./babys-first
,并且输入can_ya_dig_it?
。
显示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;
假设此处:
- points[] 是一个指针数组
- 此points[]数组已经在EBX中了
- 变量i已经在EAX中了
- xcoord 和 ycoord都是int类型,也就是32bits,4bytes
- 变量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
。
进入函数checkPassword
,这里其实我已经更改了,这是更改之后的结果,直接将原本的 cmp al, 63h
改为了 cmp al, al
,这就会导致,永远成立,也就是直接将我们本来的条件语句完全绕过了。这样一来我们也不用再输入任何正确的语句了,我们随便输入一个语句应该就能得到That is correct!
,而且也任意字数都可以,因为即使指针超过了变量的大小,自己和自己比,也肯定是条件成立的。
5.3 结果
很明显,我们成功了 :D
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,如下图:
结果,显而易见,成功!
总结
第一次的逆向工程。结果看来,还是需要认真地学习计算机操作系统 和 汇编语言等计算机基础知识。
都是很重要的!
但同时第一次逆向工程也很开心~~!
而且自己还进行了进一步的探索,更加开心~~~!
参考
[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.