首页
论坛
课程
招聘
[原创] KCTF 2020 Win. 第五题 砸开套娃思路
2020-11-27 23:35 3724

[原创] KCTF 2020 Win. 第五题 砸开套娃思路

HHHso 活跃值
21
2020-11-27 23:35
3724

摘要:本文提供一种从样本弱性VM到人性化阅读反编译代码的思路。反编译器前后经历了两个版本,xdisasm.py和ydisasm.py,这里只提供最终版,也只是大概展示了相关成果,心路历程还需各位道友亲自走上一走。
大致经历过的心路:线性反编译,到启发式反编译,代码分块,流程图可视化,反编译到正编译再到反编译。反编译器简单模仿IDAPython或unicorn或miasm的部分思想。xdisasm.py快速的线性反编译能很好应对展平的第0层vm代码,对后面涉及多个函数,尤其再堆栈平衡追踪上有涉及缺陷,所以使用了启发式思路,即后续的反编译由已经反编译的指令引用决定,这也是多数反编译器的合理思路。

 

如图,程序将将ikey通过hex解码得到bkey(应该是32字节,16个short int)
vmi_t 声明了每层vm使用的参数信息
info_type 前三个vm为6,第四个为3,用来定位后面的pkey所在的表
vmID用来激发各层vm的memset和memcpy函数额外代码。
vmc_ptr指向下一个vm信息项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typedef struct vmi_t{ //size:0x20
  int flag;
  int next;
  void* vm_fptr;
  int vm_zs;
  int init;
  int vmID;
  int rev1;
  int rev2;
} vmi;
 
typedef struct _vmx4{
  unsigne char* pbkey;
  unsigne char* pmem;
  void* vmx_table;
} vmx4;
 
vmi_t vmx_info_table[] = {
  .00 vix0{flag.6,next:=.20,vmcptr,vmzs,0,vmID.0,rev1,rev2}
  .20 vix1{flag.6,next:=.40,vmcptr,vmzs,0,vmID.0,rev1,rev2}
  .40 vix2{flag.6,next:=.60,vmcptr,vmzs,0,vmID.0,rev1,rev2}
  .60 vix3{flag.3,next:=.80,vmcptr,vmzs,0,vmID.0,rev1,rev2}
  .80 vix4{.00hww.pbkey,.04hww.pmem{.00hww.bkLen},&vmx_info_table[0]}
}
pmem{ dword m[]
  .00 bkLen
  .04 m1
  .08 m2 should==0xDE05629C
  .0C
  .10...
}
}

图片描述

 

我们通过IDAPython剥离四个vm代码,用于反编译测试

1
2
3
4
5
import idc
idc.savefile(r'.\403000_0790.bin',0,0x403000,0x0790)
idc.savefile(r'.\403794_1CBD.bin',0,0x403794,0x1CBD)
idc.savefile(r'.\405454_134A.bin',0,0x405454,0x134A)
idc.savefile(r'.\4067A0_0309.bin',0,0x4067A0,0x0309)

通过Hi_start_vm_with_vmInfoType_vmInfoTable函数得到每个vm执行的内存结构,和第0层vm的操作码。

 

