Unicorn

Unicorn&&Unidbg

Unicorn简介

Unicorn-In-Android

Unicorn是一个轻量级、多平台、多架构的CPU模拟器框架,使用Unicorn的API可以轻松控制CPU寄存器、内存等资源,调试或调用目标二进制代码

Unicorn安装

pip install unicorn即可

Unicorn使用

首先导入我们想要模拟执行代码需要的库模块,这里是x86

1
2
from unicorn import *
from unicorn.x86_const import *

接着指定我们想要执行的数据必须是bytes型的数据

1
code=bytes([0x55, 0x8B, 0xEC, 0x51, 0x8B, 0x55, 0x0C, 0xB9, 0xFF, 0x00, 0x00, 0x00, 0x89, 0x4D, 0xFC, 0x85, 0xD2, 0x74, 0x51, 0x53, 0x8B, 0x5D, 0x08, 0x56, 0x57, 0x6A, 0x14, 0x58, 0x66, 0x8B, 0x7D, 0xFC, 0x3B, 0xD0, 0x8B, 0xF2, 0x0F, 0x47, 0xF0, 0x2B, 0xD6, 0x0F, 0xB6, 0x03, 0x66, 0x03, 0xF8, 0x66, 0x89, 0x7D, 0xFC, 0x03, 0x4D, 0xFC, 0x43, 0x83, 0xEE, 0x01, 0x75, 0xED, 0x0F, 0xB6, 0x45, 0xFC, 0x66, 0xC1,0xEF, 0x08, 0x66, 0x03, 0xC7, 0x0F, 0xB7, 0xC0, 0x89, 0x45, 0xFC, 0x0F, 0xB6, 0xC1, 0x66, 0xC1, 0xE9, 0x08, 0x66, 0x03, 0xC1, 0x0F, 0xB7, 0xC8, 0x6A, 0x14, 0x58, 0x85, 0xD2, 0x75, 0xBB, 0x5F, 0x5E, 0x5B, 0x0F, 0xB6, 0x55, 0xFC, 0x8B, 0xC1, 0xC1, 0xE1, 0x08, 0x25, 0x00, 0xFF, 0x00, 0x00, 0x03, 0xC1, 0x66, 0x8B, 0x4D, 0xFC, 0x66, 0xC1, 0xE9, 0x08, 0x66, 0x03, 0xD1, 0x66, 0x0B, 0xC2])

指定我们想要代码模拟运行时的地址

1
ADDRESS=0x400000

使用Uc类来初始化Unicorn实例,参数一是硬件架构、参数二是硬件模式,这里创建的环境是32位X86架构

1
mu=Uc(UC_ARCH_X86,UC_MODE_32)

使用mem_map()分配内存

1
2
mu.mem_map(ADDRESS,2*1024*1024)
#内存的大小必须是1024的倍数,这里分配的是2MB

将要模拟运行的代码写入内存中,使用mem_write(ADDRESS,code)

1
mu.mem_write(ADDRESS,code)

通过reg.write()可以设置寄存器的值

1
mu.reg_write(UC_X86_REG_EDX,0x1234)

使用emu_start()方法模拟运行

1
2
mu.emu_start(ADDRESS, ADDRESS+len(code))
#需要指定起始地址和终止地址

运行完毕后使用reg_read()方法读取寄存器的值即可

1
data=mu.reg_read(UC_X86_REG_EDX)

Unicorn使用案例

Flare-on4 3

中间一大段无法被正确反编译的明显是被加密过的,而前面do……while进行的正是解密操作,经过socket_receive函数处理后的buf的第一个字节用于解密操作

进入socket_receive函数

使用socket对本地的2222端口设置监听,accept函数如果没有接受到连接会一直处于阻塞状态,所以在这里会卡住

可以看到2222端口正在被监听

我们可以编写python脚本来发送数据

1
2
3
4
5
6
7
8
9
10
11
12
from socket import *

