前言

Bomb Lab来自《深入理解计算机系统》(CSAPP)一书的第三章“程序的机器级表示”的配套实验,该实验的目的是通过反汇编可执行程序,来反推出程序执行内容,进而能够正确破解”密码“,解除“炸弹”。

Bomb Lab文件目录如下:

├── bomb
├── bomb.c
└── README
  • bomb: 可执行程序,我们需要对其进行反汇编和gdb调试。
  • bomb.c: bomb的主函数main的源文件。
  • README: 无关紧要。

通过阅读 bomb.c 可以知道该程序的用法:

/***************************************************************************
 * Dr. Evil's Insidious Bomb, Version 1.1
 * Copyright 2011, Dr. Evil Incorporated. All rights reserved.
 *
 * LICENSE:
 *
 * Dr. Evil Incorporated (the PERPETRATOR) hereby grants you (the
 * VICTIM) explicit permission to use this bomb (the BOMB).  This is a
 * time limited license, which expires on the death of the VICTIM.
 * The PERPETRATOR takes no responsibility for damage, frustration,
 * insanity, bug-eyes, carpal-tunnel syndrome, loss of sleep, or other
 * harm to the VICTIM.  Unless the PERPETRATOR wants to take credit,
 * that is.  The VICTIM may not distribute this bomb source code to
 * any enemies of the PERPETRATOR.  No VICTIM may debug,
 * reverse-engineer, run "strings" on, decompile, decrypt, or use any
 * other technique to gain knowledge of and defuse the BOMB.  BOMB
 * proof clothing may not be worn when handling this program.  The
 * PERPETRATOR will not apologize for the PERPETRATOR's poor sense of
 * humor.  This license is null and void where the BOMB is prohibited
 * by law.
 ***************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include "support.h"
#include "phases.h"

/* 
 * Note to self: Remember to erase this file so my victims will have no
 * idea what is going on, and so they will all blow up in a
 * spectaculary fiendish explosion. -- Dr. Evil 
 */

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

可以看出该可执行程序要求从命令行或者文件以 行 为单位读入字符串,每行字符串对应一个phase的输入。如果phase执行完毕,会调用phase_defused 函数表明该 phase 成功搞定。

该实验共有6个 phase,难度是逐级提升,考点也不尽相同。

首先需要对bomb进行反汇编:

objdump -d bomb > bomb.s

可以在 bomb.s 中找到这6个phase的汇编:

0000000000400ee0 <phase_1>:.
...
0000000000400efc <phase_2>:
...
0000000000400f43 <phase_3>:
...
000000000040100c <phase_4>:
...
0000000000401062 <phase_5>:
...
00000000004010f4 <phase_6>:
...

对于汇编代码的解读,我自己的方法是三步走,第一步可读性调整,不改变汇编的顺序只是改变符号为c风格;第二步是c-like风格调整,将控制结构改为c风格;第三步是说人话风格调整,c代码。

phase 1

开胃菜,考点:字符串。

本题就无须三步了,直接对汇编稍作c-like风格调整后,可以表示为以下代码:

phase_1(rdi)
{
	esi = 0x402400;
	string_no_equal(rdi, esi);
	if (eax == 0)
		callq explode_bomb;
	retq
}

其中0x402400是常量字符串的地址,我们可以通过 gdb 调试 bomb,来打印出这段字符串。

$ gdb bomb
...
...
(gdb) x/s 0x402400
0x402400:	"Border relations with Canada have never been better."

可以知道这串字符串是 Border relations with Canada have never been better.

那么这段汇编用c来表示就是:

void phase_1(char* output)
{
    if( string_not_equal(output, "Border relations with Canada have never been better.") == 0) 
        explode_bomb();
    return;
}

其中 string_not_equal是接受两个参数,对两个字符串进行比较,如果不同则输出0。

因此phase_1的功能:

  • 字符串比较,如果输入的字符串与预设的字符串不相等,则”炸弹爆炸“。

答案:“Border relations with Canada have never been better.”

phase 2

考点:

  • 数组
  • 循环

