DDCTF2019 RE Confused

先感谢40K0师傅的指点,要不然本人真的做不出这道题来。

文件清单 confused.zip

原题文件

confused.app.zip 就是原题文件。

源文件

  1. main.c
  2. vm.c
  3. code.h
  4. vm.h

原题文件核心程序: xia0Crackme

xia0Crackme.i64 为它的分析文件,IDA分析文件来源于40K0师傅。

模拟程序流文件

由于大部分人都没有Mac,所以本人基于原题的程序流,编译了两个版本的可执行文件,其行为应该基本与原题一致。

crackme 为Linux下编译的64为程序,带了debug Symbols。gcc 7.1编译。

crackme.exe 为Windows下编译的64程序,带了debug Symbols。编译器为 Microsoft (R) C/C++ Optimizing Compiler Version 19.16.27027.1 for x64。(MSVC),crackme.ilk、crackme.pdb为生成的对应的调试文件。

分析

刚拿到题目是有点搓手不急的,第一是因为是mac程序,环境就满足不了,后面经大佬指点后才知道了,做这种题目不需要把全部代码都看懂,只要分析核心代码就好了,

[ViewController checkCode:]

if ((unsigned int)check(*((__int64 *)&v14 + 1)) == 1)
  objc_msgSend(v17, "onSuccess");
else
  objc_msgSend(v17, "onFailed");

主要是为了通过check函数。

后面模拟的是虚拟器的运行,主要作用任然是为了混淆我们。具体原理可以了解这篇文章:https://www.52pojie.cn/forum.php?mod=viewthread&tid=860237&page=1

虚拟机运行

__int64 __fastcall check(__int64 a1)
{
  char vm; // [rsp+20h] [rbp-C0h]
  __int64 v3; // [rsp+D8h] [rbp-8h]

  v3 = a1;
  memset(&vm, 0, 0xB8uLL);
  init_vm((vm_struc *)&vm, (char *)a1);
  return (unsigned int)sub_100001F00((vm_struc *)&vm);
}
__int64 __fastcall sub_100001F00(vm_struc *a1)
{
  a1->pc = (__int64)&loc_100001980 + 4;
  while ( *(unsigned __int8 *)a1->pc != 0xF3 )
    run(a1);
  free(buffer);
  return (unsigned int)a1->buffer;
}

从上面可以看出,验证是否真确,主要取决于a1->buffer。而a1->buffer的值由下面的指令来决定。

__int64 __fastcall mov_buf_imm(vm_struc *a1)
{
  __int64 result; // rax

  result = *(unsigned int *)(a1->pc + 1);
  a1->buffer = result;
  a1->pc += 5LL;
  return result;
}

首先将(__int64)&loc_100001980 + 4内存的内容dump下来,后面要分析的。dump下来之后,数据如下:

code.h

unsigned char code_bin[] = {
  0xf0, 0x10, 0x66, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x30, 0xf6, 0xc1, 0xf0,
  0x10, 0x63, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x31, 0xf6, 0xb6, 0xf0, 0x10,
  0x6a, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x32, 0xf6, 0xab, 0xf0, 0x10, 0x6a,
  0x00, 0x00, 0x00, 0xf8, 0xf2, 0x33, 0xf6, 0xa0, 0xf0, 0x10, 0x6d, 0x00,
  0x00, 0x00, 0xf8, 0xf2, 0x34, 0xf6, 0x95, 0xf0, 0x10, 0x57, 0x00, 0x00,
  0x00, 0xf8, 0xf2, 0x35, 0xf6, 0x8a, 0xf0, 0x10, 0x6d, 0x00, 0x00, 0x00,
  0xf8, 0xf2, 0x36, 0xf6, 0x7f, 0xf0, 0x10, 0x73, 0x00, 0x00, 0x00, 0xf8,
  0xf2, 0x37, 0xf6, 0x74, 0xf0, 0x10, 0x45, 0x00, 0x00, 0x00, 0xf8, 0xf2,
  0x38, 0xf6, 0x69, 0xf0, 0x10, 0x6d, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x39,
  0xf6, 0x5e, 0xf0, 0x10, 0x72, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x3a, 0xf6,
  0x53, 0xf0, 0x10, 0x52, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x3b, 0xf6, 0x48,
  0xf0, 0x10, 0x66, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x3c, 0xf6, 0x3d, 0xf0,
  0x10, 0x63, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x3d, 0xf6, 0x32, 0xf0, 0x10,
  0x44, 0x00, 0x00, 0x00, 0xf8, 0xf2, 0x3e, 0xf6, 0x27, 0xf0, 0x10, 0x6a,
  0x00, 0x00, 0x00, 0xf8, 0xf2, 0x3f, 0xf6, 0x1c, 0xf0, 0x10, 0x79, 0x00,
  0x00, 0x00, 0xf8, 0xf2, 0x40, 0xf6, 0x11, 0xf0, 0x10, 0x65, 0x00, 0x00,
  0x00, 0xf8, 0xf2, 0x41, 0xf6, 0x06, 0xf7, 0x01, 0x00, 0x00, 0x00, 0xf3,
  0xf7, 0x00, 0x00, 0x00, 0x00, 0xf3, 0x5d, 0xc3, 0x0f, 0x1f, 0x84, 0x00,
  0x00, 0x00, 0x00, 0x00
};
unsigned int code_bin_len = 220;

