
Return Address Overwrite
Canary 란?
Canary 작동 원리(간단 설명)
실습을 통한 작동 원리 이해
ex) 아래 발췌한 코드에서 get_shell() 함수로 ret을 조작하려면?
void get_shell() {
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
execve(cmd, args, NULL);
}
int main() {
char buf[0x28];
init();
printf("Input: ");
scanf("%s", buf);
return 0;
}
pwndbg> disass main
Dump of assembler code for function main:
0x00000000004006e8 <+0>: push rbp
0x00000000004006e9 <+1>: mov rbp,rsp
0x00000000004006ec <+4>: sub rsp,0x30
0x00000000004006f0 <+8>: mov eax,0x0
0x00000000004006f5 <+13>: call 0x400667 <init>
0x00000000004006fa <+18>: lea rdi,[rip+0xbb] # 0x4007bc
0x0000000000400701 <+25>: mov eax,0x0
0x0000000000400706 <+30>: call 0x400540 <printf@plt>
0x000000000040070b <+35>: lea rax,[rbp-0x30]
0x000000000040070f <+39>: mov rsi,rax
0x0000000000400712 <+42>: lea rdi,[rip+0xab] # 0x4007c4
0x0000000000400719 <+49>: mov eax,0x0
0x000000000040071e <+54>: call 0x400570 <__isoc99_scanf@plt>
0x0000000000400723 <+59>: mov eax,0x0
0x0000000000400728 <+64>: leave
0x0000000000400729 <+65>: ret
End of assembler dump.

이것을 통해서 버퍼를 오염시키고 ret 조작 가능

옛날에 탄광에 들어갈 때 일산화탄소 중독의 위험을 알려주는 새로 이용됨
컴퓨터에서는 ? -> 버퍼 오버플로우를 감지하는 도구로 이용됨

이렇게 중간에 버퍼와 ret 사이에 canary가 위치해 있어 ret을 변조하려면 카나리까지 덮어야 하는 상황이 나오게 됨.
위의 그림과 같이, 위의 스택 프레임의 순서는 낮은 스택부터 buffer, canary, sfp, return address 순이다. (실제는 많이 다를 수 있음)
그런데 버퍼 오버플로우를 일으키려고 카나리를 덮는 순간, 컴퓨터는 카나리가 변조되었음을 확인하고, 그 즉시 프로그램을 종료한다.
먼저 코드에 canary를 적용시킨 것과, 적용시키지 않은 것의 바이너리 비교를 통해 차이점을 비교한다
그 차이점이 어떤 결과를 만드는지(어떻게 BOF를 보호하는지) 분석한다
Canary 정적 분석
// Name: canary.c
#include <unistd.h>
int main() {
char buf[8];
read(0, buf, 32);
return 0;
}
위의 간단한 예시 코드를 이용해서 하나는 그냥 실행 파일을 만들고, 다른 하나는 -fno-stack-protector gcc 옵션을 이용해서 canary를 제거한다.



둘의 차이가 보인다.
canary를 적용하여 다시 컴파일하고, 긴 입력을 주면 Segmentation fault가 아니라 stack smashing detected와 Aborted라는 에러가 발생한다. 이는 스택 버퍼 오버플로우가 탐지되어 프로세스가 강제 종료되었음을 의미한다.
no_canary와 디스어셈블 결과를 비교하면, main 함수의 프롤로그와 에필로그에 각각 다음의 코드들이 추가되었다.
0x00000000000006b2 <+8>: mov rax,QWORD PTR fs:0x28
0x00000000000006bb <+17>: mov QWORD PTR [rbp-0x8],rax
0x00000000000006bf <+21>: xor eax,eax
0x00000000000006dc <+50>: mov rcx,QWORD PTR [rbp-0x8]
0x00000000000006e0 <+54>: xor rcx,QWORD PTR fs:0x28
0x00000000000006e9 <+63>: je 0x6f0 <main+70>
0x00000000000006eb <+65>: call 0x570 <__stack_chk_fail@plt>
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]
- lea rax,[rbp-0x8]
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
그러면 이제 각각 추가된 코드가 어떤 역할을 하는지 알아보자
Canary 동적 분석
0x00000000000006b2 <+8>: mov rax,QWORD PTR fs:0x28
0x00000000000006bb<+17>: mov QWORD PTR [rbp-0x8],rax
0x00000000000006bf <+21>: xor eax,eax
$ 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
main+8은 fs:0x28의 데이터를 읽어서 rax에 저장한다. fs는 세그먼트 레지스터의 일종으로, 리눅스는 프로세스가 시작될 때 fs:0x28에 랜덤 값을 저장한다. 따라서 main+8의 결과로 rax에는 리눅스가 생성한 랜덤 값이 저장된다.
pwndbg> print /a $rax
$1 = 0xf80f605895da3c00
rax에 저장된 값을 확인해보니 첫번째는 널바이트로 저장되어 있고, 랜덤 값이 저장되어 있음을 확인할 수 있다.
그리고 rax에 있는 값은 main+17에서 rbp - 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> x/gx $rbp-0x8
0x7fffffffe238: 0xf80f605895da3c00
추가된 에필로그의 코드에 중단점을 설정하고 바이너리를 계속 실행시켜보자
main+50은 rbp-8에 저장한 카나리를 rcx로 옮긴다. 그 뒤, main+54에서 rcx를 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
코드를 한 줄 실행시키면, rbp-0x8에 저장된 카나리 값이 버퍼 오버플로우로 인해 “0x4848484848484848”이 된 것을 확인할 수 있다.
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이 아니므로(xor 값이 동일하지 않으므로) main+63에서 main+70으로 분기하지 않고 main+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.