DDCTF2019 RE Windows Reverse2

源文件、脱壳文件、IDA分析文件打包下载:http://file.eonew.cn/ctf/re/reverse2_final.zip

一道比较简单的逆向题,简述一下思路。

查壳

先用Exeinfope来查看是否有壳,发现是aspack壳,由于鄙人没有相对应的工具,但是aspack壳也符合esp定律,所以直接用OD脱壳即可,然后用import fix修复导入表。

核心代码

主函数

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char Dest; // [esp+8h] [ebp-C04h]
  char v5; // [esp+9h] [ebp-C03h]
  char v6; // [esp+408h] [ebp-804h]
  char Dst; // [esp+409h] [ebp-803h]
  char v8; // [esp+808h] [ebp-404h]
  char v9; // [esp+809h] [ebp-403h]

  v6 = 0;
  memset(&Dst, 0, 0x3FFu);
  v8 = 0;
  memset(&v9, 0, 0x3FFu);
  printf("input code:");
  scanf("%s", &v6);
  if ( !check(&v6) )
  {
    printf("invalid input\n");
    exit(0);
  }
  change_str(&v6, (int)&v8);
  Dest = 0;
  memset(&v5, 0, 0x3FFu);
  sprintf(&Dest, "DDCTF{%s}", &v8);
  if ( !strcmp(&Dest, "DDCTF{reverse+}") )
    printf("You've got it !!! %s\n", &Dest);
  else
    printf("Something wrong. Try again...\n");
  return 0;
}

主函数看出,程序先会对我们的输入进行检查(check),然后进行字符处理(change_str)。

check

char __usercall check@<al>(const char *a1@<esi>)
{
  signed int v1; // eax
  signed int v2; // edx
  int v3; // ecx
  char v4; // al

  v1 = strlen(a1);
  v2 = v1;
  if ( v1 && v1 % 2 != 1 )
  {
    v3 = 0;
    if ( v1 <= 0 )
      return 1;
    while ( 1 )
    {
      v4 = a1[v3];
      if ( (v4 < '0' || v4 > '9') && (v4 < 'A' || v4 > 'F') )
        break;
      if ( ++v3 >= v2 )
        return 1;
    }
  }
  return 0;
}

可以看出这个check函数要求输入一定要0-9或者A-F,也就是十六进制的字符。

int __usercall change_str@<eax>(const char *a1@<esi>, int a2)
{
  signed int v2; // edi
  signed int v3; // edx
  char v4; // bl
  char v5; // al
  char v6; // al
  unsigned int v7; // ecx
  char v9; // [esp+Bh] [ebp-405h]
  char v10; // [esp+Ch] [ebp-404h]
  char Dst; // [esp+Dh] [ebp-403h]

  v2 = strlen(a1);
  v10 = 0;
  memset(&Dst, 0, 0x3FFu);
  v3 = 0;
  if ( v2 > 0 )
  {
    v4 = v9;
    do
    {
      v5 = a1[v3];
      if ( (unsigned __int8)(a1[v3] - '0') > 9u )
      {
        if ( (unsigned __int8)(v5 - 'A') <= 5u )
          v9 = v5 - '7';
      }
      else
      {
        v9 = a1[v3] - '0';
      }
      v6 = a1[v3 + 1];
      if ( (unsigned __int8)(a1[v3 + 1] - '0') > 9u )
      {
        if ( (unsigned __int8)(v6 - 'A') <= 5u )
          v4 = v6 - '7';
      }
      else
      {
        v4 = a1[v3 + 1] - '0';
      }
      v7 = (unsigned int)v3 >> 1;
      v3 += 2;
      *(&v10 + v7) = v4 | 16 * v9;
    }
    while ( v3 < v2 );
  }                                             // 通过调试发现,上面的功能str => hex
  return base64encode(v2 / 2, (void *)a2);
}

上面的代码比较复杂,可以直接动态调试来猜测其规则,代码静态分析反而更耗费时间,通过调试发现他的功能是将我们输入的十六进制字符转换成相对应的字节流。然后是base64encode,这个是我自己改的symbols,因为并不难判断,里面有标志性的三段位移,就是参数的传递有坑,不看汇编代码的很难发现:

loc_4012EC:
mov     eax, edi
cdq
sub     eax, edx
sar     eax, 1
push    ebp
push    eax
lea     ecx, [esp+418h+var_404]
call    base64encode
mov     ecx, [esp+418h+var_4]
add     esp, 8
pop     edi
pop     ebp
xor     ecx, esp
call    sub_40145E
add     esp, 408h
retn
change_str endp

调用base64encode用的是ecx传递参数,所以IDA会识别不出来。

思路

知道了程序的功能,直接将其逆向过程写出来即可,下面是我的脚本:

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

import base64
import binascii

key = b'reverse+'

print (
    binascii.b2a_hex( 
        base64.b64decode(b'reverse+')
    ).upper()
)

运行实例

root@36851e23817c:~# ./main.py 
b'ADEBDEAEC7BE'
root@36851e23817c:~# wine reverse2_final.exe 
input code:ADEBDEAEC7BE
You've got it !!! DDCTF{reverse+}
root@36851e23817c:~# 

总结

手动脱壳还是很重要的,还有不能过度依赖工具,注重原理也很重要的。