HAI DOMO. This post is for 武蔵野 Advent Calendar 2017 and also for CTF Advent Calendar 2017.

Introduction

In May this year, I participated in DEF CON CTF Qualifier 2017 as a member of a certain 武蔵野-related team. Actually, I’m not a top-tier CTF player, but I did my best and solved 4 challenges:

  • crackme1
  • beatmeonthedl
  • enlightenment
  • Pepperidge Farm

Write-ups already exist except for Pepperidge Farm. So I decided to write about it. FYI: The binaries are available at legitbs/quals-2017.

Pepperidge Farm

Pepperidge Farm is categorized into Reverse Engineering. The problem statement is below:

Remember when the first CTF was run with a custom architecture? Pepperidge Farm remembers:
https://github.com/JonathanSalwan/VMNDH-2k12

pepperidge-farm_edb4ad2f103a9efde038346d2cd86a1e.quals.shallweplayaga.me:2012

Files
https://2017.notmalware.ru/8a45cf7171e89594cfd1d51671fef0bec3f24d9d/pepperidge_farm

It seems like a keygenning challenge on the custom virtual machine–VMNDH-2k12.

1
2
3
$ ./vmndh -file pepperidge_farm
Enter your registration code: 31337
Code does not match any known registered users

VMNDH-2k12

VMNDH-2k12 is the VM built for Nuit du Hack CTF Quals 2012 as its name suggests. The architecture is described in shell-storm | Useless emulator for fun (VMNDH-2k12). This VM parses given serialized binary and repeats fetch, decode and execution.

Writing IDA loader/processor module is a common way to analyze VM-based obfuscated binary. The modules for VMNDH-2k12 and for modified version VMNDH-2k13 already exist:

Note that when you try to solve this challenge with above-mentioned processor modules with IDA Pro 7.0, the backward-compatibility issue will occur. For example, you have to change self.regFirstSreg toself.reg_first_sreg in a module.

Also, the Binary Ninja plugin has been released after quals:

But in this post, I describe a solution without both IDA Pro and Binary Ninja. Because VMNDH-2k12 is open-sourced and easy to modify.

Surface Analysis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ ./vmndh -debug -file pepperidge_farm
[Console]#> help
<enter> Execute next instruction
run Run program
bp <addr> Set breakpoint
info bp Display info breakpoint
info reg Display registers
show sp Display SP memory
show pc Display PC memory
dis <addr>:<size> Disassembly X bytes from ADDR
x/x <addr>:<size> Print X bytes from ADDR
x/s <addr> Print string addr
set reg=value Set value in register
syscall Execute 'syscall' instruction
help Display this help
quit Quit console debugging
[Console]#> dis
Error: Set addr between 0x8000 and 0x882a
[Console]#> dis 0x8000:82a
0x8000: jmpl 0x0720
0x8003: push r2
0x8007: .byte 0x00
0x8008: nop
...

Yes, the VM has own debugger and disassembler.

Modifying the Disassembler

However, there are pitfalls here.

Because it is a unique architecture, the destination of the control flow instructions are different from it shown on the disassembly dump. For example, take a look at src_vm/op_call.c:

1
2
3
4
5
6
7
8
9
10
11
12
static void op_call_dir16(void)
{
uint16_t operande;
operande = *(uint16_t *)(core.memory.vmem + core.memory.pc + 2);
if (core.flagmode.noverbose == 0)
fprintf(stdout, "%s0x%4.x%s > call 0x%.2x\n", RED, core.memory.pc, ENDC, operande);
core.memory.sp -= 2; /* sub sp */
*(uint16_t *)(core.memory.vmem + core.memory.sp) = core.memory.pc + 4;
core.memory.pc = core.memory.pc + operande + 4;
}

Acording to this, I modified the dissassembler in src_vm/disass.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@@ -532,6 +537,7 @@
uint16_t operande;
uint8_t reg;
char flag;
+ uint16_t dst;
flag = *(core.memory.vmem + addr + 1);
switch (flag)
@@ -543,7 +549,8 @@
case OP_FLAG_DIRECT16:
operande = *(uint16_t *)(core.memory.vmem + addr + 2);
- fprintf(stdout, "%s0x%.4x%s: call 0x%.4x\n", RED, addr, ENDC, operande);
+ dst = addr + operande + 4;
+ fprintf(stdout, "%s0x%.4x%s: call 0x%.4x\n", RED, addr, ENDC, dst);
return (addr + 4);
default:

This makes it possible to correctly display the address of the call destination in the disassembly dump. In addition, jump instructions need to be modified. In the case of jnz:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@@ -463,10 +470,11 @@
static uint16_t dis_jnz(uint16_t addr)
{
- uint16_t operande;
+ uint16_t operande, dst;
operande = *(uint16_t *)(core.memory.vmem + addr + 1);
- fprintf(stdout, "%s0x%.4x%s: jnz 0x%.4x\n", RED, addr, ENDC, operande);
+ dst = addr + operande + 3;
+ fprintf(stdout, "%s0x%.4x%s: jnz 0x%.4x\n", RED, addr, ENDC, dst);
return (addr + 3);
}

Herewith,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0x86a1: jnz 0x0076
0x86a4: mov r0, r2
0x86a8: call 0xfdc5
0x86ac: test r0, r0
0x86af: jnz 0x0068
0x86b2: mov r0, r2
0x86b6: call 0xfddf
0x86ba: test r0, r0
0x86bd: jnz 0x005a
0x86c0: mov r0, r2
0x86c4: call 0xfe17
0x86c8: test r0, r0
0x86cb: jnz 0x004c
0x86ce: mov r0, r2
0x86d2: call 0xfe2c
0x86d6: test r0, r0
...

becomes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0x86a1: jnz 0x871a
0x86a4: mov r0, r2
0x86a8: call 0x8471
0x86ac: test r0, r0
0x86af: jnz 0x871a
0x86b2: mov r0, r2
0x86b6: call 0x8499
0x86ba: test r0, r0
0x86bd: jnz 0x871a
0x86c0: mov r0, r2
0x86c4: call 0x84df
0x86c8: test r0, r0
0x86cb: jnz 0x871a
0x86ce: mov r0, r2
0x86d2: call 0x8502
0x86d6: test r0, r0
...

Good.

Also, after 0x8759 it looks like a data section.

1
2
3
4
5
6
0x8759: .byte 0x45 (E)
0x875a: .byte 0x6e (n)
0x875b: .byte 0x74 (t)
0x875c: .byte 0x65 (e)
0x875d: .byte 0x72 (r)
...

However, parts such as 0x87e8 are misinterpreted as codes.

1
0x87e8: inc r82

The data section was not obfuscated.

1
2
3
4
5
00007e0: 6c61 6700 3ce3 5a9e 9113 8c05 e42e 0a52 lag.<.Z........R
00007f0: 3dd8 7cf5 9f4b 9f06 d7a8 e9a0 a636 a649 =.|..K.......6.I
0000800: 3136 9308 eb70 ebbf df28 500d 7be8 96fa 16...p...(P.{...
0000810: e8b7 a5df c24d f0b8 3b78 6272 b748 1697 .....M..;xbr.H..
0000820: f019 2d6b 085b 9f94 4987 e624 3752 efb8 ..-k.[..I..$7R..

So I gave first aid to src_vm/disass.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@@ -322,8 +322,15 @@
static uint16_t dis_inc(uint16_t addr)
{
uint8_t operande;
+ char OP, OP2;
+ OP = *(core.memory.vmem + addr);
+ OP2 = *(core.memory.vmem + addr + 1);
operande = *(core.memory.vmem + addr + 1);
+ if(addr > 0x8759)
+ fprintf(stdout, "%s0x%.4x%s: [DEBUG] .byte 0x%.2x\n", RED, addr, ENDC, (unsigned char)OP);
+ fprintf(stdout, "%s0x%.4x%s: [DEBUG] .byte 0x%.2x\n", RED, addr + 1, ENDC, (unsigned char)OP2);
+
fprintf(stdout, "%s0x%.4x%s: inc r%d\n", RED, addr, ENDC, operande);
return (addr + 2);

Awful… who cares?

1
2
3
4
0x87e8: [DEBUG] .byte 0x0a
0x87e9: [DEBUG] .byte 0x52
0x87e8: inc r82
0x87ea: .byte 0x3d (=)

First Attempt With KLEE

This is a failure case.

As we have seen so far, VMNDH-2k12 is open-sourced. So I tried to solve the challenge with source code-based symbolic execution tool–KLEE.

I modified src_vm/syscall_write.c for assertion:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@@ -18,6 +18,8 @@
*/
#include "vmndh.h"
+#include <klee/klee.h>
+#include <string.h>
void syscall_write(void)
{
@@ -32,7 +34,12 @@
if (core.flagmode.noverbose == 0)
write(1, txt, strlen(txt));
+
+ if (strstr(arg2, "Thank") != NULL)
+ klee_assert(0);
+
core.regs.r0 = write(arg1, arg2, arg3);
+
if (core.flagmode.noverbose == 0)
write(1, "\n", 1);
}

Here is a modified Makefile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@@ -17,14 +17,15 @@
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
##
-DEBUG = no
+DEBUG = yes
RM = rm -f
INCLUDE = ./includes
SRC_DIR = ./src_vm
+KLEE_PATH = ../klee_src/include
NAME = vmndh
ifeq ($(DEBUG),yes)
- CFLAGS = -c -ggdb3 -Wextra -Wall -D _BSD_SOURCE -I$(INCLUDE)
+ CFLAGS = -c -ggdb3 -Wextra -Wall -D _BSD_SOURCE -I$(INCLUDE) -I$(KLEE_PATH) -emit-llvm -g
LDFLAGS =
CC = clang
else

I’d left all of it to KLEE and get to bed…

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
$ make
$ llvm-link src_vm/*.o -o vmndh.bc
$ klee --optimize --emit-all-errors --libc=uclibc --write-smt2s --posix-runtime vmndh.bc -file pepperidge_farm -sym-stdin 65
KLEE: NOTE: Using klee-uclibc : /home/klee/klee_build/klee/Release+Debug+Asserts/lib/klee-uclibc.bca
KLEE: NOTE: Using model: /home/klee/klee_build/klee/Release+Debug+Asserts/lib/libkleeRuntimePOSIX.bca
KLEE: output directory is "/home/klee/VMNDH-2k12/klee-out-1"
KLEE: Using STP solver backend
KLEE: WARNING ONCE: function "__user_main" has inline asm
KLEE: WARNING ONCE: function "syscall_accept" has inline asm
KLEE: WARNING ONCE: function "syscall_bind" has inline asm
KLEE: WARNING ONCE: function "syscall_listen" has inline asm
KLEE: WARNING ONCE: function "syscall_recv" has inline asm
KLEE: WARNING ONCE: function "syscall_send" has inline asm
KLEE: WARNING ONCE: function "syscall_socket" has inline asm
KLEE: WARNING ONCE: calling external: syscall(16, 0, 21505, 44258832) at /home/klee/klee_src/runtime/POSIX/fd.c:1044
KLEE: WARNING ONCE: Alignment of memory from call "malloc" is not modelled. Using alignment of 8.
Enter your registration code: KLEE: WARNING ONCE: calling external: vprintf(57126208, 46345808) at /home/klee/klee_build/klee-uclibc/libc/stdio/fprintf.c:36
[SYSCALL output]: 64
KLEE: WARNING ONCE: skipping fork (memory cap exceeded)
...
KLEE: WARNING: killing 175 states (over memory cap)
Code does not match any known registered users
Code does not match any known registered users
Code does not match any known registered users
Code does not match any known registered users
Code does not match any known registered users
Code does not match any known registered users
Code does not match any known registered users
Code does not match any known registered users
Code does not match any known registered users
Code does not match any known registered users
Code does not match any known registered users
...
Code does not match any known registered users
KLEE: done: total instructions = 5117896332
KLEE: done: completed paths = 81092
KLEE: done: generated tests = 81092

… It’s not going to be easy.

I also wrote solver with angr. Which symbolizes stdin, but… let’s not talk about it.

An example of insufficient SMTLIB2 representations is:

1
2
3
4
5
6
7
(set-logic QF_AUFBV )
(declare-fun model_version () (Array (_ BitVec 32) (_ BitVec 8) ) )
(declare-fun stdin () (Array (_ BitVec 32) (_ BitVec 8) ) )
(declare-fun stdin-stat () (Array (_ BitVec 32) (_ BitVec 8) ) )
(assert (let ( (?B1 (select stdin (_ bv25 32) ) ) (?B2 (select stdin (_ bv31 32) ) ) (?B3 (select stdin (_ bv18 32) ) ) (?B4 (select stdin (_ bv40 32) ) ) (?B5 (select stdin (_ bv36 32) ) ) (?B6 (select stdin (_ bv23 32) ) ) (?B7 (select stdin (_ bv27 32) ) ) (?B8 (select stdin (_ bv33 32) ) ) (?B9 (select stdin (_ bv6 32) ) ) (?B10 (select stdin (_ bv8 32) ) ) ) (let ( (?B13 ((_ zero_extend 16) ((_ zero_extend 8) ?B7 ) ) ) (?B14 ((_ zero_extend 16) ((_ zero_extend 8) ?B8 ) ) ) (?B12 ((_ zero_extend 16) ((_ zero_extend 8) ?B2 ) ) ) (?B11 ((_ zero_extend 16) ((_ zero_extend 8) ?B1 ) ) ) ) (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (and (= false (= (_ bv0 64) (bvand (concat (select stdin-stat (_ bv15 32) ) (concat (select stdin-stat (_ bv14 32) ) (concat (select stdin-stat (_ bv13 32) ) (concat (select stdin-stat (_ bv12 32) ) (concat (select stdin-stat (_ bv11 32) ) (concat (select stdin-stat (_ bv10 32) ) (concat (select stdin-stat (_ bv9 32) ) (select stdin-stat (_ bv8 32) ) ) ) ) ) ) ) ) (_ bv2147483647 64) ) ) ) (bvult (concat (select stdin-stat (_ bv63 32) ) (concat (select stdin-stat (_ bv62 32) ) (concat (select stdin-stat (_ bv61 32) ) (concat (select stdin-stat (_ bv60 32) ) (concat (select stdin-stat (_ bv59 32) ) (concat (select stdin-stat (_ bv58 32) ) (concat (select stdin-stat (_ bv57 32) ) (select stdin-stat (_ bv56 32) ) ) ) ) ) ) ) ) (_ bv65536 64) ) ) (= (_ bv1 32) (concat (select model_version (_ bv3 32) ) (concat (select model_version (_ bv2 32) ) (concat (select model_version (_ bv1 32) ) (select model_version (_ bv0 32) ) ) ) ) ) ) (= (_ bv48 8) (select stdin (_ bv0 32) ) ) ) (= (_ bv48 8) (select stdin (_ bv1 32) ) ) ) (= (_ bv57 8) (select stdin (_ bv2 32) ) ) ) (= (_ bv70 8) (select stdin (_ bv3 32) ) ) ) (= (_ bv48 8) (select stdin (_ bv4 32) ) ) ) (= (_ bv48 8) (select stdin (_ bv5 32) ) ) ) (= false (= (_ bv48 8) ?B9 ) ) ) (= false (bvult (_ bv48 32) ((_ zero_extend 16) ((_ zero_extend 8) ?B9 ) ) ) ) ) (= false (= (_ bv48 8) ?B10 ) ) ) (= false (bvult (_ bv48 32) ((_ zero_extend 16) ((_ zero_extend 8) ?B10 ) ) ) ) ) (= (_ bv48 8) (select stdin (_ bv12 32) ) ) ) (= (_ bv48 8) (select stdin (_ bv13 32) ) ) ) (= (_ bv48 8) (select stdin (_ bv14 32) ) ) ) (= (_ bv57 8) (select stdin (_ bv15 32) ) ) ) (= (_ bv48 8) (select stdin (_ bv16 32) ) ) ) (= (_ bv48 8) (select stdin (_ bv17 32) ) ) ) (= false (= (_ bv48 8) ?B3 ) ) ) (= false (bvult (_ bv48 32) ((_ zero_extend 16) ((_ zero_extend 8) ?B3 ) ) ) ) ) (= (_ bv48 8) (select stdin (_ bv20 32) ) ) ) (= (_ bv48 8) (select stdin (_ bv21 32) ) ) ) (= (_ bv57 8) (select stdin (_ bv22 32) ) ) ) (= false (= (_ bv48 8) ?B6 ) ) ) (= false (bvult (_ bv48 32) ((_ zero_extend 16) ((_ zero_extend 8) ?B6 ) ) ) ) ) (= (_ bv48 8) (select stdin (_ bv24 32) ) ) ) (= false (= (_ bv48 8) ?B1 ) ) ) (bvult (_ bv48 32) ?B11 ) ) (= false (= (_ bv57 8) ?B1 ) ) ) (= false (bvult (_ bv57 32) ?B11 ) ) ) (= (_ bv48 8) (select stdin (_ bv26 32) ) ) ) (= false (= (_ bv48 8) ?B7 ) ) ) (bvult (_ bv48 32) ?B13 ) ) (= false (= (_ bv57 8) ?B7 ) ) ) (= false (bvult (_ bv57 32) ?B13 ) ) ) (= (_ bv48 8) (select stdin (_ bv28 32) ) ) ) (= (_ bv48 8) (select stdin (_ bv29 32) ) ) ) (= (_ bv48 8) (select stdin (_ bv30 32) ) ) ) (= false (= (_ bv48 8) ?B2 ) ) ) (bvult (_ bv48 32) ?B12 ) ) (= false (= (_ bv57 8) ?B2 ) ) ) (= false (bvult (_ bv57 32) ?B12 ) ) ) (= (_ bv48 8) (select stdin (_ bv32 32) ) ) ) (= false (= (_ bv48 8) ?B8 ) ) ) (bvult (_ bv48 32) ?B14 ) ) (= false (= (_ bv57 8) ?B8 ) ) ) (bvult (_ bv57 32) ?B14 ) ) (= false (= (_ bv70 8) ?B8 ) ) ) (bvult (_ bv70 32) ?B14 ) ) (= false (= (_ bv48 8) ?B5 ) ) ) (= false (bvult (_ bv48 32) ((_ zero_extend 16) ((_ zero_extend 8) ?B5 ) ) ) ) ) (= false (= (_ bv48 8) ?B4 ) ) ) (= false (bvult (_ bv48 32) ((_ zero_extend 16) ((_ zero_extend 8) ?B4 ) ) ) ) ) ) ) )
(check-sat)
(exit)

Solution With Z3

Since there is no choice, I read all the disassembly. After some twists and turn, I realized that:

  • Pepperidge Farm checks character codes against transformed 0x20 bytes values.
  • 0x8247(x, y) returns x * 100 + y.

Now Z3 time. For example, subroutine 0x8269:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0x8269: push r1
0x826d: pop r2
0x826f: mov r7, r0
0x8273: addb r0, #00
0x8277: call 0x8247
0x827b: mov r1, r0
0x827f: mov r0, r7
0x8283: addb r0, #14
0x8287: call 0x8247
0x828b: mov r2, r0
0x828f: movl r0, #0x87ec
0x8294: call 0x8247
0x8298: xorl r1, #4936
0x829d: add r1, r2
0x82a1: xor r0, r1
0x82a5: pop r2
0x82a7: pop r1
0x82a9: ret
...
0x87ec: .byte 0x7c (|)
0x87ed: .byte 0xf5

becomes:

1
s.add(((sub_8247(0) ^ 0x4936) + sub_8247(0x14)) ^ 0x7cf5 == 0)

This is satisfiable. But not enough. We need to add rest of constraints.

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
#!/usr/bin/env python
#coding: utf-8
import telnetlib, sys
from z3 import *
REMOTE = len(sys.argv) >= 2 and sys.argv[1] == 'r'
if REMOTE:
host = 'pepperidge-farm_edb4ad2f103a9efde038346d2cd86a1e.quals.shallweplayaga.me'
port = 2012
else:
host = '127.0.0.1'
port = 4000
values = [BitVec('x%d' % i, 8) for i in range(0x20)]
def sub_8247(x): return ZeroExt(8, values[x]) << 8 | ZeroExt(8, values[x + 1])
s = Solver()
# sub_8269
s.add(((sub_8247(0) ^ 0x4936) + sub_8247(0x14)) ^ 0x7cf5 == 0)
# sub_82aa
s.add(sub_8247(6) * sub_8247(8) * (sub_8247(2) ^ 0xfdf) ^ 0x3dd8 == 0)
# sub_8304
s.add(((sub_8247(4) ^ 0xc7df) + sub_8247(0xe) * sub_8247(0xc)) ^ 0xeb70 == 0)
# sub_835e
s.add(((sub_8247(6) ^ 0xc5db) + 0x14aa) ^ 0x500d == 0)
# sub_838b
s.add(sub_8247(8) * sub_8247(0x1e) ^ 0x7be8 == 0)
# sub_83c7
s.add((sub_8247(0xa) + sub_8247(6) + sub_8247(0xC)) ^ 0xdf28 == 0)
# sub_841c
s.add(((sub_8247(0xC) + 0x5432) | 0x3008) ^ 0x3b78 == 0)
# sub_8449
s.add((sub_8247(0xE) + 0x1212) ^ 0x1697 == 0)
# sub_8471
s.add((sub_8247(0x10) ^ 0x8703) ^ 0x3136 == 0)
# sub_8499
s.add(((sub_8247(0x12) + 0x4004) + (sub_8247(0x14) ^ 0xA52)) ^ 0x6272 == 0)
# sub_84df
s.add()
# sub_8502
s.add((sub_8247(0x16) + sub_8247(0x10)) ^ 0x9308 == 0)
# sub_853e
s.add(sub_8247(0x18) ^ 0x085b == 0)
# sub_8561
s.add(((sub_8247(0x1a) ^ 0x863c) + 0x1234) ^ 0x9113 == 0)
# sub_858e
s.add((sub_8247(0x1c) + sub_8247(8) + sub_8247(0x12)) ^ 0xf0b8 == 0)
# sub_85e3
s.add(((sub_8247(0x1e) & 0x0f00) + sub_8247(0)) ^ 0x9f94 == 0)
print s.check()
print s.model()
input = ''
for i in range(0x20):
try:
input += '%02x' % s.model()[values[i]].as_long()
except:
input += '??'
input = input.upper()
print input
t = telnetlib.Telnet(host, port)
t.write(input + '\n')
t.interact()

Even with halfway constraints, the process proceeds. So inscount with Pin or other dynamic binary instrumentation tools might be helpful.

Finally I got:

1
2
3
4
Enter your registration code: 90940C6017E5FEB82083F932E73E0485B635796DA353DCD3085BF8E356C87FF8
Thank you for your patronage!
Your username is: pepperidge
This is the flag: th0s3_wh0_l0ok_0nly_to_th3_p4stur3_ar3_cert4in_t0_miss_th3_futur3

The conclusive SMTLIB2 representation is:

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
; benchmark
(set-info :status unknown)
(set-logic QF_AUFBV)
(declare-fun x1 () (_ BitVec 8))
(declare-fun x0 () (_ BitVec 8))
(declare-fun x31 () (_ BitVec 8))
(declare-fun x30 () (_ BitVec 8))
(declare-fun x19 () (_ BitVec 8))
(declare-fun x18 () (_ BitVec 8))
(declare-fun x9 () (_ BitVec 8))
(declare-fun x8 () (_ BitVec 8))
(declare-fun x29 () (_ BitVec 8))
(declare-fun x28 () (_ BitVec 8))
(declare-fun x27 () (_ BitVec 8))
(declare-fun x26 () (_ BitVec 8))
(declare-fun x25 () (_ BitVec 8))
(declare-fun x24 () (_ BitVec 8))
(declare-fun x17 () (_ BitVec 8))
(declare-fun x16 () (_ BitVec 8))
(declare-fun x23 () (_ BitVec 8))
(declare-fun x22 () (_ BitVec 8))
(declare-fun x21 () (_ BitVec 8))
(declare-fun x20 () (_ BitVec 8))
(declare-fun x15 () (_ BitVec 8))
(declare-fun x14 () (_ BitVec 8))
(declare-fun x13 () (_ BitVec 8))
(declare-fun x12 () (_ BitVec 8))
(declare-fun x7 () (_ BitVec 8))
(declare-fun x6 () (_ BitVec 8))
(declare-fun x11 () (_ BitVec 8))
(declare-fun x10 () (_ BitVec 8))
(declare-fun x5 () (_ BitVec 8))
(declare-fun x4 () (_ BitVec 8))
(declare-fun x3 () (_ BitVec 8))
(declare-fun x2 () (_ BitVec 8))
(assert
(let ((?x206 (bvand (bvor (bvshl ((_ zero_extend 8) x30) (_ bv8 16)) ((_ zero_extend 8) x31)) (_ bv3840 16))))
(let ((?x209 (bvxor (bvadd ?x206 (bvor (bvshl ((_ zero_extend 8) x0) (_ bv8 16)) ((_ zero_extend 8) x1))) (_ bv40852 16))))
(let (($x210 (= ?x209 (_ bv0 16))))
(let ((?x82 (bvor (bvshl ((_ zero_extend 8) x8) (_ bv8 16)) ((_ zero_extend 8) x9))))
(let ((?x200 (bvadd (bvor (bvshl ((_ zero_extend 8) x28) (_ bv8 16)) ((_ zero_extend 8) x29)) ?x82)))
(let ((?x201 (bvadd ?x200 (bvor (bvshl ((_ zero_extend 8) x18) (_ bv8 16)) ((_ zero_extend 8) x19)))))
(let (($x204 (= (bvxor ?x201 (_ bv61624 16)) (_ bv0 16))))
(let ((?x190 (bvxor (bvor (bvshl ((_ zero_extend 8) x26) (_ bv8 16)) ((_ zero_extend 8) x27)) (_ bv34364 16))))
(let (($x195 (= (bvxor (bvadd ?x190 (_ bv4660 16)) (_ bv37139 16)) (_ bv0 16))))
(let ((?x183 (bvxor (bvor (bvshl ((_ zero_extend 8) x24) (_ bv8 16)) ((_ zero_extend 8) x25)) (_ bv2139 16))))
(let (($x184 (= ?x183 (_ bv0 16))))
(let ((?x174 (bvadd (bvor (bvshl ((_ zero_extend 8) x22) (_ bv8 16)) ((_ zero_extend 8) x23)) (bvor (bvshl ((_ zero_extend 8) x16) (_ bv8 16)) ((_ zero_extend 8) x17)))))
(let (($x177 (= (bvxor ?x174 (_ bv37640 16)) (_ bv0 16))))
(let ((?x165 (bvxor (bvor (bvshl ((_ zero_extend 8) x20) (_ bv8 16)) ((_ zero_extend 8) x21)) (_ bv2642 16))))
(let ((?x163 (bvadd (bvor (bvshl ((_ zero_extend 8) x18) (_ bv8 16)) ((_ zero_extend 8) x19)) (_ bv16388 16))))
(let (($x169 (= (bvxor (bvadd ?x163 ?x165) (_ bv25202 16)) (_ bv0 16))))
(let ((?x154 (bvxor (bvor (bvshl ((_ zero_extend 8) x16) (_ bv8 16)) ((_ zero_extend 8) x17)) (_ bv34563 16))))
(let (($x157 (= (bvxor ?x154 (_ bv12598 16)) (_ bv0 16))))
(let ((?x145 (bvadd (bvor (bvshl ((_ zero_extend 8) x14) (_ bv8 16)) ((_ zero_extend 8) x15)) (_ bv4626 16))))
(let (($x148 (= (bvxor ?x145 (_ bv5783 16)) (_ bv0 16))))
(let ((?x107 (bvor (bvshl ((_ zero_extend 8) x12) (_ bv8 16)) ((_ zero_extend 8) x13))))
(let (($x143 (= (bvxor (bvor (bvadd ?x107 (_ bv21554 16)) (_ bv12296 16)) (_ bv15224 16)) (_ bv0 16))))
(let ((?x78 (bvor (bvshl ((_ zero_extend 8) x6) (_ bv8 16)) ((_ zero_extend 8) x7))))
(let ((?x132 (bvadd (bvor (bvshl ((_ zero_extend 8) x10) (_ bv8 16)) ((_ zero_extend 8) x11)) ?x78)))
(let (($x136 (= (bvxor (bvadd ?x132 ?x107) (_ bv57128 16)) (_ bv0 16))))
(let ((?x124 (bvmul ?x82 (bvor (bvshl ((_ zero_extend 8) x30) (_ bv8 16)) ((_ zero_extend 8) x31)))))
(let (($x127 (= (bvxor ?x124 (_ bv31720 16)) (_ bv0 16))))
(let (($x119 (= (bvxor (bvadd (bvxor ?x78 (_ bv50651 16)) (_ bv5290 16)) (_ bv20493 16)) (_ bv0 16))))
(let ((?x108 (bvmul (bvor (bvshl ((_ zero_extend 8) x14) (_ bv8 16)) ((_ zero_extend 8) x15)) ?x107)))
(let ((?x109 (bvadd (bvxor (bvor (bvshl ((_ zero_extend 8) x4) (_ bv8 16)) ((_ zero_extend 8) x5)) (_ bv51167 16)) ?x108)))
(let (($x112 (= (bvxor ?x109 (_ bv60272 16)) (_ bv0 16))))
(let ((?x90 (bvmul (bvmul ?x78 ?x82) (bvxor (bvor (bvshl ((_ zero_extend 8) x2) (_ bv8 16)) ((_ zero_extend 8) x3)) (_ bv4063 16)))))
(let (($x93 (= (bvxor ?x90 (_ bv15832 16)) (_ bv0 16))))
(let ((?x50 (bvadd (bvxor (bvor (bvshl ((_ zero_extend 8) x0) (_ bv8 16)) ((_ zero_extend 8) x1)) (_ bv18742 16)) (bvor (bvshl ((_ zero_extend 8) x20) (_ bv8 16)) ((_ zero_extend 8) x21)))))
(let (($x54 (= (bvxor ?x50 (_ bv31989 16)) (_ bv0 16))))
(and $x54 $x93 $x112 $x119 $x127 $x136 $x143 $x148 $x157 $x169 $x177 $x184 $x195 $x204 $x210)))))))))))))))))))))))))))))))))))))
(check-sat)

Final Words

I enjoyed this challenge. It seems to be easy or medium-easy difficulty rating. If only I could have solved more difficult Reverse Engineering challenges during quals–liberty, godzilla, and so on.

Recently I read T. Blazytko et al. USENIX Security’17. The paper says the system named Syntia automatically deobfuscate binaries with program synthesis. Program synthesis is a method to synthesize some pieces of program from given I/O samples and possible operators–like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python
#coding: utf-8
from z3 import *
x, y, h = BitVecs(['x', 'y', 'h'], 8)
# I/O samples
t1 = And(x == 1, y == 2)
t2 = And(x == 2, y == 4)
t3 = And(x == 3, y == 6)
phi_1 = Or(t1, t2, t3)
# h is subject to synthesize
phi_2 = y == x << h
s = Solver()
s.add(ForAll([x, y], Implies(phi_1, phi_2)))
print s.check() # sat
print s.model() # [h = 2]

This is just a simple example. In practical, program slicing and path pruning will be needed. Both symbolic execution and program synthesis depend on SMT solver, but according to the paper, program synthesis is more suitable for deobfuscation tasks… really? I’ll investigate further.

Introduction

As you know, IDAPython is quite useful. And Triton concolic execution engine has python binding. Then… why not integrate them? I tried to stand on the shoulders of giants.

Backward Program Slicing

Roughly speaking, program slicing is a method to extract subset of program which is relevant to given statement. Here is an excerpt from M. Weiser. ICSE’81:

Starting from a subset of a program’s behavior, slicing reduces that program to a minimal form which still produces that behavior. The reduced program, called a “slice”, is an independent program guaranteed to faithfully represent the original program within the domain of the specified subset of behavior.

Kudos to Jonathan Salwan, we can easily apply backward program slicing to binary analysis process with minor modification of backward_slicing.py and proving_opaque_predicates.py. I wrote a simple, tiny glue between Triton and IDA Pro:

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
#!/usr/bin/env python
#coding: utf-8
from idc import *
from idaapi import *
from triton import *
ctx = TritonContext()
regs = {
'eax': REG.X86.EAX,
'ebx': REG.X86.EBX,
'ecx': REG.X86.ECX,
'edx': REG.X86.EDX,
}
def symbolization_init():
Triton.convertRegisterToSymbolicVariable(Triton.registers.eax)
Triton.convertRegisterToSymbolicVariable(Triton.registers.ebx)
Triton.convertRegisterToSymbolicVariable(Triton.registers.ecx)
Triton.convertRegisterToSymbolicVariable(Triton.registers.edx)
return
def opaque_predicate_detection(pc):
ctx.setArchitecture(ARCH.X86)
ctx.setAstRepresentationMode(AST_REPRESENTATION.PYTHON)
symbolization_init()
ast_ctx = ctx.getAstContext()
while True:
instruction = Instruction()
instruction.setAddress(pc)
opcode = GetManyBytes(pc, ItemSize(pc)) # Disassemble with IDA Pro
instruction.setOpcode(opcode)
ctx.processing(instruction) # Emulate instruction without Pintool
for se in instruction.getSymbolicExpressions():
se.setComment(str(instruction))
if str(instruction.getDisassembly()).split()[0] == 'cmp': # if instruction.isCompare():
reg = str(instruction.getOperands()[0]).split(':')[0] # Get first operand
if reg in regs:
Expr = ctx.getSymbolicRegisters()[regs[reg]]
slicing = ctx.sliceExpressions(Expr)
for k, v in sorted(slicing.items()):
print v.getComment()
print instruction
elif instruction.isBranch():
print instruction
if instruction.isConditionTaken():
print 'branch will taken'
else:
print 'branch will not taken'
break
pc = NextHead(pc)
print '-' * 50
def main():
ea = ScreenEA() # Get address corresponding to cursor
opaque_predicate_detection(ea)
if __name__ == '__main__':
view = main()

Showcase

The snippet extracts subset of program which is relevant to branch condition. We can run this from File -> Script file in IDA Pro menu.

Conditional Branch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.text:004074DD loc_4074DD:
.text:004074DD 040 mov dword ptr [edi+18h], 0Ah
.text:004074E4 040 mov dword ptr [edi+14h], 0
.text:004074EB 040 mov word ptr [edi+8], 8235h
.text:004074F1 040 mov dword ptr [edi+0Ch], 3E8h
.text:004074F8 040 mov dword ptr [edi+10h], 0BB8h
.text:004074FF 040 sub esp, 4
.text:00407502 044 mov [esp+40h+Memory], 0B8h ; Size ; cursor
.text:00407509 044 call ??2@YAPAXI@Z ; operator new(uint)
.text:0040750E 044 add esp, 4
.text:00407511 040 mov [esi+18h], eax
.text:00407514 040 mov eax, dword_591EE4
.text:00407519 040 mov ecx, dword_591EE0
.text:0040751F 040 imul eax, eax
.text:00407522 040 imul edx, eax, 7
.text:00407525 040 dec edx
.text:00407526 040 mov eax, ecx
.text:00407528 040 imul eax, eax
.text:0040752B 040 cmp edx, eax
.text:0040752D 040 jz short loc_407538

becomes:

1
2
3
4
5
6
7
0x407514: mov eax, dword ptr [0x591ee4]
0x40751f: imul eax, eax
0x407522: imul edx, eax, 7
0x407525: dec edx
0x40752b: cmp edx, eax
0x40752d: je 0x407538
branch will not taken

Unconditional Branch

1
2
3
4
5
6
7
8
9
.text:004078C3 loc_4078C3:
.text:004078C3 000 mov esp, edi ; cursor
.text:004078C5 000 mov eax, dword_591EAC
.text:004078CA 000 lea ecx, [eax+4]
.text:004078CD 000 mov edx, eax
.text:004078CF 000 sar edx, cl
.text:004078D1 000 lea eax, [eax+edx*2]
.text:004078D4 000 mov dword_591EAC, eax
.text:004078D9 000 jmp short loc_4

becomes:

1
2
0x4078d9: jmp 0x40789a
branch will taken

Looks nice.

Last Words

Triton’s emulation iteration is compatible to IDAPython manner. Therefore, The combination of IDA Pro and Triton is pretty good.

Cheers,

Introduction

IDAPython is a powerful feature of IDA Pro, and there are many open-sourced IDAPython projects. However, we cannot use every GUI-based IDAPython script due to some Qt-related breaking changes between IDA Pro 6.8 and 6.9 or later. The main problem is about migrating no longer supported PySide code to PyQt5.

Recently I ported PySide code within idasec–one of the most sophisticated deobfuscation frameworks, which tackles opaque predicates and call stack tampering in terms of infeasibility questions, by utilizing Backward-Bounded Dynamic Symbolic Execution proposed in the remarkably well written paper S. Bardin et al. IEEE S&P’17–to PyQt5.

That’s why I decided to write this blog post for a note to self and for someone trying to do similar thing.

Related Work

There are 2 guidances to migrate PySide code to PyQt5:

Please read them before. I only give supplemental information in addition to predecessors.

How to Migrate

Now let’s get started.

Change QtGui methods to QtWidgets

Most methods in QtGui migrated to QtWidgets. Therefore,

1
from PySide import QtGui, QtCore

becomes:

1
from PyQt5 import QtCore, QtGui, QtWidgets

As an example, QTextEdit described in Hex Blog. In additions, the methods to be rewritten are as follows:

  • QtWidgets.QLayout
  • QtWidgets.QVBoxLayout
  • QtWidgets.QHBoxLayout
  • QtWidgets.QWidget
  • QtWidgets.QTableWidget
  • QtWidgets.QListWidget
  • QtWidgets.QTabWidget
  • QtWidgets.QDockWidget
  • QtWidgets.QTreeWidget
  • QtWidgets.QTreeWidgetItem
  • QtWidgets.QPushButton
  • QtWidgets.QRadioButton
  • QtWidgets.QToolButton
  • QtWidgets.QButtonGroup
  • QtWidgets.QGroupBox
  • QtWidgets.QSpinBox
  • QtWidgets.QCheckBox
  • QtWidgets.QComboBox
  • QtWidgets.QTextEdit
  • QtWidgets.QLineEdit
  • QtWidgets.QApplication
  • QtWidgets.QLabel
  • QtWidgets.QSizePolicy
  • QtWidgets.QMenu
  • QtWidgets.QFrame
  • QtWidgets.QProgressBar
  • QtWidgets.QStyle
  • QtWidgets.QSpacerItem
  • QtWidgets.QScrollArea
  • QtWidgets.QSplitter
  • There might be more…

My experience says that other than the following 3 methods may be rewritten:

  • QtGui.QPixmap
  • QtGui.QIcon
  • QtGui.QFont

idacute may overwrite all of QtGui methods, so I think there still needs to be manual works.

Overwrite _fromUtf8

We also need to overwrite _fromUtf8.

1
2
3
4
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
    _fromUtf8 = lambda s: s

Others

These issues are described by predecessors:

  • Handling SIGNAL
  • Change FormToPySideWidget to FormToPyQtWidget
  • Change setResizeMode to setSectionResizeMode

Conclusion

This time, I was able to run idasec on IDA Pro 7.0 with some bug fixes and dirty patches – like this cool video:

If you are an IDA Pro 7.0 user, note that other backward-compatibility issue described in IDA: IDAPython backward-compatibility with 6.95 APIs will occur.

Enjoy!

HAI DOMO VIRTUAL YOUTUBER KIZUNA AI DESU. I’m still working on my English.

 なにもかも忘れかけている.

はじめに

 z3pyはSMTソルバZ3のPythonバインディング.たとえば

1
2
3
4
5
...
if(x * 2 + 3 * y == 4
&& x * 3 + y == -1){
puts("congrats!");
}

のようなプログラムに対して

1
2
3
4
5
6
7
8
9
10
11
from z3 import *
x = Int('x')
y = Int('y')
s = Solver()
s.add(x * 2 + 3 * y == 4)
s.add(x * 3 + y == -1)
print(s.check())
print(s.model())

としてやれば期待される入力値[x = -1, y = 2]を算出できる.これだけではだからなに? という感じだが.シンボリック実行は各命令とメモリの状態からこの割当を自動生成するものだと思えばよい.これがangrやらTritonやらmiasmやらmanticoreやら,その他いまどきのバイナリ解析ツール群の基盤となっているというわけです.

参考になったサイト

 で,メモっとかないと忘れるので.SAT/SMTソルバのしくみやシンボリック実行全般まで広げると膨大になるということで,z3pyに限っています.前者については高速SATソルバーの原理 - 基盤(S)離散構造処理系プロジェクト[PDF]SAT/SMTソルバの仕組み - SlideShareを読むとよいでしょう.いまどきのSAT/SMTソルバにはVSIDSやらLubyリスタートやらいろいろな工夫が盛り込まれているが,とりあえずユーザとしてはDPLLとCDCLさえ抑えておけば問題ないはず.後者,シンボリック実行全般に関するおすすめの文献は秘密.Pythonバインディング以外の記事,angrやTritonなどサードパーティのツール群に関する記事も省いてある.

ウェブサイト 概要
CTF/Toolkit/z3py - 電気通信大学MMA 日本語で書かれた入門資料.これには書かれてないがBitVecSortも便利.
Theorem prover, symbolic execution and practical reverse-engineering z3pyのチュートリアル.まずはこの通りに手を動かす.
Breaking Kryptonite’s Obfuscation: A Static Analysis Approach Relying on Symbolic Execution シンボリック実行の最小構成の実装.
0vercl0k/z3-playground 上2つの資料で参照されているソースコード群.
Solving a simple crackme using Z3 – Aneesh Dogra’s Blog 簡単なcrackmeのwriteup.
Playing with Z3, hacking the serial check. – rosako’s blog 簡単なcrackmeのwriteup.
Using SAT and SMT to defeat simple hashing algorithms - LSE Blog オレオレハッシュ関数の解析.
Reversing the petya ransomware with constraint solvers ランサムウェアPetyaのSalsa実装の不備を突くdecryptor.
thomasjball/PyExZ3 シンボリック実行の実装例.z3公式からリンクが貼られている.symbolic/以下,z3_wrap.py, loader.pyが参考になる.
Quick introduction into SAT/SMT solvers and symbolic execution (DRAFT) [PDF] いろいろリンク貼ったけどこれだけ読めばいい.SMTソルバの紹介.数独の解き方.マインスイーパの自動化.デコンパイラ.難読化解除.シンボリック実行.ハッシュ関数の解析.全部載っている.Dennis Yurichev氏,Reverse Engineering for Beginnersも書いていて,慈善事業家か?

 はてなブックマークを使っていればよかったのではという気がしてきた.

おわりに

 z3py便利最高ですね.みなさんは便利最高ですか?
 これで次のようなCTFの問題を解くことができます:

問題 大会
Unbreakable Enterprise Product Activation Google CTF 2016
Ropsynth SECCON 2016 Online CTF
baby-re DEF CON CTF Qualifier 2016
amandhj DEF CON CTF Qualifier 2016
Pepperidge Farm DEF CON CTF Qualifier 2017

 まあ明らかにangr使ったほうが楽.とりあえずいくつかやってみただけで,ほかにもたくさんあると思う.
 Bareflank Hypervisorを読むのはいつになるかわかりません.

はじめに

 今年のセキュリティ・キャンプでは,うっかり「なぜマルウェア解析は自動化できないのか」という題の講義を行ってしまったが,それだけセキュリティの世界には自動化の波が来ている.本稿では,脆弱性分析の自動化をめざして開発されているangr, AFL, Drillerをざっくり紹介する.

angr

 angrはUCSBの研究チームにしてCTFチームShellphishを中心に開発されているバイナリ解析フレームワーク.論文[PDF]はIEEE S&P 2016に採択されている.手法の新規性というよりは実装力でゴリ押しするタイプ.評価には,アメリカ国防高等研究計画局が5,500万ドル(約56億円)の資金を投じてまで開催した脆弱性分析・修正の自動化コンペ,DARPA Cyber Grand Challenge (CGC) のデータセットが用いられている.CGCの決勝戦に進出したチームには75万ドル(約7,600万円),優勝したチームは200万ドル(約2億円)が与えられる.angr開発の目的のひとつが,CGCでの勝利にあることは疑いようもない——最終的な戦績は,CMUのツールMAYHEMに優勝を譲って3位だったが.
 さて,angrはシンボリック実行やプログラムスライシングを通して,プログラムの特定位置に到達するための入力値を抽出することができる.次のコードで雰囲気をつかめるだろうか:

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
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import sys
import angr
import simuvex
# 解析対象を指定
p = angr.Project(sys.argv[1])
# 制御フローグラフの生成
cfg = p.analyses.CFG()
print [x for x in cfg.functions.iteritems()]
# シンボルを取得
target_addr = p.loader.main_bin.get_symbol("main").addr
# パス分析クラスのインスタンス
pg = p.factory.path_group()
# シンボルへのパスを分析
pg.explore(find = target_addr)
# avoidを避け,findに到達する入力値を探索してくれる
a = p.surveyors.Explorer(find = FIND_ADDR, avoid = AVOID_ADDR).run()
# フック
p.hook_symbol('strlen', simuvex.SimProcedures['stubs']['ReturnUnconstrained'])
# 実行結果のダンプ
a.found[0].state.posix.dumps(1)

 解析対象の規模が大きい場合,エントリポイントを起点とした解析に時間を要するあるいは失敗することがあるが,プログラムの途中から解析を始めることも可能だ.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
p = angr.Project(sys.argv[1])
# 解析の起点となるアドレス
state = p.factory.blank_state(addr = START_ADDR)
# その地点までプログラムを実行したときのスタックの状態
state.regs.ebp = BASE_ADDR
state.regs.esp = STACK_ADDR
# 入力値の設定
for i in range(INPUT_LENGTH):
s = state.se.BVS('Var[{}]'.format(i), 32, explicit_name = True)
state.memory.store(INPUT_ADDR + i * 4, s)
path = p.factory.path(state)
a = p.surveyors.Explorer(start = path, find= FIND_ADDR, avoid= AVOID_ADDR)
a.found[0].state.posix.dumps(1)

 シンボリック実行はSSA形式の中間表現を前提とするが,angrはValgrindのVEX IRを用いている.バックエンドのSMTソルバはZ3だが,claripyという自前のラッパが噛ませてある.
 これ以上の説明は公式ドキュメントに譲ろう.

AFL

 AFL (American Fuzzy Lop) はGoogleのエンジニアlcamtufを中心に開発されているファジングツール.遺伝的アルゴリズムによって正常な入力を次々と変異させていき,自動的にプログラムのバグを検出する.AFLにはbashやtcpdump, OpenSSHといったさまざまなソフトウェアのバグを検出した実績があり,いまや脆弱性分析の研究になくてはならない存在だ.一般的なファジングはプログラムの浅い箇所にしか到達できない.AFLは,大量の正常な入力をトリミングしてシードとするコーパス蒸留 (corpus distillation) と,遺伝的アルゴリズムを用いてシードを変異させるGAFuzzingのいいとこ取りを図ったものだ.その実行フローは次のようになる:

  1. ユーザーから与えられた初期テストケースをキューに入れる
  2. キューから次の入力テストケースをひとつ取り出し,
  3. プログラムの振る舞いに影響を与えない最小サイズにトリミングする
  4. バランスのよい探索となるよう,さまざまな変異戦略を用いて入力を繰り返し変異させる
  5. 新たな状態遷移が計測されたら,出力をキューに入れる
  6. 2に戻る

 ここでAFLはカバレッジ計測のため,解析対象のプログラムに計測用のコードを埋め込む.これには,解析対象のソースコードが手元にある場合gccやclangの独自フロントエンドが,解析対象のソースコードが手元にない場合QEMUが用いられる.
 ファジング機能の中核は,afl-fuzz.cmain()から呼び出されるfuzz_one()にある.実装されている変異戦略は次の通り:

  • SIMPLE BITFLIP
  • ARITHMETIC INC/DEC
  • INTERESTING VALUES
  • DICTIONARY STUFF
  • RANDOM HAVOC
  • SPLICING

 CGCで優勝したCMUのMAYHEMは,このAFLにシンボリック実行機能を追加していたようだ.MAYHEMといえば同名のシンボリック実行エンジンの論文[PDF]がIEEE S&P 2012で発表されているが,当時からの変更点がどれほどなのかはわからない.
 また,AFLの拡張に,解析対象のパスを枝刈りすることで高速化を図ったAFLFastがある.これもCGC決勝進出チームによるもので,論文[PDF]はACM CCS 2016に採択されている.600行程度のパッチでトップカンファレンス.ちょっと妬ましい.

Driller

 Drillerはangrの開発陣によるAFLの拡張.論文[PDF]はNDSS 2016に採択されている.AFLのREADMEには次のようにある:

Other, more sophisticated research has focused on techniques such as program flow analysis (“concolic execution”), symbolic execution, or static analysis. All these methods are extremely promising in experimental settings, but tend to suffer from reliability and performance problems in practical uses - and currently do not offer a viable alternative to “dumb” fuzzing techniques.

 シンボリック(コンコリック)実行は実用的ではないと切って捨てている.DrillerはangrとAFLを組み合わせることで,この批判を克服し,ファジングとシンボリック実行のいいとこ取りを図ったものだ:

 たとえば,次のコードの解析には,ファジングよりシンボリック実行が適している:

1
2
3
4
5
6
7
int main(void)
{
int x;
read(0, &x, sizeof(x));
if(x == 0x123ABCD)
vulnerable();
}

 なぜなら,ファジングによって0x123ABCDという値を生成するには大量の試行を必要とするからだ.一方で,次のコードの解析には,シンボリック実行よりファジングが適している:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int check(char *x, int depth)
{
if(depth >= 100){
return 0;
}else{
int count = (*x == 'B') ? 1 : 0;
count += check(x+1, depth+1);
return count;
}
}
int main(void)
{
char x[100];
read(0, x, 100);
if(check(x, 0) == 25)
vulnerable();
}

 なぜなら,単純なシンボリック実行ではパス爆発を解決できないからだ.
 Drillerは両手法の長所と短所を踏まえた上で,それらを堅実に組み合わせている.やはりハッシュ関数の充足はDrillerをもってしても難しいようだが,現行MAYHEMもAFLとシンボリック実行を併用しているようだし,このアプローチが現在の着地点なのだろう.

おわりに

 CGCとそれにともなう研究によって,脆弱性分析の自動化手法は大きな進歩を遂げつつあることが伝わっただろうか.学会のOSS重視傾向も相まって,さまざまなツールを手元で試せるようになってきている.大変ありがたいことだ.

 本稿は情報セキュリティ系論文紹介 Advent Calendar 2015の11日目である.

TL;DR:

 テイント解析(taint analysis)やプログラムスライシング(program slicing),記号的実行(symbolic execution)やファジング(fuzzing)といったバイナリ解析技術を総動員して,マルウェアが通信するC&CサーバのフィンガープリントをC&Cサーバと通信せずとも生成する手法–AUTOPROBE[1]を紹介する.

背景

 マルウェアによるサイバー犯罪はC&Cサーバやコンフィグファイルの配布元,二次検体の配布元など様々な悪性サーバ(malicious server)からなるインフラによって支えられている.攻撃者はこれらの悪性サーバを頻繁に閉鎖・移転させて対策から逃れようとする.とりわけ攻撃者の命令を送信してマルウェアの動作を決定するC&Cサーバはその後の攻撃の起点となるため早期発見が望ましい.したがって不審なサーバがC&Cサーバかどうか判定するフィンガープリントが必要となる.
 しかしながら従来のフィンガープリント生成手法にはC&Cサーバとの長期的な通信を前提としている[2],サーバとして動作するマルウェアを前提としている[3]といった問題点があった.

問題定義

 あるマルウェアのファミリFに属する検体Pを入力として受け取り,Fが用いるC&Cサーバのフィンガープリントφを出力することがAUTOPROBEの目的である.C&Cサーバ側のコードは参照できず,マルウェアの検体はシンボル情報やソースコードを含んでいなくともよい.またマルウェアは複数のリクエストをC&Cサーバに送信するものとする.

提案手法

 あるサーバにマルウェアと同様のリクエストを送って,マルウェアがコマンドとして解釈できるレスポンスが返ってきたら,そのサーバは疑いようもなくC&Cサーバである–AUTOPROBEの鍵となる発想は至極単純だ.
 AUTOPROBEは検体の命令・API・システムコールの実行を–おそらくQEMUによって–トレースし,リクエストを生成する処理とレスポンスをハンドルする処理をそれぞれマルウェアから抽出する.つづいて前者によって不審なサーバに対するリクエストを生成し,後者によってレスポンスをハンドルする.レスポンスハンドリングの可否をもってフィンガープリントとするから,その処理の抽出さえできれば不審なサーバがC&Cサーバかどうか判定するまで実際のC&Cサーバと通信する必要はない–でもどうやって?

リクエスト生成

 マルウェアの多くは時間や擬似乱数,OS情報などの環境依存の値によって動作を変更する.たとえばWin32/LoadMoney.AFはレジストリキーの有無によって送信するリクエストを変更する.

 不審なサーバがC&Cサーバかどうか判定するためには攻撃者の期待するリクエストをより多く送信しより多くレスポンスを得たい.そこでAUTOPROBEは以下のアルゴリズムにしたがって複数の実行トレースを得る.これは条件分岐の度に直前のシステムコールを参照する,ある種の深さ優先探索として実装される.

 さらにAUTOPROBEはトレースをスライスし,システムコールの結果に依存する値を環境依存の値として決定論的な値・定数と区別する.ここで環境依存の値を書き換えれば攻撃者の期待するリクエストを送信できるようになる.また環境依存の値に影響するシステムコールの戻り値に時間,IPアドレス,擬似乱数,OS情報など200種類のラベルを設定しているとのことだ.

レスポンスハンドリング

 生成したリクエストをC&Cサーバに送信してレスポンスを得たら,AUTOPROBEはレスポンスの各バイトを記号値として扱い,実行トレースから検体の分岐制約を充足する解およびスライスθ1を得る.ここで探索の終了条件はclosesocketexitprocessに到達した場合,データを受信してから50個の条件分岐にわたってそのデータを参照するものが存在しない場合である.つづいてレスポンスとして正しく解釈できない値を検体に与え,実行トレースから分岐制約を充足する解およびスライスθ2を得る.ここでηは以下の式によって得られる実行経路の類似度である.

 bnとfnはそれぞれ各スライスに含まれるユニークなコードブロックとシステムコールの数を示す.AUTOPROBEはηが閾値10を下回ればレスポンスを正しく解釈していない実行経路とみなしてスライスを破棄し,上回れば別々のハンドラであるとしてそれぞれの分岐制約をフィンガープリントとして保持する.たとえばこんなふうに.

 C&Cサーバからレスポンスを得られなければ,AUTOPROBEは記号的実行とファジング,フラグレジスタの書き換えによる強制実行(forced execution)を併用してレスポンスに依存する実行経路を探索する.

 このファジングでは検体にランダム値を与えるが,検体が用いるアルゴリズムがHTTPなど既知のものであればエラーメッセージのハンドラを起点に探索する.
 AUTOPROBEはこのように探索したレスポンスのハンドラをフィンガープリントとするが,期待されるレスポンスが得られずさきほどのアルゴリズムによって探索した場合は以下のようにC&Cサーバの尤もらしさを算出する.

評価

 論文ではふたつのデータセットを用いてAUTOPROBEの性能を評価している.ひとつはSality, ZeroAccess, Ramnit, Bamital, Taidoorを含む37ファミリ10亜種計370検体.もうひとつはネットワーク通信の特徴量に基づいてフィンガープリントを生成する既存研究–CYBERPROBE[2]で用いられた19ファミリ. “which have been kindly provided to us by the authors of CYBERPROBE”って書いてるけどAUTOPROBEと同じメンバーじゃねえか.
 検体の実行時間は5分.

リクエスト生成

 まずはリクエストの生成から.可能であればC&Cサーバとの接続をともなうがAlexaトップ10,000サイトは除外している.ここでAUTOPROBEはC&Cサーバに接続せずとも検体のバイナリを分析してCYBERPROBEと同様の結果を得ている.

 AUTOPROBEはふたつのデータセットに含まれる56ファミリのトレースから105のリクエストを生成した.生成にかかった時間は平均13.2分.リクエストはすべてHTTPであったとのこと.

レスポンスハンドリング

 生成したリクエストのうち76件がC&Cサーバからのレスポンスを引き出した.さらにAUTOPROBEはHTTP 200レスポンスコードを含む同数のランダムなレスポンスを生成し,検体に与えた.これはレスポンスハンドリングの可否によって検体の異なる挙動を確認したいためだ.結果として76件のテストケース中71件(93%)で検体は異なる挙動を示した–期待される受信データを得たマルウェアは一般に10以上のシステムコールと50以上のコードブロックを実行する.残りの5件はC&Cサーバではなかった.つまりAUTOPROBEはマルウェアの通信先がC&Cサーバかどうか正しく判定できている.

ケーススタディ

 たとえばBamitalのリクエストはファイル名・GetVersionExによって得たOS情報・DGA (domain generation algorithm) によって生成したホスト名を含んでいた.AUTOPROBEはこれらの値が依存するシステムコールを得ている.

 C&Cサーバと接続せずともHTTP/1.1 200 OKを与えればBamitalのハンドラは分析できるとのこと.
 また標的型攻撃に用いられるTaidoorのリクエストはGetAdaptersInfoによって得たMACアドレスに依存する値を含んでいた.これによってTaidoorのC&Cサーバは感染端末のみで動作するレスポンスを送信していたようだ.
 ドライブバイダウンロード検体であるSalityのC&Cサーバはspm/s_tasks.php, logos_s.gif, 231013_d.exeというファイルをレスポンスに含んでいた.AUTOPROBEはこれらのファイルの存在するサーバをSalityのC&Cサーバとみなす.などなど.

限定的な探索

 Malware Domain Listに含まれる9,500アドレスの近傍/24サブネット・2.6Mアドレスのスキャン結果.

 VirusTotal, Malware Domain List, そしてURLQueryに発見されていないC&Cサーバを特定できている.

インターネット全体の探索

 ここではBGPからインターネット全体をスキャンし,7,100万ものHTTPサーバを発見している.

 このうちマルウェア3ファミリのC&Cサーバをどれだけ発見できるかCYBERPROBEと比較した結果.

 CYBERPROBEが40件のC&Cサーバを特定しているのにたいしてAUTOPROBEは54件のC&Cサーバを特定している.
 高度化するマルウェアが通信内容を難読化・秘匿する傾向にあることを鑑みると,CYBERPROBEのようにネットワーク通信の特徴量を用いる手法よりもAUTOPROBEのようにバイナリ解析を応用した手法こそ吟味されるべきだろう.

感想

 AUTOPROBEは折しも私が昨年度のインターンシップでほとんど同じようなテーマに取り組んでいたとき発表された.テイント解析のソースを受信データではなくシステムコールの結果に設定することで,リクエストのセマンティクスを復元しようとするAUTOPROBEの発想は野心的であり,さまざまなバイナリ解析技術を結集して未知のC&Cサーバを発見する手際は鮮やかというほかない.
 私は来年書くことになる卒業論文のテーマとして,インターネット接続のない閉環境におけるマルウェアの分析に本手法を応用できないか検討している.Ryzhykら[4]はデバイスドライバと環境(デバイスおよびOS)との相互作用を有限オートマトンを用いて表現し,デバイスドライバを半自動的に合成する手法を提案している.問題領域は異なるがこうした手法も検討したい.

用語解説

  • テイント解析
    • 任意のデータに設定したタグをルールにしたがって伝搬させることで,データ間の依存関係を分析する手法
  • プログラムスライシング
    • 任意のデータに依存する処理部分をプログラムから抽出する手法
  • 記号的実行
    • プログラムの分岐条件を充足論理問題とし,制約を充足する解を得る手法
  • ファジング
    • 開発者が意図しない入力を自動生成してプログラムに与えることで脆弱性を顕在化させる手法

参考文献

TL;DR:

 ソフトウェアの多くは外部からの入力に依存する実行パス(trigger-based code)をもつ.
 これを記号的実行(symbolic execution, シンボリック実行)などの解析手法から隠蔽する手法として,コラッツの問題を用いた線型難読化(linear obfuscation)がある[1].
 本稿ではしかし,線型難読化されたコードはコンパイラ最適化によってある程度除去できることを示す.

コラッツの問題

 コラッツの問題は数論の未解決問題のひとつである.
 任意の1でない自然数nに対して,nが偶数ならば2で割り,nが奇数ならば3倍して1を足す.この操作を繰り返していくと,どのような自然数nから出発しても,有限回の操作のうちに必ず1に到達する.
 この定理は経験則的に正しいと考えられているが,いまだ証明はなされていない.

線型難読化

 たとえば次のプログラムtr.cは外部からの入力に依存する実行パスをもつ.

1
2
3
4
5
6
7
8
9
10
11
// tr.c
#include <stdio.h>
int main(int argc, char *argv[])
{
int x=argc;
if(x==2)
printf("triggered!\n");
return 0;
}

 LLVM bitcodeレベルでのtr.cの制御フローグラフは次のようになる.

1
2
3
# clang -emit-llvm -c -g tr.c
# opt tr.bc -dot-cfg > /dev/null
# dot -Tpng cfg.main.dot > tr.png

 この単純なプログラムにたいして,コラッツの問題にもとづくループを挿入する.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// tr2.c
#include <stdio.h>
int main(int argc, char *argv[])
{
int x = argc;
int y = x + 1000;
while(y > 1)
{
if(y % 2 == 1)
y = 3 * y + 1;
else
y = y / 2;
if((x - y > 0)&&(x + y < 4))
{
printf("triggered!\n");
break;
}
}
return 0;
}

 変数yは必ず1に到達する,いわば偽の変数である.
 難読化されたtr2.cの制御フローグラフは次のようになる.

 ループが挿入されたことによって実行パスが複雑化していることが見て取れる.

記号的実行

 では,線型難読化がどれほど記号的実行にたいして効力をもつか見てみよう.
 今回はLLVM bitcodeを扱う記号的実行ツールKLEEを用いる.
 まず次のコードを解析対象のソースに追記する必要がある.これは,変数xにたいして記号的実行を適用するという意味である.

1
2
3
4
5
6
7
@@ -3,6 +3,7 @@
int main(int argc, char *argv[])
{
int x=argc;
+ klee_make_symbolic(&x, sizeof(x), "x");
if(x==2)
printf("triggered!\n");

 次に,LLVM bitcodeを生成する.

1
2
3
4
5
6
# clang -emit-llvm -c -g tr.c
tr.c:6:5: warning: implicit declaration of function 'klee_make_symbolic' is
invalid in C99 [-Wimplicit-function-declaration]
klee_make_symbolic(&x, sizeof(x), "x");
^
1 warning generated.

 警告が出るが,気にしてはいけない.この関数呼び出しがKLEEにトラップされることになるのだ.
 ソースコードのないマルウェアなどを分析するにあたっては,IDAなどのデコンパイラでソースを出力し,型情報やシグナルハンドラなどの記述を整えたのち上記のようなコードを挿入するか,あるいはS2EやPANDAといった動的解析環境を頼ることになるだろう.
 それはさておき,KLEEを動かしてみよう.少し前までKLEEをビルドして動かすのはとても面倒だったが,いまではDocker Imageが提供されている.

1
2
# sudo docker pull klee/klee
# sudo docker run --rm -ti klee/klee

 線型難読化をおこなう前のtr.cについて記号的実行をおこなった結果を示す.

1
2
3
4
5
6
7
8
9
10
11
12
13
klee@4d625535c122:~$ time klee tr.bc
KLEE: output directory is "/home/klee/klee-out-1"
KLEE: WARNING: undefined reference to function: printf
KLEE: WARNING ONCE: calling external: printf(39707328)
triggered!
KLEE: done: total instructions = 17
KLEE: done: completed paths = 2
KLEE: done: generated tests = 2
real 0m0.032s
user 0m0.009s
sys 0m0.012s

 パスは2つしか存在しないため,32msecで解析が終わっている.
 ならば,線型難読化を施した後のtr2.cについてはどうか.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
klee@4d625535c122:~$ time klee tr2.bc
KLEE: output directory is "/home/klee/klee-out-2"
KLEE: WARNING: undefined reference to function: printf
KLEE: WARNING ONCE: calling external: printf(49859696)
triggered!
triggered!
triggered!
triggered!
triggered!
...
KLEE: done: total instructions = 285809
KLEE: done: completed paths = 158
KLEE: done: generated tests = 158
real 6m11.933s
user 3m56.240s
sys 2m14.245s

 さきほどに比べ,実行パスは158に増加し,解析に11622.9倍(!)もの時間がかかっている.
 今回の単純なプログラムでさえこのようになるならば,複数の入出力に依存する実行パスに線型難読化が施されたらどうなることか.

コンパイラ最適化

 難読化とはえてしてコンパイラ最適化の逆写像である.
 KLEEがLLVMにもとづいているということもあって,LLVMの最適化が線型難読化を除去できるかどうか興味をもった.検証してみよう.

1
# opt -O3 tr2.bc -o tr3.bc

 -O3をもって最適化した後の制御フローグラフは次のようになる.

 最初のtr.cほどではないが,いくらか単純になっていることがわかる.printf()puts()に変換されている.
 では,実行パスは減少しているだろうか.記号的実行をおこなった結果は次の通り.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
klee@4d625535c122:~$ time klee tr3.bc
KLEE: output directory is "/home/klee/klee-out-4"
KLEE: WARNING: undefined reference to function: puts
KLEE: WARNING ONCE: calling external: puts(32090720)
triggered!
triggered!
triggered!
triggered!
triggered!
triggered!
triggered!
triggered!
triggered!
KLEE: done: total instructions = 3383
KLEE: done: completed paths = 10
KLEE: done: generated tests = 10
real 0m2.845s
user 0m2.490s
sys 0m0.357s

 実行パスは10とさきほどのtr2.cよりも減少している.実行時間はtr.cの88.9倍であった.

おわりに

 線型難読化は脅威ではないことがわかった–少なくとも提唱者の思惑ほどには.
 塵も積もれば山となるように,線型難読化を多数の箇所に施せばその効力は増すだろう.しかしそれはクラスタリングなどの手法で対処される可能性を高めるだけである.もちろん,どれほどの範囲で難読化を適用すれば効果的かという閾値を探ることに価値はある.
 LLVMの-O3最適化は複数の最適化パスを組み合わせ,再帰的に適用することによっておこなわれる.どのパスが線型難読化の除去にもっとも寄与しているか調べてみるとおもしろいかもしれない(やる気がない).

参考文献

はじめに

2015.08.11~15にわたって開催されたセキュリティ・キャンプ全国大会 2015に解析トラックの講師として参加した.講義では「仮想化技術を用いてマルウェア解析」と題して,QEMUをベースに開発が行われているDECAFという解析プラットフォームを用いて演習を行った.

講義資料

講義内容

演習では実際のマルウェアに用いられている解析妨害機能を備えたサンプルプログラムを扱った.素のDECAFには解析妨害機能への対策が施されていない.そこで,受講者にはDECAFのプラグインを拡張し,対策手法を実装して頂いた.
演習で用いたプログラムはGitHub上で公開している.

  • 解析妨害機能を備えたサンプルプログラム
  • DECAFプラグインのひな形

ひな形にある通り,IsDebuggerPresent()をフックするDECAFプラグインは以下のように書ける.

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
static DECAF_Handle isdebuggerpresent_handle = DECAF_NULL_HANDLE;
typedef struct {
uint32_t call_stack[1]; //paramters and return address
DECAF_Handle hook_handle;
} IsDebuggerPresent_hook_context_t;
/*
* BOOL IsDebuggerPresent(VOID);
*/
static void IsDebuggerPresent_ret(void *param)
{
IsDebuggerPresent_hook_context_t *ctx = (IsDebuggerPresent_hook_context_t *)param;
hookapi_remove_hook(ctx->hook_handle);
DECAF_printf("EIP = %08x, EAX = %d\n", cpu_single_env->eip, cpu_single_env->regs[R_EAX]);
free(ctx);
}
static void IsDebuggerPresent_call(void *opaque)
{
DECAF_printf("IsDebuggerPresent ");
IsDebuggerPresent_hook_context_t *ctx = (IsDebuggerPresent_hook_context_t*)malloc(sizeof(IsDebuggerPresent_hook_context_t));
if(!ctx) return;
DECAF_read_mem(NULL, cpu_single_env->regs[R_ESP], 4, ctx->call_stack);
ctx->hook_handle = hookapi_hook_return(ctx->call_stack[0], IsDebuggerPresent_ret, ctx, sizeof(*ctx));
}
static void geteip_loadmainmodule_callback(VMI_Callback_Params* params)
{
if(strcmp(params->cp.name,targetname) == 0)
{
DECAF_printf("Process %s you spcecified starts \n", params->cp.name);
target_cr3 = params->cp.cr3;
isdebuggerpresent_handle = hookapi_hook_function_byname("kernel32.dll", "IsDebuggerPresent", 1, target_cr3, IsDebuggerPresent_call, NULL, 0);
}
}

現在のプロセスがデバッガのコンテキストで実行されていない場合,IsDebuggerPresent()は0を返す.ここで,IsDebuggerPresent()の戻り値を0にするには,モジュール(この場合はkernel32.dll)から戻る段階でeaxを書き換えてやればよい.

1
2
3
4
5
6
7
8
static void IsDebuggerPresent_ret(void *param)
{
IsDebuggerPresent_hook_context_t *ctx = (IsDebuggerPresent_hook_context_t *)param;
hookapi_remove_hook(ctx->hook_handle);
cpu_single_env->regs[R_EAX] = 0; // 追加
DECAF_printf("EIP = %08x, EAX = %d\n", cpu_single_env->eip, cpu_single_env->regs[R_EAX]);
free(ctx);
}

このようにDECAFのプラグインを書くことで,ゲストOSに解析用のエージェントを挿入することなくAPIの戻り値を書き換えることができる.
APIの引数を書き換えたい場合はモジュールに入る段階でコンテキスト構造体のcall_stack[]を書き換えてやればよい.

おわりに

受講者にサンドボックス開発の楽しさと難しさを実感してもらえたなら,講師として冥利に尽きる.
サンプルプログラムには4種類の解析妨害機能を実装しており,APIフックで対処できるのはうち前半2つだけとなっている.限られた演習時間の制約上,3つ目以降の解析妨害機能を回避できた受講者はいなかった.解析トラックリーダーの岩村さんから,受講者の2割がギリギリ解けないような問題を作るようにと仰せつかっていたが,やや意地悪な問題設定だったと思う.
なお,今回は拙作のサンプルを用いたが,より多くの解析妨害機能を備えたOSSにpafishがある.pafishはBackdoor.Win32.Agent.dkbp(MD5: de1af0e97e94859d372be7fcf3a5daa5)など一部のマルウェアに流用されている.

はじめに

V2E: Combining Hardware Virtualization and Software Emulation for Transparent and Extensible Malware Analysis[PDF]“という論文を通して,Record and Replayを用いた解析環境の設計を学ぶ.

著者

この論文のラストオーサーであるYinはSyracuse Universityの助教である.彼はテイント解析の第一人者として知られ,現在はDECAFの開発を主導している.またBitBlaze ProjectのDawn Songや,マルウェア解析の大御所であるChristopher Kruegelと過去に共著を出している.
ファーストオーサーのYanは提案手法をAndroidに適用し,2013年に博士論文を著している.Android向けの拡張にあたっては,DECAFのサブプロジェクトであるDroidScopeを用いているようだ.

Record and Replay

あるいはLogging and Replay, Lockstep, 順序再演法,最小情報トレースなどと呼ばれるこれらは,仮想マシンモニタ上のイベントを記録し,再生するための技術である.乱暴に言うとhistoryからdockerfileやvagrantfileを作成し,deployするようなものだ.
一般にステートマシンにおける命令の出力は,内部状態から一意に与えられる.そこで,ある環境の初期状態と入力のみを記録(Record)し,同じ環境を別の環境の上に再生(Raplay)するといった試みがなされてきた.実マシンにおいては,時刻や割り込みなどの非決定性とその記録の困難性からRecord and Replayは不可能であるとされる.だが,仮想マシンモニタの世界ではこれらの問題をある程度無視できる.実際,VMware Workstationなどにこの機能は実装されている.
本論文では,マルウェア解析に求められるサンドボックスの特性を加味したRecord and Replayについて論じられている.かつて大居さん(@a4lg)が研究されていた内容に近い.

背景

マルウェアを解析するためのサンドボックスは検出不可能(transparent)であり,なおかつ拡張可能であるべきだ.
いかに仮想マシンモニタの存在を隠蔽しようと,Timing Attackなどが成り立つ以上,transparentなサンドボックスを実現することは不可能である.しかし,Intel VTなどハードウェア仮想化技術を用いることで,ほぼtransparentな環境を作り出すことは可能である.例えば,Etherは命令・システムコール単位の解析機能を備えているが,Xenをベースに高いtransparent性を実現している.
だが,ハードウェア仮想化技術に依存した解析環境には拡張性の点で問題がある.特に,instrumentationによる詳細な解析が不可能であるという点は,大きな痛手である.
他方,QEMUなどのソフトウェアエミュレーションは動的バイナリ変換を用いることでDBI(dynamic binary instrumentation)と解析効率の向上を実現している.しかし,transparentなサンドボックスには至れない.
QEMUの動的バイナリ変換はバイナリをコードブロック単位で変換し,softmmuからTLBを参照してアドレス変換を行うというものだ.
そしてその性質から,以下のような自己書き換えコードによって検出されうる.

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
__asm{
xor eax, eax
mov edi, target
mov esi, copy_insn
mov ecx, 3
target:
rep movsw // f3 66 a5
jmp qemu_found // eb XX
nop // 90
non_qemu:
...
qemu_found:
...
copy_insn:
_emit 0xeb // eb
_emit 0x01 // 01
_emit 0x90 // 90
_emit 0xeb
_emit 0x01
_emit 0x90
}

実マシンでは,ecx*word=6バイトが1回で書き換わるが,QEMUの動的バイナリ変換では2バイトずつ3回で書き換わる.そのため,1回目の書き換えで0xeb 0x01 0xa5すなわちjmp +01になってしまい,qemu_foundにジャンプしてしまう.
その他には,ページ境界を越えてブロックの変換が行われた場合にページフォルトが発生してしまうことも考えられるし,ブロック境界でのみ割り込みが行われる,CPUサイクルの消費が著しいといった実マシンとの相違点もある.
さらに問題なのが,フラグの遅延評価だ.例えば,cmpjzの組み合わせなどの条件分岐は,EFLAGSを更新する.だが,QEMUにおいてはcmpが実行される段階でEFLAGSの計算は行われない.実際に計算されるのはjzの実行時,それも分岐を決定するためのZFのみが計算される.この設計はエミュレーションの高速化に寄与しているが,もちろん検出に用いることが可能だ.
当然ながら命令のエミュレーション自体にも限界がある.SIMDさえ厳しいのだ,システム管理モードやIntel TXTなんてものは考えたくないだろう.
このように,QEMUによるエミュレーションが検出される余地は枚挙に暇がない.
なお,解析環境検出をテーマとした最近の研究では,第2回システム系論文輪読会で紹介したBareCloudや忠鉢さん(@yuzuhara)のTENTACLEなどがある.

研究目的

ハードウェア仮想化技術を用いる解析環境とソフトウェアエミュレーションを用いる解析環境にはそれぞれ問題がある.そこで,ハードウェア仮想化技術を用いる解析環境でRecordを行い,ソフトウェアエミュレーションを用いる解析環境でReplayを行うことで,検出不可能性と拡張可能性という二つの目的について達成したのが,今回紹介するV2Eである.

形式的定義

本論文におけるRecord and Replayの設計はどのようなものか.
Recorderにおける遷移関数fについて,毎時iにおけるプログラムの状態をSiとし,入力をIiとする.すなわちfSi= f(Si-1, Ii-1)と表される.
次にReplayerにおける遷移関数f'についてプログラムの初期状態をS0とし,全ての入力をIとしたとき,f = f'と言えないだろうか.
これは二つのチューリング機械の同値性が解決可能であるかという問題に相当する.だがEQTM= {(M1, M2)|M1とM2はTMであり,L(M1) = L(M2)}は判定不可能とされ(『計算理論の基礎』における定理5.4),実装上でもハードウェアの割り込みなどの要因からf != f'となってしまう.
そこでV2Eは,プログラムの初期状態と全ての入力を保存することに加え,Sj= S'jとなるようなjについて状態の変化を保存することにした.これはj= Sj- Sj-1と表される.
ここで,新たな遷移関数f'rS'i= f'r(S'i-1, Ii-1, ⊿i)として定義する.これは,i!= nullのときS'i-1+ ⊿iと同値であり,それ以外についてf'(Si-1, Ii-1)と同値をとる.
なお,S0, I, およびf'r, S'i= Sii ∈ [0, n]について恒真である.
これを実装に起こすと,特定の命令やイベントが正しくエミュレート可能な場合は単にそれらをエミュレートし,そうでなければ状態の変化を記録し,Replayにあたって変更を適用するというアプローチになる.

理論と実際

プログラムの大部分を占めるmov, push, popなどのデータ転送命令,call, ret, jz, jmpなどの制御転送命令,add, shlなどの整数演算命令におけるエミュレーションは失敗しないものと見做せる.これらはそのままRecorderでエミュレートされる.
一方で,割り込み,MMIO, Port IO, DMA, TSCについては,V2Eは既存研究を踏襲し,監視領域においてのみこれらをRecordするようになっている.
では,例外,モデル固有レジスタ,cpuidはどうすべきだろうか.既存手法は,これらのエミュレーションは困難であるという理由から,そもそも入力として扱わない戦略を採っていたようだ.V2Eではこれらを, すなわちエミュレートが困難な状態の変化としてRecordすることで,Replayの正確性を高めている.
次に問題となるのが浮動小数点演算とSIMDの扱いだ.MMXやSSEを正確にエミュレートするのは難しい.だが,として命令の結果を記録する設計は大幅なパフォーマンスの低下を招く.そこでV2EはReplayにあたってこれらの命令をパススルーする.もちろん,ReplayerはSIMDをサポートしているマシンで実行されることが前提にある.

Transparent Recorder

RecorderはKVMを用いて実装されている.
V2EはRecord対象の領域とそれ以外のシステムを分割するため,TDP(two dimensional paging)を用いている.何のことかと思ったら,Intel EPTやAMD NPTの総称らしい.

要するに,TDPとは仮想マシンと物理マシン間のページテーブルのことだ.通常のページングでは,メモリアクセスに応じてMMUによってページテーブルが参照され,仮想アドレスが物理アドレスへと変換される.TDPでは,ゲストマシンからの仮想メモリ空間へのアクセスに応じてCR3にセットされたページテーブルが参照され,ゲスト物理アドレスがホスト物理アドレスへと変換される.
この仕組を用いたV2Eは,監視対象用のTDPテーブルと,それ以外用のTDPテーブルを別々に作成する.マルウェアに属するページはCR3の監視に基づき監視対象用のTDPテーブルに書き込まれる.マルウェアとそれ以外の部分のインタラクションはTDPページフォルトやVMExitによって媒介される.共有されるデータは読み取り専用として双方に与えられる.なお,TDPページフォルトに応じてCPUの状態が入力Iiとして保存される.

Precise Replayer

ReplayerはTEMUを用いて実装されている.バイナリ変換器に中間表現がなく,単純に古いQEMU 0.9.1をベースとしているのが玉に瑕だが,TEMUは動的テイント解析に求められる機能をほぼ網羅している.
TEMUはプラグインを共有ライブラリとしてロードし,コールバック関数からテイント解析の機能を呼び出すプラットフォームとなっている.V2EのReplayerは既存のプラグインであるtracecap, unpackerを用いる.だがRecordされたログには監視対象の情報のみが記述されているため,
return ((*TEMU_cpu_hflags & HF_CPL_MASK) != 3)といった,現在実行しているコードが解析対象のものかどうか判定するコードは除去されているようだ.TEMUのプラグインについては,こうした僅かな変更しか施されていない.
一方でその下で動くQEMUには結構手が加えられている.フラグの遅延評価は廃止され,ページフォルト以外の例外は除去されており,SIMDについては独自のヘルパー関数が追加されたようだ.QEMUのdyngenに手を加えるのはなかなか骨の折れる作業だと思う.
さて,Replayを行うためには,ReplayerはRecorderと同様のページングの仕組みをエミュレーションによって再現しなければならない.そこで,V2Eは物理ページコンテナという仕組みを用いている.これは,物理ページがログからロードされていることを示すものである.通常,物理ページコンテナは監視対象用のTDPテーブルを複製する.Replayされたプログラムが物理ページコンテナに存在しないページにアクセスした場合,Replayerは適切なタイミングでログからCPUの状態を復元し,ロードするようになっている.
V2Eにおけるテイント解析はおそらく,マルウェアのメモリ領域を正確に把握するためのものではない.それは,Recorderの段階でTDPテーブルの分割というアプローチによって実現されるべきものだからだ.TEMUのプラグインを使いたかったのだろうが,論文中からはあまりテイント解析を導入することのメリットが読み取れなかった.ここでのテイント解析は解析環境検出の対策に先立つものとして設定されているのだろうか.

評価

解析環境検出については問題なし.cpuid, rdtsc, cmpxch8b, icbp, rep stosb, fnstcwといった命令や,一般保護例外などについてテストされている.なお,rdtscについてはVMRESUME前にホストのTSCを参照することで対応している.
in-the-wildのマルウェアについても実験が行われており,アンパッカーとして期待できるパフォーマンスを見せている.
Recordにおける速度だが,コンテキストスイッチが頻繁に発生するカーネルモードルートキットで17倍,Internet Explorerで5倍と高速だ.KVMのシングルステップモードには3000倍のオーバーヘッドがあると言うのに.
全体的にpositiveな結果で,かなり良い.

おわりに

マルウェアによる解析環境検出に対して,復数の環境を組み合わせたRecord and Replayを用いる研究について紹介した.何より,監視対象とそれ以外で別個にTDPテーブルを作るというアプローチが素晴らしい.自分も研究でテイント解析を扱っているが,この流れを包摂していきたい.
ただ,これはサンドボックス全般に言えることなのだが,感染ホストにおけるユーザーのブラウザ操作が条件分岐に影響するMITBマルウェアについて,どう対処すべきなのだろうか.正常系のユーザーのブラウザ操作をデータセットとしてRecorderに与えたいところだが,果たしてどうなるのか.
このエントリはシステム系論文紹介 Advent Calendar 2014の9日目として書かれた.

参考文献

はじめに

選択的シンボリック実行について紹介する.

シンボリック実行

シンボリック実行とは,プログラムに含まれる変数に具体値を入力せず,その代わりとして値を代表するシンボルの操作を通じてプログラムを模擬的に実行し,結果を評価する技術である.シンボリック実行の目的は,コードカバレッジの拡大にある.シンボリック実行は全てのケースに対してforkする,あるいは,条件分岐の制約をもとにテストケースを生成するといった形態でソフトウェアテストに用いられている.
ひとまず,以前書いたシンボリック実行に入門しようとしたをご覧頂きたい.

選択的シンボリック実行

選択的シンボリック実行(selective symbolic execution)は,シンボリック実行の弱点を改善すべくS2Eにて提案,実装された.
シンボリック実行には実行パスにおける計算爆発(path explosion)の問題があった.プログラム中の全ての実行パスを通るための制約はあまりにも多い.そして,全ての実行パスというのは解析対象の実行パスだけではない.考えてもみよう,実システムでプログラムを実行した際,プログラムは自身以外の様々なものを呼び出す.呼び出されるlibcなどのライブラリ,そしてカーネルやデバイスドライバ,さらにそのファームウェアは,一体どこまで解析対象における実行パスの分岐に影響を与えるのか.
この頭が痛い問題に対応するべく編み出されたのが選択的シンボリック実行,すなわちシンボリック実行を行う範囲の限定である.S2Eはシンボリック実行を行いたい部分以外に具体値(concrete value)を用いることで,解析対象のプログラムだけにシンボリック実行を適用する(concolic testing).具体的には,S2Eは指定した変数が使用されている部分のみシンボリック実行を適用している.

S2E

動的バイナリ変換

S2Eは,QEMUをベースに開発された.QEMUはエミュレーションを実現するべく,以下のような流れで動的バイナリ変換を行う.

1.ゲストコードの逆アセンブル
2.マイクロオペレーションに変換
3.コード辞書を参照してホストコードに変換

S2Eはこのコード辞書をLLVM bitcodeに差し替えることで,x86のバイナリをLLVM bitcodeに変換する.そして,変換後のLLVM bitcodeをKLEEに渡すことで,シンボリック実行を行う.
このとき,解析対象の全部分がLLVM bitcodeに変換されるわけではない

指定した変数が使用されている部分のみシンボリック実行を適用すると書いたように,S2Eはシンボル化したデータにアクセスしているか否かによって,解析対象の実行方式を切り替えている.実行方式は以下の二通りだ.

  1. 具体値にアクセスしている場合,通常通り実行
  2. シンボルにアクセスしている場合,LLVM bitcodeに変換してKLEE上で実行

これは,解析対象を多数のコードブロックに分割するというQEMUのバイナリ変換方式に極めて依存している.

S2E opcodes

S2Eは独自の拡張命令S2E opcodesを用いてシンボリック実行のための機能をinstrumentする.S2E opcodesは以下のような機能を提供する.

  • S2SYM: データのシンボル化
  • S2ENA: 複数パスの実行を有効化
  • S2DIS: 複数パスの実行を無効化
  • S2OUT: デバッグ情報の出力

この中身はs2e-x86.hs2e.hに記述されている.例としてS2SYMの実装を見てみよう.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define S2E_INSTRUCTION_COMPLEX(val1, val2) \
".byte 0x0F, 0x3F\n" \
".byte 0x00, 0x" #val1 ", 0x" #val2 ", 0x00\n" \
".byte 0x00, 0x00, 0x00, 0x00\n"
#define S2E_INSTRUCTION_SIMPLE(val) \
S2E_INSTRUCTION_COMPLEX(val, 00)
~ 略 ~
static inline void s2e_make_symbolic(void *buf, int size, const char *name)
{
__s2e_touch_string(name);
__s2e_touch_buffer(buf, size);
__asm__ __volatile__(
S2E_INSTRUCTION_SIMPLE(03)
: : "a" (buf), "d" (size), "c" (name) : "memory"
);
}

例えば,解析対象のソースコード上でs2e_make_symbolic()の引数にシンボル化したい変数を渡すことで,この関数を利用することができる.

Windowsデバイスドライバに対する選択的シンボリック実行

Analyzing Windows Drivers: Step-by-Step Tutorialという公式チュートリアルでは,プラグインを用いてWindowsデバイスドライバにアノテーションを付加する方法が紹介されている.

1
2
3
4
5
6
7
8
9
10
11
12
13
function annotation_example(state, plg)
-- Write custom Lua code here (e.g., to inject symbolic values)
end
pluginsConfig.Annotation =
{
init1 = {
active=true,
module="pcntpci5_sys_1",
address=0x169c9,
instructionAnnotation="annotation_example"
}
}

チュートリアルの例ではpcntpci5.sysというドライバが0x169c9というアドレスを呼び出す際にannotation_example()が実行される.BSoDもフックできるので,NotMyFaultドライバで遊ぼう.

オーバーヘッド

いつだって問題となるのは実行速度だ.論文によると,S2Eは具体値による実行時(concrete mode)にQEMUの6倍,シンボリック実行時にQEMUの78倍のオーバーヘッドが生ずるとされている.注意したいのは,実機の78倍ではなくQEMUの78倍である点だ.

おわりに

S2Eについて紹介した.
このエントリはソフトウェアテストあどべんとかれんだー2014の3日目として書かれた.

参考文献

より詳しくは以下の論文を参照されたい.これも全てVitaly Chipounovって奴の仕業なんだ.