二进制炸弹实验

在这个实验中,我们需要使用反汇编工具得到汇编代码,并且通过分析和使用gdb调试工具来拆除6个炸弹.

首先通过objdump -d bomb 反汇编bomb得到bomb的汇编代码


首先看phase_1的代码

二进制炸弹实验_第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


输入字符串后拆除成功

二进制炸弹实验_第2张图片

虽然通过函数名字猜出了用途,但是为了学习还是看一下strings_not_equal函数

二进制炸弹实验_第3张图片二进制炸弹实验_第4张图片

这个函数思路比较简单,先计算%rdi处字符串的长度,将结果保存在%r12d,再计算%rsi的长度,将结果保存在

%rax,然后比较%rsi和%rax的值,不相等返回1,相等则循环比较字符串的内容,直到某个字符不等或者遇到某个字符串结束为止.



接着看phase_2

二进制炸弹实验_第5张图片

开始时将栈顶寄存器赋值给了%rsi作为参数,调用,以下是read_six_numbers的代码

二进制炸弹实验_第6张图片

开始是一系列的开辟栈帧,调用者保存寄存器和参数传递先不看,可以看到之后调用了<__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中的数据

二进制炸弹实验_第7张图片

可以看出是将结果存储在从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();
            }
        }
    }
}
phase_2是一个等比数列,因此答案为1 2 4 8 16 32


然后是phase_3
二进制炸弹实验_第8张图片

有了phase_2中调用scanf的经验,就可以很容易的看出sscanf的具体作用,用x/s $rsi查看格式串


得出我们需要输入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

二进制炸弹实验_第9张图片

使用x/s $rsi得格式串


可知输入是两个整数,调用sscanf后将第一个输入数与14比较可知第一个输入数必须小于等于14,然后将func的3个参数分别设置为第一个输入数,0,0xe,随后调用func4函数二进制炸弹实验_第10张图片

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;
	}
}

写出c代码后还是难以理解,开始以为是二分查找,后来发现不是,先计算出b,c的中点y,如果y等于a,则返回0这里还比较好懂,当ya时,调用func4(b,y-1),即左半部分,看到这里明白了函数何时返回0,即给定输入b,c时,不断取其中点,然后取左半部分再取中点,直到b与c相等为止,这时就可以得出结果了,输入为0和14,第一次取中点得7,然后依次得到3,1,0,总共4个解
回到phase_4


易知第二个输入应该为0,因此拆除第4个炸弹的答案应该有4种,分别是

7,0

3,0

1,0

0,0


接着看phase_5,感觉phase_5比phase_4要简单

二进制炸弹实验_第11张图片


直接跟着代码看一条一条翻译就可以了,先调用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,看了几个小时,经常是看到一半不记得某个寄存器的值了,因此最好拆成几个部分看,先看第一部分代码.

二进制炸弹实验_第12张图片

这部分比较简单,保存调用者保存的寄存器,开辟栈帧,然后将输入的字符流转换为6个整数存放在%rsp开始的连续24个字节空间中,检查是否成功读入6个后进入第二个部分二进制炸弹实验_第13张图片

第二部分看了一段时间,发现是一个二重循环,初始时%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();
	}
}
下一段
二进制炸弹实验_第14张图片
对数组中的每个元素执行a[i] = 7 - a[i]
原始输入的数字为 1 2 3 4 5 6,就会变成6,5,4,3,2,1。这段比较简单,没什么好说的,下一段

二进制炸弹实验_第15张图片

又是一个二重循环,这个是我的最久的一段,可能是对汇编的数据结构不太熟悉,看了很久才发现是个链表,循环还是对数字进行处理,看内循环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个节点的地址,顺着表头找到剩下的节点

二进制炸弹实验_第16张图片

可以看出一共有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个结点的地址,下一段

二进制炸弹实验_第17张图片

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,然后是最后一段

二进制炸弹实验_第18张图片

知道是对链表进行操作就比较容易理解了,将每个节点的数据的值和下一个节点进行比较,且必须大于下一个节点的值,就是链表必须是降序的形式,这样就能得出答案了。

原链表的数据分别为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个炸弹就全部解除了

二进制炸弹实验_第19张图片

听说还有一个隐藏关,以后再说吧.


























你可能感兴趣的:(csapplab)