逆向工程——二进制炸弹(CSAPP Project)

实验文件:http://files.cnblogs.com/remlostime/bomb.zip

题中给出了一个二进制文件(可执行文件),共6个关卡,每关要输入一个密码才能过关,就像解谜游戏一样,还是很有意思的,同时对于程序(函数,返回值,堆栈的组织)如何运行的有更深的理解。

破解唯一可用的线索就只有这个二进制文件了。这题是对于反汇编能有更深入练习,加上还能熟悉gdb,objdump这类调试工具和反汇编工具。每一关的考察点也是由浅入深。

最开始的时候很没头绪,就只是按照提示用objdump –d bomb把汇编代码整个打印出来,然后大致浏览了一下,差不多几十页的样子,发现其中有六个函数phase_1……phase_6,基本上也就可以确定就是这六个关卡了。

===============phase_1===============

知识点:string,函数调用,栈

phase_1

用了差不多一个星期断断续续地寻找感觉的phase_1,最主要不知道从何入手。虽然phase_1也不过10+行指令,但最初我的出发点错了:完全依靠人工去读代码而不使用更便捷的gdb去调试和查看内存和寄存器的情况。

比方说,困扰了我好几天之久的strings_not_equal函数。现在看来,函数的名字已再清楚不过的体现了函数的意义。而我最初还深入到函数中去读各种寄存器、各种内存,最后被一个内存地址卡住了,死活不知道内存里的值是多少,还用人工去计算。最后终于恍然大悟,不就是放入栈中两个字符串,然后比较它们是否相等吗?

可以看到0x8048b22和0x8048b27指令中分别放入了两个字符串,一个在地址0x8049678中,另一个在0x8(%ebp)中。而0x8(%ebp)是函数phase_1的参数,所以依此可以判断0x8(%ebp)的内存地址中的值是我们输入的字符串,而0x8049678则可能是程序中硬编码的一个字符串。那么,找到这个内存地址中的字符串便能解决问题了。那么如何去寻找呢,于是乎我又翻看我的那几十页代码纸,企图人工计算出来。后来发现数据段的的值没有包含。终于,我开始想到了gdb这个工具。干嘛不在0x8048b22处设个断点,然后到时打印0x8049678地址中的值不就行了吗?终于的终于,最强调试器上场。

break

print

如图,密钥就是:The future will be better tomorrow.

经过如此简单的一个函数,基本上学会了函数是如何被调用,参数是如何放置的,以及学会了一点简单的gdb调试。另外,应当集中精力在主要问题上,对于strings_not_equal的函数的深入解读就是不明智的,只有当发现stings_not_equal的运作方式真正重要时才去关注。

===============phase_2===============

经过phase_1的训练,以后的关卡的破解模式也终于找到了,所以越来越难但花的时间却并非是线性增长的。

知识点:循环语句,数组

phase_2

在1中可以看到%eax由于是调用者维护的寄存器,所以调用函数read_six_numbers时需要先入栈。0x8(%ebp)保存的应该是六个数字的地址。看了下read_six_numbers函数,基本上就是判断是否输入的数字为六个,所以重点还是phase_2函数本身。

在看到0x8048b54地址中的指令,cmpl $0x1,-0x20(%ebp)和je 8048b5f,为了确定-0x20(%ebp)中的值是否为1。而-0x20(%ebp)中的值是什么呢?猜测可能和输入的六个数有关,但我也懒得计算到底是哪个了。于是先在初始时随便输入六个数,比如:1 2 3 4 5 6,然后在0x8048b54处设立一个断点,用命令:p *(int *) ($ebp - 0x20) 查看其中的值,发现是输入的第一个整数。然后,就可以确定了第一个参数必须为1,炸弹才不会爆炸(0x8048b5a黄线标出)。

可以看到2是一个循环语句,共作了5次,其中%ebx是循环变量,而%esi中保存的是第一个整数的地址值,相当于一个数组中的起始地址(基地址),而后面的key points中的代码就是确定后续的五个数应该是多少的关键了,可以看到a-0x8(%esi, %ebx, 4)b-0x4(%esi, %ebx, 4)的地址就差0x4,也就是一个32位整数的地址,所以猜测可能是相邻两个数的一个比较。假设有数组A,由于数组地址是从小到大增长,所以地址数组索引b=a+1,根据指令可以得出A[b]=A[a]*%eax,其中%eax就是%ebx,每次%eax就等于循环变量(其中变量=2,3,4,5,6)。初始时,A[1]=1。所以,A[2] = A[1] * 2 = 2,A[3] = A[2] * 3 = 6,A[4] = A[3] * 4 = 24,A[5] = A[4] * 5 = 120,A[6] = A[5] * 6 = 720。

所以答案就是:1 2 6 24 120 720

===============phase_3===============

知识点:switch语句

phase_3