HOST = '127.0.0.1'
POST = 2222
BufSize = 1024
ADDR = (HOST, POST)
data = b'hello'
#初始化socket对象,AF_INET/SOCK_STREAM表示是TCP
socket_ = socket(AF_INET, SOCK_STREAM)
socket_.connect(ADDR)
socket_.send(data)
#这里的data必须是bytes型数据

accept之后创建一个socket对象,然后将使用recv函数接收并存入buf中

然后就将我们发送数据的第一个字节作为key进行SMC自解密

接着将SMC后数据的起始地址和长度121传入check进行校验

然后根据校验的情况send不同的数据到客户端

socket爆破

由于SMC解密用到的只有一个字节,所以我们可以爆破,不断传入数据直到接收到Congratulations! But wait, where’s my flag?

由于接收消息之后程序就直接关闭了,所以我们需要使用到os.startfile(exe)来运行程序,我们还需要将整数转为bytes型数据后发送,要用到struct.pack(“I”,data),其作用如下

  • 按照指定格式将Python数据转换为字符串,该字符串为字节流,如网络传输时,不能传输int,此时先将int转化为字节流,然后再发送;
  • 按照指定格式将字节流转换为Python指定的数据类型;

同时struct也可以指定大小端序

爆破脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from socket import *
import os
import time
import struct
HOST = '127.0.0.1'
POST = 2222
BufSize = 1024
ADDR = (HOST, POST)
data = b'hello'
exe_file = "greek_to_me.exe"
print(struct.pack("I", 0xA2))
for i in range(0, 256):
os.startfile(exe_file)
time.sleep(0.1)
socket_ = socket(AF_INET, SOCK_STREAM)
socket_.connect(ADDR)
socket_.send(struct.pack('I', i)) # 接收到错误的数据之后就关闭了
data = socket_.recv(BufSize)
print(data)
socket_.close()
if b'Congratulations' in data:
print("The key is %x" % i)
break

Unicorn

也是采用爆破的方式,这里模拟运行的是check函数,不断将SMC解密后的数据和数据的长度写入Unicorn的内存中并执行check函数,直到其返回值为0xFB5E为止

首先我们需要提取出被加密的数据和要执行的代码的机器码并转为字节流

1
2
3
4
5
enc_data = [0x33, 0xe1, 0xc4, 0x99, 0x11, 0x6, 0x81, 0x16, 0xf0, 0x32, 0x9f, 0xc4, 0x91, 0x17, 0x6, 0x81, 0x14, 0xf0, 0x6, 0x81, 0x15, 0xf1, 0xc4, 0x91, 0x1a, 0x6, 0x81, 0x1b, 0xe2, 0x6, 0x81, 0x18, 0xf2, 0x6, 0x81, 0x19, 0xf1, 0x6, 0x81, 0x1e, 0xf0, 0xc4, 0x99, 0x1f, 0xc4, 0x91, 0x1c, 0x6, 0x81, 0x1d, 0xe6, 0x6, 0x81, 0x62, 0xef, 0x6, 0x81, 0x63, 0xf2, 0x6, 0x81, 0x60, 0xe3, 0xc4, 0x99, 0x61, 0x6, 0x81, 0x66, 0xbc, 0x6, 0x81, 0x67, 0xe6, 0x6, 0x81, 0x64, 0xe8, 0x6, 0x81, 0x65, 0x9d, 0x6, 0x81, 0x6a, 0xf2, 0xc4, 0x99, 0x6b, 0x6, 0x81, 0x68, 0xa9, 0x6, 0x81, 0x69, 0xef, 0x6, 0x81, 0x6e, 0xee, 0x6, 0x81, 0x6f, 0xae, 0x6, 0x81, 0x6c, 0xe3, 0x6, 0x81, 0x6d, 0xef, 0x6, 0x81, 0x72, 0xe9, 0x6, 0x81, 0x73, 0x7c, 0x6a]
enc_data = bytes(enc_data)