这里分三步走,首先对汇编进行可读性调整:

phase_2(rdi)
{
	rsi = rsp;
	callq read_six_number;
	if (*rsp == 1)
		goto 400f30;
	else 
		callq explode_bomb;
	goto 400f30;
400f17:
	eax = *(rbx-4);
	eax += eax;
	if (eax == *rbx)
		goto 400f25;
	else 
		callq explode_bomb;
400f25:
	rbx += 4;			# 下一个元素
	if (rbx != rbp)
		goto 400f17;	# 循环
	else 
		goto 400f3c;
400f30:
	rbx = rsp + 4;		# 初始化 init
	rbp = rsp + 24;		# 数组结束
	goto 400f17;
400f3c:
	retq
}

然后我们接着分析汇编表达的意思,从以上代码可以看出,read_six_number接受两个参数 rdi和rsi,rdi是输入的字符串,rsi 是栈上分配的整数数组。

通过阅读 read_six_number 汇编代码,可以直到 read_six_number 是把字符串转为6个数字,存储在整数数组中。

从 400f17 到 400f3c,可以看出这是一个循环,从rbx到rbp,指向的是栈上的整数数组。

因此6个数字的地址为

rsprsp+0x4rsp+0x8rsp+0xcrsp+0x14rsp+0x18

第二步是c-like风格的可读性调整:

phase_2(edi)
{
	rsi = rsp;
	read_six_number(rdi, rsi);
	if (*rsp != 1)
		explode_bomb();
	rbx = rsp + 4;
	rbp = rsp + 24;
	do {
		eax = *(rbx-4);
		eax += eax;
		if (eax != *rbx)
			explode_bomb();
		rbx += 4;
	} while(rbx != rbp);
	retq;
}

形势逐渐明朗,可以比较明确看出phase_2程序所表达的意思:

读入字符串并解析,然后通过循环对数组的每个元素进行处理。

第三步,说人话阶段,使用c代码来表示 phase_2:

void phase_2(char* output)
{
    int array[6];
    read_six_numbers(output, array);
    if (array[0] != 1)    
    	explode_bomb();
    for (int i = 1; i < 6; i++) {
        array[i-1] += array[i-1];
    	if (array[i] != array[i-1])
            explode_bomb();
    }
    return;
}

通过c代码,可以看出phase_2的功能:

  • 将在字符串解析为6个数字的整数数组array
  • array第一个整数必须为1
  • array中每个元素必须是前一个元素的两倍

答案:1 2 4 8 16 32

phase 3

考点:

  • switch-case

首先对汇编代码可读性调整:

phase_3(rdi)
{
	# 省略分配栈上空间
	rcx = rsp + 0xc;
	rdx = rsp + 0x8;
	esi = 0x4025cf;
	eax = 0;
	sccanf(rdi, esi, rdx, rcx);
	if (eax <= 1)
		explode_bomb();
	if(*(rsp + 8) > 7) {
		explode_bomb();
	}
	goto *(0x402470+rax*8);
400f7c:
	eax = 0xcf;
	goto 400fbe;
400f83:
	eax = 0x2c3;
	goto 400fbe;
400f8a:
	eax = 0x100;
	goto 400fbe;
400f91:
	eax = 0x185;
	goto 400fbe;
400f98:
	eax = 0xce;
	goto 400fbe;
400f9f:
	eax = 0x2aa;
	goto 400fbe;
400fa6:
	eax = 0x147;
	goto 400fbe;
	eax = 0;
	goto 400fbe;
400fb9:
	eax = 0x137;
400fbe:
	if (eax != *(rsp+0xc))
		explode_bomb();
	retq;
}

首先是通过 sscanf 解析字符串,通过gdb调试可看出sscanf的format是%d %d。 因此推断输出两个数字。

(gdb) x/s 0x4025cf
0x4025cf:	"%d %d"

其次通过一个跳转表来进行地址跳转,该跳转表内容同样可以用gdb查看:

(gdb) x/16a 0x402470
0x402470:	0x400f7c <phase_3+57>	0x400fb9 <phase_3+118>
0x402480:	0x400f83 <phase_3+64>	0x400f8a <phase_3+71>
0x402490:	0x400f91 <phase_3+78>	0x400f98 <phase_3+85>
0x4024a0:	0x400f9f <phase_3+92>	0x400fa6 <phase_3+99>

可以看出是正好对应着8个对eax赋值的位置:

index01234567
address0x400f7c0x400fb90x400f830x400f8a0x400f910x400f980x400f9f0x400fa6
%eax207311707256389206682327

c代码表示如下:

void phase_3(char* output)
{
    int x, y;
    if(sscanf(output, "%d %d", &x, &y) <= 1)
        explode_bomb();
    if(x > 7)
        explode_bomb();
    int num;
    switch(x) {
    case 0:
        num = 207;
    	break;
    case 1:
        num = 311;
        break;
    case 2:
        num = 707;
        break;
    case 3:
        num = 256;
        break;
    case 4:
        num = 389;
        break;
    case 5:
        num = 206;
        break;
    case 6:
        num = 682;
		break;
    case 7:
        num = 327;
    }
    if (num != y)
        explode_bomb();
    return;
}

通过分析,phase_3的功能:

  • 将字符串解析为两个数字x, y
  • x不能大于7
  • 通过switch-case可以将x的值映射为一个num值
  • y必须等于num

答案(多选):

  • 0 207
  • 1 311
  • 2 707
  • 3 256
  • 4 389
  • 5 206
  • 6 682
  • 7 327

phase 4

考点:

  • 递归

phase 4会有两个函数,先来看主函数,同样第一步先对原汇编进行可读性调整:

phase_4(rdi)
{
	# 省略分配栈上空间
	rcx = rsp + 12;			# sscanf 参数 4
	rdx = rsp + 8;			# sscanf 参数 3
	rsi = 0x4025cf;			# sscanf 参数 2
    rax = 0;
	callq sscanf;
	if (rax != 2) {			# sscanf 返回值判断
		goto 401035;
	}
	if (0x8(rsp) <= 14) {	# 第一个数字
		goto 40103a;
	}
401035:
	goto explode_bomb;
40103a:
	edx = 14;			# func4 参数 3
	rsi = 0;			# func4 参数 2
	rdi = 0x8(rsp);		# func4 参数 1
	goto func4;
	if (rax != 0) {		# func4 返回值判断
		goto 401058;
	}
	if (0xc(rsp) == 0) {	# # 第二个数字
		goto 40105d;
	}
401058:
	goto exlode_bomb;
40105d:
	return;
}

然后进行c-like风格调整:

phase_4(rdi)
{
	rcx = rsp + 12;
	rdx = rsp + 8;
	rsi = 0x4025cf;
	rax = 0;
	rax = sscanf(rdi, rsi, rdx, rcx);
	if (rax != 2) {
		explode_bomb();
	}
	if (*(rsp + 8) <= 14) {
        rdx = 14;
        rsi = 0;
        rdi = *(rsp + 8);
        rax = func4(rdi, rsi, rdx);
        if (rax != 0) {
            explode_bomb();
        }
        if (*(rsp + 12) != 0) {
            explode_bomb();
        }
	}
	retq;
}

从代码中可以看出大概分为两个阶段:

  • 解析字符串
  • 调用 func4

解析字符串部分,sscanf 的format部分可以通过gdb查看:

(gdb) x/s 0x4025cf
0x4025cf:	"%d %d"

可以知道是将字符串解析为两个数字。

第二个阶段,是将第一个数字、0、14作为func4的参数来调用func4,第二个数字必须为0。

然后用c代码表示:

void phase_4(char* input)
{
    int x, y;
    int ret = sscanf(input, "%d %d", &x, &y);
	if (ret != 2 || x > 14) {
		explode_bomb();
	}
    else {
        int ret = func4(x, 0, 14);
        if (ret != 0 || y != 0) {
            explode_bomb();
        }
	}
	return;
}

通过c代码可以看出phase_4的功能:

  • 解析字符串为两个数字x、y

  • x必须小于等于14

  • 调用func4,x、0、14分别为其参数

  • func4返回值必须为0,y必须为0

    因此一顿分析发现主要得看func4的实现,同样对func4进行汇编代码可读性调整:

func4(rdi, rsi, rdx)
{
	eax = edx;
	eax -= esi;
	ecx = eax;
	ecx >>= 31;
	eax += ecx;
	eax >>= 1;				# 对eax一顿操作
	ecx = rax + rsi;		# 对ecx一顿操作
	if (ecx <= edi) {
		goto 400ff2;
	}
	edx = rcx - 1;
	goto func4;				# 递归
	eax += eax;
	goto 401007;
400ff2:
	eax = 0;
	if (ecx >= edi) {		# 递归基
		goto 401007;
	}
	esi = rcx + 1;
	goto func4;				# 递归
	eax = rax + rax + 1;
401007:
	return;
}

还是一脸懵比,只知道是递归,继续c-like风格调整:

func4(edi, esi, edx)
{
	eax = edx - esi;
	eax = (eax + eax >> 31) >> 1;
	ecx = rax + rsi;
	if (ecx <= edi) {
		eax = 0;
        if (ecx >= edi) {
            return;
        }
        esi = rcx + 1;
        rax = func4(edi, esi, edx);
        eax = 2 * rax + 1;
	}
	else {
        edx = rcx - 1;
        rax = func4(edi, esi, edx);
        eax = 2 * rax;	
	}
	retq;
}

局势逐渐明朗,可以看出是该递归有两个分支,ecx <= edi 和 ecx > edi。

第一个分支,esi = rcx +1,然后递归;第二个分支,edx = rcx -1,然后递归。

c代码如下:

int func4(int x, int a, int b)
{
    int num = b - a;
    num = (num + num >> 31) / 2;
    int c = num + a;
    if (c <= x) {
    	if (c >= x) return 0;
        return 2 * func4(x, num+1, b) + 1;
    }
    return 2 * func4(x, a, num-1);
}

其中c其实是通过 (b-a)/2 + a = b/2 + a/2 = c 得到,其中a和b的初始值是0,14,因此c的初始值是 0/2+14/2 = 7。

所以事实上 c 是求 (b-a)/2,然后通过 c 与 x 进行比较决定不同的递归分支,因此这是递归版的二分法。

因此func4的功能:

  • 二分法找出元素x,目标值在数组的前半部分则返回奇数,后半部分则返回偶数;
  • 找到元素x返回0;
  • 如果目标值在二分查找过程中一直在左半边,则会返回0。

经验证,0 1 3 7在二分过程中一直在左半边。

答案:

  • 0 0
  • 1 0
  • 3 0
  • 7 0

phase 5

考点:

  • 字符串
  • 数组
  • for循环

这一题的难度不小了,代码也需要细细打磨,因此继续三步走,先可读性调整:

phase_5(rdx)
{
	rbx = rdi;
	rax = fs:0x28;				# 金丝雀
	*(rsp+0x18) = rax;
	eax = eax ^ eax;			# 清0
	callq string_length;		# 计算字符串长度
	if (eax == 6) {
		goto 4010d2;
	}
	else callq explode_bomb;
	goto 4010d2;
40108b:
	ecx = rbx + rax;
	*rsp = cl;
	rdx = *rsp;
	edx = edx & 0xf;
	edx = rdx + 0x4024b0;
	*(rsp+rax+0x10) = dl;	# 数组操作
	rax += 1;				# 自增
	if (rax != 6)			# 循环退出条件
		goto 40108b;
	*(rsp + 0x16) = 0;
	esi = 0x40245e;
	rdi = rsp + 10;
	callq strings_not_equal;
	if (eax == 0)
		callq explode_bomb;
	nopl
	goto 4010d9;
4010d2:
	eax = 0;				# init
	goto 40108b;			# 循环
4010d9:
	rax = *(rsp + 0x18);
	rax = rax ^ fs:0x28;
	if (rax == 0)			# 栈是否被破坏
		goto 4010ee;
	else
		callq __stack_chk_fail@plt;
4010ee:
	retq;
}

