ISCC2019 Reverse Rev01 writeup

本题主要考察的是动态调试能力。

分析

拿到程序,光看到程序文件的大小都有点夸张,拖到IDA分析,更是一些奇奇怪怪的函数,而且字符串查找也找不到明显的正确、错误的回显,让我们很难进行静态分析。

思路

根据我们的输入进行硬件断点分析。

找到输入点

.text:000000000000F470 loc_F470:                               ; CODE XREF: std::io::append_to_string::he82ae2a7713c4960+64j
.text:000000000000F470                 mov     rdx, [rbp+18h]
.text:000000000000F474                 mov     rsi, [rbp+10h]  ; buf
.text:000000000000F478                 test    rdx, rdx
.text:000000000000F47B                 mov     rax, 7FFFFFFFFFFFFFFFh
.text:000000000000F485                 cmovs   rdx, rax        ; nbytes
.text:000000000000F489                 xor     edi, edi        ; fd
.text:000000000000F48B                 call    _read
.text:000000000000F490                 mov     rbx, rax
.text:000000000000F493                 cmp     rax, 0FFFFFFFFFFFFFFFFh
.text:000000000000F497                 jz      loc_F5AB

通过进行int终端程序,然后查看栈的返回地址,我们确定了是在0xF48B这个地址读取我们的输入,所以在0xF490下断点,读取输入后对于我们输入的字符串下硬件断点,进行动态调试。

硬件断点

首先触发的是下面的硬件断点(在0xF4D3处),但是通过后期的调试,会发现这个断点不重要。

.text:000000000000F4BC loc_F4BC:                               ; CODE XREF: std::io::append_to_string::he82ae2a7713c4960:loc_F64Cj
.text:000000000000F4BC                 sub     rbx, r13
.text:000000000000F4BF                 add     r13, [rbp+10h]
.text:000000000000F4C3                 mov     esi, 0Ah        ; c
.text:000000000000F4C8                 mov     rdi, r13        ; s
.text:000000000000F4CB                 mov     rdx, rbx        ; n
.text:000000000000F4CE                 call    _memchr
.text:000000000000F4D3                 test    rax, rax        ; 不重要
.text:000000000000F4D6                 jz      short loc_F530

第二个停下来的硬断点是0xFB98,这里程序将我们输入的字符串转移到了一块新的内存上了,所以对于新内存继续下硬件断点。

.text:000000000000FB8D                 mov     rsi, rdx        ; src
.text:000000000000FB90                 mov     rdx, rax        ; n
.text:000000000000FB93                 call    _memcpy
.text:000000000000FB98                 add     rsp, 0B8h       ; memcpy() 进行字符转移,继续下断点
.text:000000000000FB9F                 retn

继续运行,新内存的硬件断点被触发(0x4CB94),但这里并没有很大的用处,仅仅是判断是否达到了字符末尾。

.text:000000000004CB90 loc_4CB90:                              ; CODE XREF: core::str::run_utf8_validation::h2a9edbcb1794d209+48j
.text:000000000004CB90                                         ; core::str::run_utf8_validation::h2a9edbcb1794d209+83j ...
.text:000000000004CB90                 movzx   ebx, byte ptr [rsi+rax]
.text:000000000004CB94                 test    bl, bl          ; 这里进行 ‘\0’ 判断,不重要
.text:000000000004CB96                 js      short loc_4CBF0

继续运行,又是一个 NULL 判断(0x4CBDD),这里通过动态调试,也能确定为不重要的节点。

.text:000000000004CBD9                 cmp     byte ptr [rsi+rax], 0
.text:000000000004CBDD                 jns     short loc_4CBD0

进行int数组扩展

继续运行,在0x6876处停下,这里是一个重点

.text:0000000000006870 loc_6870:                               ; CODE XREF: beginer_reverse::main::h80fa15281f646bc1+26Dj
.text:0000000000006870                 movzx   r12d, byte ptr [r13+rbp+0]
.text:0000000000006876                 mov     rbx, rsi
.text:0000000000006879                 cmp     rbp, rsi
.text:000000000000687C                 jnz     short loc_68FD

反汇编代码:

do
{
  temp = input[v16];
  v18 = v14;
  if ( v16 == v14 )
  {
    v18 = v14 + 1;
    if ( v14 >= 0xFFFFFFFFFFFFFFFFLL )
      goto LABEL_68;
    if ( v18 < 2 * v14 )
      v18 = 2 * v14;
    if ( !is_mul_ok(4uLL, v18) )
LABEL_68:
      alloc::raw_vec::capacity_overflow::hbc659f170a622eae();
    if ( v14 )
    {
      _rust_realloc();
      int_array = (signed int *)v19;
      v13 = v32;
      if ( !v19 )
        goto LABEL_63;
    }
    else
    {
      _rust_alloc();
      int_array = (signed int *)v20;
      v13 = v32;
      if ( !v20 )
LABEL_63:
        alloc::alloc::handle_alloc_error::h9e3787e5722c870d();
    }
    *(_QWORD *)&v30 = int_array;
    *((_QWORD *)&v30 + 1) = v18;
    v14 = v18;
  }
  int_array[v16++] = temp;
  v31 = v16;
}
while ( v13 != v16 );

可以通过动态调试,结合它的反汇编代码,确定这里将我们的字符串数组扩展为int型数组。

重点是这两句:

temp = input[v16];
int_array[v16++] = temp;

确定功能之后继续对扩展的int数组下硬件断点。

进行检查

之后继续运行,触发int数组的硬件断点(0x6939)。这个断点很重要。

.text:0000000000006935                 mov     edx, [r14+rcx]
.text:0000000000006939                 add     edx, 0FFFFFFE0h ; 开始进行检查,重点
.text:000000000000693C                 add     rcx, 4
.text:0000000000006940                 cmp     edx, 5Fh ; '_'
.text:0000000000006943                 jb      short loc_6930

反汇编代码:

while ( v16 != v21 )
{
v22 = int_array[v21] - 32;
++v21;
if ( v22 >= 0x5F )
  std::panicking::begin_panic::h770c088eb8f42530(
    "an error occuredSubmit this and get you'r points!\n",
    16LL,
    &off_64F10,
    v21 * 4);
}

这里主要是检查我们的int数组是否合理,不合理直接报错。

进行flag判断

继续运行,程序在0x6995出因为硬件断点而停下,这里是重中之重。

.text:0000000000006985                 mov     edi, [r15+rsi*4]
.text:0000000000006989                 sar     edi, 2
.text:000000000000698C                 xor     edi, 0Ah
.text:000000000000698F                 xor     eax, eax
.text:0000000000006991                 cmp     edi, [r14+rsi*4]
.text:0000000000006995                 lea     rsi, [rsi+1]
.text:0000000000006999                 setz    al
.text:000000000000699C                 add     rcx, rax
.text:000000000000699F                 add     rdx, 0FFFFFFFFFFFFFFFCh
.text:00000000000069A3                 cmp     rsi, rbp
.text:00000000000069A6                 jb      short loc_6980

反汇编代码:

result = ((flag[i] >> 2) ^ 0xA) == int_array[i];

做到这里flag已经很明显了,我们只要把flag的内容提取出来,然后进行还原就可以得到flag。

提取处理过的flag

下面的0x00007F8D0DA29000为动态调试时,flag的地址,注意这里每次调试地址都是不同的,需要自己计算。而0x00007F8D0DA29000就是该处理过的flag的结尾。

Python>get_bytes(0x00007F8D0DA29000, 0x00007F8D0DA29088 - 0x00007F8D0DA29000).encode('hex')
5401000080010000fc010000e4010000f80100005401000090010000bc010000bc010000b801000054010000f80100009401000054010000b4010000bc010000f801000054010000f401000088010000ac010000f8010000540100008c010000e40100005401000090010000bc010000bc010000b8010000bc010000b80100005401000090000000
还原

利用脚本进行还原:

#!/usr/bin/python
# -*- coding:utf-8 -*-

import binascii
import struct

data = binascii.a2b_hex('5401000080010000fc010000e4010000f80100005401000090010000bc010000bc010000b801000054010000f80100009401000054010000b4010000bc010000f801000054010000f401000088010000ac010000f8010000540100008c010000e40100005401000090010000bc010000bc010000b8010000bc010000b80100005401000090000000')

out = ''
while(data):
    value = struct.unpack('i', data[:4])[0]
    new_value = ((value >> 2) ^ 0xA)
    out += chr(new_value)
    data = data[4:]

print(out)
运行实例
ex@Ex:~/test$ python3 main.py 
_just_need_to_get_what_is_needed_.
ex@Ex:~/test$ python main.py 
_just_need_to_get_what_is_needed_.
ex@Ex:~/test$ ./reverse 
_just_need_to_get_what_is_needed_.
Submit this and get you'r points!

到此,我们拿到了flag。

总结

这题很考验我们的调试能力,其实刚开始的时候我只是调试了一下就觉得这个思路不太行,但是还好有ZhouYetao师傅的指点,我才能继续调试下去。与其说是考验调试能力,其实我觉得更是在考验耐心。所以说耐心也很重要,特别是面对难题的时候。