图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
{
  esi = vmi.vm_ptr
  ebp = &vmi.vm_ptr+vmi.zone_size*2
  edi = ebp  <----vm_stack_bottom frame_ptr  fr
  py vm_push(0x2B
  px vm_push(0x2B11) <----vm_ret {push r;exit;exit;}//@eax=r
  p3 vm_push(vmi.info_type)   
  p2 vm_push(vmi* vmi_ptr)     
  p1 vm_push(vm_ret)         
  cpu{;esi as pc;ebx as r;edi as frp;ebp as stp;
    opcode = *pc
    switch(opcode){
      case 00h: lea_pb      #r=&stk.frp[+(*(byte*)pc++)]
      case 01h: lea_vb      #r=&stk.frp[-(*(byte*)pc++)] 
      case 02h: lea_pdw     #r=&stk.frp[+(*(dword*)pc)];pc+=4
      case 03h: movzx_imm8  #r=(unsigned int)(*(unsigne byte*)pc++)
      case 04h: mov_imm32   #r=(unsigned int)(*(unsigne  int*)pc++)
      case 06h: jmp         #pc+=(*(unsigne  int*)pc)
      case 07h: call        #push pc+4;pc+=(*(unsigne  int*)pc)  //*(--stp)=pc+4
      case 08h: jz          #r==0? pc+=(*(unsigne  int*)pc):pc+=4
      case 09h: jnz         #r!=0? pc+=(*(unsigne  int*)pc):pc+=4
      case 0Ah: begin       #push frp;stp-=(*(unsigne  int*)pc);pc+=4  //push ebp;mov ebp,esp;sub esp,xxx
      case 0Bh: stk_free    #stp+=(*(unsigne  int*)pc);pc+=4
      case 0Ch: end         #stp=frp;pop frp;ret      //frp=*((unsigned int*)stp++);pc=*((unsigned int*)stp++)
      case 0Dh: read_dw     #r=*r
      case 0Eh: read_b      #r=(unsigned int)(*(byte*)r)
      case 0Fh: write_dw    #*((unsigned int*)stp++)=r
      case 10h: write_b     #*((byte*)stp++)=(byte)r; r=(int)(byte)r 
      case 11h: push        #push r
      case 12h: or          #r|=*(stp++)
      case 13h: xor         #r^=*(stp++)
      case 14h: and         #r&=*(stp++)
      case 15h: eq          #r=(*(stp++)==r)
      case 16h: ne          #r=(*(stp++)!=r)
      case 17h: lt          #r=(*(stp++)< r)
      case 18h: gt          #r=(*(stp++)> r)
      case 1Ah: ge          #r=(*(stp++)>=r)
      case 1Bh: shl         #r=(*(stp++)<<(unsigne byte)r)
      case 1Ch: shr         #r=(*(stp++)>>(unsigne byte)r)
      case 1Dh: add         #r=(*(stp++)+r
      case 1Eh: subr        #r=(*(stp++)-r
      case 1Fh: imul        #r=(*(stp++)*r
      case 20h: divr        #r=(*(stp++)/r
      case 21h: modr        #r=(*(stp++)%r
      case 28h: memset      #size=*(stp++);val=*(byte*)(stp++);dst=*(stp++);r=dst memset(dst,val,size)
      case 2Ah: memcpy      #size=*(stp++);src=*(stp++);       dst=*(stp++);r=dst memset(dst,scr,size)
      case 2Bh: exit        #@eax=*stp #pop edi,esi,ebp,ebx,ecx
    }
  }
}

图片描述

 

第0层vm操作码定义,其他同理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class VmOps0(IntEnum):
  lea_pb = 0
  lea_vb = 1
  lea_pww = 2
  movzx = 3
  mov = 4
  jmp = 6
  call = 7
  jz = 8
  jnz = 9
  begin = 0x0A
  stk_free = 0x0B
  end = 0x0C
  read_ww = 0x0D
  read_b = 0x0E
  write_ww = 0x0F
  write_b = 0x10
  push = 0x11
  _or = 0x12
  xor = 0x13
  _and = 0x14
  ifeq = 0x15
  ifne = 0x16
  iflt = 0x17
  ifgt = 0x18
  ifge = 0x1A
  shl = 0x1B
  shr = 0x1C
  add = 0x1D
  subr = 0x1E
  imul = 0x1F
  divr = 0x20
  modr = 0x21
  memset = 0x28
  memcpy = 0x2A
  exit = 0x2B

有了第0层vm操作码,启用ydisasm.py下图代码,执行下述命令,
需要有unicorn,keystone的。会生成四个文件,其中
(1)fun0.v.asm 是vm指令反汇编文件。
(2)fun0.gv是vm指令反汇编代码的控制流程图dot,可以通过graphviz的的dot.exe编译生成图片等。如附件的fun0.png
(3)fun0.bin是通过keystone编译的代码,结果类似(4)。
(4)fun0.asm 是yasm(即以前的nasm)可以编译的代码。

 

其中(3)和(4)是快速打开后续vm的关键,基本原理是用x86汇编,维持栈平衡的各种运算条件下,替换vm指令,然后通过keystone编译为机器码或通过yasm编译器编译为机器码,然后通过IDA反编译为高级伪码。

1
python ydisasm.py

图片描述

 

通过yasm将fun0.asm编译为coff文件

1
yasm.exe -a x86 -f coff -o fun0.o fun0.asm

通过IDA反编译fun0.o,如图,这与vm0的代码就类似。
我们可以看到memset(dst,0x33333333,100)在vmID.1层触发的动作
实际效果是memset(dst,1,100),且dst赋值给了pkey位置

 

图片描述

 

图片描述

 

有了高级伪码,我们可以轻易得到下一层vm的操作码

 

图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class VmOps1(IntEnum):
  lea_pb  = 0x1B
  lea_vb  = 0x07
  lea_pww  = 0x05
  movzx  = 0x24
  mov  = 0x22
  jmp  = 0x20
  call  = 0x00
  jz  = 0x0E
  jnz  = 0x06
  begin  = 0x09
  stk_free  = 0x0A
  end  = 0x01
  read_ww  = 0x13
  read_b  = 0x1C
  write_ww  = 0x02
  write_b  = 0x0D
  push  = 0x29
  _or  = 0x0C
  xor  =  0x11
  _and  =  0x1D
  ifeq  = 0x2A
  ifne  = 0x27
  iflt  = 0x15
  ifgt  = 0x1A
  ifge  = 0x28
  shl  = 0x10
  shr  = 0x2B
  add  = 3
  subr  = 0x17
  imul  = 0x08
  divr  = 0x21
  modr  = 0x26
  memset  = 0x0B
  memcpy  = 0x0F
  exit  = 0x1F

同理,ydiasm.py启用fun1相关部分代码,根据选择生成asm,bin,gv等

 

图片描述

 

依次类推,可以得到各vm的可阅读代码,举例如下
各vm中的memset等敏感操作,从fun3.o开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
nt __cdecl xFun_00060000(int *p_last_vi, int a2)
{
  BOOL v3; // eax
  BOOL v4; // eax
  char lv_len; // [esp+0h] [ebp-2Ch]
  vmi *lv_vit; // [esp+4h] [ebp-28h]
  char *v7; // [esp+8h] [ebp-24h]
  char v8; // [esp+Ch] [ebp-20h]
  int *lv_vm3_ea; // [esp+10h] [ebp-1Ch]
  int *lv_vm2_ea; // [esp+14h] [ebp-18h]
  _DWORD *lv_vm1_ea; // [esp+18h] [ebp-14h]
  int *lv_vm0_ea; // [esp+1Ch] [ebp-10h]
  int *lv_rLen; // [esp+20h] [ebp-Ch]
  int *lv_pbkey; // [esp+24h] [ebp-8h]
  int v15; // [esp+28h] [ebp-4h]
 
  v15 = 0;
  if ( a2 != 3 )
    return 0;
  lv_pbkey = (int *)*p_last_vi;
  lv_rLen = (int *)p_last_vi[1];
  lv_vit = (vmi *)p_last_vi[2];
  lv_vm0_ea = (int *)lv_vit->vm_fptr;
  lv_vit = (vmi *)lv_vit->next;
  lv_vm1_ea = lv_vit->vm_fptr;
  lv_vit = (vmi *)lv_vit->next;
  lv_vm2_ea = (int *)lv_vit->vm_fptr;
  lv_vm3_ea = *(int **)(lv_vit->next + 8);
  v7 = &v8;
  lv_vit = (vmi *)lv_rLen;
  *(_DWORD *)&v8 = lv_rLen[2];
  lv_rLen[2] = 0;
  v7 = *(char **)&v8;
  if ( *(_DWORD *)&v8 != 0xDE05629C )
    return v15;
  lv_rLen[5] = 15;
  lv_vm1_ea[304] = 65008;
  *(_DWORD *)((char *)lv_vm1_ea + 0x4C6) = 0xFDF3;
  *(_DWORD *)((char *)lv_vm1_ea + 0x1279) = 1;
  *(int *)((char *)lv_vm2_ea + 0xA2D) = 1;
  lv_vm0_ea[339] = 0;
  v7 = &lv_len;
  lv_vit = (vmi *)lv_rLen;
  *(_DWORD *)&lv_len = *lv_rLen;
  v7 = (char *)(lv_rLen + 1024);
  lv_vit = (vmi *)(xFun_00060000 + 1);
  memset(lv_rLen + 1024, 1, 0x64u);
  lv_vm1_ea[0x130] = 0xCCCCCCCC;
  lv_vm2_ea = (_DWORD *)((char *)lv_vm1_ea + 0x4C6);
  *lv_vm2_ea = 0xEEEEEEEE;
  lv_vm2_ea = (int *)((char *)lv_vm2_ea + 0xA2D);
  *lv_vm2_ea = 0x22222200;
  lv_vm0_ea[0x153] = 0x33333333;
  lv_vm2_ea = *(int **)&lv_len;
  if ( *(_DWORD *)&lv_len == 0x38 )
  {
    lv_rLen[5] = 30;
    lv_vm2_ea = lv_rLen;
    lv_vm2_ea = (int *)lv_rLen[2];
    v3 = lv_vm2_ea == (int *)0x4A752000;
    if ( lv_vm2_ea == (int *)0x4A752000 )
    {
      lv_vm2_ea = lv_rLen;
      lv_vm2_ea = (int *)lv_rLen[4];
      v3 = lv_vm2_ea == (int *)0xD540;
    }
    if ( v3 )
    {
      lv_rLen[5] = 0x2D;
      lv_vm2_ea = lv_pbkey;
      lv_vm2_ea = (int *)lv_pbkey[5];
      v4 = lv_vm2_ea == (int *)0x204BCEF1;
      if ( lv_vm2_ea == (int *)0x204BCEF1 )
      {
        lv_vm2_ea = lv_pbkey;
        lv_vm2_ea = (int *)lv_pbkey[6];
        v4 = lv_vm2_ea == (int *)0x7AD17AD1;
      }
      if ( v4 )
      {
        lv_vm2_ea = lv_pbkey;
        lv_vm2_ea = (int *)lv_pbkey[7];
        v4 = lv_vm2_ea == (int *)0x514F89F9;
      }
      if ( v4 )
      {
        lv_rLen[5] = 60;
        lv_vm2_ea = lv_rLen;
        lv_vm2_ea = (int *)*((unsigned __int8 *)lv_rLen + 0xC);
        if ( lv_vm2_ea == (int *)((char *)xFun_00060000 + 1) )
        {
          lv_rLen[5] = 90;
          *((_BYTE *)lv_rLen + 12) = 0;
          lv_vm2_ea = lv_rLen + 0x400;
          lv_vm3_ea = (_DWORD *)(xFun_00060000 + 1);
          memset(lv_rLen + 0x400, 1, 0x640u);
          *(_DWORD *)((char *)lv_vm1_ea + 0x1279) = 0x22222200;
          lv_rLen = &v15;
          lv_vm0_ea = &v15;
          v15 = (unsigned __int8)p_last_vi;
        }
      }
    }
    lv_rLen += 2;
    *lv_rLen = 0;
    lv_rLen += 3;
    *lv_rLen = 0;
    lv_rLen += 4;
    *lv_rLen = 0;
    return v15;
  }
  lv_vm2_ea = (int *)&v8;
  for ( *(_DWORD *)&v8 = 0; ; lv_vm2_ea = *(int **)&v8 )
  {
    lv_vm2_ea = *(int **)&v8;
    if ( *(_DWORD *)&v8 >= 20 )
      break;
    lv_vm2_ea = (int *)((char *)lv_rLen + *(_DWORD *)&v8);
    lv_vm3_ea = lv_rLen;
    lv_vm3_ea = (int *)*((unsigned __int8 *)lv_rLen + *(_DWORD *)&v8);
    *(_BYTE *)lv_vm2_ea = (unsigned __int8)lv_vm3_ea ^ v8;
    lv_vm3_ea = (int *)(*(_DWORD *)&v8)++;
  }
  return 0;
}

除了对底层代码段一些数据的比对读写,看下fun3.o的memset触发点,
阅读IDA的高级伪码如果有不清楚的地方,估计还得到汇编代码取看一眼,如下
fun3.o的开头位置,没有任何运算就进行了比较,肯定不对劲,且m2位置传入是零。

 

图片描述

 

图片描述

 

回到汇编我们看到F5伪码过滤掉了memcpy(pbkey,pbkey,0)操作,这回触发底层运算,其再fun2.o触发的操作如图,pmem+8即上图的m2

 

图片描述

 

图片描述

 

memcpy交给fun1.so继续

 

图片描述

 

fun0.o的memcpy没有额外操作

 

图片描述

 

1
2
3
4
5
6
7
8
9
10
m2 = *(dword*)pbkey  //m2为四字节
m2 = fun2._crc_0xEB31D82E(0xffffffff,&m2)
m2 = fun2._crc_0xEB31D82E(0xffffffff,&m2)
m2 = fun2._crc_0xEB31D82E(0xffffffff,&m2)
m2 = fun2._crc_0xEB31D82E(0xffffffff,&m2)
m2 = fun1._crc_0xEB31D82E(0xffffffff,&m2)
m2 = fun1._crc_0xEB31D82E(0xffffffff,&m2)
m2 = fun1._crc_0xEB31D82E(0xffffffff,&m2)
m2 = fun1._crc_0xEB31D82E(0xffffffff,&m2)
m2 应等于 0xDE05629C

再回到fun3.o的后续代码。图下,途中对vmcptr1即fun1代码做修改,
位置是hex(0x130*4)=0x4C0位置,

 

图片描述

 

我们看下fun1.v.asm(即vm指令反汇编,不是x86反汇编fun1.asm)
如图,
图片描述
实际是对fun1.o 的memset触发的代码的变动

 

图片描述

 

图片描述

 

图片描述

 

图片描述
其会交给下一层的fun2.o的memset处理,同理,会先触发fun1.so的memset操作(再往下是fun0.o),再调用后续fun2.so运算函数处理,如图
图片描述
再fun1.o中,如图,对于大小100的操作,其会先运算,再调用memset,这时触发fun0.so层的memset;
图片描述
fun1.o对于大小0x640的操作,会再memset前后加入运算。同理,memset会触发fun0.o的memset
图片描述
fun0.o的情况如下,再特定参数如(dst,0x33333333,100)会先做一些操作再迁移数据,这时fun0.o触发的memset就是主程序的memset操作。
图片描述
fun3.png 原始vm指令流程图
生成命令

1
c:Graphviz\bin\dot.exe -Tpng -o fun3.png fun3.gv

图片描述

 

附件内容

1
2
3
4
5
403000_0790.bin   403794_1CBD.bin   405454_134A.bin   4067A0_0309.bin   Butterfly.exe     fun0.asm
fun0.bin          fun0.gv           fun0.o            fun0.png          fun0.v.asm        fun1.asm          fun1.bin          fun1.gv
fun1.o            fun1.png          fun1.v.asm        fun2.asm          fun2.bin          fun2.gv           fun2.o            fun2.png
fun2.v.asm        fun3.asm          fun3.bin          fun3.gv           fun3.o            fun3.png          fun3.v.asm        KCTF05.rar
ydisasm.py

[看雪官方培训] Unicorn Trace还原Ollvm算法!《安卓高级研修班》2021年6月班火热招生!!

最后于 2020-11-28 16:47 被HHHso编辑 ,原因:
上传的附件:
收藏
点赞3
打赏
分享
打赏 + 2.00
打赏次数 1 金额 + 2.00
 
赞赏  kanxue   +2.00 2020/11/28 精品文章~
最新回复 (3)
雪    币: 9347
活跃值: 活跃值 (1522)
能力值: ( LV15,RANK:1960 )
在线值:
发帖
回帖
粉丝
ccfer 活跃值 15 2020-11-28 12:52
2
0

强大强大强大

最后于 2020-11-28 12:58 被ccfer编辑 ,原因:
雪    币: 15620
活跃值: 活跃值 (16308)
能力值: (RANK:75 )
在线值:
发帖
回帖
粉丝
Editor 活跃值 2020-11-28 13:40
3
0
佩服!
雪    币: 1642
活跃值: 活跃值 (451)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Dyingchen 活跃值 2020-11-29 10:41
4
0
强啊 ,关注了 
游客
登录 | 注册 方可回帖
返回