ISCC2019 Reverse Rev01 writeup

TOC

  1. 1. 分析
  2. 2. 思路
    1. 2.1. 找到输入点
    2. 2.2. 硬件断点
      1. 2.2.1. 进行int数组扩展
      2. 2.2.2. 进行检查
      3. 2.2.3. 进行flag判断
        1. 2.2.3.1. 提取处理过的flag
        2. 2.2.3.2. 还原
        3. 2.2.3.3. 运行实例
  3. 3. 总结

源文件、IDA分析文件下载,百度网盘:Rev01.zip

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

分析

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

思路

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

找到输入点

.text:000000000000F470 loc_F470:                               ; CODE XREF: std::io::append_to_string::he82ae2a7713c4960+64↑j
.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_F64C↓j
.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+48↓j
.text:000000000004CB90 ; core::str::run_utf8_validation::h2a9edbcb1794d209+83↓j ...
.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+26D↓j
.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师傅的指点,我才能继续调试下去。与其说是考验调试能力,其实我觉得更是在考验耐心。所以说耐心也很重要,特别是面对难题的时候。