• CSAPP-BombLab详解


    Bomb Lab

    引言:主要任务是“拆炸弹”。所谓炸弹,其实就是一个二进制的可执行文件,要求输入六个字符串,每个字符串对应一个phase。如果字符串输入错误,系统就会提示BOOM!!!解决这次实验需要将二进制文件反汇编,通过观察理解汇编语言描述的程序行为来猜测符合条件的字符串。可以看出该可执行程序要求从命令行或者文件以 行 为单位读入字符串,每行字符串对应一个phase的输入。如果phase执行完毕,会调用phase_defused 函数表明该 phase 成功搞定。实验共有6个 phase,难度是逐级提升,考点也不尽相同。首先执行命令objdump -d bomb > bomb.txt得到反汇编代码。

    Phase1

    考察点:字符串的传递方式

    查看bomb.txt文件的反汇编代码,如下所示,首先栈顶指针向下移动了8个字节,在64位机器下就是一格,然后将0x402400传递给了esi寄存器(保存函数参数的寄存器),在0x400ee9处调用了string_not_equal函数,调用返回后如果eax寄存器的值为0的话,我们就会跳转到phase_1 + 0x17 = 400ef7的位置,否则的话调用explode_bomb函数就失败了,显然,我们需要让其判断相等,利用gdb查看0x402400处的字符串,

    0000000000400ee0 :
      400ee0:       48 83 ec 08             sub    $0x8,%rsp
      400ee4:       be 00 24 40 00          mov    $0x402400,%esi
      400ee9:       e8 4a 04 00 00          callq  401338 
      400eee:       85 c0                   test   %eax,%eax
      400ef0:       74 05                   je     400ef7 0x17>
      400ef2:       e8 43 05 00 00          callq  40143a 
      400ef7:       48 83 c4 08             add    $0x8,%rsp
      400efb:       c3                      retq
    

    image-20221020190902442

    我们按 s 单步执行时也可以看到这个字符串

    image-20221020191004747

    我们gdb bomb时,将上面的字符串输入,可以看到第一关就过了,Border relations with Canada have never been better.

    image-20221020191159666

    Phase2

    考察点:汇编代码中数组的表示

    还是首先查看汇编代码

    0000000000400efc :
    400efc:       55                      push   %rbp                      	# 保存rbp
    400efd:       53                      push   %rbx				 	  # 保存rbx
    400efe:       48 83 ec 28             sub    $0x28,%rsp				   # 扩大栈空间,扩大0x2840个字节 
    400f02:       48 89 e6                mov    %rsp,%rsi				   # 保存栈顶元素到rsi寄存器
    # 对应的C语言格式汇编代码
    rsi = rsp;
    callq read_six_number;
    if (*rsp == 1)
    	goto 400f30;
    else 
    	callq explode_bomb;
    goto 400f30;
    400f05:       e8 52 05 00 00          callq  40145c      # 读入六个数字
    400f0a:       83 3c 24 01             cmpl   $0x1,(%rsp)				  # 比较rsp必须为1
    400f0e:       74 20                   je     400f30 0x34>		# 如果m[rsp] = 1则跳转到0x400f30
    400f10:       e8 25 05 00 00          callq  40143a 		# 显然不能执行这条指令
    400f15:       eb 19                   jmp    400f30 0x34>  
    400f17:       8b 43 fc                mov    -0x4(%rbx),%eax   # 下面是一段循环  eax = M[rbx - 4]
    # 400f17 - 400f25:
    eax = *(rbx-4);  # 每次取出M[rbx - 4]的值给eax
    eax += eax;      # eax每次都会变为 *(ebx - 4)的二倍
    if (eax == *rbx) # rbx为存放的第二个元素的值  即上一个元素的二倍必须等于下一个元素的值
    	goto 400f25;
    else 
    	callq explode_bomb;
    400f1a:       01 c0                   add    %eax,%eax         # eax = eax + eax 
    400f1c:       39 03                   cmp    %eax,(%rbx)		 # if (eax == m[rbx]) goto 0x400f25
    400f1e:       74 05                   je     400f25 0x29> # 跳过下面的bomb  显然我们需要让eax = m[rbx]
    400f20:       e8 15 05 00 00          callq  40143a 
    400f25:       48 83 c3 04             add    $0x4,%rbx        # rbx = rbx + 4
    # 400f25 - 400f2e:
    rbx += 4;			# 下一个元素,下一次的 rbx - 4就相当于这一次的rbx了
    # 不难看出,下面是一个以rbx为搜索指针,以rbp为结尾信号的循环
    if (rbx != rbp)     # 只要rbx还没有到rbp   rbx其实就相当于for循环的i  rbp6 
    	goto 400f17;	# 循环
    else 
    	goto 400f3c;
    400f29:       48 39 eb                cmp    %rbp,%rbx        # if (rbx-rbp!=0) goto 0x400f17,回到循环开始
    400f2c:       75 e9                   jne    400f17 0x1b>
    400f2e:       eb 0c                   jmp    400f3c 0x40> # rbx==rbp的话就会到这里  0x400f3c
    400f30:       48 8d 5c 24 04          lea    0x4(%rsp),%rbx   # rbx = rsp + 4  lea指令传递的是寄存器的内容
    400f35:       48 8d 6c 24 18          lea    0x18(%rsp),%rbp  # rbp = rsp + 24
    400f3a:       eb db                   jmp    400f17 0x1b> # 接着循环
    400f3c:       48 83 c4 28             add    $0x28,%rsp
    400f40:       5b                      pop    %rbx
    400f41:       5d                      pop    %rbp
    400f42:       c3                      retq
    # 六个数分别存放到 rsp rsp+0x4  rsp+0x8  rsp+0xc  rsp+0x
    # read_six_numbers代码  需要我们输入6个数字然后进行比较这里还有如果数字不满足6个的健壮性判断  注意rdirsi寄存器已经被用来保存read_six_numbers的两个参数了,
    000000000040145c :
    40145c:       48 83 ec 18             sub    $0x18,%rsp  # 6个数, 4 * 6 = 24 = 0x18
    401460:       48 89 f2                mov    %rsi,%rdx   # 在上面的函数中我们将rsp存储到了rsi401463:       48 8d 4e 04             lea    0x4(%rsi),%rcx  # rsp + 4的地址,存放输入的第二个数
    401467:       48 8d 46 14             lea    0x14(%rsi),%rax # 用rax暂存输入的第六个数(rsp + 0x14)
    40146b:       48 89 44 24 08          mov    %rax,0x8(%rsp)  # rsp + 8 = rax = rsi(之前的rsp) + 0x14  
    401470:       48 8d 46 10             lea    0x10(%rsi),%rax # 存放第五个数,存放到了rax寄存器中
    401474:       48 89 04 24             mov    %rax,(%rsp) 	   # rsp = rsi(之前的rsp) + 0x10(多的参数存到内存)
    401478:       4c 8d 4e 0c             lea    0xc(%rsi),%r9   # 存放第四个数   这时候六个寄存器已经用完了
    40147c:       4c 8d 46 08             lea    0x8(%rsi),%r8   # 存放第三个数  
    401480:       be c3 25 40 00          mov    $0x4025c3,%esi  # 给rsi 赋值为 0x4025c3
    401485:       b8 00 00 00 00          mov    $0x0,%eax
    40148a:       e8 61 f7 ff ff          callq  400bf0 <__isoc99_sscanf@plt>  # 调用 sscanf 函数读取输入
    40148f:       83 f8 05                cmp    $0x5,%eax # 比较上面函数的返回值  如果大于5,说明读取的合法
    401492:       7f 05                   jg     401499 0x3d>
    401494:       e8 a1 ff ff ff          callq  40143a   # 否则执行炸弹bomb
    401499:       48 83 c4 18             add    $0x18,%rsp  # 恢复堆栈
    40149d:       c3                      retq
    

    这次汇编代码比较长了,分析的结果都写在注释里了,下面通过gdb动态调试一下,首先b phase_2然后run,可以看到四个寄存器均保存了我们的输入

    image-20221020222529333

    查看寄存器的内容i reg或者p $eax,接着查看内存中该地址的内容,/s表示以字符形式显示。可以看到我们输入的内容都是以字符串格式先保存的,然后通过sscanf格式化输出为了6个整数

    image-20221020211142721

    image-20221020222606108

    执行到调用read_six_numbers函数之前,我们可以看到该函数的第一个参数传递给了rdi,即我们输入的字符串,然后将rsi寄存器置为0,注意这里的反汇编第一个操作数是目的操作数,rsi保存的是提升堆栈后的rsp的值,用来保存数组的起始地址。

    image-20221020222653690

    使用 f 可以查看当前栈信息,利用 bt 指令可以查看函数调用栈之间的关系

    image-20221020214339314

    一步步执行下去,直到上面不是很懂的mov $0x4025c3, %esi指令,可以看到该地址的内容如下,其实就是作为sscanf函数的参数,rdi寄存器的内容始终都没有被修改,这里也可以看出端倪,输入的字符串保存在rdi中,此时作为sscanf函数的第一个参数

    image-20221020214013511

    image-20221020214546574

    调用下面这个函数将rax寄存器的值设置为6,从而可以下面可以cmp $0x5, %eax使eax的值大于5,直接跳转回phase_2函数。回到phase_2函数,之后执行的就是一个循环判断了,判断存进去的数是否满足后一个数是前一个数的二倍,rbx保存的地址是从2开始的,格式化后的6个数字存储到了从ebp开始的连续的内存空间,查看可见下图

    image-20221020223141926

    这里的汇编代码比较好理解,第一行rbx = rsp + 4,第二行rbp = rsp + 0x18,保存循环结束位置(6个数,每个数4字节,共16+8=24字节),将M[rbx - 4]赋值给eax,此时eax保存的即为输入的第一个数 1,然后add eax, eaxeax保存的数变为原来的二倍,接着比较eax保存的值与当前内存中M[rbx]是否相等,相等的话接下来让rbx + 4(这里的+4其实就对应数组元素的+1),看是否满足循环终止条件,不满足就跳到上面phase_2+27继续执行。

    image-20221020220540839

    动态执行完后就可以看到过掉了

    image-20221020223615620

    Phase3

    考察点:switch语句,索引表的汇编表示

    0000000000400f43 :
    400f43:       48 83 ec 18             sub    $0x18,%rsp					; 首先提升堆栈	
    400f47:       48 8d 4c 24 0c          lea    0xc(%rsp),%rcx				; rcx = rsp + 0xc
    400f4c:       48 8d 54 24 08          lea    0x8(%rsp),%rdx				; rdx = rsp + 0x8  保存的是参数 
    400f51:       be cf 25 40 00          mov    $0x4025cf,%esi 			; 猜测这里与上面一样 是sscanf用到的参数 %%
    400f56:       b8 00 00 00 00          mov    $0x0,%eax					; 用作返回值
    400f5b:       e8 90 fc ff ff          callq  400bf0 <__isoc99_sscanf@plt> ;这个函数与上面的一样,先输入字符串再格式化
    400f60:       83 f8 01                cmp    $0x1,%eax					; 上面的函数返回值
    400f63:       7f 05                   jg     400f6a 0x27>		  ; 0x400f6a 跳过爆炸的函数,返回值需要大于1
    400f65:       e8 d0 04 00 00          callq  40143a 		  
    400f6a:       83 7c 24 08 07          cmpl   $0x7,0x8(%rsp)				# rsp + 8存储第一个参数,rsp+c存储第二个
    400f6f:       77 3c                   ja     400fad 0x6a>;0x400fad 会爆炸,所以rsp+8<7,用ja当a[0]<0是也no
    400f71:       8b 44 24 08             mov    0x8(%rsp),%eax				; eax = rsp + 8 < 7  将第一个数存到eax
    400f75:       ff 24 c5 70 24 40 00    jmpq   *0x402470(,%rax,8)			; 跳转到 M[0x402470+rax*8] 处其实就是400fb9
    400f7c:       b8 cf 00 00 00          mov    $0xcf,%eax
    400f81:       eb 3b                   jmp    400fbe 0x7b>
    400f83:       b8 c3 02 00 00          mov    $0x2c3,%eax
    400f88:       eb 34                   jmp    400fbe 0x7b>
    400f8a:       b8 00 01 00 00          mov    $0x100,%eax
    400f8f:       eb 2d                   jmp    400fbe 0x7b>
    400f91:       b8 85 01 00 00          mov    $0x185,%eax
    400f96:       eb 26                   jmp    400fbe 0x7b>
    400f98:       b8 ce 00 00 00          mov    $0xce,%eax
    400f9d:       eb 1f                   jmp    400fbe 0x7b>
    400f9f:       b8 aa 02 00 00          mov    $0x2aa,%eax
    400fa4:       eb 18                   jmp    400fbe 0x7b>
    400fa6:       b8 47 01 00 00          mov    $0x147,%eax
    400fab:       eb 11                   jmp    400fbe 0x7b>
    400fad:       e8 88 04 00 00          callq  40143a 
    400fb2:       b8 00 00 00 00          mov    $0x0,%eax
    400fb7:       eb 05                   jmp    400fbe 0x7b>
    400fb9:       b8 37 01 00 00          mov    $0x137,%eax  ; 如果 a[0]=1时会跳转到这里  eax=0x137
    400fbe:       3b 44 24 0c             cmp    0xc(%rsp),%eax ; 比较第二个参数与eax的值是否相同 相同的话就过了
    400fc2:       74 05                   je     400fc9 0x86>
    400fc4:       e8 71 04 00 00          callq  40143a 
    400fc9:       48 83 c4 18             add    $0x18,%rsp
    400fcd:       c3                      retq
    

    下面利用gdb动态调试,在进入sscanf函数之前,查看0x4025cf处存储的要传入sscanf的字符串,所以可以知道sscanf这次要读取的是两个整数,不用跟进去猜测sscanf的作用就知道,它将输入的标准字符串格式化为了两个整数,

    image-20221021143641675

    一步一步执行下去,知道进入sscanf函数之前,可以看到与该函数有关的信息如下所示:

    image-20221021144749548

    退出sscanf函数后,可以查看该函数将格式化后的数字存储到了哪里,其中0x7fffffffe3d0为栈指针rsp的地址,rsp + 4存储返回地址,rsp + 8存储输入的第一个数,rsp + c存放输入的第二个数,

    image-20221021145501463

    读取堆栈的数据 --两种方式  入栈(edx为栈顶,ebx为栈底) 
    1、base加偏移  栈底为高地址
    读第一个压入的数据:mov esi,dword ptr ds:[ebx-4]
    读第四个压入的数据:mov esi,dword ptr ds:[ebx-0x10]
    2.top加偏移    栈顶为低地址
    读第二个压入的数据:mov edi,dword ptr ds:[edx+8]    
    读第三个压入的数据:mov edi,dword ptr ds:[edx+4]
    

    rsp和rbp寄存器不用我们指定内容,是由编译器确定的,接下来是比较rsp + 8 和 0x7的大小,需要满足rsp + 8 < 0x7,即第一个参数小于7, 注意这里的ja指令可以同时处理输入的a[0] > 7a[0] < 0的情况,之后会做一个无条件的jmp *0x402470(,%rax,8),根据rax的值去找对应的语句,猜测是一个以rax为索引的索引表,类比switch语句,对于我们输入的每一对数,都会根据第一个数的值去确定第二个数的值。查看以地址0x402470为基址的索引表的信息如下所示,我们输入的是1,所以取0x400fb9的地址寻找

    image-20221021151450464

    当我们跳转到指定地址后可以看到(这里输入的第一个参数为1),将eax赋值为0x137,然后比较我们输入的第二个数与这个数是否相等,即我们可以输入的有1 311或者其他六种其他的数。

    image-20221021151834957

    对应的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;
    }
    

    Phase4

    考察点:递归函数的参数及返回值

    000000000040100c :
    40100c:       48 83 ec 18             sub    $0x18,%rsp
    401010:       48 8d 4c 24 0c          lea    0xc(%rsp),%rcx
    401015:       48 8d 54 24 08          lea    0x8(%rsp),%rdx
    40101a:       be cf 25 40 00          mov    $0x4025cf,%esi
    40101f:       b8 00 00 00 00          mov    $0x0,%eax
    401024:       e8 c7 fb ff ff          callq  400bf0 <__isoc99_sscanf@plt>  # 同样调用了sscanf这个函数
    401029:       83 f8 02                cmp    $0x2,%eax		# 如果上面函数的返回值与2不相等的话就bomb了
    40102c:       75 07                   jne    401035 0x29>  # 跳到 0x401035 即bomb函数
    40102e:       83 7c 24 08 0e          cmpl   $0xe,0x8(%rsp)		# 这里需要满足 M[rsp+8] <= 0xe 这样才能跳过bomb
    401033:       76 05                   jbe    40103a 0x2e>  # jbe是小于等于 
    401035:       e8 00 04 00 00          callq  40143a 
    40103a:       ba 0e 00 00 00          mov    $0xe,%edx   # edx = 0xe   下面三行应该都是 func4函数的参数
    40103f:       be 00 00 00 00          mov    $0x0,%esi   # esi = 0x0
    401044:       8b 7c 24 08             mov    0x8(%rsp),%edi # edi = a[0](我们输入的第一个参数的值)
    401048:       e8 81 ff ff ff          callq  400fce  # 这里又调用了一个函数
    40104d:       85 c0                   test   %eax,%eax		# 判断 eax 是否为0,即func4函数的返回值是否为0
    40104f:       75 07                   jne    401058 0x4c> # 如果不为0的话跳转到 bomb,所以需要使eax0
    401051:       83 7c 24 0c 00          cmpl   $0x0,0xc(%rsp)  # 比较输入的第二个数和0是否相等  不相等会bomb
    401056:       74 05                   je     40105d 0x51>
    401058:       e8 dd 03 00 00          callq  40143a 
    40105d:       48 83 c4 18             add    $0x18,%rsp
    401061:       c3                      retq
    
    ; func4函数
    0000000000400fce :
    400fce:       48 83 ec 08             sub    $0x8,%rsp   # 栈空间扩大8个字节 这里的 0x1就代表地址空间可以多存储一个字节
    400fd2:       89 d0                   mov    %edx,%eax   # eax作为sscanf的返回值一直没有修改  edx为第三个参数0xe
    400fd4:       29 f0                   sub    %esi,%eax   # eax = eax - esi = 0xe - 0 = 0xe
    400fd6:       89 c1                   mov    %eax,%ecx   # ecx = eax = 0xe
    400fd8:       c1 e9 1f                shr    $0x1f,%ecx  # shr逻辑右移指令 ecx = ecx >> 0x1f = 0
    400fdb:       01 c8                   add    %ecx,%eax   # eax = eax + ecx = 0xe
    400fdd:       d1 f8                   sar    %eax  # sar 算术右移指令 省略了一个操作数  gdb中显示为 1 1110>> 1 =111=7
    到这里就可以看出端倪了:eax = (eax + eax >> 0x1f) >> 1   其中 eax = edx - esi = 0xe
    400fdf:       8d 0c 30                lea    (%rax,%rsi,1),%ecx # ecx = rax + rsi * 1 = 7 + 0 = 7
    400fe2:       39 f9                   cmp    %edi,%ecx   # 将我们输入的第一个参数与 7 比较
    400fe4:       7e 0c                   jle    400ff2 0x24> # 如果7 <= a[0] 跳转到 0x400ff2 执行
    400fe6:       8d 51 ff                lea    -0x1(%rcx),%edx # 否则 a[0] < 7  edx = rcx - 1 = 6
    400fe9:       e8 e0 ff ff ff          callq  400fce   # 递归调用 func4 函数
    400fee:       01 c0                   add    %eax,%eax		# 2 * func()
    400ff0:       eb 15                   jmp    401007 0x39>
    400ff2:       b8 00 00 00 00          mov    $0x0,%eax  # eax = 0
    400ff7:       39 f9                   cmp    %edi,%ecx  # if(ecx(7) >= edi(a[0])) goto 401007; else func4();
    400ff9:       7d 0c                   jge    401007 0x39>
    400ffb:       8d 71 01                lea    0x1(%rcx),%esi
    400ffe:       e8 cb ff ff ff          callq  400fce  # 这里如果 a[0] > 7的话也会进行递归  a[0] = 7就是边界条件
    401003:       8d 44 00 01             lea    0x1(%rax,%rax,1),%eax  # eax = rax + rax + 1 递归调用
    401007:       48 83 c4 08             add    $0x8,%rsp
    40100b:       c3                      retq
    递归函数其实就是
    int func(int x, int a, int b)  (edi esi edx)
    {
    	int c = b - a;(c存储在 ecx   b在edx里)
    	c = (c + c >> 31) >> 1;  这里c又存储到了 eax 里
    	int d = c + a; (rax + rsi(用来传递第二个参数))  d 存储在 ecx
    	if (d <= x)  goto 0x400ff2
    	{
    		if (d >= x) return 0;
    		; 递归调用 注意第二个参数变了  lea  0x1(%rcx),%esi  esi = d + 1
    		return 2 * func4(x, d + 1, b) + 1
    	}
    	goto 0x400fe6  lea  -0x1(%rcx),%edx  b = b - 1
    	return 2 * func(x, a, b - 1);  只有第三个参数变了  别的都没变
    }
    

    由上面的分析可知,输入的第二个数一定为0,第一个数作为func4函数的第一个参数进行了运算,需要满足func4函数的返回值为0。下面利用gdb动态调试,可以看到地址0x4025cf处存储的是% %,所以我们要输入的参数个数是两个

    image-20221021184432913

    由上面汇编的分析可知,我们输入的第一个参数需要小于等于14,第二个参数一定为0。执行到调用func4函数时界面如下,可以看到func4函数有四个参数,第一个参数就是我们输入的第一个数。分析可知,func4函数是一个递归函数,递归终止条件为 a[0] >=7,且下面还有一个判断如果a[0] > 7也会递归调用函数func4,所以我们令第一个参数为7即可,如第二张图。

    image-20221021185851957

    image-20221021192523619

    仔细分析后可以得知,func4函数这个递归函数的代码如下所示

    int func4(int x, int a, int b)
    {
        int num = b - a;
        num = (num + num >> 31) / 2;  // 31就是 0x1f
        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);
    }
    

    Phase5

    考察点:字符数组,循环,ASCII,与运算

    0000000000401062 :
    401062:       53                      push   %rbx
    401063:       48 83 ec 20             sub    $0x20,%rsp		# 开辟 32 字节的栈空间
    401067:       48 89 fb                mov    %rdi,%rbx		# rbx = rsi
    40106a:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax	# %fs:0x28保存的是 输入的值 
    401071:       00 00
    401073:       48 89 44 24 18          mov    %rax,0x18(%rsp) # rsp + 0x18 = rax(存放的是我们输入的参数)
    401078:       31 c0                   xor    %eax,%eax	# eax = 0
    40107a:       e8 9c 02 00 00          callq  40131b  # 获取输入的字符串长度(包括空格)
    40107f:       83 f8 06                cmp    $0x6,%eax	# 如果输入的字符串长度不为6  会爆炸
    401082:       74 4e                   je     4010d2 0x70> # 跳转到 0x4010d2
    401084:       e8 b1 03 00 00          callq  40143a 
    401089:       eb 47                   jmp    4010d2 0x70>
    ; 下面这段指令的含义:遍历输入字符串的每一个字符,然后逐次将每个字符与0xf与操作,得到的值做为0x4024b0处字符串的下标
    40108b:       0f b6 0c 03             movzbl (%rbx,%rax,1),%ecx # movzbl零扩展指令 move zero byte to double word
    ; rax此时为0  ecx = rax + rbx (零扩展后再传送)  一般用于使用小字节变量给大字节变量赋值
    40108f:       88 0c 24                mov    %cl,(%rsp)  # M[rsp] = cl(ecx的低8位) = 0x31(1的ASCII码)
    401092:       48 8b 14 24             mov    (%rsp),%rdx # rdx = M[rsp] = 0x31
    401096:       83 e2 0f                and    $0xf,%edx   # edx = edx & 1111 = 110001 & 1111 = 1
    401099:       0f b6 92 b0 24 40 00    movzbl 0x4024b0(%rdx),%edx # edx = M[rdx + 0x4024b0] 根据上面与的结果去内存寻找
    4010a0:       88 54 04 10             mov    %dl,0x10(%rsp,%rax,1) # 将edx的第八位存到后面指定的内存地址
    4010a4:       48 83 c0 01             add    $0x1,%rax	# rax = rax + 1
    4010a8:       48 83 f8 06             cmp    $0x6,%rax  # if(rax!=6) goto 40108b; else goto 4010ae  需要执行6次
    4010ac:       75 dd                   jne    40108b 0x29> # 回到上面继续循环
    4010ae:       c6 44 24 16 00          movb   $0x0,0x16(%rsp) # 6次循环结束后  执行到这里 M[rsp+0x16] = 0
    4010b3:       be 5e 24 40 00          mov    $0x40245e,%esi  # 函数的参数
    4010b8:       48 8d 7c 24 10          lea    0x10(%rsp),%rdi
    4010bd:       e8 76 02 00 00          callq  401338 
    4010c2:       85 c0                   test   %eax,%eax
    4010c4:       74 13                   je     4010d9 0x77>
    4010c6:       e8 6f 03 00 00          callq  40143a 
    4010cb:       0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
    4010d0:       eb 07                   jmp    4010d9 0x77>
    4010d2:       b8 00 00 00 00          mov    $0x0,%eax  # eax = 0
    4010d7:       eb b2                   jmp    40108b 0x29> # 又跳转到上面了
    4010d9:       48 8b 44 24 18          mov    0x18(%rsp),%rax
    4010de:       64 48 33 04 25 28 00    xor    %fs:0x28,%rax
    4010e5:       00 00
    4010e7:       74 05                   je     4010ee 0x8c>
    4010e9:       e8 42 fa ff ff          callq  400b30 <__stack_chk_fail@plt>
    4010ee:       48 83 c4 20             add    $0x20,%rsp
    4010f2:       5b                      pop    %rbx
    4010f3:       c3                      retq
    

    打开gdb进行动态调试,首先看到我们输入的长度为6的字符串如下所示

    image-20221022085546280

    前面的指令都很简单,我们直接看movzbl这条指令,是一个带零扩展的数据传送指令,在gdb中查看该指令是如下形式,明确给出了byte类型,此时rax = 0rbx = 0x6038c0(我们输入的字符串的地址),执行完这条指令后rcx = 0x31 = 49(1的ASCII码)

    image-20221022090910626

    可以看到0x6038c0处存储的内容如下,存储的是我们输入的123456的ASCII码,

    image-20221022091517491

    这里还发现了python中对变量做and运算时的一些有意思的点,python中所有变量的位操作都是通过强制转换成bool实现的,严格遵循短路逻辑,只有and,如果每个表达式都不为假,返回第二个,只有or,从左往右有一个不为假就返回这个值。

    image-20221022092546230

    下一条指令mov %cl,(%rsp)是将ecx寄存器的低八位赋值给M[rsp],存放到栈指针指向的地址,0x7f开头的往往就是栈所在地址

    image-20221022092742223

    中间经过一些处理后此时rdx = 49 & 0xf = 1,然后又是一条零扩展指令movzbl 0x4024b0(%rdx),%edx,根据上面相与的结果取内存中寻找对应的值赋值给edx,可以看到这里是0x61,可以看到内存中存储的字符串为下面的maduiersnfotvbyl

    image-20221022093111895

    image-20221022094539847

    接下来gdb中的指令更容易理解,mov byte ptr [rsp + rax + 0x10], dl,将0x61存储到rsp+0x10开始的内存地址(即存储变化后的字符到一个栈中新开辟的字符数组里),rax此时仍为0,先查看未执行前,该地址存储的数为:0x10,执行之后就变成了了0x61

    image-20221022093507081

    下面首先rax = rax + 1,然后判断rax != 6的话回到上面循环之前的操作,可知这里是一个6次的循环,下一次循环,rbx存储的还是我们输入的字符串的地址,但是rax就变成1了,取到的字符由之前的0x31变为了0x32,直到遍历完6个长度的字符串

    image-20221022093726972

    总结一下,这一段循环的意义是遍历输入的每一个字符,将每一个字符的ASCII码与0xf相与,与后的结果作为索引去指定内存地址0x4024b0处找对应的字符存储起来。循环结束后,我们再往下看,下面就是传递参数,然后调用了strings_not_equal这个函数

    该函数的第一个参数为我们输入的字符串的每一个字符与上0xf后作为索引去内存中找到的maduiersnfotvbyl这个字符串的子串,第二个参数为内存中存储的正确结果flyers,显然我们需要让这两个字符串相等,这样这个函数才会返回0,才会跳过下面的explode_bomb下面要做的就很清晰了,找到所有与上0xf后的索引为flyers0x4024b0为起始地址的索引表中的位置即可,索引依次为9 15 14 5 6 7,我们需要找到与上0xf后为以上索引的字符,x & 1111 = 1001 x = 1001001或者111001或者1111001,可以看出后四位即为索引,我们先尝试第一个1001001(73),对应的输入为IONEFG,第二种,对应的输入为9?>567,第三种输入y(112+15=127)不是可打印字符。

    因此,关键步骤用C语言来写就是

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

    至此,第五关也就过了

    image-20221022102306862

    Phase6

    考察点:多重循环,链表,结构体,eax比较数值时只会比较低32位,冗长的汇编

    00000000004010f4 :
    4010f4:       41 56                   push   %r14		
    4010f6:       41 55                   push   %r13
    4010f8:       41 54                   push   %r12
    4010fa:       55                      push   %rbp
    4010fb:       53                      push   %rbx
    ; 传入参数  为read_six_numbers做准备
    4010fc:       48 83 ec 50             sub    $0x50,%rsp  # 提供80字节的栈空间
    401100:       49 89 e5                mov    %rsp,%r13   # r13 = rsp
    401103:       48 89 e6                mov    %rsp,%rsi   # rsi = rsp  第二个参数
    401106:       e8 51 03 00 00          callq  40145c  # 读取六个数字,这个函数在 p2 见过
    40110b:       49 89 e6                mov    %rsp,%r14  # r14 = rsp 此时rsp根进去之前其实还是一样的  所以r14=r13
    40110e:       41 bc 00 00 00 00       mov    $0x0,%r12d # r12d = 0
    401114:       4c 89 ed                mov    %r13,%rbp  # rbp = r13 = rsp
    401117:       41 8b 45 00             mov    0x0(%r13),%eax # eax = M[r13] = M[rsp]  M[rsp]=0x200000001 因为eax32位寄存器,只能存储下来 0x200000001 的低4字节 即 00000001  所以此时 eax = 0x1
    40111b:       83 e8 01                sub    $0x1,%eax # eax = eax - 1
    40111e:       83 f8 05                cmp    $0x5,%eax # eax需要 < 5
    401121:       76 05                   jbe    401128 0x34>
    401123:       e8 12 03 00 00          callq  40143a 
    401128:       41 83 c4 01             add    $0x1,%r12d # r12d += 1  每次循环加1
    40112c:       41 83 fc 06             cmp    $0x6,%r12d # 循环终止条件 r12d = 6
    401130:       74 21                   je     401153 0x5f>
    401132:       44 89 e3                mov    %r12d,%ebx # rbx = r12d  循环变量暂存到rbx401135:       48 63 c3                movslq %ebx,%rax # 符号位扩展,l->q 字到双字, rax = ebx(符号位扩展) 正数用0
    401138:       8b 04 84                mov    (%rsp,%rax,4),%eax
    40113b:       39 45 00                cmp    %eax,0x0(%rbp)  
    40113e:       75 05                   jne    401145 0x51>  # *rbp 不能等于 eax
    401140:       e8 f5 02 00 00          callq  40143a 
    401145:       83 c3 01                add    $0x1,%ebx
    401148:       83 fb 05                cmp    $0x5,%ebx
    40114b:       7e e8                   jle    401135 0x41>  # ebx <= 5 继续循环
    40114d:       49 83 c5 04             add    $0x4,%r13
    401151:       eb c1                   jmp    401114 0x20>
    ; 上面是一个循环
    phase_6(rdi)  ; 我们输入的字符串传入到 rdi 中
    {
    	r13 = rsp;
    	rsi = rsp;
    	read_six_numbers(rdi, rsi);  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;  符号位扩展  e->r  ebx为正数用0填充高位   为负数用1填充高位
    			eax = *(rsp + rax * 44);
    			if (*rbp == eax)
    				explode_bomb();
    		}
    		r13 += 4;
    	}
    }
    401153:       48 8d 74 24 18          lea    0x18(%rsp),%rsi
    401158:       4c 89 f0                mov    %r14,%rax
    40115b:       b9 07 00 00 00          mov    $0x7,%ecx
    401160:       89 ca                   mov    %ecx,%edx
    401162:       2b 10                   sub    (%rax),%edx
    401164:       89 10                   mov    %edx,(%rax)
    401166:       48 83 c0 04             add    $0x4,%rax
    40116a:       48 39 f0                cmp    %rsi,%rax  # rax != rsi 的话继续循环
    40116d:       75 f1                   jne    401160 0x6c>
    ; 这里也是一个循环  单独写在这里  
    rsi = rsp + 0x18;
    rax = r14;
    ecx = 0x7;
    for (rax = r14  rax != rsi  rax += 4)
    {
    	edx = ecx;
    	edx = edx - *rax;
    	*rax = edx;
    }
    40116f:       be 00 00 00 00          mov    $0x0,%esi
    401174:       eb 21                   jmp    401197 0xa3>
    401176:       48 8b 52 08             mov    0x8(%rdx),%rdx
    40117a:       83 c0 01                add    $0x1,%eax
    40117d:       39 c8                   cmp    %ecx,%eax
    40117f:       75 f5                   jne    401176 0x82>
    401181:       eb 05                   jmp    401188 0x94>
    401183:       ba d0 32 60 00          mov    $0x6032d0,%edx # ebx = 0x6032d0
    401188:       48 89 54 74 20          mov    %rdx,0x20(%rsp,%rsi,2) # *(rsp + rsi*2) = rdx
    40118d:       48 83 c6 04             add    $0x4,%rsi  # rsi += 4
    401191:       48 83 fe 18             cmp    $0x18,%rsi 
    401195:       74 14                   je     4011ab 0xb7> # rsi = 0x18的话 就跳到下面了
    ;	因为这是最外层开始的循环  所以也可以通过这条语句跳转到的地址确定本次循环的层数,即最内层循环的语句在哪里结束
    401197:       8b 0c 34                mov    (%rsp,%rsi,1),%ecx
    40119a:       83 f9 01                cmp    $0x1,%ecx  # ecx <= 1
    40119d:       7e e4                   jle    401183 0x8f>
    40119f:       b8 01 00 00 00          mov    $0x1,%eax
    4011a4:       ba d0 32 60 00          mov    $0x6032d0,%edx
    4011a9:       eb cb                   jmp    401176 0x82>
    ; 小tips  怎么看循环到哪里结束呢    找下面最远的跳到上面的指令往往就是最内层循环
    for (esi = 0  rsi != 0x18  rsi += 4)
    {
    	ecx = *(rsp + rsi);
    	if (ecx <= 1)
    	{
    		edx = 0x6032d0;
    		*(rsp + rsi * 2 + 0x20) = rdx;  这句话两个分支 都会跳转到那里执行
    	}
    	else 
    	{
    		edx = 0x6032d0;
    		for (eax = 1  eax != ecx  eax++)
    		{
    			rdx = *(rdx + 8);
    		}
    		*(rsp + rsi * 2 + 0x20) = rdx;
    	}
    }
    4011ab:       48 8b 5c 24 20          mov    0x20(%rsp),%rbx
    4011b0:       48 8d 44 24 28          lea    0x28(%rsp),%rax
    4011b5:       48 8d 74 24 50          lea    0x50(%rsp),%rsi
    4011ba:       48 89 d9                mov    %rbx,%rcx
    4011bd:       48 8b 10                mov    (%rax),%rdx
    4011c0:       48 89 51 08             mov    %rdx,0x8(%rcx)
    4011c4:       48 83 c0 08             add    $0x8,%rax
    4011c8:       48 39 f0                cmp    %rsi,%rax
    4011cb:       74 05                   je     4011d2 0xde>
    4011cd:       48 89 d1                mov    %rdx,%rcx
    4011d0:       eb eb                   jmp    4011bd 0xc9>
    ; 又是一个循环
    rbx = *(rsp + 0x20);
    rsi = rsp + 0x50;
    rcx = rbx;
    for (rax = rsp + 0x28  rax != rsi  rax += 8)
    {
    	rdx = *rax;
    	*(rcx + 0x8) = rdx;
    	rcx = rdx;
    }
    4011d2:       48 c7 42 08 00 00 00    movq   $0x0,0x8(%rdx)
    4011d9:       00
    4011da:       bd 05 00 00 00          mov    $0x5,%ebp
    4011df:       48 8b 43 08             mov    0x8(%rbx),%rax
    4011e3:       8b 00                   mov    (%rax),%eax
    4011e5:       39 03                   cmp    %eax,(%rbx)  # *rbx 需要大于 eax 
    4011e7:       7d 05                   jge    4011ee 0xfa>
    4011e9:       e8 4c 02 00 00          callq  40143a 
    4011ee:       48 8b 5b 08             mov    0x8(%rbx),%rbx
    4011f2:       83 ed 01                sub    $0x1,%ebp
    4011f5:       75 e8                   jne    4011df 0xeb>
    4011f7:       48 83 c4 50             add    $0x50,%rsp
    4011fb:       5b                      pop    %rbx
    4011fc:       5d                      pop    %rbp
    4011fd:       41 5c                   pop    %r12
    4011ff:       41 5d                   pop    %r13
    401201:       41 5e                   pop    %r14
    401203:       c3                      retq
    *(rdx + 0x8) = 0;
    for (ebp = 0x5  ebp != 0x1  ebp -= 1)
    {
    	rax = *(rbx + 0x8);
    	eax = *rax;
    	if (*rbx < eax)
    		explode_bomb();
    	rbx = *(rbx + 0x8);
    }
    

    代码太长,这里我考虑直接用gdb分析,前面入栈的六个寄存器的值如下图所示

    image-20221022165542351

    前面的指令没什么好说的,注意此时r13rsi中保存的都是栈指针rsp的内容,调试到调用read_six之前,这个函数需要两个参数,第一个是我们输入的字符串,存储在寄存器rdi中,第二个返回值的6个int型元素数组的首地址,存储在寄存器rsi中,

    image-20221022170328819

    这个函数内部同样调用了sscanf,在次就不再详细展开

    image-20221022170245086

    从该函数退出之后,mov r14, rsprsp的值又赋给了r14,注意rsp进入read_six函数之后又回来栈是被平衡了的,所以此时 r13 = r14 = rsp,下一步是mov r12d, 0,很有意思,r12dr12寄存器的??,接着将r13中保存的rsp地址又传给了rbp,将M[rsp]传给了eax,这里要注意,M[rsp] = 0x200000001,但是eax寄存器是32位寄存器,只能存储低四个字节,即0x00000001,之后eax -= 1 变成了0

    整理一下汇编代码,其对应的C风格如下,分成了以空行间隔的五段代码,

    phase_6(rdi)  //  我们输入的字符串传入到 rdi 中
    {
    	r13 = rsp;
    	rsi = rsp;
    	read_six_numbers(rdi, rsi); //  rdi 为我们输入的字符串,  rsi为 %%%%%%
    	r14 = rsp;
        // 总结一下  这个循环的含义:输入6个1-6的数,且不能重复
    	for (r12 = 0;  r12 != 6;  r12++)   // r12d猜测应该是 r12 的低32位
    	{
    		rbp = r13; // 这里 rbp = r13 =rsp  rsp 存储的就是我们输入的字符串格式化后的数字
    		eax = *r13;  // 去内存中找  第一次 eax = 1  第二次eax= 2
    		eax -= 1;  // eax -= 1 = 0   这里限制了输入的数字必须为 1-6
    		if (eax > 5)
    			explode_bomb();
    		for (ebx = r12 + 1;  ebx <= 5;  ebx++)  // 初始 ebx = r12+1 = 1
    		{
    			rax = ebx;  // 符号位扩展  e->r  ebx为正数用0填充高位   为负数用1填充高位  rax = ebx 这里是整数 rax = 0x1
    			eax = *(rsp + rax * 44);  // eax = *(rsp + i * 4)  依次遍历 1 2 3 4 5 6  初始rax=1 所以eax = 2
               // 之后 *rbp 是不变的,始终是1  但是 eax 会依次遍历所有 2 3 4 5 6  都不会相等 所以最终ebx = 5  eax=6退出循环
    			if (*rbp == eax)   // 2 != 1(*rbp)
    				explode_bomb();
    		}
    		r13 += 4; // r13 = rsp + 4   相当于下一次循环 rbp + 4    取下一个数判断是否有与它相同的数
    	}
        // 下面这段循环的含义:将 a[i] 变为 7 - a[i] 存储到原先a[i]所在的位置  即 esp + 4*i
        rsi = rsp + 0x18;  // 刚好是我们输入的6个字符的下一个位置   24个字节   其实是我们输入的字符数组的 '\0'
        rax = r14;  // rax = r14 = rsp
        ecx = 0x7;
        for (rax = r14;  rax != rsi;  rax += 4)  // 遍历所有字符数组
        {
            edx = ecx;               // edx = 7
            edx = edx - *rax;		// edx = 7 - a[i]   同样也是 1-6 的数
            *rax = edx;				// *rax = rdx  存回内存   
        }
        // 下面含义:
        for (esi = 0; rsi != 0x18;  rsi += 4)  // 遍历所有字符数组   0x18很明显 遍历7次 刚好到'\0'结束循环
        {
            ecx = *(rsp + rsi);  // 取出对应的字符数组的值  输入的是123456  经过上面变换后成了 654321
            if (ecx <= 1) // 只有 输入的为 6 时才会执行
            {
                edx = 0x6032d0;  // 此地址处是一个结构体
                // 下面的含义:将edx存储的结构体信息  存储到rsp + rsi * 2 + 0x20 处的地址  就是 rsp + 8*i + 0x20
                //*(rsp + rsi * 2 + 0x20) = rdx;  // 这句话  无论进入哪个分支都会跳转到那里执行  可以写到外面
            }
            else  // 只要 输入的 不为6
            {
                edx = 0x6032d0;  // 与上面一样
                for (eax = 1;  eax != ecx;  eax++) // 第一个 ecx = 6 循环6次 最终7 - 1存储到了 node6
                {  // 循环一次  对应node1   循环两次 对应node2  即  node{7-a[i]}
                    rdx = *(rdx + 8);  // 从 6032d0(node1) -> 6032e0(node2) 
                }
                //*(rsp + rsi * 2 + 0x20) = rdx;  
            }
            *(rsp + rsi * 2 + 0x20) = rdx; // 第一次  rcx=7-1=6  rdx指向node6  rsp+0x20 = node6
        }
        // 这段好像没什么用
        rbx = *(rsp + 0x20);  // 距离栈指针最近的 node  对应输入的第一个数  node的编号即为 7-a[i]  node6
        rsi = rsp + 0x50; // node 的结束地址
        rcx = rbx;  // 保存输入
        for (rax = rsp + 0x28;  rax != rsi;  rax += 8)  // rax 从 第二个node 开始遍历    node5 
        {
            // 典型的交换操作
            rdx = *rax; // 暂存遍历到的node  rdx = node5    rdx = node4
            *(rcx + 0x8) = rdx; 		// node5 = node6  node4=node5
            rcx = rdx;				    // node6 = node5
        }
        // 分析: node[7-input[i]]->data >= node[7-input[i+1]]->data
        *(rdx + 0x8) = 0; // 此时 rdx 保存最后一个node 
        for (ebp = 0x5;  ebp != 0x1;  ebp -= 1) // 循环5次
        {
            rax = *(rbx + 0x8); // rbx 仍指向距离栈指针最近的node   rax = node
            eax = *rax; // 取出node的值(注意eax,取得是低32位)   只看低32位 node的大小顺序为 3 4 5 6 1 2
            if (*rbx < eax) // 如果第一个node的值小于下一个  就会爆炸  所以需要保证输入的数对应的node是降序排列在栈中的
            // 即 node6 node5 .. node1  只看低32位 node的大小顺序为 3 4 5 6 1 2  所以我们输入的应该为 4 3 2 1 6 5 (7-a)
                explode_bomb();
            rbx = *(rbx + 0x8);
        }
    }
    

    首先确认我们输入的数据的位置,可以看到在rsp rsp + 4处依次存放着格式化后的数字 1 2 ..,然后根据gdb看上面的分析即可

    image-20221022203539484

    到第三段代码时,可以看到程序会将edx 设置为0x6032d0,查看该地址处信息可知,猜测这里应该是一个结构体node1,在gdb中也明确地告诉了我们

    image-20221022212435575

    image-20221022213252560

    执行完mov rdx, qword ptr [rdx + 8]这条指令后,rdx存储的内容由 node1变成了node2,接着循环又会变成node3node4一直到node,然后执行qword ptr [rsp + rsi*2 + 0x20], rdx指令,将node6存储到了栈上我们输入的字符串的上面。同样,循环6次,找到每一个变化后的 7- a[i] 对应的node,并存储到栈的对应位置。

    image-20221022214023503

    循环结束后,可以看到栈的情况(右侧的数字即为node->data):

    image-20221022214652857

    查看六个node结构体的信息

    image-20221022221221892

    经过分析,得知最后一段代码的作用是将结构体的data按非升序排列,每一个node的data如上图所示,注意我们比较时用的是eax来存储结构体node->data,只能存储低32位,所以按node->data的低32位排序,可以得到降序排列为node 3,4,5,6,1,2,而7-input[i]刚好与node的编号是一一对应的,所以我们的输入为4 3 2 1 6 5。 终于完成了🚩

    image-20221022222615380

  • 相关阅读:
    MobileViT
    2022年全球市场喷他佐辛原料药总体规模、主要生产商、主要地区、产品和应用细分研究报告
    SQL 基础知识梳理(一)- 数据库与 SQL
    3000+价位投影仪该怎么选?双十一投影仪选购攻略
    GBase 8d的特性-可扩展性
    MySQL进阶02_索引概述_结构_分类_语法
    JavaScript:实现使用 2 个堆栈形成队列算法(附完整源码)
    Java Tomcat内存马——Listener内存马
    基于HTML5和CSS3搭建一个Web网页(二)
    【Vivado】Xilinx UG994 Addressing for Block Designs
  • 原文地址:https://www.cnblogs.com/SVicen/p/16817503.html