南邮网络攻防训练平台逆向入门题目 Writeup

2018/02/04

共三道题,前边几道题比较简单,就不写 writeup 了。

WxyVM

二话不说拖进 IDA,找到 main() 后 F5,顺便给作用明显的变量命个名:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  char v4; // [sp+Bh] [bp-5h]@1
  signed int i; // [sp+Ch] [bp-4h]@3

  puts("[WxyVM 0.0.1]");
  puts("input your flag:");
  scanf("%s", &scanf_buffer_bytes);
  v4 = 1;
  sub_4005B6();
  if ( strlen(&scanf_buffer_bytes) != 24 )
    v4 = 0;
  for ( i = 0; i <= 23; ++i )
  {
    if ( *(&scanf_buffer_bytes + i) != goal_dword[i] )
      v4 = 0;
  }
  if ( v4 )
    puts("correct");
  else
    puts("wrong");
  return 0LL;
}

代码逻辑很清晰,先读字符串到 scanf_buffer_bytes,然后调用 sub_4005B6(),推测是进行加密,子函数跑完后,让加密后的数据跟 goal_dword 进行比对。读懂代码后直接跟进去子函数,同样,先对意图明显的变量进行命名:

__int64 func()
{
  unsigned int v0; // ST04_4@3
  __int64 result; // rax@3
  signed int i; // [sp+0h] [bp-10h]@1
  char v3; // [sp+8h] [bp-8h]@3

  for ( i = 0; i <= 14999; i += 3 )
  {
    v0 = raw_data[(signed __int64)i];
    v3 = raw_data[(signed __int64)(i + 2)];
    result = v0;
    switch ( v0 )
    {
      case 1u:
        result = raw_data[(signed __int64)(i + 1)];
        *(&scanf_buffer_bytes + result) += v3;
        break;
      case 2u:
        result = raw_data[(signed __int64)(i + 1)];
        *(&scanf_buffer_bytes + result) -= v3;
        break;
      case 3u:
        result = raw_data[(signed __int64)(i + 1)];
        *(&scanf_buffer_bytes + result) ^= v3;
        break;
      case 4u:
        result = raw_data[(signed __int64)(i + 1)];
        *(&scanf_buffer_bytes + result) *= v3;
        break;
      case 5u:
        result = raw_data[(signed __int64)(i + 1)];
        *(&scanf_buffer_bytes + result) ^= *(&scanf_buffer_bytes + raw_data[(signed __int64)(i + 2)]);
        break;
      default:
        continue;
    }
  }
  return result;
}

读一下代码,大意是从 raw_data 依次读取数据,3 Bytes 为一组。Byte 1 用来 switch,Byte 2 用来指定相对于 buffer 的偏移量,Byte 3 用以参与计算。

所以代码到这就很明显了,从 goal_dword 逆操作还原出 scanf_buffer_bytes 即可。不过代码里有两个坑:

  1. goal_dword 取数据时需要每 4 位只保留最低位。
  2. 会溢出
#IDAPython

import sys
raw_data = 0x00000000006010C0
answer   = 0x0000000000601060
buffer   = 0x0000000000604B80

# 逆操作
op = {
    1: lambda x, y: (x-y)%sys.maxint+1 if x-y<0 else x-y,
    # 这里为了模拟溢出,比较粗暴
    2: lambda x, y: (x+y)%maxint,
    3: lambda x, y: x^y,
    4: lambda x, y: x/y,
}

for i in range(24):
    PatchByte(buffer+i, Byte(answer+4*i))

for i in range(14997, -1, -3):
    v0 = Byte(raw_data + i)
    v1 = Byte(raw_data + i + 1)
    v2 = Byte(raw_data + i + 2)
    if v0 > 0 and v0 < 5:
        PatchByte(buffer+v1, op[v0](Byte(buffer+v1), v2))
    elif v0 == 5:
        PatchByte(buffer+v1, Byte(buffer+v1)^Byte(buffer+v2))
        break
    else:
        continue

# find flag at buffer

maze