然后本人把整个虚拟机的实现的代码全部移植到vm.c中,vm.c可以从上面下载。然后写出对应的vm.h文件。

vm.h


#define LOBYTE(arg) (*(char *)(&(arg)))

#define __int8 char

char *buffer;

typedef struct vm_struc
{
    int eax_;
    int ebx_;
    int ecx_;
    int edx_;
    int flag;
    int field_14;
    unsigned char *pc;
    long long code_F0;
    unsigned char *mov_reg_imm;
    long long code_F1;
    unsigned char *xor_eax_ebx;
    long long code_F2;
    unsigned char *cmp_eax_imm;
    long long code_F4;
    unsigned char *add_eax_ebx;
    long long code_F5;
    unsigned char *sub_eax_ebx;
    long long code_F3;
    unsigned char *nop;
    long long code_F6;
    unsigned char *jz_imm;
    long long code_F7;
    unsigned char *mov_buf_imm;
    long long code_F8;
    unsigned char *enc_eax_2;
    int buffer;
} vm_struc;

int init_vm(vm_struc *a1, char *input);

int xor_eax_ebx(vm_struc *a1);

vm_struc *cmp_eax_imm(vm_struc *a1);

vm_struc *jz_imm(vm_struc *a1);

void nop();

int sub_100001B80(char a1, int a2);

int enc_eax_2(vm_struc *a1);

int add_eax_ebx(vm_struc *a1);

int sub_eax_ebx(vm_struc *a1);

// 最主要是这条指令
int mov_buf_imm(vm_struc *a1);

vm_struc *mov_reg_imm(vm_struc *a1);

int run(vm_struc *a1);

上述文件只能在64位的Linux上编译行为才会正常,否则会因为字节偏移错误导致行为异常。

下面这段代码模拟虚拟机运行,当操作码为0xF3,则结束运行。

while (*(unsigned __int8 *)a1->pc != 0xF3)
    run(a1);
free(buffer);

经过分析操作码发现,程序的行为如下:

mov_reg_imm(a1);

for (i = 0; i < 18; i++)
{
    enc_eax_2(a1);
    cmp_eax_imm(a1);
    jz_imm(a1);
    mov_reg_imm(a1);
}

enc_eax_2(a1);
cmp_eax_imm(a1);
jz_imm(a1);
mov_buf_imm(a1);

mov_reg_imm

vm_struc *mov_reg_imm(vm_struc *a1)
{
  vm_struc *result; // rax
  int *v2;          // [rsp+Ch] [rbp-18h]

  v2 = (int *)(a1->pc + 2);
  switch (*(unsigned __int8 *)(a1->pc + 1))
  {
  case 0x10u:
    a1->eax_ = *v2;
    break;
  case 0x11u:
    a1->ebx_ = *v2;
    break;
  case 0x12u:
    a1->ecx_ = *v2;
    break;
  case 0x13u:
    a1->edx_ = *v2;
    break;
  case 0x14u:
    a1->eax_ = buffer[*v2];
    break;
  default:
    break;
  }
  result = a1;
  a1->pc += 6LL;
  return result;
}

经过操作码分析之后你会发现,在mov_reg_imm中,只有case 0x10u这种情况,其他都是用来混淆的。也就是这个函数的功能就是将操作码后面的一个字节赋值给eax。

enc_eax_2

int enc_eax_2(vm_struc *a1)
{
  int result; // rax

  result = sub_100001B80(a1->eax_, 2); // 凯撒密码,key=2
  a1->eax_ = (char)result;
  ++a1->pc;
  return result;
}

对eax进行加密变换。

cmp_eax_imm

vm_struc *cmp_eax_imm(vm_struc *a1)
{
  vm_struc *result; // rax

  a1->flag = (a1->eax_ == buffer[*(unsigned __int8 *)(a1->pc + 1)]);
  result = a1;
  a1->pc += 2LL;
  return result;
}

注意:这里的flag一定要为1,否则后面就会执行错误,所以我们的任务就是构建输入使得每次进行cmp_eax_imm时,得到的flag都要是1,这里用的(unsigned int8 )(a1->pc + 1)进行buf偏移,所有的偏移路径通过操作码得到:{0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41},到时候下面的脚本有会有代码进行计算。这刚好对应了int init_vm(vm_struc a1, char input)中的这行代码memcpy_chk((buffer + 48), input, 18LL, -1LL);

jz_imm

