二进制炸弹阶段2 - 解码汇编代码

时间:2017-10-17 22:12:30

标签: assembly x86 reverse-engineering

我刚开始学习汇编代码,我遇到了这个二元炸弹实验室,觉得这是一个很好的练习。

这是二进制炸弹的第二阶段,我需要从这个汇编代码中找出一个6号码密码才能进入下一阶段。

我现在已经好好看了一会儿,我似乎无法弄明白。

非常感谢任何关于如何解决这个问题的输入或解释。我希望对此有一个很好的理解,以便进入更复杂的阶段。

    Dump of assembler code for function phase_2:
   0x0000000000400f49 <+0>: push   %rbp
   0x0000000000400f4a <+1>: push   %rbx
   0x0000000000400f4b <+2>: sub    $0x28,%rsp
   0x0000000000400f4f <+6>: mov    %fs:0x28,%rax
   0x0000000000400f58 <+15>:    mov    %rax,0x18(%rsp)
   0x0000000000400f5d <+20>:    xor    %eax,%eax
   0x0000000000400f5f <+22>:    mov    %rsp,%rsi
   0x0000000000400f62 <+25>:    callq  0x401682 <read_six_numbers>
   0x0000000000400f67 <+30>:    cmpl   $0x1,(%rsp) //first password number = 1?
   0x0000000000400f6b <+34>:    je     0x400f72 <phase_2+41>
   0x0000000000400f6d <+36>:    callq  0x40164c <explode_bomb>
   0x0000000000400f72 <+41>:    mov    %rsp,%rbx
   0x0000000000400f75 <+44>:    lea    0x14(%rsp),%rbp
   0x0000000000400f7a <+49>:    mov    (%rbx),%eax
=> 0x0000000000400f7c <+51>:    add    %eax,%eax 
   0x0000000000400f7e <+53>:    cmp    %eax,0x4(%rbx)
   0x0000000000400f81 <+56>:    je     0x400f88 <phase_2+63>
   0x0000000000400f83 <+58>:    callq  0x40164c <explode_bomb>
   0x0000000000400f88 <+63>:    add    $0x4,%rbx
   0x0000000000400f8c <+67>:    cmp    %rbp,%rbx
   0x0000000000400f8f <+70>:    jne    0x400f7a <phase_2+49> // loop? What is it doing?
   0x0000000000400f91 <+72>:    mov    0x18(%rsp),%rax
---Type <return> to continue, or q <return> to quit---return
   0x0000000000400f96 <+77>:    xor    %fs:0x28,%rax
   0x0000000000400f9f <+86>:    je     0x400fa6 <phase_2+93>
   0x0000000000400fa1 <+88>:    callq  0x400b90 <__stack_chk_fail@plt>
   0x0000000000400fa6 <+93>:    add    $0x28,%rsp
   0x0000000000400faa <+97>:    pop    %rbx
   0x0000000000400fab <+98>:    pop    %rbp
   0x0000000000400fac <+99>:    retq   
End of assembler dump.

1 个答案:

答案 0 :(得分:3)

你应该一步一步地解决问题。

首先让我们从转储中删除无用的东西(只添加冗长的额外地址);我也喜欢我的汇编是英特尔语法,内存访问和比较/减法读取方式更好。

从快速浏览一下,我们可以立即观察:

 <+0>:    push   rbp        ; save clobbered registers
 <+1>:    push   rbx
 <+2>:    sub    rsp,0x28   ; 40 bytes of locals
 <+6>:    mov    rax,dword ptr fs:0x28  ; stuff referenced from fs are generally
                                        ; thread-local variables of some kind
<+15>:    mov    qword ptr[rsp+0x18],rax; at offset 0x18 there's a long long local
<+20>:    xor    eax,eax
<+22>:    mov    rsi,rsp    ; rsi in System V ABI is the first parameter;
                            ; it's passing straight the start of our locals as a
                            ; pointer
<+25>:    call   <read_six_numbers> ; we can imagine that read_six_numbers takes
                                    ; an array of 6 int values, which are probably
                                    ; the locals between rsp and rsp+0x18 (0x18 =
                                    ; 24 = 6*sizeof(int))