通过这三个部分:

1:
rax += 1;				# 自增
2:
if (rax != 6)			# 循环退出条件
3:
eax = 0;				# init

可以判断这是一个for loop,rax范围是从0到5。

干掉多余的部分和临时变量,继续c-like风格调整如下:

phase_5(rdi)
{
	rbx = rdi;
	eax = eax ^ eax;
	eax = string_length(rdi);
	if (eax == 6) {
		goto 4010d2;
	}
	else explode_bomb();
	for (eax = 0; eax != 6; eax++) {
        ecx = rbx + rax;
        *rsp = cl;
        rdx = *rsp;
        edx = edx & 0xf;
        edx = *(rdx + 0x4024b0);
        *(rsp+rax+0x10) = dl;
	}
    *(rsp + 0x16) = 0;
    esi = 0x40245e;
    rdi = rsp + 10;
    strings_not_equal(rdi, rsi);
    if (eax != 0) {
    	explode_bomb();
    }
}

不容易理解的是这一段:

# 取output字符串rax位置的字符
ecx = rbx + rax;
*rsp = cl;
rdx = *rsp;
# 该值与0xf与操作
edx = edx & 0xf;
# 取0x4024b0数组edx位置的值
edx = *(rdx + 0x4024b0);

这一段事实上是在取output字符串的字符,然后逐次将每个字符与0xf与操作,得到的值做为0x4024b0处字符串的下标。

0xf与操作意味着能取到0x4024b0处字符串的范围是0-15,通过gdb查看0x4024b0处字符串:

(gdb) x/s 0x4024b0
0x4024b0 <array.3449>:	"maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"

可以得到前16为字符串:maduiersnfotvbyl

通过for循环能够生成一个字符串,该字符串由output的 每个字符&0xf 来作为maduiersnfotvbyl的下标,来选择字符。

然后*(rsp + 0x16) = 0是字符串结尾符,最后将该字符串与0x40245e处的字符串进行比较。

0x40245e处的字符串:

(gdb) x/s 0x40245e
0x40245e:	"flyers"

说人话就是:

const char g_str[16] = "maduiersnfotvbyl";
void phase_5(char* output)
{
    char str[7];
	if (string_length(output) != 6) {
		explode_bomb();
	}
        // 9 15 14 5 6 7
    	// I O N E F G
	for (int i = 0; i != 6; i++) {
        str[i] = g_str[output[i] & 0xf];
	}
    str[7] = '\0';
    if(string_not_equal(str, "flyers") != 0) {
        explode_bomb();
    }
}

最后可以分析出该代码的功能:

  • output长度必须等于6
  • 生成一个新的字符串str,由output每个字符&0xf作为maduiersnfotvbyl下标得到
  • str必须为flyers

可以发现flyers六个字母对应maduiersnfotvbyl的下标分别为 9 15 14 5 6 7。

查ascii表,可以找到六个&0xf分别为 9 15 14 5 6 7的字符:

  • IONEFG
  • 其他省略

phase 6

考点:

  • 循环
  • 结构体(异构数组)
  • 链表

phase 6代码看似又臭由长,实际上也是比较难…

这一题需要细细品位,耐心打磨,先不遗余力地可读性调整:

phase_6(rdi)
{
	r13 = rsp;
	rsi = rsp;
	callq read_six_numbers;
	r14 = rsp;
	r12 = 0;					# i = 0
401114:
	rbp = r13;
	eax = *r13;
	eax -= 1;
	if (eax <= 5)
		goto 401128;
	else callq explode_bomb;
401128:
	r12 += 1;					# i++
	if (r12 == 6)
		goto 401153;			# 外层循环退出条件
	ebx = r12;
401135:
	rax = ebx;					# i = j
	eax = *(rsp+rax*4);
	if (*rbp != eax)
		goto 401145;
	else callq explode_bomb;
401145:
	ebx += 1;					# j++
	if (ebx <= 5)				# 内层循环退出条件
		goto 401135;			# 内层循环
		
	r13 += 4;
	goto 401114;				# 外层循环
--------------------------
401153:	
	rsi = rsp + 0x18;
	rax = r14;					# i = r14
	ecx = 7;
401160:
	edx = ecx;
	eax -= *rax;
	*rax - edx;
	rax += 4;					# i += 4
	if (rax != rsi)				# 循环退出条件
		goto 401160;
---------------------------
	esi = 0;					# i = 0
	goto 401197;
401176:
	rdx = *(rdx+8);
	eax += 1;					# j++
	if (eax != ecx)				# 内层循环退出条件
		goto 401176;
	else goto 401188;
401183:
	edx = 0x6032d0;
401188:
	*(rsp+2*rsi+20) = rdx;
	rsi += 4;					# i += 4
	if (rsi == 0x18)			# 外层循环退出条件
		goto 4011ab;
401197:
	ecx = *(rsp+rsi)
	if (ecx <= 1)
		goto 401183;
	eax = 1;					# j = 1
	edx = 0x6032d0;
	goto 401176;
-----------------------------
4011ab:
	rbx = *(rsp+0x20);
	rax = *(rsp+0x28);			# i = *(rsp+0x28);
	rsi = *(rsp+0x50);
	rcx = rbx;
4011bd:
	rdx = *rax;
	*(rcx+8) = rdx;
	rax += 8;					# i += 8
	if (rax == rsi)				# 循环退出条件
		goto 4011d2;
	rcx = rdx;
	goto 4011bd;
-----------------------------
4011d2:
	*(rdx+8) = 0;
	ebp = 5;					# i = 5
4011df:
	rax = *(rbx+0x8);
	eax = *rax;
	if(eax >= *(rbx))
		goto 4011ee;
	else callq explode_bomb;
4011ee:
	rbx = *(rbx+8);
	ebp -= 1;				 	# i--
	if(ebp != 0)				# 循环退出条件
		goto 4011df;
}

我们可以大致分出五部分,分别是五个循环,其中第一个和第三个是双重循环(果真是又长又臭)。

分析清楚整体逻辑,可以进一步改为c-like风格:

phase_6(rdi)
{
	r13 = rsp;
	rsi = rsp;
	read_six_numbers(rdi, rsi);
	r14 = rsp;
    for (r12 = 0; r12 != 6; r12++) {
        rbp = r13;
        eax = *r13;
        eax -= 1;
        if (eax > 5)
            explode_bomb();
        for (ebx = r12+1; ebx <= 5; ebx++) {
            rax = ebx;
            eax = *(rsp+rax*4);
            if (*rbp == eax)
                explode_bomb();
        }
        r13 += 4;
    }
    
	rsi = rsp + 0x18;
	rax = r14;
	ecx = 7;
	for (rax = r14; rax != rsi; rax += 4) {
        edx = ecx;
        eax -= *rax;
        *rax = edx;
	}
	
	for (rsi = 0; rsi != 0x18; rsi += 4) {
        ecx = *(rsp+rsi);
        if (ecx <= 1)
            edx = 0x6032d0;
        else {
            edx = 0x6032d0;
            for (eax = 1; eax != ecx; eax++) {
                rdx = *(rdx+8);
            }
        }
        *(rsp+2*rsi+20) = rdx;
	}

    rbx = *(rsp+0x20);
    rcx = rbx;
    for (rax = rsp+0x28; rax != rsp+0x50; rax += 8) {
        rdx = *rax;
        *(rcx+8) = rdx;
        rcx = rdx; 
    }

	*(rdx+8) = 0;
	for (ebp = 5; ebp != 0; ebp -= 1) {
		rax = *(rbx+0x8);
        eax = *rax;
        if(eax < *(rbx))
            explode_bomb();
        rbx = *(rbx+8);
	}

还是一头雾水,需要细细分析这些逻辑的意思,我们首先大概知道需要输入6个数字。

  • 第一个循环对这个整数数组做了一些判断,应该是要符合某种规则;
  • 第二个循环修改了整数数组的值;
  • 第三个循环根据某种可以 嵌套解指针的结构 构造了一个指针数组
  • 第四个循环对指针数组做了修改;
  • 第五个循环对 嵌套解指针的结构 进行了一些判断,需要符合某种规则。

这里的难点是

  • 判断数组符合某种规则的逻辑;
  • 可以 嵌套解指针的结构 是什么;

经过查阅才恍然大悟,嵌套解指针的结构就是链表。

该链表的地址和值可以通过gdb查出:

332       168       924       691       477       443
0x6032d0->0x6032e0->0x6032f0->0x603300->0x603310->0x603320

这里有一个细节,第一个循环中将 eax -= 1,然后和5比较通过 jbe跳转,目的防止eax取负数和0,eax -= 1是防止取0,jbe防止取负数。

因为jbe跳转是用无符号数比较,负数在无符号数编码下是很大的数所以必然大于5,因此输入的数字范围是1-6。

经过整理可以得到c代码:

typedef struct {
    int val;
    ListNode* next;
} ListNode;

void phase_6(char* output)
{
    int array[6];
    ListNode* node_array[6];
    read_six_numbers(output, array);
    // 数字范围必须为1-6且互不重复
    for (int i = 0; i != 6; i++) {
        int num = array[i];
        num--;
        if ((unsigned int)num > 5)		// 最大为6
            explode_bomb();
        for (int j = i+1; j <= 5; j++) {
            if (array[i] == array[j])	// 每个元素都不重复
                explode_bomb();
        }
    }
    // 修改 array
	for (int i = 0; i < 6; i ++) {
        array[i] = (7 - array[i]);
	}
    // 生成 node_array
	for (int i = 0; i < 6; i ++) {
        int cur = array[i];
        ListNode* node = 0x6032d0;		// 链表head
        if (cur > 1) {
            for (int j = 1; j < cur; j++) {
                node = node->next;
            }
        }
        node_array[i] = node;
	}
    for (int i = 0; i < 5; i++) {
        node_array[i]->next = node_array[i+1];
    }
    //0x6032d0 0x6032e0 0x6032f0 0x603300 0x603310 0x603320
    //332 168 924 691 477 443
    // 6 5 4 3 2 1
    ListNode* ptr = node_array[0];
    for (int i = 5; i > 0; i--) {
        if (ptr->val < ptr->next->val)
            explode_bomb();
        ptr = ptr->next;
    }
}

相信c代码写出,逻辑就比较清晰了,可以知道功能为下:

  • 输入的数组array必须为 1-6,且不可重复;
  • 对array每个数字 7 - array[i];
  • 根据array的值,取到对应位置的listnode,并由ptr_array存储;
  • 将ptr_array每个元素按序连接(链表重排);
  • 链表必须为降序。

因此phase_6就是链表排序,输入的数字为原链表位置,输入的次序为新链表的位置。

又因为第二个循环对array每个数字 7 - array[i],因此输入的数字需要将链表重排成为升序。

原链表的从小到大顺序是 5 6 1 2 3 4, 重排后 4 3 2 1 6 5。

总结和感想

《CSAPP》其实看了挺久了,第三章汇编一直搁着是一个遗憾,总觉得是一块硬骨头(当时觉得太枯燥)。这次重新又杀回来通过视频+阅读+做题的方式对整个第三章来了一次横扫,结果是收获巨大,每次理解完一个知识点都有种恍然大悟之感。

看了相关教学视频后才知道第三章的重要性,整整花了五节课,占了将近课程量的1/3,可以看出这一章的重要性。

CSAPP的实验系列一直是经典,我认为在某种程度上书甚至可以不看,但lab不可不做…,配套的bomb lab更是如此。我从phase_1到phase_6的过程中对c的理解也是更上一层楼:

  • 字符串的传递方式
  • 数组的表示方法
  • 函数入参和返回值
  • 栈破坏检测
  • 链表的操作方式
  • 栈空间的使用

Bomb Lab带来的成就感是巨大的,所以尽量自己硬着头皮啃一啃,忍住不要看答案…

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