-
-
[原创] DEFCON-2023-Quals Blackbox 好玩的黑盒虚拟机代码执行
-
发表于: 2024-2-29 20:43 3049
-
165: Blackbox
题目环境搭建
说明
源码是rust
修复
代码有问题,src\main.rs
第377行改为println!("Executed too many instructions");
构建(rust x86_64-unknown-linux-gnu)
题目根目录执行cargo build
结果在target\debug\blackbox
部署
根据题目描述可知这题解题时不提供可执行文件,只有一个远程端口可以与程序交互
使用socat将题目绑定到10001端口
cd /home/yssf/ctf/blackbox/target/debug
socat tcp-listen:10001,fork exec:./blackbox,reuseaddr
初探
猜测开头输入格式
发送ascii格式字符串,"00"
等数字,回显Program too large!
p.send('00')
r = p.recv()
print(r)
# b'Program too large!\n'
猜测格式不是ascii
猜输入\x01
没有回显
p.sendline(b'\x01')
print(p.recv(timeout=1).decode())
# 无回显
直到`\x01\x00,回显一句"执行停止",后面的A、B、C、D、PC、SP应该是寄存器
p.sendline(b'\x01\x00')
print(p.recv(timeout=1).decode())
'''
Execution halted!
A: 0 B: 0 C: 0 D: 0: PC: 1 SP: 0
Would you like to start over? (y/n)
'''
开头接收一个2字节长度的bin数据,看样子是程序长度
start over
这个发送y,start over就是重新输入一次
发送n就会结束会话
p.sendline(b'\x01\x00')
print(p.recv(timeout=1).decode())
p.sendline(b'y')
print(p.recv(timeout=1).decode())
p.sendline(b'\x01\x00')
print(p.recv(timeout=1).decode())
'''
Execution halted!
A: 0 B: 0 C: 0 D: 0: PC: 1 SP: 0
Would you like to start over? (y/n)
Execution halted!
A: 0 B: 0 C: 0 D: 0: PC: 1 SP: 0
Would you like to start over? (y/n)
'''
猜测开头输入内容及后续
继续测试开头输入的内容
p.sendline(b'\x01\x00') # 无回显
p.sendline(b'\x01\x00\x00') # 回显
p.sendline(b'\x02\x00\x00\x00') # 无回显
p.sendline(b'\x02\x00\x00\x00\x00') # 回显
p.sendline(b'\x03\x00\x00\x00\x00\x00') # 无回显
p.sendline(b'\x03\x00\x00\x00\x00\x00\x00') # 无回显
开头的格式,用c语言结构体可以表示为:
unsigned short len;
unsigned short[len] code;
这是一个虚拟机,接收机器码并执行
def send(sc):
sc = int.to_bytes(len(sc) // 2, 2, 'little') + sc
p.send(sc)
f.write(p.recvuntil(b'Would you like to start over? (y/n)\n').decode())
p.sendline(b'y')
探索指令1-初探与爆破
按小端序,爆破看看情况
单指令-爆第一字节
爆破脚本,爆第一个字节,0x00??(0x0000 ~ 0x00FF):
for i in range(0x100):
sc = int.to_bytes(i, 1, 'little') + b'\x00'
f.write('0x'+sc[::-1].hex().upper())
send(sc)
爆破结果:
0x0000 ~ 0x0080
A: 0 B: 0 C: 0 D: 0: PC: 1 SP: 0
0x0081 ~ 0x00FF
A: 1 B: 0 C: 0 D: 0: PC: 1 SP: 0
A: 2 B: 0 C: 0 D: 0: PC: 1 SP: 0
...
A: 127 B: 0 C: 0 D: 0: PC: 1 SP: 0
猜测:
0x0080 ~ 0x00FF是设置A为立即数,或加上一个立即数
汇编A = x
或A += x
,对应机器码0x0080+x
测试两条0x0081,结果A=2,可以得出这个是A+=1而不是
A=1`:
sc = b'\x81\x00' + b'\x81\x00'
# A: 2 B: 0 C: 0 D: 0: PC: 2 SP: 0
同理爆破0x10??(0x0100~0x01FF)等的第一个字节
汇编和机器码对应关系:
x取值范围0~0xff
A += x => 0x0080 + x
B += x => 0x0180 + x
C += x => 0x0280 + x
D += x => 0x0380 + x
尝试爆破0x04??的第一个字节,发现似乎是无效指令
0x0400~0x04F
A: 0 B: 0 C: 0 D: 0: PC: 1 SP: 0
单指令-爆第二字节
爆破第二个字节0x??81(0x0081~0xFF81)
for i in range(0x100):
try:
sc = b'\x81' + int.to_bytes(i, 1, 'little')
f.write('0x'+sc[::-1].hex().upper())
send(sc)
except:
p.close()
p = remote(ip, port)
f.write('\nexception\n')
爆破结果,:
0x0081 ~ 0x0381
A/B/C/D += 1
0x0481 ~ 0x0781
A: 0 B: 0 C: 0 D: 0: PC: 1 SP: 0
似乎无效
0x0881 ~ 0x0981
PC/SP += 1
0x0A81 ~ 0x0F81
A: 0 B: 0 C: 0 D: 0: PC: 1 SP: 0
似乎无效
0x1081 ~ 0x1381
A: 65535 B: 0 C: 0 D: 0: PC: 1 SP: 0
A: 0 B: 65535 C: 0 D: 0: PC: 1 SP: 0
A: 0 B: 0 C: 65535 D: 0: PC: 1 SP: 0
A: 0 B: 0 C: 0 D: 65535: PC: 1 SP: 0
A/B/C/D -= 1
0x1481 ~ 0x1781
A: 0 B: 0 C: 0 D: 0: PC: 1 SP: 0
似乎无效
0x1881
没有回显,猜测是PC -= 1
0x1981
A: 0 B: 0 C: 0 D: 0: PC: 1 SP: 65535
SP -= 1
0x1A81 ~ 0x4F81
A: 0 B: 0 C: 0 D: 0: PC: 1 SP: 0
似乎无效
.....
整理1
故整理前面的爆破的部分结果得到:
x取值范围0~0xff
A += x => 0000 0000 1000 0000 + x
B += x => 0000 0001 1000 0000 + x
C += x => 0000 0010 1000 0000 + x
D += x => 0000 0011 1000 0000 + x
PC += x => 0000 1000 1000 0000 + x
SP += x => 0000 1001 1000 0000 + x
A -= x => 0001 0000 1000 0000 + x
B -= x => 0001 0001 1000 0000 + x
C -= x => 0001 0010 1000 0000 + x
D -= x => 0001 0011 1000 0000 + x
PC -= x => 0001 1000 1000 0000 + x
SP -= x => 0001 1001 1000 0000 + x
猜测指令集架构:
0000 0000 1 000'0000
bits ~12 11~8 7 6~0
desc op ri t imm
t(ype)=1:
imm: 无符号,0~127
ri: 0, A
1, B
2, C
3, D
4~7, unknown
8, PC
9, SP
10~15, unknown
op: 0, reg[ri] += imm
1, reg[ri] -= imm
2~15, unknown
t(ype)=0: unknown
指令组合器:
def combine(op, ri, ty, imm):
sc = (imm) & 0x7f
sc |= (ty << 7) & 1
sc |= (ri << 8) & 0xf
sc |= (op << 12) & 0xf
return int.to_bytes(sc, 2, 'little')
探索指令2-找type=1, 其余op的语义
type=1,爆破op
整理1中,op 2~15的操作未知,猜测是与或非等运算,测试一下
填充一下寄存器,然后爆破op:
for i in range(16):
try:
sc = b''
# 填充ABCD和SP=(88, 115, 53, 46, 77)
sc += combine(op=0, ri=0, ty=1, imm=0x58)
sc += combine(op=0, ri=1, ty=1, imm=0x73)
sc += combine(op=0, ri=2, ty=1, imm=0x35)
sc += combine(op=0, ri=3, ty=1, imm=0x2e)
sc += combine(op=0, ri=9, ty=1, imm=0x4d)
# ri=0(A), ty=1, imm=0x66=102, 爆破op
sc += combine(op=i, ri=0, ty=1, imm=0x66)
f.write('op={} '.format(i)+'0x'+sc[::-1].hex().upper())
send(sc, detail=True)
except BaseException as e:
p.close()
p = remote(ip, port)
f.write('\nException type={}\n'.format(type(e)))
结果,0~7是加减与或非等一元二元运算(2、3还不是很确定作用),15是syscall(imm),8~14不太确定:
ABCD SP=(88, 115, 53, 46, 77) imm=102
>>> 88+102
190
>>> 88-102+0x10000
65522
>>> 88&102
64
>>> 88|102
126
>>> (~88)+0x10000
65447
>>> 88^102
62
op=0 0x00E609CD03AE02B501F300D8
A: 190 B: 115 C: 53 D: 46: PC: 6 SP: 77
A += imm
op=1 0x10E609CD03AE02B501F300D8
A: 65522 B: 115 C: 53 D: 46: PC: 6 SP: 77
A -= imm
op=2 0x20E609CD03AE02B501F300D8
A: 0 B: 115 C: 53 D: 46: PC: 6 SP: 77
A=0?
op=3 0x30E609CD03AE02B501F300D8
A: 88 B: 115 C: 53 D: 46: PC: 6 SP: 77
A没变?
op=4 0x40E609CD03AE02B501F300D8
A: 64 B: 115 C: 53 D: 46: PC: 6 SP: 77
A &= imm
op=5 0x50E609CD03AE02B501F300D8
A: 126 B: 115 C: 53 D: 46: PC: 6 SP: 77
A |= imm
op=6 0x60E609CD03AE02B501F300D8
A: 62 B: 115 C: 53 D: 46: PC: 6 SP: 77
A ^= imm
op=7 0x70E609CD03AE02B501F300D8
A: 65447 B: 115 C: 53 D: 46: PC: 6 SP: 77
A = ~A
op=8 0x80E609CD03AE02B501F300D8
没回显 EOFError
op=9 0x90E609CD03AE02B501F300D8
A: 1 B: 115 C: 53 D: 46: PC: 6 SP: 77
A = 1
op=10 0xA0E609CD03AE02B501F300D8
A: 88 B: 115 C: 53 D: 46: PC: 102 SP: 77
PC = 102 (PC = imm)
op=11 0xB0E609CD03AE02B501F300D8
A: 88 B: 115 C: 53 D: 46: PC: 109 SP: 78
PC = 109, SP += 1 (PC = 6 + 1 + imm)
op=12 0xC0E609CD03AE02B501F300D8
A: 88 B: 115 C: 53 D: 46: PC: 6 SP: 77
不变
op=13 0xD0E609CD03AE02B501F300D8
A: 88 B: 115 C: 53 D: 46: PC: 6 SP: 78
SP += 1
op=14 0xE0E609CD03AE02B501F300D8
A: 0 B: 115 C: 53 D: 46: PC: 6 SP: 76
A = 0, SP -= 1 (A = stack[SP--] or A = stack[--SP])
op=15 0xF0E609CD03AE02B501F300D8
Invalid syscall number 88
A: 88 B: 115 C: 53 D: 46: PC: 6 SP: 77
将imm设为0x40=64,sp初始设为2,再爆破一次,0~8结果是一样的,主要关注9~15:
op=9 0x90C0098203AE02B501F300D8
A: 88 B: 115 C: 53 D: 46: PC: 6 SP: 2
不变
op=10 0xA0C0098203AE02B501F300D8
A: 88 B: 115 C: 53 D: 46: PC: 64 SP: 2
PC = 64 (PC = imm)
op=11 0xB0C0098203AE02B501F300D8
A: 88 B: 115 C: 53 D: 46: PC: 71 SP: 3
PC = 71, SP += 1 (PC = 6 + 1 + imm)
op=12 0xC0C0098203AE02B501F300D8
A: 88 B: 115 C: 53 D: 46: PC: 6 SP: 2
不变
op=13 0xD0C0098203AE02B501F300D8
A: 88 B: 115 C: 53 D: 46: PC: 6 SP: 3
SP += 1
op=14 0xE0C0098203AE02B501F300D8
A: 0 B: 115 C: 53 D: 46: PC: 6 SP: 1
A = 0, SP -= 1 (A = stack[SP--] or A = stack[--SP])
op=15 0xF0C0098203AE02B501F300D8
Invalid syscall number 88
A: 88 B: 115 C: 53 D: 46: PC: 6 SP: 2
op=11有点点像相对位移call;sp+=1,push了一个返回地址?
op=13、14看起来像一对push pop指令
op=15应该是syscall,调用号在A
验证栈指令猜测
op=13, 14看起来像push和pop指令,验证一下
sc = b''
# 填充AB=(88, 115) SP=0x40
sc += combine(op=0, ri=0, ty=1, imm=0x58)
sc += combine(op=0, ri=1, ty=1, imm=0x73)
sc += combine(op=0, ri=9, ty=1, imm=0x40)
# op=13, ri=0(A), ty=1, push A?
sc += combine(op=13, ri=0, ty=1, imm=0)
# op=14, ri=1(B), ty=1, pop B?
sc += combine(op=14, ri=1, ty=1, imm=0)
f.write('0x'+sc[::-1].hex().upper())
send(sc)
结果,14似乎不是pop,B被设置为了0:
0xE180D08009C001F300D8
A: 88 B: 0 C: 0 D: 0: PC: 5 SP: 64
整理2
目前已知的指令集信息:
0000 0000 1 000'0000
bits ~12 11~8 7 6~0
desc op ri t imm
t(ype)=1:
imm: 无符号,0~127
ri: 0, A
1, B
2, C
3, D
4~7, unknown
8, PC
9, SP
10~15, unknown
op: 0, reg[ri] += imm
1, reg[ri] -= imm
2, reg[ri] = 0?
3, 不变?
4, reg[ri] &= imm
5, reg[ri] |= imm
6, reg[ri] ^= imm
7, reg[ri] = ~reg[ri]
9, 不变?
10, PC = imm?
11, PC += 1 + imm, SP += 1?
12, 不变?
13, SP += 1?
14, reg[ri] = 0, sp -= 1?
15, syscall(callnum=A)
t(ype)=0: unknown
探索指令3-爆破type=0
探索指令1中,对type=0的指令一无所知,现在爆破这种情况
type=0,爆破第二字节
猜测sp可会影响结果,将sp设置一个比较小的值
充一下寄存器,然后爆破第二字节:
for i in range(0x100):
try:
sc = b''
# 填充ABCD和SP=(88, 115, 53, 46, 2)
sc += combine(op=0, ri=0, ty=1, imm=0x58)
sc += combine(op=0, ri=1, ty=1, imm=0x73)
sc += combine(op=0, ri=2, ty=1, imm=0x35)
sc += combine(op=0, ri=3, ty=1, imm=0x2e)
sc += combine(op=0, ri=9, ty=1, imm=0x2)
# ty=1, imm=3, 爆破第二字节
sc += b'\x03' + int.to_bytes(i, 1, 'little')
f.write('0x'+sc[::-1].hex().upper())
send(sc)
except BaseException as e:
p.close()
p = remote(ip, port)
f.write('\nException type={}\n'.format(type(e)))
结果,有点类似单指令-爆破第二字节的情况,不同是第二操作数原本解释为imm,现在变成寄存器寻址:
ABCD SP=(88, 115, 53, 46, 2) imm=3
0x0003098203AE02B501F300D8
A: 134 B: 115 C: 53 D: 46: PC: 6 SP: 2
88 + 46 = 134 (A += D)
0x0103098203AE02B501F300D8
A: 88 B: 161 C: 53 D: 46: PC: 6 SP: 2
115 + 46 = 134 (B += D)
0x0203098203AE02B501F300D8
A: 88 B: 115 C: 99 D: 46: PC: 6 SP: 2
C += D
0x0303098203AE02B501F300D8
A: 88 B: 115 C: 53 D: 92: PC: 6 SP: 2
D += D
0x0403~0x0703 0x0403098203AE02B501F300D8
A: 88 B: 115 C: 53 D: 46: PC: 6 SP: 2
似乎没有变化
0x0803098203AE02B501F300D8
A: 88 B: 115 C: 53 D: 46: PC: 52 SP: 2
PC += D
0x0903098203AE02B501F300D8
A: 88 B: 115 C: 53 D: 46: PC: 6 SP: 48
SP += D
0x0A03~0x0F03 0x0A03098203AE02B501F300D8
A: 88 B: 115 C: 53 D: 46: PC: 6 SP: 2
似乎没有变化
0x1003098203AE02B501F300D8
A: 42 B: 115 C: 53 D: 46: PC: 6 SP: 2
0x1103098203AE02B501F300D8
A: 88 B: 69 C: 53 D: 46: PC: 6 SP: 2
0x1203098203AE02B501F300D8
A: 88 B: 115 C: 7 D: 46: PC: 6 SP: 2
0x1303098203AE02B501F300D8
A: 88 B: 115 C: 53 D: 0: PC: 6 SP: 2
REG -= D
0x1403~0x1703 0x1403098203AE02B501F300D8
A: 88 B: 115 C: 53 D: 46: PC: 6 SP: 2
似乎没有变化
......
type=0,爆破第一字节imm
第一字节的第7位是type,猜测0~6位是寄存器寻址,验证一下:
for i in range(0x80):
try:
sc = b''
# 填充ABCD和SP=(88, 115, 53, 46, 2)
sc += combine(op=0, ri=0, ty=1, imm=0x58)
sc += combine(op=0, ri=1, ty=1, imm=0x73)
sc += combine(op=0, ri=2, ty=1, imm=0x35)
sc += combine(op=0, ri=3, ty=1, imm=0x2e)
sc += combine(op=0, ri=9, ty=1, imm=0x2)
# op=0 (+=), ri=0 (A), type=0, 爆破第一字节
sc += int.to_bytes(i, 1, 'little') + b'\x00'
f.write('imm=0x{:X} '.format(i)+'0x'+sc[::-1].hex().upper())
send(sc)
except BaseException as e:
p.close()
p = remote(ip, port)
f.write('\nException type={}\n'.format(type(e)))
结果,0~6位只有低4位是有效的:
imm=0x0 0x0000098203AE02B501F300D8
A: 176 B: 115 C: 53 D: 46: PC: 6 SP: 2
...
imm=0x10 0x0010098203AE02B501F300D8
A: 176 B: 115 C: 53 D: 46: PC: 6 SP: 2
...
imm=0x20 0x0020098203AE02B501F300D8
A: 176 B: 115 C: 53 D: 46: PC: 6 SP: 2
...
整理3
type=0和1的相同点是8~11位作为第一源操作数及目的操作数,使用寄存器寻址;0~6位都是作为第二源操作数
不同点是type=0第二操作数是寄存器寻址,type=1第二操作数直接解释为立即数
目前已知的指令集信息:
0000 0000 0 000'0000
bits ~12 11~8 7 6~0
desc op ri t imm
t(ype): 0, E = reg[imm&0xf]
1, E = imm
imm: 无符号,0~127
ri: 0, A
1, B
2, C
3, D
4~7, unknown
8, PC
9, SP
10~15, unknown
op: 0, reg[ri] += E
1, reg[ri] -= E
2, reg[ri] = 0?
3, 不变?
4, reg[ri] &= E
5, reg[ri] |= E
6, reg[ri] ^= E
7, reg[ri] = ~reg[ri]
8, 无回显
9, 不变?
10, PC = E
11, PC = 1 + ri[imm] / PC = PC + 1 + imm
12, 不变?
13, SP += 1, push?
14, reg[ri] = 0, sp -= 1?
15, syscall(callnum=A)
t(ype)=0: unknown
syscall
找调用号
type=0,爆破第二字节时发现,op=15(syscall)时,ri必须为0,ty好像随意
linux调用号5是open,猜一波,看来和linux调用号不完全一样:
sc = b''
# A+=5 linux-open
sc += combine(op=0, ri=0, ty=1, imm=5)
# syscall
sc += combine(op=15, ri=0, ty=0, imm=0)
f.write('0x'+sc[::-1].hex().upper())
send(sc)
回显内容(更改syscall的imm回显是一样的):
Invalid file descriptor 0
Execution halted!
A: 5 B: 0 C: 0 D: 0: PC: 2 SP: 0
Would you like to start over? (y/n)
开爆调用号:
for callnumber in range(10):
if callnumber == 3:
continue
sc = b''
# A = callnum
sc += combine(op=0, ri=0, ty=1, imm=callnumber)
# syscall
sc += combine(op=15, ri=0, ty=0, imm=0)
f.write('cn={} '.format(callnumber)+'0x'+sc[::-1].hex().upper())
send(sc)
测了几个调用号:
cn=0 0xF0000080
A: 0 B: 0 C: 0 D: 0: PC: 2 SP: 0
cn=1 0xF0000081
A: 1 B: 0 C: 0 D: 0: PC: 2 SP: 0
cn=2 0xF0000082
A: 2 B: 0 C: 0 D: 0: PC: 2 SP: 0
cn=3
没有回显,也不会断开,猜是读控制台
cn=4 0xF0000084
Error: Unable to open file ''
cn=5 0xF0000085
Invalid file descriptor 0
cn=6 0xF0000086
Invalid syscall number 6
cn=7 0xF0000087
Invalid syscall number 7
cn=8 0xF0000088
Invalid syscall number 8
cn=9 0xF0000089
Invalid syscall number 9
结果:
0~2 未知
3 读控制台?
4 open
5 fd相关
syscall_4 open
猜传参方式是op=13 push
sc = b''
# push 'f'
sc += combine(op=0, ri=0, ty=1, imm=ord('f')) # A += 'f'
sc += combine(op=13, ri=0, ty=0, imm=0) # push A
sc += combine(op=1, ri=0, ty=1, imm=ord('f')) # A -= 'f'
# push 'l'
sc += combine(op=0, ri=0, ty=1, imm=ord('l')) # A += 'l'
sc += combine(op=13, ri=0, ty=0, imm=0) # push A
sc += combine(op=1, ri=0, ty=1, imm=ord('l')) # A -= 'l'
# syscall_4
callnumber = 4
sc += combine(op=0, ri=0, ty=1, imm=callnumber)
sc += combine(op=15, ri=0, ty=0, imm=0)
send(sc, detail=True)
回显:
Error: Unable to open file 'fl'
Execution halted!
A: 4 B: 0 C: 65535 D: 0: PC: 8 SP: 2
Would you like to start over? (y/n)
猜测正确,C应该是返回值
push字符串的方式:
def sc_push_str(s):
sc = b''
for c in s:
sc += combine(op=0, ri=0, ty=1, imm=ord(c)) # A += 'f'
sc += combine(op=13, ri=0, ty=0, imm=0) # push A
sc += combine(op=1, ri=0, ty=1, imm=ord(c)) # A += 'f'
return sc
尝试open('flag')
,:
sc = b''
sc += sc_push_str('flag')
# syscall_4
callnumber = 4
sc += combine(op=0, ri=0, ty=1, imm=callnumber)
sc += combine(op=15, ri=0, ty=0, imm=0)
# sc += combine(op=15, ri=0, ty=0, imm=0)
send(sc, detail=True)
open一次回显:
Execution halted!
A: 4 B: 0 C: 0 D: 0: PC: 14 SP: 4
Would you like to start over? (y/n)
open两次回显:
Execution halted!
A: 4 B: 0 C: 1 D: 0: PC: 15 SP: 4
Would you like to start over? (y/n)
故C就是返回的fd
意外发现
在测试push参数,然后call open的时候
以为op=2,ty=1的效果是reg[ri] = 0
,尝试用来清零寄存器,结果发现syscall时提示调用号不对
一看我push时用的A,然后用op=2, ri=0, ty=1, imm=0
尝试清零A,结果这是个读栈指令,A并未被清零
发现ty=1时,op=2/13的功能:
ty=1
op: 2, reg[ri] = stack[imm], 读栈
13, stack[SP++] = reg[ri], push到栈
syscall_5 read
测试出来syscall 5的调用约定
传入
A call number
B fd
C ?
D count
返回
B count
可以通过传入一个很大的count(D),然后看返回的count(B)得知文件长度
读出flag文件,这里我本地放的测试文件count是20
count = 20
flag = ''
for i in range(count):
sc = b''
sc += sc_open('flag') # ret: B = fd
sc += combine(op=0, ri=0, ty=1, imm=5) # A = syscall-5-read
sc += combine(op=0, ri=3, ty=1, imm=40) # D = count
sc += combine(op=15, ri=0, ty=0, imm=0) # syscall
sc += combine(op=1, ri=0, ty=1, imm=5) # A = 0
sc += combine(op=2, ri=0, ty=1, imm=i) # A = stack[i]
regA = int(send(sc).split('\n')[2].split(' ')[1])
flag += chr(regA)
print(flag)
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课