这关基本上就是对switch语句的反汇编,最重要的是理解了switch的跳转表的结构就能重新刻画出switch的结构了,问题就迎刃而解了。可以看到1中最后call 08048878<sscanf@plt>,猜测sscanf可能是C语言的内部函数,于是查到其定义为:int sscanf(const char *str, const char *format,…),给出一个使用实例:sscanf(“s 1”, “%s %d”, str, &a),函数返回2(因为接收了2个参数),str为char*类型,保存”s”;a为int类型,保存1。由此,压入栈中的四个地址:-0x8(%ebp), -0x4(%ebp), 0x8049968 和 0x8(%ebp)这四个参数就是传递给sscanf。于是查看0x8049968中的值,得到:

p2

由此,便推断出此关需要输入两个整数类型。

再看0x8048bae处的指令,为了不让指令跳转到explode_bomb处,得出表达式*(-0x4(%ebp)) – 0x7 <= 0(*(addr)是取出addr地址中的值),由于ja是对无符号数比较,所以第一个参数取值范围:0 1 2 3 4 5 6 7

在2中的指令块就是一个比较明显的switch语句块了,最主要的一个线索就是jmp *0x80496cc(, %eax, 4),根据地址0x80496cc + 4%eax中值确定跳转地址,也就是说跳转表存在0x80496cc ~ 0x80496cc + 4 * 7 的地址段中。

于是,将这个跳转表打印如下,就可以很清晰的得到2中的跳转结构,于是根据此结构又可以得到2下方用高级语言重写的switch代码。

switch

再往下看,可以看到0x8048c23中-0x4(%ebp)和5进行比较,如果大于5则爆炸,所以第一个参数进一步去除了6和7。根据switch中的ret值,我们得到了第二个参数,于是答案就有6组(任意皆可):

0 179

1 -678

2 -199

3 -900

4 -169

5 -411

发现当代码很长时,从后向前读会很有帮助,既从结果推知过程该干些什么能目的更明确。在之后的关卡中都用的是从结果推到过程中。

===============phase_4===============

知识点:递归

phase_4

1中可以看到sscanf(0x8(%ebp), 0x804996b, %eax)中的*0x804996b=”%d”,所以需要传入一个整数。另外由0x8048c8b的指令进一步确认了只能传入一个整数。而接下来的指令可以看出传入的整数应该>0,否则炸弹爆炸。而这个整数又作为func4的参数传入。0x8048ca6的指令中,我们可以确定func4(-0x4(%ebp))=144才能过关。

那么深入一下func4的运作机理就是很有必要的,如下图给出了func4:

func4

在上图中1作为func4的递归主体根据jg 8048c4e可以看出是一个循环语句,另外%ebx(即传入的参数)作为循环变量,此变量每次-2。另外在0x8048c42处,给出了递归结束的条件(%ebx<=1则返回1),根据如上的一些分析,大致的就可以还原出函数的一个全貌了。用C语言重写一下,得出:

int func4(int n)
{
	if (n <= 1)
		return 1;
	int ret = 0;
	for(int i = n; i > 1; i -= 2)
	{
		int m = i - 1;
		ret += func4(m);
	}
	return ret + 1;
}

最后,只要暴力搜索一下func4(n)=144的n的值就可以了,n=11。

===============phase_5===============

知识点:字串变换,ascii转换,寻址

phase_5

这关可以说是phase_1的增强版,1可以看出此段指令对我们输入的原字符串做了些处理,所以很可能并不能简单的输入在2中的地址*0x80496c=”titans”的字符串。那么先从2入手,首先%ecx中存的是什么地址呢?倒推到0x8048cd6可以看到%ecx = -0xb(%ebp),所以这个地址并非是我们输入字符串的地址。而这个地址中的赋值可能就是在1中进行的。由movsbl -0x1(%ebx, %edx, 1), %eax和%edx的取值1,2,3,4,5,6可以推断出源地址的值。再由mov %al, -0x1(%edx, %ecx, 1)来推断目标地址。得出如下的对应关系:

sd其中%ebx=0x8(%ebp)输入字符串首字符地址,%ecx = -0xb(%ebp)

另外,根据and $0xf, %eax和mov 0x804a5c0(%eax), %al这两条指令可以知道,source->dest的赋值是根据我们输入每个字符串各位的最低四位(%eax)+0x804a5c0中地址所对应的字符回传给dest的对应地址。于是,将0x804a5c0中的字符串打印如下:

mem

我们的目标字符串”titans”所对应的位置是cd,c0,cd,c5,cb,c1。所以,当输入字符串时,它们的末4位分别应为d,0,d,5,b,1。打印出0~126的ascii所对应的字符表,我选取了mpmeka(6d 70 6d 65 6b 61)作为答案。

===============phase_6===============

知识点:寻址

phase_6

总算到了最后的关卡,但似乎觉得比前几关容易些,可能是因为我用了tricky的方法,偷懒了。可以看到1中的函数定义long int strtol(const char* str, char **endptr, int base),基本上就是输入一个字符串,然后此函数中base=10(0xa),endptr=0x0(NULL),而str就是我们输入的参数,函数将其转换成10进制数。而2中调用了fun6函数,由于fun6函数实在太长懒得看了,发现传入的参数是一个硬编码地址0x804a630,所以应该和输入的参数没关系。也就是说fun6的返回值是固定的,那么干嘛不在fun6运行结束的地方设个断点,然后查看%eax的值不就完了嘛。如下:

eax

又看到3是一个循环8次的语句,于是只要找出最后%eax为多少就行了,如下:

8

可以看到最后%eax=0x804a5d0,然后在打印出此地址中的值就行了:

res

转换成十进制就是198。

这题的fun6实在长到吐血,但理解fun6对解题的帮助并不大,所以直接看结果会明智些。

 

 

 

 

当当,最后一题解决啦!终于打倒大boos了!放礼花啦!

succ

 

 

 

 

 

 

 

 

 

 

 

 

 

===============secret_phase===============

知识点:加强版递归,改变寄存器变量

咳咳,你还在看吗?好吧,最终的隐藏大boss来了。不负众望,果然是最有挑战的一关。

首先,你得发现有这么个隐藏关卡。发现了还不够,你得把它调出来。这还不够,你还得输入正确的密码,最后赢得比赛。

首先要发现隐藏关卡,那你就得通读代码,所以把代码打在纸上能更清晰些。

其次,如何调出关卡就得分析secret_phase在哪里被调用了,可以发现其被调用的地方只有一处,在phase_defused的代码中,而phase_defused是每关拆完之后会被调用的。于是拿来phase_defused分析一下:

defused

可以看到,必须要过了第6关隐藏关卡才能被调用(其实用点特殊手段也能执行)。紧接着,就是一个sscanf(“11”, %d %s”, %eax, %ebx)的调用,由于0x804943e的指令告诉我们%eax(sscanf的返回值)要等于2才能顺利进行。”11”只能传入一个整型%d,所以sscanf返回1,而且由于硬编码,所以当时我就想到改变”11”所在的内存值,变成”1 s”就能顺利返回2了。但转念一想既然改变内存那寄存器%eax不也可以改变吗?何必舍近求远,找到最关键的那个变量直接改了不就行了。于是直接在0x80493e处设断点,在那里用命令p $eax=2,把%eax改成2就行了。同时在0x8049451出如法炮制,把%eax改成0就行了。于是乎,哇咔咔,终于见到最终大boss了。(另外,经过此番醒悟,我终于觉得连输入的密码都是浮云,只要每次在每个关卡的判断的关键处设断点,直接修改寄存器,连代码都不用理解就成了。这想法实在是邪恶!)

第三阶段最艰苦的征程开始了。放上大boss的真容:

secret_phase

程序首先是读入一行read_line,接着是strtol(%eax, NULL, 10)的调用,也就是我们需要输入一个10进制数。接下来这个10进制数会被作为fun7的参数(0x8048e28,另外一个参数在0x804a6e4(查看得知为0x24))。而此参数首先应该满足的条件为%eax – 1 – 0x3e8 <= 0,既: %eax <= 0x3e9 (1001)。而从0x8048e39中,可以知道%eax=0x5时才能成功过关,也就是fun7要返回5。

那么,查看fun7的代码如下:

fun7

这是一个加强版的递归汇编,但也并非难道还原不出。最开始,我犯了个极大的错误在0x8048dcd处,勿把pushl 0x4(%ecx)当作把(0x4+%ecx)入栈,其实应该是把(0x4+%ecx)地址中的值入栈,所以怎么做怎么不对,从头到尾的检查了好几遍,花掉了3、4个小时终于是领悟了这么个bug。如上图,其实原理和phase_4的递归是一样的,只不过稍微分支多了点。可以写出如下的C代码的递归版本:

int fun7(const int *a, int b)
{
	if (a == NULL)
		return -1;
	int ret = 0;
	if (*a - b > 0)
	{
		ret = fun7(*(a + 4), b);
		ret *= 2
	}
	else if (*a - b == 0)
		return 0;
	else
	{
		ret = fun7(*(a + 8), b);
		ret = ret * 2 + 1;
	}
	return ret;
}

由于最后的返回值是5,根据函数数的结构可以想到这样的结构:

递归

用穷举的方法试了下,似乎也只有这样一个唯一的结构可以得出5。

根据以上的结构以及C代码,就可以知道要做些什么了。

1)*0x804a6e4=0x24,之后在*a-b<0 (0x24 – b < 0)的分支中*(a + 8)=0x804a6cc,所以fun7(0x804a6cc, b)。

2)*0x804a6cc=0x32,*a-b>0 (0x32 - b > 0),递归fun7(*(0x804a6cc + 4) = 0x804a6b4, b)。

3)*0x804a6b4=0x2d,*a-b<0 (0x2d - b < 0),fun7(*(0x804a6b4 + 8) = 0x804a648, b)。

4)*0x804a648=0x2f,*a – b == 0 (0x2f – b == 0),所以b = 0x2f,递归返回。

得到3个不等式和一个等式:

1) 0x24 – 0x2f < 0

2) 0x32 – 0x2f > 0

3) 0x2d – 0x2f < 0

4) b = 0x2f

都符合要求,所以我们得出最后输入的值就是十进制数47(0x2f)。

总算是最终完结篇了,现在又对hello,world的运作方式更进一步了。

posted @ 2011-05-21 13:16 chkkch 阅读(...) 评论(...) 编辑 收藏