#include <stdio.h>
int main(){
char buf[8];
read(0, buf, 32);
return 0;
}
위의 코드에는 스택 버퍼 오버플로우 취약점이 존재함
추가된 프롤로그의 코드에 중단점을 설정하고 바이너리를 실행시킴
main+8은 fs:0x28의 데이터를 읽어서 rax에 저장합니다. fs는 세그먼트 레지스터의 일종으로, 리눅스는 프로세스가 시작될 때 fs:0x28에 랜덤 값을 저장합니다. 따라서 main+8의 결과로 rax에는 리눅스가 생성한 랜덤 값이 저장됨
$ gdb -q ./canary
pwndbg> break *main+8
Breakpoint 1 at 0x6b2
pwndbg> run
► 0x5555555546b2 <main+8> mov rax, qword ptr fs:[0x28] <0x5555555546aa>
0x5555555546bb <main+17> mov qword ptr [rbp - 8], rax
0x5555555546bf <main+21> xor eax, eax
코드를 한 줄 실행하면 rax에 다음과 같이 첫 바이트가 널 바이트인 8바이트 데이터가 저장되어 있음
pwndbg> ni
0x5555555546b2 <main+8> mov rax, qword ptr fs:[0x28] <0x5555555546aa>
► 0x5555555546bb <main+17> mov qword ptr [rbp - 8], rax
0x5555555546bf <main+21> xor eax, eax
pwndbg> print /a $rax
$1 = 0xf80f605895da3c00
생성한 랜덤값은 main+17에서 rbp-0x8에 저장됨
pwndbg> ni
0x5555555546b2 <main+8> mov rax, qword ptr fs:[0x28] <0x5555555546aa>
0x5555555546bb <main+17> mov qword ptr [rbp - 8], rax
► 0x5555555546bf <main+21> xor eax, eax
pwndbg> x/gx $rbp-0x8
0x7fffffffe238: 0x2619d41073c14900
이제 추가된 에필로그의 코드에 중단점을 설정하고, 바이너리를 계속 실행시킴
main+50은 rbp-8에 저장한 카나리를 rcs로 옮김 -> 그 뒤, main+54에서 rcs를 fs:0x28에 저장된 카나리와 xor함
만약 두 값이 동일하면 연산 결과가 0이 되면서 je의 조건을 만족하게 되고, main 함수가 정상적으로 반환되지만, 두 값이 동일하지 않으면 __stack_chk_fail이 호출되면서 프로그램이 강제로 종료됨
여기서는 16개의 H를 입력으로 카나리를 변조하고, 실행 흐름이 어떻게 되는지 살펴보면,
pwndbg> break *main+50
pwndbg> continue
HHHHHHHHHHHHHHHH
Breakpoint 2, 0x00000000004005c8 in main ()
► 0x5555555546dc <main+50> mov rcx, qword ptr [rbp - 8] <0x7ffff7af4191>
0x5555555546e0 <main+54> xor rcx, qword ptr fs:[0x28]
0x5555555546e9 <main+63> je main+70 <main+70>
↓
0x5555555546f0 <main+70> leave
0x5555555546f1 <main+71> ret
pwndbg> ni
0x5555555546dc <main+50> mov rcx, qword ptr [rbp - 8] <0x7ffff7af4191>
► 0x5555555546e0 <main+54> xor rcx, qword ptr fs:[0x28]
0x5555555546e9 <main+63> je main+70 <main+70>
pwndbg> print /a $rcx
$2 = 0x4848484848484848
main+54의 연산 결과가 0이 아니므로 main+63에서 main+70으로 분기하지 ㅇ낳고 amin+65의 __stack_chk_fail이 실행하게 됨
pwndbg> ni
pwndbg> ni
pwndbg> ni
0x5555555546dc <main+50> mov rcx, qword ptr [rbp - 8] <0x7ffff7af4191>
0x5555555546e0 <main+54> xor rcx, qword ptr fs:[0x28]
0x5555555546e9 <main+63> je main+70 <main+70>
► 0x5555555546eb <main+65> call __stack_chk_fail@plt <__stack_chk_fail@plt>
이 함수가 실행되면 다음의 메세지가 출력되며 프로세스가 강제로 종료됨
*** stack smashing detected ***: <unknown> terminated
Program received signal SIGABRT, Aborted.
카나리 값은 프로세스가 시작될 때, TLS에 전역 변수로 저장되고, 각 함수마다 프롤로그와 에필로그에서 이 값을 참조함
fs는 TLS를 가리키므로 fs의 값을 TLS의 주소를 알 수 있지만, 리눅스에서 fs의 값은 특정 시스템 콜을 사용해야만 조회하거나 설정할 수 있음
fs의 값을 설정할 때 호출되는 arch_prctl(int code, unsigned long addr) 시스템 콜에 중단점을 설정하여 fs가 어떤 값으로 설정되는지 조사해보자. -> 이 시스템 콜을 arch_prctl(ARCH_SET_FS, addr)의 형태로 호출하면 fs의 값은 addr로 설정됨
gdb에는 특정 이벤트가 발생했을 때, 프로세스를 중지시키는 catch라는 명령어가 있음. 이 명령어로 arch_prctl에 catchpoint를 설정하고 실습에 사용했던 canary를 실행해보자
$ gdb -q ./canary
pwndbg> catch syscall arch_prctl
Catchpoint 1 (syscall 'arch_prctl' [158])
pwndbg> run
catchpoint에 도달했을 때, rdi의 값이 0x1002인데 이 값은 ARCH_SET_FS의 상숫값임.
rsi의 값이 0x7ffff7fdb4c0이므로, 이 프로세스는 TLS를 0ㅌ7ffff7fdb4c0에 저장할 것이며, fs는 이를 가리키게 될 것임
카나리가 저장될 fs+0x28(0x7ffff7fdb4c0+0x28)의 값을 보면, 아직 어떠한 값도 설정되어 있지 않은 것을 확인할 수 있음
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
TLS의 주소를 알았으므로, gdb의 watch 명령어로 TLS+0x28에 값을 쓸 때 프로세스를 중단시켜보자. watch는 특정 주소에 저장된 값이 변경되면 프로세스를 중단시키는 명령어임
pwndbg> watch *(0x7ffff7fdb4c0+0x28)
Hardware watchpoint 4: *(0x7ffff7fdb4c0 + 0x28)
watchpoint를 설정하고 프로세스를 계속 진행시키며 security_init함수에서 프로세스가 멈춤
pwndbg> continue
Continuing.
Hardware watchpoint 4: *(0x7ffff7fdb4c0 + 0x28)
Old value = 0
New value = -19422582016
security_init () at rtld.c:807
807 in rtld.c
여기서 TLS+0ㅌ28의 값을 조회하면 0x2f35207b8c368d80이 카나리로 설정된 것을 확인할 수 있음
pwndbg> x/gx 0x7ffff7fdb4c0+0x28
0x7ffff7fdb4e8 : 0x2f35207b8c368d00
실제로 이 값이 main함수에서 사용하는 카나리값인지 확인하기 위해 main함수에서 사용하는 카나리값인지 확인하기 위해 main함수에 중단점을 설정하고, 계속 실행시켜본다.
mov rax, QWORD PTR fs:0x28를 실행하고 rax 값을 확인해보면 security_init에서 설정한 값과 같은 것을 확인할 수 있음
Breakpoint 3, 0x00005555555546ae in main ()
pwndbg> x/10i $rip
► 0x5555555546ae <main+4>: sub rsp,0x10
0x5555555546b2 <main+8>: mov rax,QWORD PTR fs:0x28
0x5555555546bb <main+17>: mov QWORD PTR [rbp-0x8],rax
0x5555555546bf <main+21>: xor eax,eax
0x5555555546c1 <main+23>: lea rax,[rbp-0x10]
0x5555555546c5 <main+27>: mov edx,0x20
0x5555555546ca <main+32>: mov rsi,rax
0x5555555546cd <main+35>: mov edi,0x0
0x5555555546d2 <main+40>: call 0x555555554580 <read@plt>
0x5555555546d7 <main+45>: mov eax,0x0
pwndbg> ni
0x00005555555546b2 in main ()
pwndbg> ni
0x00005555555546bb in main ()
pwndbg> i r $rax
rax 0x2f35207b8c368d00 3401660808553729280
pwndbg>
카나리가 등장하자 해커들은 카나리를 우회할 수 있는 방법을 연구함. 카나리를 우회하는 방법으로는 다음이 알려져 있음
x64 아키텍처에서는 8바이트의 카나리가 생성되며, x86 아키텍처에서는 4바이트의 카나리가 생성됨
각각의 카나리에는 NULL 바이트가 포함되어 있으므로, 실제로는 7바이트와 3바이트의 랜덤한 값이 포함됨
즉, 무차별 대입으로 x64 아키텍처의 카나리 값을 알아내려면 최대 256^7번, x86에서는 최대 256^3번의 연산이 필요하므로, 실제 서버를 대상으로 저정도 횟수의 무차별 대입을 시도하는 것은 불가능함
카나리는 TLS에 전역변수로 저장되며, 매 함수마다 이를 참조해서 사용함
TLS의 주소는 매 실행마다 바뀌지만, 만약 실행마다 TLS의 주소를 알 수 있고, 임의 주소에 대한 읽기 또는 쓰기가 가능하다면 TLS에 설정된 카나리 값을 알거나, 이를 임의의 값으로 조작할 수 있음
그 뒤, 스택 버퍼 오버플로우를 수행할 때 알아낸 카나리 값 또는 조작한 카나리 값으로 스택 카나리를 덮으면, 카나리 검사를 우회할 수 있음