vm_struc *jz_imm(vm_struc *a1)
{
  vm_struc *result; // rax

  // 要求flag尽可能为 1 ,不发生跳转
  if (a1->flag)
    a1->flag = 0;
  else
    a1->pc += *(unsigned __int8 *)(a1->pc + 1);
  result = a1;
  a1->pc += 2LL;
  return result;
}

这里会用到上面cmp_eax_imm函数设置的flag值,要求flag尽可能为 1 ,才不会发生跳转。

上面的步骤会重复判断19次,然后进行mov_buf_imm

mov_buf_imm

// 最主要是这条指令
int mov_buf_imm(vm_struc *a1)
{
  int result; // rax

  result = *(unsigned int *)(a1->pc + 1);
  a1->buffer = result;
  a1->pc += 5LL;
  return result;
}

这里会把a1->buffer的值赋值为操作码后面4字节组成的一个int型数据。而a1->buffer决定了check的返回值,所以只要执行mov_buf_imm时,pc的操作码后面4字节组成的一个int型数据不为0的话,就能check成功。

由于分析时,发现除了最前面要填充5个字节意外,其他的指令都是11个字节的。也就是下面的指定序列是11字节的。这对用我们后面的脚本分析有很大的用处。

enc_eax_2(a1); # 1 字节
cmp_eax_imm(a1); # 2 字节
jz_imm(a1); # 2 字节
mov_reg_imm(a1); # 6 字节

具体脚本

我们只需要写脚本来模拟判断正确的情况,然后就能得到输入的值。

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

import binascii
# 操作码
s =         "F01066000000F8F230F6C1F0" + \
    "1063000000F8F231F6B6F0106A000000" + \
    "F8F232F6ABF0106A000000F8F233F6A0" + \
    "F0106D000000F8F234F695F010570000" + \
    "00F8F235F68AF0106D000000F8F236F6" + \
    "7FF01073000000F8F237F674F0104500" + \
    "0000F8F238F669F0106D000000F8F239" + \
    "F65EF01072000000F8F23AF653F01052" + \
    "000000F8F23BF648F01066000000F8F2" + \
    "3CF63DF01063000000F8F23DF632F010" + \
    "44000000F8F23EF627F0106A000000F8" + \
    "F23FF61CF01079000000F8F240F611F0" + \
    "1065000000F8F241F606F701000000F3" + \
    "F700000000F35DC30F1F840000000000"

# 头部的另外计算 F0 10 66 00  00 00
s = '0' * 5 * 2 + s

l = int(len(s)/22)
# 每11个字节的操作进行打印
print('操作码整理')
for i in range(l):
    print('%2d' % (i+1), s[22*i:22*(i+1)])
print()

print('打印偏移的值')
output = '{'
for i in range(1, 19):
    data = s[22*i:22*(i+1)]
    output += '0x%s, ' % (data[4:6])

output = output[:-2] + '}'
print(output)

print()

print('打印对应的秘钥')
key = []
output = '{'
for i in range(18):
    data = s[22*i:22*(i+1)]
    output += '0x%s, ' % (data[14:16])
    key += [int(data[14:16], 16)]

output = output[:-2] + '}'
print(output)

def sub_100001B80(a1, a2):
    v4 = 0
    v5 = -1
    if (a1 >= 65):
        v4 = (a1 <= 90)
    if (v4):
        v5 = (a2 + a1 - ord('A')) % 26 + ord('A')
    else:
        v3 = 0
        if (a1 >= ord('a')):
            v3 = a1 <= ord('z')
        if (v3):
            v5 = (a2 + a1 - ord('a')) % 26 + ord('a')
        else:
            v5 = a1

    return int(v5)

print()

# 解密
plaintext = ''
for v in key:
    result = sub_100001B80(v, 2)
    plaintext += chr(result)

print('flag:')
print(plaintext)

运行实例:

ex@Ex:~/test/confused$ python3 main.py 
操作码整理
 1 0000000000F01066000000
 2 F8F230F6C1F01063000000
 3 F8F231F6B6F0106A000000
 4 F8F232F6ABF0106A000000
 5 F8F233F6A0F0106D000000
 6 F8F234F695F01057000000
 7 F8F235F68AF0106D000000
 8 F8F236F67FF01073000000
 9 F8F237F674F01045000000
10 F8F238F669F0106D000000
11 F8F239F65EF01072000000
12 F8F23AF653F01052000000
13 F8F23BF648F01066000000
14 F8F23CF63DF01063000000
15 F8F23DF632F01044000000
16 F8F23EF627F0106A000000
17 F8F23FF61CF01079000000
18 F8F240F611F01065000000
19 F8F241F606F701000000F3
20 F700000000F35DC30F1F84

打印偏移的值
{0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41}

打印对应的秘钥
{0x66, 0x63, 0x6A, 0x6A, 0x6D, 0x57, 0x6D, 0x73, 0x45, 0x6D, 0x72, 0x52, 0x66, 0x63, 0x44, 0x6A, 0x79, 0x65}

flag:
helloYouGotTheFlag

总结

学习要看到核心,抓住本质。