<+30>:    cmp    dword ptr[rsp],0x1 ; this is the first read value
<+34>:    je     <phase_2+41>       ; --\
<+36>:    call   <explode_bomb>     ;   |      first bomb explosion
<+41>:    mov    rbx,rsp            ; <-/   rbx points to the first number
<+44>:    lea    rbp,[rsp+0x14]     ; 0x14 = 20 = 5th element of the numbers array
<+49>:    mov    eax,dword ptr[rbx]
<+51>:    add    eax,eax
<+53>:    cmp    dword ptr[rbx+4],eax   ; rbx points to an integer, so +4 is
                                        ; really the integer that follows it
<+56>:    je     <phase_2+63>
<+58>:    call   <explode_bomb>     ; second bomb explosion
<+63>:    add    rbx,0x4    ; again, this is a +1 in C pointer notation
<+67>:    cmp    rbx,rbp
<+70>:    jne    <phase_2+49>
<+72>:    mov    rax,qword ptr[rsp+0x18]    ; again that long long
<+77>:    xor    rax,qword ptr fs:0x28      ; again that thread-local
<+86>:    je     <phase_2+93>
<+88>:    call   <__stack_chk_fail@plt> ; aaaah they have to do with stack smashing
                                        ; protection; we can ignore them, it's
                                        ; compiler-injected stuff
<+93>:    add    rsp,0x28               ; epilogue
<+97>:    pop    rbx
<+98>:    pop    rbp
<+99>:    ret

此外,我们可以合理地假设函数不带参数并且不返回任何值,因为它不会查看rsi的初始状态或上面的地址(=高于){的初始值{ {1}},似乎没有让rsp处于特别有意义的状态。 1

现在,让我们在C中重写它,暂时将跳转保留为gotos,并留下寄存器名称来代替有意义的变量名称。我们将完全忽略raxfs:0x28,因为它们只是用于gcc注入堆栈粉碎保护的金丝雀。

rsp+0x18

第一个重要的步骤是将炸弹爆炸的“短跳”重写为void phase_2() { int numbers[6]; read_six_numbers(numbers); if(numbers[0] == 1) goto l_41; explode_bomb(); l_41: int *rbx = &numbers[0]; int *rbp = &numbers[5]; l_49: int eax = *rbx; eax += eax; if(eax == rbx[1]) goto l_63; explode_bomb(); l_63: rbx++; if(rbx != rbp) goto l_49; } s(反转条件),最后的转移为if ...... {{1} }:

do

然后我们可以通过删除额外的变量并给出更合理的名称来清理汇编原点的剩余部分;另外,while ... void phase_2() { int numbers[6]; read_six_numbers(numbers); if(numbers[0] != 1) explode_bomb(); int *rbx = &numbers[0]; int *rbp = &numbers[5]; do { int eax = *rbx; eax += eax; if(eax != rbx[1]) explode_bomb(); rbx++; } while(rbx != rbp); } 可以轻松地重写为do循环。

while

最后,如果对理解算法有任何帮助,我们可以从指针转移到索引,这可能更接近于人类如何编写它:

for

因此,这里的秘密组合必须是1,2,4,8,16,32,因为每个数字必须是前一个数字的两倍,第一个必须是1。

作为最后的奖励步骤,我们可以将反编译的代码汇集回gcc example2 - 成功!

注释

  1. 当然,尽管所有假设都是合理的,但可以通过进一步的检查来反驳,通常是当你在接下来的步骤中向上移动并发现你以前没有考虑过的东西时。 / p>

    在这种情况下,代码非常简单,我所有的第一次有根据的猜测似乎都很好,而逆转过程只是一系列步骤,我逐渐转向更高级别的结构,但请记住逆向工程是一般来说,这是一个迭代过程,你可能会在稍后发现你开始做的一些工作假设实际上是错误的,你可能不得不回到早期阶段来调整它们。

    此外,即使你对汇编和编码器发出的代码有最好的了解,做出这些假设也是不可避免的。编译本质上是一个有损的过程,因此要重构高级构造,你总是需要应用一些启发式和猜测。