code = [0x55, 0x8B, 0xEC, 0x51, 0x8B, 0x55, 0x0C, 0xB9, 0xFF, 0x00, 0x00, 0x00, 0x89, 0x4D, 0xFC, 0x85, 0xD2, 0x74, 0x51, 0x53, 0x8B, 0x5D, 0x08, 0x56, 0x57, 0x6A, 0x14, 0x58, 0x66, 0x8B, 0x7D, 0xFC, 0x3B, 0xD0, 0x8B, 0xF2, 0x0F, 0x47, 0xF0, 0x2B, 0xD6, 0x0F, 0xB6, 0x03, 0x66, 0x03, 0xF8, 0x66, 0x89, 0x7D, 0xFC, 0x03, 0x4D, 0xFC, 0x43, 0x83, 0xEE, 0x01, 0x75, 0xED, 0x0F, 0xB6, 0x45, 0xFC, 0x66, 0xC1,0xEF, 0x08, 0x66, 0x03, 0xC7, 0x0F, 0xB7, 0xC0, 0x89, 0x45, 0xFC, 0x0F, 0xB6, 0xC1, 0x66, 0xC1, 0xE9, 0x08, 0x66, 0x03, 0xC1, 0x0F, 0xB7, 0xC8, 0x6A, 0x14, 0x58, 0x85, 0xD2, 0x75, 0xBB, 0x5F, 0x5E, 0x5B, 0x0F, 0xB6, 0x55, 0xFC, 0x8B, 0xC1, 0xC1, 0xE1, 0x08, 0x25, 0x00, 0xFF, 0x00, 0x00, 0x03, 0xC1, 0x66, 0x8B, 0x4D, 0xFC, 0x66, 0xC1, 0xE9, 0x08, 0x66, 0x03, 0xD1, 0x66, 0x0B, 0xC2]
code = bytes(code)

接着将加密后的数据进行解密后写入Unicorn中

1
2
3
4
5
6
def decode_bytes(key):
for i in range(len(enc_data)):
# 声明为全局变量之后才能在函数内部修改
global decoded_bytes
decoded_bytes[i] = ((enc_data[i] ^ key)+0x22) & 0xff
#由于python没有对变量进行限制,所以需要&0xFF

Unicorn模拟执行部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def run_code(bytes):
# 指定代码模拟执行的地址
ADDRESS = 0x400000
SIZE = 2*1024*1024
STACK_ADDRESS = 0x410000
DEC_BYTES_ADDR = 0x420000
# 使用Uc类来初始化Unicorn对象,其中第一个参数是硬件架构,第二个参数是硬件模式
mu = Uc(UC_ARCH_X86, UC_MODE_32)
# 创建模拟代码运行的内存空间,这里设置为2MB,在这里运行的代码只能访问该内存,内存默认属性为
mu.mem_map(ADDRESS, SIZE)
# 接下来将代码和解密后的数据写入地址中
mu.mem_write(ADDRESS, code)
mu.mem_write(DEC_BYTES_ADDR, bytes)
# 由于函数取出了栈的内容,所以我们要先将数据写入栈中

mu.reg_write(UC_X86_REG_ESP, STACK_ADDRESS) # 将栈的地址存入ESP中
# 使用struct.pack将地址转为bytes之后写入栈中
mu.mem_write(STACK_ADDRESS+4, struct.pack('<I',
DEC_BYTES_ADDR)) # 将堆栈的情况还原
mu.mem_write(STACK_ADDRESS+8, struct.pack('<I', 0x79))
# 执行代码,第一二个参数为起始地址和终止地址
mu.emu_start(ADDRESS, ADDRESS+len(code))
check_sum = mu.reg_read(UC_X86_REG_AX)
return check_sum

需要注意的是check函数是有参数的,所以我们需要先设置栈和ESP,并且将解密数据的地址和解密数据的长度按照顺序存入栈中,根据IDA的栈视图可以更快地确定函数参数在栈中地关系

爆破主体部分

1
2
3
4
5
6
7
8
9
for i in range(0, 256):
decode_bytes(i)
check_sum = run_code(bytes(decoded_bytes))
if check_sum == 0xFB5E:
print("The key is ", hex(i))
md = Cs(CS_ARCH_X86, CS_MODE_32)
for j in md.disasm(bytes(decoded_bytes), 0x40107C):
print("0x%x:\t%s\t%s" % (j.address, j.mnemonic, j.op_str))
break

当check的函数的返回值等于0xFB5E之后

使用capstone中的Cs指定代码的架构和模式,disasm(bytes_code,offset)将bytes字节流转为汇编代码,disasm的参数分别是机器码的字节流和偏移(我们指定)

完整代码

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
from pickletools import bytes1
from socket import *
from capstone import *
import binascii
import time
import os
import sys
from unicorn import *
from capstone import x86_const
from unicorn.x86_const import *
import struct


# 下面使用unicorn模拟执行check函数,进而得到我们想要的key
enc_data = [0x33, 0xe1, 0xc4, 0x99, 0x11, 0x6, 0x81, 0x16, 0xf0, 0x32, 0x9f, 0xc4, 0x91, 0x17, 0x6, 0x81, 0x14, 0xf0, 0x6, 0x81, 0x15, 0xf1, 0xc4, 0x91, 0x1a, 0x6, 0x81, 0x1b, 0xe2, 0x6, 0x81, 0x18, 0xf2, 0x6, 0x81, 0x19, 0xf1, 0x6, 0x81, 0x1e, 0xf0, 0xc4, 0x99, 0x1f, 0xc4, 0x91, 0x1c, 0x6, 0x81, 0x1d, 0xe6, 0x6, 0x81, 0x62, 0xef, 0x6, 0x81, 0x63, 0xf2, 0x6,
0x81, 0x60, 0xe3, 0xc4, 0x99, 0x61, 0x6, 0x81, 0x66, 0xbc, 0x6, 0x81, 0x67, 0xe6, 0x6, 0x81, 0x64, 0xe8, 0x6, 0x81, 0x65, 0x9d, 0x6, 0x81, 0x6a, 0xf2, 0xc4, 0x99, 0x6b, 0x6, 0x81, 0x68, 0xa9, 0x6, 0x81, 0x69, 0xef, 0x6, 0x81, 0x6e, 0xee, 0x6, 0x81, 0x6f, 0xae, 0x6, 0x81, 0x6c, 0xe3, 0x6, 0x81, 0x6d, 0xef, 0x6, 0x81, 0x72, 0xe9, 0x6, 0x81, 0x73, 0x7c, 0x6a]
enc_data = bytes(enc_data)

code = [0x55, 0x8B, 0xEC, 0x51, 0x8B, 0x55, 0x0C, 0xB9, 0xFF, 0x00, 0x00, 0x00, 0x89, 0x4D, 0xFC, 0x85, 0xD2, 0x74, 0x51, 0x53, 0x8B, 0x5D, 0x08, 0x56, 0x57, 0x6A, 0x14, 0x58, 0x66, 0x8B, 0x7D, 0xFC, 0x3B, 0xD0, 0x8B, 0xF2, 0x0F, 0x47, 0xF0, 0x2B, 0xD6, 0x0F, 0xB6, 0x03, 0x66, 0x03, 0xF8, 0x66, 0x89, 0x7D, 0xFC, 0x03, 0x4D, 0xFC, 0x43, 0x83, 0xEE, 0x01, 0x75, 0xED, 0x0F, 0xB6, 0x45, 0xFC, 0x66, 0xC1,
0xEF, 0x08, 0x66, 0x03, 0xC7, 0x0F, 0xB7, 0xC0, 0x89, 0x45, 0xFC, 0x0F, 0xB6, 0xC1, 0x66, 0xC1, 0xE9, 0x08, 0x66, 0x03, 0xC1, 0x0F, 0xB7, 0xC8, 0x6A, 0x14, 0x58, 0x85, 0xD2, 0x75, 0xBB, 0x5F, 0x5E, 0x5B, 0x0F, 0xB6, 0x55, 0xFC, 0x8B, 0xC1, 0xC1, 0xE1, 0x08, 0x25, 0x00, 0xFF, 0x00, 0x00, 0x03, 0xC1, 0x66, 0x8B, 0x4D, 0xFC, 0x66, 0xC1, 0xE9, 0x08, 0x66, 0x03, 0xD1, 0x66, 0x0B, 0xC2]
code = bytes(code)
# print(code)

# 首先先对enc_data进行smc,之后将code和encdata、encdata的长度写入unicorn的栈中模拟执行(因为check是从栈中取值的),通过eax的值传入

decoded_bytes = [0]*len(enc_data)


def decode_bytes(key):
for i in range(len(enc_data)):
# 声明为全局变量之后才能在函数内部修改
global decoded_bytes
decoded_bytes[i] = ((enc_data[i] ^ key)+0x22) & 0xff


def run_code(bytes):
# 指定代码模拟执行的地址
ADDRESS = 0x400000
SIZE = 2*1024*1024
STACK_ADDRESS = 0x410000
DEC_BYTES_ADDR = 0x420000
# 使用Uc类来初始化Unicorn对象,其中第一个参数是硬件架构,第二个参数是硬件模式
mu = Uc(UC_ARCH_X86, UC_MODE_32)
# 创建模拟代码运行的内存空间,这里设置为2MB,在这里运行的代码只能访问该内存,内存默认属性为
mu.mem_map(ADDRESS, SIZE)
# 接下来将代码和解密后的数据写入地址中
mu.mem_write(ADDRESS, code)
mu.mem_write(DEC_BYTES_ADDR, bytes)
# 由于函数取出了栈的内容,所以我们要先将数据写入栈中

mu.reg_write(UC_X86_REG_ESP, STACK_ADDRESS) # 将栈的地址存入ESP中
# 使用struct.pack将地址转为bytes之后写入栈中
mu.mem_write(STACK_ADDRESS+4, struct.pack('<I',
DEC_BYTES_ADDR)) # 将堆栈的情况还原
mu.mem_write(STACK_ADDRESS+8, struct.pack('<I', 0x79))
# 执行代码,第一二个参数为起始地址和终止地址
mu.emu_start(ADDRESS, ADDRESS+len(code))
check_sum = mu.reg_read(UC_X86_REG_AX)
return check_sum


for i in range(0, 256):
decode_bytes(i)
check_sum = run_code(bytes(decoded_bytes))
if check_sum == 0xFB5E:
print("The key is ", hex(i))
md = Cs(CS_ARCH_X86, CS_MODE_32)
for j in md.disasm(bytes(decoded_bytes), 0x40107C):
print("0x%x:\t%s\t%s" % (j.address, j.mnemonic, j.op_str))
break

Unicorn-Hook

Unicorn还提供了hook_add()方法来Hook

这种类型的hook在执行每条指令前都会先执行hook_code

hook_code函数 该函数需要以下参数:

  • Uc实例
  • 指令的地址
  • 指令的大小
  • 用户数据(我们可以在hook_add()的可选参数中传递这个值)
1
2
mu.hook_add(UC_HOOK_CODE,hook_code,start,end)
#hook_code是实现Hook的函数,start是被Hook代码的起始地址,end是被Hook代码的终止地址

Unicorn执行程序

Unicorn执行程序其实和前面的执行一段执行是类似的,唯一不同的就是需要read文件。最后按照IDA中的偏移来设置开始执行的地址,这样方便我们对照和写入数据。

但是要注意有些外部函数比如printf之类的,由于加载进虚拟内存中所以无法使用

通过Hook来修改修改EIP/RIP跳过执行即可

对于写入内存中的int型数据首先使用struct.pack()转为bytes,然后再写入,也可以通过pwntools直接转换

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
from unicorn import *
from unicorn import *
from capstone import x86_const
from unicorn.x86_const import *
import struct
from pwn import *

instruction_skip_list = [
0x4004ef, 0x4004f6, 0x400502, 0x40054f, 0x4004d0, 0x4004a0, 0x4004a6, 0x4004b0]
ENTRY = 0x400670
END = [0x400709, 0x4006F1]
stack = []
d = {}


def read(name):
with open(name, "rb") as f:
return f.read()
# 将int型数据转为bytes


# def p32(num):
# return struct.pack("I", num)

# # 将bytes转为int


# def u32(data):
# return struct.unpack("I", data)[0]


def hook_add(mu, address, size, user_data):
#print('>>>Tracing Instruction at 0x%x, instruction size = 0x %x' %(address, size))
# 修改RSP跳过无法执行的指令
if address in instruction_skip_list:
mu.reg_write(UC_X86_REG_RIP, address+size)
# 程序puts flag的函数地址400560
elif address == 0x400560:
chr_data = mu.reg_read(UC_X86_REG_RDI)
print(chr(chr_data), end='')
# 跳过这条指令
mu.reg_write(UC_X86_REG_RIP, address+size)
elif address == ENTRY:
# 在IDA中可以看到函数参数存储的寄存器,第一个参数是RDI,第二个是RSI
arg0 = mu.reg_read(UC_X86_REG_RDI)
p_rsi = mu.reg_read(UC_X86_REG_RSI)
# RSI存储的是一个指针,需要读取出来后转为int型数据
arg1 = u32(mu.mem_read(p_rsi, 4))
# 如果之前调用过的
if (arg0, arg1) in d:
(ret_rax, ret_ref) = d[(arg0, arg1)]
# print(stack)
# print(d)
mu.reg_write(UC_X86_REG_RAX, ret_rax)
mu.mem_write(p_rsi, p32(ret_ref))
mu.reg_write(UC_X86_REG_RIP, 0x400582) # 直接结束
else:
# 存入栈中
stack.append((arg0, arg1, p_rsi))
elif address in END:
# 将末尾的元素赋值给元组
(arg0, arg1, p_rsi) = stack.pop()
ret_rax = mu.reg_read(UC_X86_REG_RAX)
ret_ref = u32(mu.mem_read(p_rsi, 4))
d[(arg0, arg1)] = (ret_rax, ret_ref) # 将返回值和传入的参数对应起来


def run(data):
# 指定架构和模式,64位
mu = Uc(UC_ARCH_X86, UC_MODE_64)
ADDRESS = 0x400000
SIZE = 1024*1024
STACK_ADDRESS = 0x0
# 分配虚拟空间
mu.mem_map(ADDRESS, SIZE)
mu.mem_map(STACK_ADDRESS, SIZE)
# 数据写入,栈顶设置指向栈的末尾,函数调用前的堆栈EBP处于栈底,ESP栈顶,之后会提升堆栈
mu.mem_write(ADDRESS, data)
mu.reg_write(UC_X86_REG_RSP, STACK_ADDRESS+SIZE-1)
mu.hook_add(UC_HOOK_CODE, hook_add, begin=ADDRESS, end=ADDRESS+SIZE)
mu.emu_start(0x4004E0, 0x400575)


data = read("./fibonacci")
run(data)

keystone和capstone

keystone将汇编代码转为机器码,而capstone将机器码转为汇编代码

Unidbg

Unibdg是基于unicorn的,项目地址

git clone下来后导入IDEA

打开在TTEncrypt运行,导入成功会得到如下输出