照常拖进 IDA 顺势 F5,读代码:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  signed __int64 offset; // rbx@4
  signed int chr; // eax@5
  bool v5; // bp@5
  bool v6; // al@8
  const char *msg; // rdi@19
  __int64 y; // [sp+0h] [bp-28h]@1
  __int32 x;  // 读汇编后觉得 &y+1 视为 &x 更好

  x = 0L;
  y = 0LL;
  puts("Input flag:");
  scanf("%s", &buffer, 0LL);
  if ( strlen(&buffer) != 24 || strncmp(&buffer, "nctf{", 5uLL) || *(&byte_6010BF + 24) != 125 )
  {
  // assert len(buffer) == 24 && buffer.startswith("nctf{") && buffer.endswith("}")
wrong_and_exit:
    puts("Wrong flag!");
    exit(-1);
  }

  offset = 5LL;  // 从 { 后面第一个字符开始,后边 ++offset 遍历 buffer

  if ( strlen(&buffer) - 1 > 5 )  // 恒为真
  {
    while ( 1 )
    {
      chr = *(&buffer + offset);
      v5 = 0;
      if ( chr > 'N' )
      {
        chr = (unsigned __int8)chr;
        if ( (unsigned __int8)chr == 'O' )
        {
          v6 = func_O((_DWORD *)&x);  // v6 = x-- > 0;
          goto assign_v6_to_v5_and_goto_label_15;
        }
        if ( chr == 'o' )
        {
          v6 = func_o((int *)&x);  // v6 = x++ < 8;
          goto assign_v6_to_v5_and_goto_label_15;
        }
      }
      else
      {
        chr = (unsigned __int8)chr;
        if ( (unsigned __int8)chr == '.' )
        {
          v6 = func_dot(&y);  // v6 = y-- > 0;
          goto assign_v6_to_v5_and_goto_label_15;
        }
        if ( chr == '0' )
        {
          v6 = func_0((int *)&y);  // v6 = y++ < 8;
assign_v6_to_v5_and_goto_label_15:
          v5 = v6;
          goto LABEL_15;
        }
      }
      // 分析到下边发现是走迷宫,O左 o右 .上 0下
LABEL_15:
      if ( !(unsigned __int8)check(asc_601060, x, y) )  // SHIDWORD(y) == x, 检查是否撞到墙壁
        // asc_601060: 8x8 array
        //   ******
        // *   *  *
        // *** * **
        // **  * **
        // *  *#  *
        // ** *** *
        // **     *
        // ********
        // return 1 if asc[x][y] == (' ' or '#') else 0
        goto wrong_and_exit;  // assert return == 1;
      if ( ++offset >= strlen(&buffer) - 1 )  // 自增,如果下一个已经是 },则:
      {
        if ( v5 )  // 防止走出迷宫的范围
          break;
wrong_and_exit_2:
        msg = "Wrong flag!";
        goto show_msg_and_exit;
      }
    }
  }
  if ( *(&asc_601060[8 * (signed int)y] + x) != '#' )  // 最后要以 # 为终点,否则 wrong
    goto wrong_and_exit_2;
  msg = "Congratulations!";
show_msg_and_exit:
  puts(msg);
  return 0LL;
}
// 综上,就是走迷宫,从 (0, 0) 走到 # 处即为 flag

WxyVM2

丢进 IDA,发现 “Sorry, this node is too big to display”,顿时觉得水深 [一脸黑线]。我不管!读汇编好费时的!F5 之后放一边,居然被我等来了 C 代码,哈哈哈,那就继续分析。

一看发现两万多行,emmm,先读下头尾。很简单,scanf0x694100 处,一番操作后与 0x6940600 进行比对而已(依旧是那个 byte 与 dword 比对的坑,小心)。所以我就去头去尾,把代码丢 Sublime 里分析了。

看了一会,发现了猫腻:

scanf 放进去那个 buffer 的范围是 0x694100 ~ 0x694118,最终比对的目标是 0x694060 ~ 0x6940C0,而且后者的数据没变过。又发现中间那两万多行很多跟解题无关,就把对无关地址的操作全剔除了,剩下两千多行有效操作。

然后思路就很清晰了,反向操作即可。先写个 py 脚本逆一下操作顺序:

l = [i for i in open('operations.txt')]
f = open('operations-reversed.txt', 'w')
for i in reversed(l):
    f.write(i)
f.close()
# 没有 close() 至少也 flush() 一下…刚开始这里忘了,卡了很久,很奇怪为啥数据少了一小部分…

然后在 Sublime 里把 -- 换成 -= 1++ 同理。再写个很丑的正则替换一下各个操作:(\w+)_(\w+) \+= (.*?); to $1(0x$2) -= $3;,这里是加法变减法,其他同理。

由于看起来很可能会有溢出,我就直接用 C 写了,省得像之前那样活生生地在 py 里模拟溢出…

#include <stdio.h>
#include <inttypes.h>

uint8_t goal[25] = {
    0xC0, 0x85, 0xF9, 0x6C, 0xE2, 0x14, 0xBB, 0xE4, 0x0D,
    0x59, 0x1C, 0x23, 0x88, 0x6E, 0x9B, 0xCA, 0xBA, 0x5C,
    0x37, 0xFF, 0x48, 0xD8, 0x1F, 0xAB, 0xA5 };

#define byte(i) goal[i-0x694100]

int main() {
    // 这里放上边得到的逆操作们

    for (int i=0; i<25; ++i)
        printf("%c", goal[i]);
    return 0;
}

Over,至此南邮的训练平台逆向入门题 All Clear~


Here are two useful pages:

  1. 汇编指令缩写, ljtcnblogs
  2. IDA逆向常用宏定义, 小小攻城师
comments powered by Disqus