【文章标题】: 经典的二进制拆弹逆向分析
【文章作者】: loongzyd
【操作平台】: linux
【逆向声明】: 处于对逆向工程的爱好,虽然这是一个linux下的可执行文件,用IDA也能逆,
但是为了熟悉一下linux环境下的逆向,就没有用IDA。 感谢范晨鹏大哥在嵌入式板块发表的关于gdb的汇编级调试的文章:http://bbs.pediy.com/showthread.php?t=83859&highlight=%E8%8C%83%E6%99%A8+%E6%99%A8%E9%B9%8F+%E9%B9%8F
【背景介绍】:二进制拆弹实验是卡内基-梅隆(CMU)开设的:计算机系统导论(ICS)课程的一道实验题,下面是《深入理解计算机系统》上面对这个实验的文字说明:
二进制炸弹是一个作为目标代码文件提供给学生们的程序。运行时,它提示用户输入6个不同的字符串。如果其中的任何一个不正确,炸弹就会“爆炸”,打印出一条错误信息,并在一个分级(gradin)服务器上记录事件日志。学生们必须通过对程序的反汇编和逆向工程来测定应该是哪6个串,从而解除他们各自炸弹的雷管。该实验教会学生理解汇编语言,并且强制他们学习怎样使用调试器。
【详细过程】:
首先bomb.rar里面给了我们一个C程序,主要的部分如下:
FILE *infile;
int main(int argc, char *argv[])
{
char *input;
/* Note to self: remember to port this bomb to Windows and put a
* fantastic GUI on it. */
/* When run with no arguments, the bomb reads its input lines
* from standard input. */
if (argc == 1) {
infile = stdin;
}
/* When run with one argument <file>, the bomb reads from <file>
* until EOF, and then switches to standard input. Thus, as you
* defuse each phase, you can add its defusing string to <file> and
* avoid having to retype it. */
else if (argc == 2) {
if (!(infile = fopen(argv[1], "r"))) {
printf("%s: Error: Couldn't open %s\n", argv[0], argv[1]);
exit(8);
}
}
/* You can't call the bomb with more than 1 command line argument. */
else {
printf("Usage: %s [<input_file>]\n", argv[0]);
exit(8);
}
/* Do all sorts of secret stuff that makes the bomb harder to defuse. */
initialize_bomb();
printf("Welcome to my fiendish little bomb. You have 6 phases with\n");
printf("which to blow yourself up. Have a nice day!\n");
/* Hmm... Six phases must be more secure than one phase! */
input = read_line(); /* Get input */
phase_1(input); /* Run the phase */
phase_defused(); /* Drat! They figured it out!
* Let me know how they did it. */
printf("Phase 1 defused. How about the next one?\n");
/* The second phase is harder. No one will ever figure out
* how to defuse this... */
input = read_line();
phase_2(input);
phase_defused();
printf("That's number 2. Keep going!\n");
/* I guess this is too easy so far. Some more complex code will
* confuse people. */
input = read_line();
phase_3(input);
phase_defused();
printf("Halfway there!\n");
/* Oh yeah? Well, how good is your math? Try on this saucy problem! */
input = read_line();
phase_4(input);
phase_defused();
printf("So you got that one. Try this one.\n");
/* Round and 'round in memory we go, where we stop, the bomb blows! */
input = read_line();
phase_5(input);
phase_defused();
printf("Good work! On to the next...\n");
/* This phase will never be used, since no one will get past the
* earlier ones. But just in case, make this one extra hard. */
input = read_line();
phase_6(input);
phase_defused();
/* Wow, they got it! But isn't something... missing? Perhaps
* something they overlooked? Mua ha ha ha ha! */
return 0;
}
这相当于给我们了一个很大的提示,我们能够得到如下的信息:
1.我们可以手动输入字符串进行拆弹或者将这些字符串放在一个文件里面。
2.一共有6个小关,其中函数read_line()应该是负责读入每一关的字符串phase_x(input)应该
是每一关的验证算法,phase_defused()目前还不清楚是干什么的.initialize_bomb()应该是进行
初始化之类的工作.
之后我们阅读那个pdf文档,根据大意我们得知,这是某个国外大学的题目,此程序与某服务器连接
,每个做题的人的解题过程的情况会返回到那个服务器,然后根据题目的描述我们应该知道可能是要
废除与那个服务器有关的功能。
好了,我们进入linux环境具体的分析吧。
打开一个终端,输入命令:gbd ./bomb
然后输入:disas main(对main函数进行反汇编),得到的情况如下:
Dump of assembler code for function main:
0x080489c4 <main+0>: lea 0x4(%esp),%ecx
0x080489c8 <main+4>: and $0xfffffff0,%esp
0x080489cb <main+7>: pushl -0x4(%ecx)
0x080489ce <main+10>: push %ebp
0x080489cf <main+11>: mov %esp,%ebp
0x080489d1 <main+13>: push %ebx
0x080489d2 <main+14>: push %ecx
0x080489d3 <main+15>: sub $0x10,%esp
0x080489d6 <main+18>: mov (%ecx),%eax
0x080489d8 <main+20>: mov 0x4(%ecx),%ebx
0x080489db <main+23>: cmp $0x1,%eax
0x080489de <main+26>: jne 0x80489ec <main+40>
0x080489e0 <main+28>: mov 0x804a800,%eax
0x080489e5 <main+33>: mov %eax,0x804a814
0x080489ea <main+38>: jmp 0x8048a50 <main+140>
0x080489ec <main+40>: cmp $0x2,%eax
0x080489ef <main+43>: jne 0x8048a32 <main+110>
0x080489f1 <main+45>: movl $0x8049972,0x4(%esp)
0x080489f9 <main+53>: mov 0x4(%ebx),%eax
0x080489fc <main+56>: mov %eax,(%esp)
0x080489ff <main+59>: call 0x80487f4 <fopen@plt>
0x08048a04 <main+64>: mov %eax,0x804a814
0x08048a09 <main+69>: test %eax,%eax
0x08048a0b <main+71>: jne 0x8048a50 <main+140>
0x08048a0d <main+73>: mov 0x4(%ebx),%eax
0x08048a10 <main+76>: mov %eax,0x8(%esp)
0x08048a14 <main+80>: mov (%ebx),%eax
0x08048a16 <main+82>: mov %eax,0x4(%esp)
0x08048a1a <main+86>: movl $0x804974c,(%esp)
0x08048a21 <main+93>: call 0x8048814 <printf@plt>
0x08048a26 <main+98>: movl $0x8,(%esp)
0x08048a2d <main+105>: call 0x80488d4 <exit@plt>
0x08048a32 <main+110>: mov (%ebx),%eax
0x08048a34 <main+112>: mov %eax,0x4(%esp)
0x08048a38 <main+116>: movl $0x8049769,(%esp)
0x08048a3f <main+123>: call 0x8048814 <printf@plt>
0x08048a44 <main+128>: movl $0x8,(%esp)
0x08048a4b <main+135>: call 0x80488d4 <exit@plt>
0x08048a50 <main+140>: call 0x80495af <initialize_bomb>
0x08048a55 <main+145>: movl $0x80497d0,(%esp)
0x08048a5c <main+152>: call 0x80488a4 <puts@plt>
0x08048a61 <main+157>: movl $0x804980c,(%esp)
0x08048a68 <main+164>: call 0x80488a4 <puts@plt>
0x08048a6d <main+169>: call 0x8049363 <read_line>
0x08048a72 <main+174>: mov %eax,(%esp)
0x08048a75 <main+177>: call 0x8048ec1 <phase_1>
0x08048a7a <main+182>: call 0x80491c1 <phase_defused>
0x08048a7f <main+187>: movl $0x8049838,(%esp)
0x08048a86 <main+194>: call 0x80488a4 <puts@plt>
0x08048a8b <main+199>: call 0x8049363 <read_line>
0x08048a90 <main+204>: mov %eax,(%esp)
0x08048a93 <main+207>: call 0x8048d1c <phase_2>
0x08048a98 <main+212>: call 0x80491c1 <phase_defused>
0x08048a9d <main+217>: movl $0x8049783,(%esp)
0x08048aa4 <main+224>: call 0x80488a4 <puts@plt>
0x08048aa9 <main+229>: call 0x8049363 <read_line>
0x08048aae <main+234>: mov %eax,(%esp)
0x08048ab1 <main+237>: call 0x8048e01 <phase_3>
0x08048ab6 <main+242>: call 0x80491c1 <phase_defused>
0x08048abb <main+247>: movl $0x80497a1,(%esp)
0x08048ac2 <main+254>: call 0x80488a4 <puts@plt>
0x08048ac7 <main+259>: call 0x8049363 <read_line>
0x08048acc <main+264>: mov %eax,(%esp)
0x08048acf <main+267>: call 0x8048db6 <phase_4>
0x08048ad4 <main+272>: call 0x80491c1 <phase_defused>
0x08048ad9 <main+277>: movl $0x8049864,(%esp)
0x08048ae0 <main+284>: call 0x80488a4 <puts@plt>
0x08048ae5 <main+289>: call 0x8049363 <read_line>
0x08048aea <main+294>: mov %eax,(%esp)
0x08048aed <main+297>: call 0x8048d6a <phase_5>
0x08048af2 <main+302>: call 0x80491c1 <phase_defused>
0x08048af7 <main+307>: movl $0x80497b0,(%esp)
0x08048afe <main+314>: call 0x80488a4 <puts@plt>
0x08048b03 <main+319>: call 0x8049363 <read_line>
0x08048b08 <main+324>: mov %eax,(%esp)
0x08048b0b <main+327>: call 0x8048c2c <phase_6>
0x08048b10 <main+332>: call 0x80491c1 <phase_defused>
0x08048b15 <main+337>: mov $0x0,%eax
0x08048b1a <main+342>: add $0x10,%esp
0x08048b1d <main+345>: pop %ecx
0x08048b1e <main+346>: pop %ebx
0x08048b1f <main+347>: pop %ebp
0x08048b20 <main+348>: lea -0x4(%ecx),%esp
0x08048b23 <main+351>: ret
得到的结果显然与题目给我们那个提示的C程序是一样的,下面我们先来看看initialize_bomb函数:
同样:disas initialize_bomb:列出关键的地方:
0x0804963c <initialize_bomb+141>: call 0x80494b1 <open_clientfd>
根据这个函数名称的意思(打开连接),应该与我们之前的猜想吻合,所以:
Disas open_clientfd:
0x080494da <open_clientfd+41>: call 0x80487a4 <socket@plt>,
Socket出现了,这个是用于网络编程的,我们大致就可以确定initialize_bomb函数应该
就是负责与服务器的连接,所以可以大胆的将其nop掉,既然要修改次可执行文件,所以我们先:quit,退出gdb,然后:gdb –write ./bomb(gdb默认的是只读权限打开),我们用之前的方法定位到0x080495af(initialize_bomb函数的第一行)和0x08048f72(send_msg函数的第一行),然后分别输入下面的命令:
set *(unsigned char *)0x8048f72=0xc3和set *(unsigned char *)0x080495af=0xc3,其中0xc3是指令ret的16进制值,也就是说对于那两个函数一旦调用就马上结束,它们的功能都没有实现。现在程序在本地就可以顺利的执行了= =
下面似乎就到了用gdb进行跟踪的时候,无奈本人太懒了,受不了那种,所以在无奈之下找到了另一个工具insight,号称是图形化的gdb,用了一下感觉真的不错,有了一点点OD的影子,当然解密太粗糙鸟= =给一个界面展示吧:
是不是比黑乎乎的界面人性化多鸟,而且为了保持向下兼容有个这个东东:
在Console Window这个窗口中就可以输入gdb的命令达到同样的效果,按r将程序运行起来,然后我们一路ni(单步跟踪,不进入调用函数内部,类似于OD的F7),当我们在终端看到输入提示之后输入一串字符串:hello word /*本来是想输入hello world的 习惯吧world打成word了= =*/然后si进入phash_1函数内部:
这段代码应该简单,关键是call 0x8048f10,下面的test %eax,%eax,再下面是je XXX
这个函数名是strings_not_equal,如果作者真的是很人性化的话,那么那个函数就应该是对字符串进行明文匹配,函数调用时堆栈的情况,我们可以看出,0x80498b0应该是内存中早已初始化了的一串字符串,我们输入的字符串应该和它一样,我们查看一下那个地址的内容:
我们可以发现字符串是:I am not part of the problem.I am a Republican.
我们输入的字符串的首地址应在0x8(%ebp)[ebp+8]里面,在运行到0x8048ed2的时候查看eax里面的值为:0x804a820,此时内存中该地址里面的内容是:
正是我们输入的内容,所以判断是正确的,所以我们我们第一关输入:I am not part of the problem. I am a Republican.
看来作者也是比较人性话的。嘿嘿,我们继续往下走吧:
- 0x8048d1c <phase_2>: push %ebp
- 0x8048d1d <phase_2+1>: mov %esp,%ebp
- 0x8048d1f <phase_2+3>: push %esi
- 0x8048d20 <phase_2+4>: push %ebx
- 0x8048d21 <phase_2+5>: sub $0x30,%esp
- 0x8048d24 <phase_2+8>: lea -0x20(%ebp),%eax
- 0x8048d27 <phase_2+11>: mov %eax,0x4(%esp) //[esp+4]=0xbfae7798
- 0x8048d2b <phase_2+15>: mov 0x8(%ebp),%eax
- 0x8048d2e <phase_2+18>: mov %eax,(%esp) //[esp]=0x804a870
- 0x8048d31 <phase_2+21>: call 0x8049295 <read_six_numbers>
- 0x8048d36 <phase_2+26>: cmpl $0x1,-0x20(%ebp)
- 0x8048d3a <phase_2+30>: je 0x8048d41 <phase_2+37>
- 0x8048d3c <phase_2+32>: call 0x8049253 <explode_bomb>
- 0x8048d41 <phase_2+37>: mov $0x2,%ebx
- 0x8048d46 <phase_2+42>: lea -0x20(%ebp),%esi
- 0x8048d49 <phase_2+45>: mov %ebx,%eax
- 0x8048d4b <phase_2+47>: imul -0x8(%esi,%ebx,4),%eax
- 0x8048d50 <phase_2+52>: cmp %eax,-0x4(%esi,%ebx,4)
- 0x8048d54 <phase_2+56>: je 0x8048d5b <phase_2+63>
- 0x8048d56 <phase_2+58>: call 0x8049253 <explode_bomb>
- 0x8048d5b <phase_2+63>: add $0x1,%ebx
- 0x8048d5e <phase_2+66>: cmp $0x7,%ebx
- 0x8048d61 <phase_2+69>: jne 0x8048d49 <phase_2+45>
- 0x8048d63 <phase_2+71>: add $0x30,%esp
- 0x8048d66 <phase_2+74>: pop %ebx
- 0x8048d67 <phase_2+75>: pop %esi
- 0x8048d68 <phase_2+76>: pop %ebp
- 0x8048d69 <phase_2+77>: ret
0x804a870里面的内容是我们第二关输入的字符串,我们来看看call 0x8049253,这个函数的名称是read_six_numbers,应该是要我们输入六个数字吧,看看函数的具体内容:
0x08049295 <read_six_numbers+0>: push %ebp
0x08049296 <read_six_numbers+1>: mov %esp,%ebp
0x08049298 <read_six_numbers+3>: sub $0x28,%esp
0x0804929b <read_six_numbers+6>: mov 0xc(%ebp),%edx
0x0804929e <read_six_numbers+9>: lea 0x14(%edx),%eax
0x080492a1 <read_six_numbers+12>: mov %eax,0x1c(%esp)
0x080492a5 <read_six_numbers+16>: lea 0x10(%edx),%eax
0x080492a8 <read_six_numbers+19>: mov %eax,0x18(%esp)
0x080492ac <read_six_numbers+23>: lea 0xc(%edx),%eax
0x080492af <read_six_numbers+26>: mov %eax,0x14(%esp)
0x080492b3 <read_six_numbers+30>: lea 0x8(%edx),%eax
0x080492b6 <read_six_numbers+33>: mov %eax,0x10(%esp)
0x080492ba <read_six_numbers+37>: lea 0x4(%edx),%eax
0x080492bd <read_six_numbers+40>: mov %eax,0xc(%esp)
0x080492c1 <read_six_numbers+44>: mov %edx,0x8(%esp)
0x080492c5 <read_six_numbers+48>: movl $0x8049aca,0x4(%esp)
0x080492cd <read_six_numbers+56>: mov 0x8(%ebp),%eax
0x080492d0 <read_six_numbers+59>: mov %eax,(%esp)
0x080492d3 <read_six_numbers+62>: call 0x80488b4 <sscanf@plt>
0x080492d8 <read_six_numbers+67>: cmp $0x5,%eax//读入的数据至少6个
0x080492db <read_six_numbers+70>: jg 0x80492e2 <read_six_numbers+77>
0x080492dd <read_six_numbers+72>: call 0x8049253 <explode_bomb>
0x080492e2 <read_six_numbers+77>: leave
0x080492e3 <read_six_numbers+78>: ret
我们可以看到cmp $0x5,%eax,下一行是jg,则读入的数据至少六个,看来出题人真的很厚道,
而前面那应该是对我们输入的数据进行复制处理,我们在看这部分:
0x8048d36 <phase_2+26>: cmpl $0x1,-0x20(%ebp)
[ebp-0x20]= 0xbfae7798,[0xbfae7798]里面的内容就是我们输入的数据的第一个值,发现它必须为1,否则就失败了,我们先关注下面这一段代码:
- 0x8048d36 <phase_2+26>: cmpl $0x1,-0x20(%ebp)
- 0x8048d3a <phase_2+30>: je 0x8048d41 <phase_2+37>
- 0x8048d3c <phase_2+32>: call 0x8049253 <explode_bomb>
- 0x8048d41 <phase_2+37>: mov $0x2,%ebx //ebx作为计数之用
- 0x8048d46 <phase_2+42>: lea -0x20(%ebp),%esi //esi指向我们输入数据
- 0x8048d49 <phase_2+45>: mov %ebx,%eax //eax复制为此时ebx的值
- 0x8048d4b <phase_2+47>: imul -0x8(%esi,%ebx,4),%eax
//eax = eax *[esi+4*ebx-8] , [esi+4*ebx-8]为第(ebx-2)个我们输入的数据
- 0x8048d50 <phase_2+52>: cmp %eax,-0x4(%esi,%ebx,4)
//此时eax的值必须要等于我们输入的第(ebx-1)个数据的值,不相等就失败
- 0x8048d54 <phase_2+56>: je 0x8048d5b <phase_2+63>
- 0x8048d56 <phase_2+58>: call 0x8049253 <explode_bomb>
- 0x8048d5b <phase_2+63>: add $0x1,%ebx //计数加1
- 0x8048d5e <phase_2+66>: cmp $0x7,%ebx 循环(7-2)=5次
- 0x8048d61 <phase_2+69>: jne 0x8048d49 <phase_2+45>
看这段的架势应该是个循环语句;
翻译成C‘伪’代码大致如下:
int arr[6]//里面是我们输入的6个数据
int eax, ebx;
if (arr[0] != 1) 失败 退出
else
{
ebx = 2;
do
{
eax = ebx;
eax = eax * arr[ebx-2];
if (eax != arr[ebx – 1]) 失败 退出
ebx++;
}while(ebx != 7);
}
这段代码应该比较好理解,转化成语言描述就是指着6个数据的值分别是1,1*2,1*2*3,1*2*3*4,1*2*3*4*5,1*2*3*4*5*6.即第n个数据为n!.
下面继续战斗
0x8048e01 <phase_3>: push %ebp
- 0x8048e02 <phase_3+1>: mov %esp,%ebp
- 0x8048e04 <phase_3+3>: sub $0x28,%esp
- 0x8048e07 <phase_3+6>: lea -0x8(%ebp),%eax
- 0x8048e0a <phase_3+9>: mov %eax,0xc(%esp)
- 0x8048e0e <phase_3+13>: lea -0x4(%ebp),%eax
- 0x8048e11 <phase_3+16>: mov %eax,0x8(%esp)
- 0x8048e15 <phase_3+20>: movl $0x8049ad6,0x4(%esp)
- 0x8048e1d <phase_3+28>: mov 0x8(%ebp),%eax
- 0x8048e20 <phase_3+31>: mov %eax,(%esp)
在调用sscanf函数之前:
[esp+0x4]=0x8049ad6(%d %d)读入数据的格式,我们知道读入的数据是两个int型(%d)
[esp+0x8]=0xbfae77b4:第一个数据存放的地址
[esp+0xc]=0xbfae77b0:第二个数据存放的地址
- 0x8048e23 <phase_3+34>: call 0x80488b4 <sscanf@plt>
- 0x8048e28 <phase_3+39>: cmp $0x1,%eax //输入的数据个数要大于1
- 0x8048e2b <phase_3+42>: jg 0x8048e32 <phase_3+49>
- 0x8048e2d <phase_3+44>: call 0x8049253 <explode_bomb>
- 0x8048e32 <phase_3+49>: cmpl $0x7,-0x4(%ebp)
- 0x8048e36 <phase_3+53>: ja 0x8048ea1 <phase_3+160>
//第一个数据与7比较,大于7就game over
- 0x8048e38 <phase_3+55>: mov -0x4(%ebp),%eax//第一个数据赋值给eax
- 0x8048e3b <phase_3+58>: jmp *0x8049900(,%eax,4)
//jmp [0x8049900+4*eax] 这是gcc中switch结构的跳转表!
从上面的图可以得知
eax=0 jmp 0x8048e72; eax=1 jmp 0x8048e79;eax=2 jmp 0x8048e69;eax=3 jmp 0x8048e62
eax=4 jmp 0x8048e59; eax=5 jmp 0x8048e52;eax=6 jmp 0x8048e49;eax=7 jmp 0x8048e42
我们设输入的两个整数为int1和int2
- 0x8048e42 <phase_3+65>: mov $0x0,%eax
- 0x8048e47 <phase_3+70>: jmp 0x8048e9a <phase_3+153> int1=7
- 0x8048e49 <phase_3+72>: mov $0x0,%eax
- 0x8048e4e <phase_3+77>: xchg %ax,%ax
- 0x8048e50 <phase_3+79>: jmp 0x8048e95 <phase_3+148>int1=6
- 0x8048e52 <phase_3+81>: mov $0x0,%eax
- 0x8048e57 <phase_3+86>: jmp 0x8048e90 <phase_3+143>int1=5
- 0x8048e59 <phase_3+88>: mov $0x0,%eax
- 0x8048e5e <phase_3+93>: xchg %ax,%ax
- 0x8048e60 <phase_3+95>: jmp 0x8048e8b <phase_3+138> int1=4
- 0x8048e62 <phase_3+97>: mov $0x0,%eax
- 0x8048e67 <phase_3+102>: jmp 0x8048e86 <phase_3+133>int1=3
- 0x8048e69 <phase_3+104>: mov $0x0,%eax
- 0x8048e6e <phase_3+109>: xchg %ax,%ax
- 0x8048e70 <phase_3+111>: jmp 0x8048e81 <phase_3+128>int1=2
- 0x8048e72 <phase_3+113>: mov $0x396,%eax
- 0x8048e77 <phase_3+118>: jmp 0x8048e7e <phase_3+125>int1=0
- 0x8048e79 <phase_3+120>: mov $0x0,%eax //int1=1
- 0x8048e7e <phase_3+125>: sub $0x53,%eax
- 0x8048e81 <phase_3+128>: add $0x139,%eax
- 0x8048e86 <phase_3+133>: sub $0x1e5,%eax
- 0x8048e8b <phase_3+138>: add $0x279,%eax
- 0x8048e90 <phase_3+143>: sub $0x3da,%eax
- 0x8048e95 <phase_3+148>: add $0x3da,%eax
- 0x8048e9a <phase_3+153>: sub $0x30e,%eax
- 0x8048e9f <phase_3+158>: jmp 0x8048eab <phase_3+170>
- 0x8048ea1 <phase_3+160>: call 0x8049253 <explode_bomb>
- 0x8048ea6 <phase_3+165>: mov $0x0,%eax
- 0x8048eab <phase_3+170>: cmpl $0x5,-0x4(%ebp)
- 0x8048eaf <phase_3+174>: jg 0x8048eb6 <phase_3+181>
- 0x8048eb1 <phase_3+176>: cmp -0x8(%ebp),%eax
- 0x8048eb4 <phase_3+179>: je 0x8048ebb <phase_3+186>
- 0x8048eb6 <phase_3+181>: call 0x8049253 <explode_bomb>
- 0x8048ebb <phase_3+186>: leave
- 0x8048ebc <phase_3+187>: lea 0x0(%esi),%esi
- 0x8048ec0 <phase_3+191>: ret
这个貌似很头大=。= 其实仔细看一下倒是很有规律的:
我们用伪代码进行描述:
int temp=0;
switch(int1)
{
case 0:
temp=0x396;
jmp _125;
case 1:
jmp _125;
case 2:
jmp _128;
case 3:
jmp _133;
case 4:
jmp _138;
case 5:
jmp _143;
case 6:
jmp _148;
case 7:
jmp _153;
_125: temp -= 0x53;
_128: temp += 0x139;
_133: temp -= 0x1e5;
_138: temp += 0x279;
_143: temp -= 0x3da;
_148: temp += 0x3da;
_153: temp -= 0x30e;
if (int1 > 5 || temp != int2) 失败
则int1只有:0,1,2,3,4,5这六种情况:
int1=0; 则int2=0x398-0x53+0x139-0x1e5+0x279-0x3da+0x3da-0x30e=514
int1=1; 则int2=0-0x53+0x139-0x1e5+0x279-0x3da+0x3da-0x30e=-404
int1=2; 则int2=0+0x139-0x1e5+0x279-0x3da+0x3da-0x30e=-321
int1=3; 则int2=0-0x1e5+0x279-0x3da+0x3da-0x30e=-634
int1=4; 则int2=0+0x279-0x3da+0x3da-0x30e=-149
int1=5; 则int2=0-0x3da+0x3da-0x30e=-782
即第三关有6组答案(0,514),(1,-404),(2,-321),(3,-634),(4,-149),(5,-782)
进过测试,key取的是第一组的结果.
继续:
Phase4:
- 0x8048db6 <phase_4>: push %ebp
- 0x8048db7 <phase_4+1>: mov %esp,%ebp
- 0x8048db9 <phase_4+3>: sub $0x28,%esp
- 0x8048dbc <phase_4+6>: lea -0x4(%ebp),%eax
- 0x8048dbf <phase_4+9>: mov %eax,0x8(%esp)
- 0x8048dc3 <phase_4+13>: movl $0x8049ad9,0x4(%esp) //%d
- 0x8048dcb <phase_4+21>: mov 0x8(%ebp),%eax
- 0x8048dce <phase_4+24>: mov %eax,(%esp)
- 0x8048dd1 <phase_4+27>: call 0x80488b4 <sscanf@plt>
- 0x8048dd6 <phase_4+32>: cmp $0x1,%eax //只能输入一个整数
- 0x8048dd9 <phase_4+35>: jne 0x8048de1 <phase_4+43>
- 0x8048ddb <phase_4+37>: cmpl $0x0,-0x4(%ebp) //输入的整数必须大于0
- 0x8048ddf <phase_4+41>: jg 0x8048de6 <phase_4+48>
- 0x8048de1 <phase_4+43>: call 0x8049253 <explode_bomb>
- 0x8048de6 <phase_4+48>: mov -0x4(%ebp),%eax
- 0x8048de9 <phase_4+51>: mov %eax,(%esp)
- 0x8048dec <phase_4+54>: call 0x8048b30 <func4> //这个是关键的算法,跟进去
- 0x8048df1 <phase_4+59>: cmp $0x2ac2,%eax
- 0x8048df6 <phase_4+64>: je 0x8048dfd <phase_4+71>
- 0x8048df8 <phase_4+66>: call 0x8049253 <explode_bomb>
- 0x8048dfd <phase_4+71>: leave
- 0x8048dfe <phase_4+72>: xchg %ax,%ax
- 0x8048e00 <phase_4+74>: ret
我们发现0x8049ad9的内容为%d,则说明要求输入一个整数,跟踪进func4:
- 0x8048b30 <func4>: push %ebp
- 0x8048b31 <func4+1>: mov %esp,%ebp
- 0x8048b33 <func4+3>: sub $0xc,%esp
- 0x8048b36 <func4+6>: mov %ebx,-0x8(%ebp)
- 0x8048b39 <func4+9>: mov %esi,-0x4(%ebp)
- 0x8048b3c <func4+12>: mov 0x8(%ebp),%esi
- 0x8048b3f <func4+15>: mov $0x1,%eax //eax初值为输入的整数
- 0x8048b44 <func4+20>: cmp $0x1,%esi //esi初值为输入的整数
- 0x8048b47 <func4+23>: jle 0x8048b63 <func4+51> //eis<=1就结束
- 0x8048b49 <func4+25>: lea -0x1(%esi),%eax
- 0x8048b4c <func4+28>: mov %eax,(%esp)
- 0x8048b4f <func4+31>: call 0x8048b30 <func4>
- 0x8048b54 <func4+36>: mov %eax,%ebx
- 0x8048b56 <func4+38>: lea -0x2(%esi),%eax
- 0x8048b59 <func4+41>: mov %eax,(%esp)
- 0x8048b5c <func4+44>: call 0x8048b30 <func4>
- 0x8048b61 <func4+49>: add %ebx,%eax
- 0x8048b63 <func4+51>: mov -0x8(%ebp),%ebx
- 0x8048b66 <func4+54>: mov -0x4(%ebp),%esi
- 0x8048b69 <func4+57>: mov %ebp,%esp
- 0x8048b6b <func4+59>: pop %ebp
- 0x8048b6c <func4+60>: ret
定眼一看里面又有调用fun4,则应该是一个递归调用了!
我们先来弄一个“伪代码”:
fun(int x)
{
esi=x;
eax=1;
while(esi>1)
{
ebx= fun(esi-1);
Eax= fun(esi-2)+ebx;
}
Return eax;
}
学过奥赛的童鞋应该并不陌生,这是有名的费波那契数列:
f(1)=1,f(2)=1,n>2;f(n)=f(n-1)+f(n-2);
我们相当于是要求f(N)= 0x2ac2,我们输入的就是N,这个编程也可,手算也行,N=20
继续:
- 0x8048d6a <phase_5>: push %ebp
- 0x8048d6b <phase_5+1>: mov %esp,%ebp
- 0x8048d6d <phase_5+3>: push %ebx
- 0x8048d6e <phase_5+4>: sub $0x4,%esp
- 0x8048d71 <phase_5+7>: mov 0x8(%ebp),%ebx
- 0x8048d74 <phase_5+10>: mov %ebx,(%esp)
- 0x8048d77 <phase_5+13>: call 0x8048ef0 <string_length>
- 0x8048d7c <phase_5+18>: cmp $0x6,%eax //输入字符串的长度必须为6
- 0x8048d7f <phase_5+21>: je 0x8048d86 <phase_5+28>
- 0x8048d81 <phase_5+23>: call 0x8049253 <explode_bomb>
- 0x8048d86 <phase_5+28>: mov $0x0,%edx
- 0x8048d8b <phase_5+33>: mov $0x0,%ecx
- 0x8048d90 <phase_5+38>: movsbl (%edx,%ebx,1),%eax //ebx存放的是我们输入的字符串的首地址:0x804a960
- 0x8048d94 <phase_5+42>: and $0xf,%eax //每个字符串与0xf异或
- 0x8048d97 <phase_5+45>: add 0x8049920(,%eax,4),%ecx//内存中存在的一个数组,ecx记录其中某几个字符的ASC码之和
- 0x8048d9e <phase_5+52>: add $0x1,%edx //edx起计数器的作用
- 0x8048da1 <phase_5+55>: cmp $0x6,%edx //6个字符
- 0x8048da4 <phase_5+58>: jne 0x8048d90 <phase_5+38>
- 0x8048da6 <phase_5+60>: cmp $0x32,%ecx//几个字符之和为0x32
- 0x8048da9 <phase_5+63>: je 0x8048db0 <phase_5+70>
- 0x8048dab <phase_5+65>: call 0x8049253 <explode_bomb>
- 0x8048db0 <phase_5+70>: add $0x4,%esp
- 0x8048db3 <phase_5+73>: pop %ebx
- 0x8048db4 <phase_5+74>: pop %ebp
- 0x8048db5 <phase_5+75>: ret
进过简单的分析我们可以知道这个算法的大致思路:
首先在内存0x8049920开始有一串字符串,然后按照我们输入的字符串的每个字符的ASC码与0xf异或得到的数字作为索引,在之前的那个字符串按照索引得到的字符的ASC码之和要为0x32(50),那么显然我们的关键点是要放在0x8049920的那一串字符串里面,截图如下:
我们可以看到字符串各字符的10进制为:
2,10,6,1,12,16,10,9,3,4,7,14,5,11,8,15,13
我表示这很疼= =不过还好这些数字里面没有重复滴 囧 刚好是1-16
从里面选六个数字加起来等于50,情况很多种= =,不想去弄了= =,
大家找六个数,找到其在字符串当中对应的索引,然后分别与0xf异或吧…
这里给出一组最后的结果:pqrwzn
终于要最后一关鸟= =
我们看到这个算法函数非常长,估计循环都有几个,只静态分析的话有点难度= =,还是动态跟踪加静态分析吧。
- 0x8048c2c <phase_6>: push %ebp
- 0x8048c2d <phase_6+1>: mov %esp,%ebp
- 0x8048c2f <phase_6+3>: push %edi
- 0x8048c30 <phase_6+4>: push %esi
- 0x8048c31 <phase_6+5>: push %ebx
- 0x8048c32 <phase_6+6>: sub $0x3c,%esp
- 0x8048c35 <phase_6+9>: lea -0x24(%ebp),%eax
- 0x8048c38 <phase_6+12>: mov %eax,0x4(%esp)
- 0x8048c3c <phase_6+16>: mov 0x8(%ebp),%eax
- 0x8048c3f <phase_6+19>: mov %eax,(%esp)
- 0x8048c42 <phase_6+22>: call 0x8049295 <read_six_numbers>
//之前已经遇到过这个函数,输入六个数字
- 0x8048c47 <phase_6+27>: mov $0x0,%ebx
- 0x8048c4c <phase_6+32>: mov -0x24(%ebp,%ebx,4),%eax
//[ebp-0x24]=0xbfc1994,就是我们输入的6个数字的起始位置
//根据后面的判断,这里使用了一个双重循环:判断我们输入的6个数字是不是两两不相等的!
- 0x8048c50 <phase_6+36>: sub $0x1,%eax
- 0x8048c53 <phase_6+39>: cmp $0x5,%eax
- 0x8048c56 <phase_6+42>: jbe 0x8048c5d <phase_6+49>
//我们输入的整数不能大于6,否则就失败
- 0x8048c58 <phase_6+44>: call 0x8049253 <explode_bomb>
- 0x8048c5d <phase_6+49>: lea 0x1(%ebx),%edi
//ebx应该起计数器的作用,这应该是一个循环
- 0x8048c60 <phase_6+52>: cmp $0x6,%edi
- 0x8048c63 <phase_6+55>: je 0x8048d03 <phase_6+215>
//当判断完毕的时候,就从这里跳出去
- 0x8048c69 <phase_6+61>: lea -0x24(%ebp,%ebx,4),%esi
//esi存放的是我们输入数字
- 0x8048c6d <phase_6+65>: mov %edi,%ebx
- 0x8048c6f <phase_6+67>: lea -0x24(%ebp),%eax
- 0x8048c72 <phase_6+70>: mov %eax,-0x40(%ebp)
- 0x8048c75 <phase_6+73>: mov -0x40(%ebp),%edx
- 0x8048c78 <phase_6+76>: mov -0x4(%edx,%edi,4),%eax
//eax里面依次存放的也是我们输入的数字
- 0x8048c7c <phase_6+80>: cmp 0x4(%esi),%eax
//[esi+4]存放的正好是eax里面的数字的下一个数字:
比如:我们输入的是1,2,3,4,5,6。 eax为2,则[esi+4]=3
- 0x8048c7f <phase_6+83>: jne 0x8048c86 <phase_6+90>
//看来估计是要求我们输入的这6个数字不能有重复的
- 0x8048c81 <phase_6+85>: call 0x8049253 <explode_bomb>
- 0x8048c86 <phase_6+90>: add $0x1,%ebx
- 0x8048c89 <phase_6+93>: add $0x4,%esi
- 0x8048c8c <phase_6+96>: cmp $0x5,%ebx
//上面几行验证了我们的推断
- 0x8048c8f <phase_6+99>: jle 0x8048c75 <phase_6+73>//跳回去第一个循环处
- 0x8048c91 <phase_6+101>: mov %edi,%ebx
- 0x8048c93 <phase_6+103>: jmp 0x8048c4c <phase_6+32>
- 0x8048c95 <phase_6+105>: mov 0x8(%ecx),%ecx //地址加0x8
- 0x8048c98 <phase_6+108>: add $0x1,%edx //edx增加1
- 0x8048c9b <phase_6+111>: cmp -0x24(%ebp,%eax,4),%edx
//一个循环:输入的数字的值与edx作比较
- 0x8048c9f <phase_6+115>: jl 0x8048c95 <phase_6+105>
- 0x8048ca1 <phase_6+117>: mov %ecx,-0x3c(%ebp,%eax,4)
//发现程序将地址放到了[ebp-0x3]开始的地址中,
- 0x8048ca5 <phase_6+121>: add $0x1,%eax//将上面的地址增加
- 0x8048ca8 <phase_6+124>: cmp $0x5,%eax
//看来这是一个外层循环,将所有的六个数依次遍历。
整个循环结束之后,我们得到了一个排列好了的六个地址,
这几个地址是0x804a5fc, 0x804a5f0,0x804a5e4,0x804a5d8,0x804a5cc,0x804a5c0,
- 0x8048cab <phase_6+127>: jg 0x8048cb9 <phase_6+141>
- 0x8048cad <phase_6+129>: mov $0x1,%edx
- 0x8048cb2 <phase_6+134>: mov $0x804a5fc,%ecx
- 0x8048cb7 <phase_6+139>: jmp 0x8048c9b <phase_6+111>
最开始:[ebp-0x3c]=[0xbf9d5d9c]的值为上面六个地址排在最前面的值,其余值依次排在后面,我们不妨设这6个地址为add1-add6
- 0x8048cb9 <phase_6+141>: mov -0x3c(%ebp),%ecx//ecx=add1
- 0x8048cbc <phase_6+144>: mov -0x38(%ebp),%eax//eax=add2
- 0x8048cbf <phase_6+147>: mov %eax,0x8(%ecx)//[add1+8]=add2
- 0x8048cc2 <phase_6+150>: mov -0x34(%ebp),%edx//edx=add3
- 0x8048cc5 <phase_6+153>: mov %edx,0x8(%eax)//[add2+8]=add3
- 0x8048cc8 <phase_6+156>: mov -0x30(%ebp),%eax//eax=add4
- 0x8048ccb <phase_6+159>: mov %eax,0x8(%edx)//[add3+8]=add4
- 0x8048cce <phase_6+162>: mov -0x2c(%ebp),%edx//edx=add5
- 0x8048cd1 <phase_6+165>: mov %edx,0x8(%eax)//[add4+8]=add5
- 0x8048cd4 <phase_6+168>: mov -0x28(%ebp),%eax//eax=add6
- 0x8048cd7 <phase_6+171>: mov %eax,0x8(%edx)//[add5+8]=add6
- 0x8048cda <phase_6+174>: movl $0x0,0x8(%eax)//[add6+8]=0
- 0x8048ce1 <phase_6+181>: mov %ecx,%ebx//ebx=add1
- 0x8048ce3 <phase_6+183>: mov $0x0,%esi
- 0x8048ce8 <phase_6+188>: mov 0x8(%ebx),%edx//edx=[add1+8]=add2
- 0x8048ceb <phase_6+191>: mov (%ebx),%eax//eax=[add1]
- 0x8048ced <phase_6+193>: cmp (%edx),%eax
//必须满足[add1]>[add2]
- 0x8048cef <phase_6+195>: jge 0x8048cf6 <phase_6+202>
- 0x8048cf1 <phase_6+197>: call 0x8049253 <explode_bomb>
相信到了这里应该就比较明朗了= =:之后的条件就是:
[add2]>[add3]>[add4]>[add5]>add[6],又
*0x804a5fc=0x1a2,*0x804a5f0=0x248,*0x804a5e4=0x39f,
*0x804a5d8=0xec,*0x804a5cc=0x32b,*0x804a5c0=0x1d0,因为这6个值从大到小的排列为:0x39f>0x32b>0x248>0x160>0x1a2>0xec对应的地址的正确排列为:
0x804a5e4,0x804a5cc,0x804a5f0,0x804a5c0,0x804a5fc,0x804a5d8.
而地址的排序与我们输入的1-6的顺序刚好是对应关系,所以我们输入的6个数字调整为:3 5 2 6 1 4即可
- 0x8048cf6 <phase_6+202>: mov 0x8(%ebx),%ebx
- 0x8048cf9 <phase_6+205>: add $0x1,%esi
- 0x8048cfc <phase_6+208>: cmp $0x5,%esi//
一个外层循环,当esi能够达到0x5时候,这关就算过了= =
- 0x8048cff <phase_6+211>: je 0x8048d14 <phase_6+232>
- 0x8048d01 <phase_6+213>: jmp 0x8048ce8 <phase_6+188>
- 0x8048d03 <phase_6+215>: mov $0x1,%edx//edx的初始值为1
- 0x8048d08 <phase_6+220>: mov $0x804a5fc,%ecx//[ecx]=0x1a2
- 0x8048d0d <phase_6+225>: mov $0x0,%eax
- 0x8048d12 <phase_6+230>: jmp 0x8048c9b <phase_6+111>
- 0x8048d14 <phase_6+232>: add $0x3c,%esp
- 0x8048d17 <phase_6+235>: pop %ebx
- 0x8048d18 <phase_6+236>: pop %esi
- 0x8048d19 <phase_6+237>: pop %edi
- 0x8048d1a <phase_6+238>: pop %ebp
- 0x8048d1b <phase_6+239>: ret
好了,到这里,6个关都分析完毕了,感觉头早都被弄晕了
}
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
上传的附件: