在这个实验中,我们需要使用反汇编工具得到汇编代码,并且通过分析和使用gdb调试工具来拆除6个炸弹.
首先通过objdump -d bomb 反汇编bomb得到bomb的汇编代码
首先看phase_1的代码
phase_1考察的是过程调用的参数传递和栈
phase_1开辟了8个字节大小的栈帧,是用来在callq时将下一条指令的地址,即400eee压栈.
输入字符串的首地址保存在寄存器%rdi中,400ee4处将地址0x402400传递给%esi,然后调用strings_not_equal,从名字可知道strings_not_equal比较%rdi和%rsi处的字符串是否相等,返回值保存在%eax中,不相等则会引爆炸弹.为此我们需要查看地址0x402400处的内容,这里需要使用gdb中的x/命令,x后跟有三个可选参数,x/nfu,n是一个正整数,表示需要显示的内存单元的个数,f表示显示的格式,如s表示以字符串格式显示,x表示以16进制形式显示,u表示从当前地址往后请求的字节数,默认为4字节,b表示单字,h表示双字,w表示4字,g表示八字.这里我们要显示字符串,可以直接用 x 0x402400
输入字符串后拆除成功
虽然通过函数名字猜出了用途,但是为了学习还是看一下strings_not_equal函数
这个函数思路比较简单,先计算%rdi处字符串的长度,将结果保存在%r12d,再计算%rsi的长度,将结果保存在
%rax,然后比较%rsi和%rax的值,不相等返回1,相等则循环比较字符串的内容,直到某个字符不等或者遇到某个字符串结束为止.
接着看phase_2
开始时将栈顶寄存器赋值给了%rsi作为参数,调用
开始是一系列的开辟栈帧,调用者保存寄存器和参数传递先不看,可以看到之后调用了<__isoc99_sscanf@plt>,大致可以看出调用了sscanf参数,从参数看出共使用了八个参数,因为寄存器最多传递6个参数,因此最后两个参数用栈传递,八个参数分别为%rdi,%rsi,%rdx,%rcx,%r8,%9,M[%rsp],M[%rsp + 8],从函数名字可以看出是读6个数,那么为什么需要8个参数呢,先使用x查询%rdi和%rsi
看到了似曾相识的字符串,第一个参数是我们从命令行输入的字符流,第二个参数类似c语言scanf()中的格式串,表示如何对从命令行输入的字符流进行解释,可以看出sscanf()将输入流解释为6个32位整数,那么后6个参数则为存储6个32位整数结果的地址,先用info registers查询寄存器中的数据,再使用x查询%rsp和%rsp+8中的数据
可以看出是将结果存储在从0x7fffffffdea0开始的24个字节
sscanf返回成功存储的数据个数,将该个数和5比较,小于等于5个则引爆炸弹,否则还原栈指针%rsp,函数结束
该函数的栈帧为24字节,参数传递16字节,保存栈指针8字节,刚好24字节。回到phase_2函数的400f0a处,易见第一个数为1,之后进入循环,循环中每次将后一个数与前一个数比较,如果不是前一个数的两倍则引爆炸弹.由此可以得出c代码
void phase_2()
{
int a[6];
int x = scanf("%d %d %d %d %d %d", &a[0], &a[1], &a[2], &a[3], &a[4], &a[5]);
if (x < 6) explode_bomb();
else{
if (a[0] != 1) explode_bomb();
else{
for (int i = 1; i < 6; i++){
if (a[i] != a[i - 1] * 2) explode_bomb();
}
}
}
}
得出我们需要输入2个整数,通过sscanf分别读到%rsp + 0x8, %rsp + 0xc中
然后将输入的第一个数和7比较,大于7炸弹爆炸,因此第一个数为1-7之间,接下来的指令比较难以理解
一步一步的来翻译,首先jmpq是跳转到一个64位地址,这个地址在后面给出,先计算出0x402470 + %rax + 8,再通过*取出地址中的内容,将这个内容作为地址,使jmpq跳转到这个地址.根据%rax的取值,跳转到不同的位置,可以看出是一个switch语句的跳转表,因%rax的取值为0-7,通过x/8gx 0x402470 16进制打印出跳转表的跳转地址
跳转到每一处都会对%eax进行赋值,并与第二个输入数进行比较.可写出对应的c代码
void phase_3()
{
int x, y, k;
int z = scanf("%d %d", &x, &y);
if (z < 2) explode_bomb();
else{
switch(x){
case 0:
k = 0xcf;
break;
case 1:
k = 0x137;
break;
case 2:
k = 0x2c3;
break;
case 3:
k = 0x100;
break;
case 4:
k = 0x185;
break;
case 5:
k = 0xce;
break;
case 6:
k = 0x2aa;
break;
case 7:
k = 0x147;
break;
}
if (k != y) explode_bomb();
}
}
由此可以看出答案有8组,分别为
0 207
1 311
2 707
3 256
4 389
5 206
6 682
7 327
phase_3主要考察了switch跳转表的运用,接下来是phase_4
使用x/s $rsi得格式串
可知输入是两个整数,调用sscanf后将第一个输入数与14比较可知第一个输入数必须小于等于14,然后将func的3个参数分别设置为第一个输入数,0,0xe,随后调用func4函数
func4有些复杂,使用了条件语句和递归,看了半天才大致看懂,首先理解前面的几句400fd2-400fdf,写的很复杂,直接上就是
(%rdx - %rsi) / 2朝0舍入,简要概括就是取中点,然后将中点与%rsi做比较,可得出大致的c代码
//返回值为0才不会引爆,初始输入为(a, 0, 14)
int func4(int a, int b, int c)
{
int y = (c - b) / 2;
if (y == a) return 0;
//若a>y返回结果必定包含1,
else if (y < a){
x = func4(a, y + 1, b);
return 2 * x + 1;
}
else{
x = func4(a, b, y - 1);
return 2 * x;
}
}
易知第二个输入应该为0,因此拆除第4个炸弹的答案应该有4种,分别是
7,0
3,0
1,0
0,0
接着看phase_5,感觉phase_5比phase_4要简单
直接跟着代码看一条一条翻译就可以了,先调用string_length计算字符串的长度,字符串的长度必须为6,判断完后就是一顿乱跳,先置%rax为0,再跳转到40108b处,又看到4010ac处的指令为跳转到10108b,可以想到应该是循环对字符串做某种操作,每次循环开始又有一堆 的赋值,简化来看就是将该字符的ascii码对0xffff,即对16进行取余,然后将结果加上0x4024b0作为地址,将地址中的字符复制到以%rsp + 0x10开始的连续6个字节中,循环结束后将%sp + 0x10的字符串与0x40245e处的字符串比较即可,解法就是先查0x40245e处的字符串,再对每个字符查找从0x4024b0处开始的相应字符的地址,减去0x4024b0,再将其转换为字符进行输入即可.
以字符显示内容
找到f在偏移为9的位置,因此输入的第一个字符只要满足其acsii码 % 16 = 9即可,后面的字符依次类推
这里给出一组答案
9?>567
最后是很长很长的phase_6,看了几个小时,经常是看到一半不记得某个寄存器的值了,因此最好拆成几个部分看,先看第一部分代码.
这部分比较简单,保存调用者保存的寄存器,开辟栈帧,然后将输入的字符流转换为6个整数存放在%rsp开始的连续24个字节空间中,检查是否成功读入6个后进入第二个部分
第二部分看了一段时间,发现是一个二重循环,初始时%r13等于%rsp,指向输入的第一个数字,每次循环结束时加4,取下一个输入数字,40111b-401123输入数字应该在1-6之间,401132-40114b为内循环,向后判断数字是否重复,这个二重循环用c代码描述就是
int a[6];
for (int i = 0; i < 6; i++)
{
if (a[i] < 0 || a[i] > 6) explode_bomb();
for (int j = i + 1; j < 6; j++){
if (a[j] == a[i]) explode_bomb();
}
}
又是一个二重循环,这个是我的最久的一段,可能是对汇编的数据结构不太熟悉,看了很久才发现是个链表,循环还是对数字进行处理,看内循环401176-40117f,这个是理解代码的关键,进入循环前先执行了40119f,4011a4处的指令,即%rdx = 0x6032d0,%eax=1,内循环中反复的令%rdx = M[%rdx + 8],并使%rax加1,直到%rax与%rcx相等,可以看出%rdx+8处存放的是一个64位地址,而%rdx存放的是4字节的数据,推断应该是一个整数
看到这个位置有一个node标签,再想到一个数字跟着一个地址的形式,可以联想到一种数据结构,链表.首先表头为0x6032d0,存放1字节数据,后面的地址为下一个节点的地址,内循环其实是取得链表的第%rcx个节点的地址,顺着表头找到剩下的节点
可以看出一共有6个节点,而这段代码其实是将输入作为节点序号,返回相应的节点地址,保存在%rsp+ 0x20开始的存储空间中.如输入1,2,3,4,5,6,经上一步的转换后变为6,5,4,3,2,1,这一段代码依次计算链表的第6,5,4,3,2,1个节点的地址,存放在%rsp + 0x20中。这时,%sp+0x20处已经存放了6个结点的地址,下一段
4011ab-4011ba,取第一个地址赋给%rcx,%rsi存放终止的地址,可以看出又要进入一个循环,第一次循环时,
M(%rax)保存了第二个地址,%rcx保存第一个地址,然后将第二个地址赋给了第一个地址的next结点,总结后得到这段代码按照输入的结点顺序进行了重组,例如输入为3,4,5,6,1,2,原先的表头为1,链表为1->2->3->4->5-6->NULL,那么排序后变成了3->4->5->6->1->2->NULL,并且链表首地址为%rbx,然后是最后一段
知道是对链表进行操作就比较容易理解了,将每个节点的数据的值和下一个节点进行比较,且必须大于下一个节点的值,就是链表必须是降序的形式,这样就能得出答案了。
原链表的数据分别为0x14c,0xa8,0x39c,0x2b3,0x1dd,0x1bb,要使得链表降序,那么节点顺序应该是3,4,5,6,1,2,因为节点按照输入输入顺序排列,因此输入为3,4,5,6,1,2,又因为前面对每个数据进行了取负加7的操作,还原后的原始输入就应该是
4,3,2,1,6,5
这样6个炸弹就全部解除了
听说还有一个隐藏关,以后再说吧.