카나리 우회 방법
1. 무차별 대입 (Brute Force)
x64 아키텍처에서는 8바이트의 카나리가 생성되고, x86 아키텍처에서는 4바이트의 카나리가 생성된다. 카나리의 첫 바이트는 NULL 바이트이므로, 실제 7바이트(x64), 3바이트(x86)의 랜덤한 값이 포함된다.
⇒ 카나리를 알아내려면 256^7(x64), 256^3(x86) 번의 연산이 필요하다. (1바이트 = 8비트 = 2^8 = 256)
⇒ 연산량이 많아 무차별 대입은 비현실적, 불가능
2. TLS 접근
카나리는 TLS에 전역변수로 저장(fs:0x28)되고, 매 함수마다 이를 참조해 사용한다. TLS 주소는 매 실행마다 바뀌지만, 실행 중 TLS의 주소를 알 수 있고, 임의 주소에 대한 읽기 또는 쓰기가 가능하다면 TLS에 설정된 카나리 값을 읽거나 조작가능하다.
-1. 알아낸 카나리를 이용해 스택 버퍼 오버플로우를 일으킬 때 알아낸 값으로 카나리 덮기
-2. 조작한 카나리 값을 이용해 스택 버퍼 오버플로우를 일으킬 때 조작한 값으로 카나리 덮기
3. 스택 카나리 릭
// Name: bypass_canary.c
// Compile: gcc -o bypass_canary bypass_canary.c
#include <stdio.h>
#include <unistd.h>
int main() {
char memo[8];
char name[8];
printf("name : ");
read(0, name, 64);
printf("hello %s\n", name);
printf("memo : ");
read(0, memo, 64);
printf("memo %s\n", memo);
return 0;
}
카나리 릭 실습을 위한 실습코드이다. 카나리의 첫 바이트가 NULL 바이트이므로 name을 입력할 때 'a' 8개 (+개행(0x0a) )입력하면 다음과 같이 이상한 값도 같이 출력되는 것을 확인할 수 있다. memo를 입력할 때는 'a'를 16개 (+개행(0x0a) ) 입력하면 name과 똑같은 이상한 값이 출력되는 것을 알 수 있다.
gdb를 이용해 하나씩 확인해보자.
fs:0x28 값 확인
pwndbg> disassemble main
Dump of assembler code for function main:
0x0000000000001189 <+0>: endbr64
0x000000000000118d <+4>: push rbp
0x000000000000118e <+5>: mov rbp,rsp
0x0000000000001191 <+8>: sub rsp,0x20
0x0000000000001195 <+12>: mov rax,QWORD PTR fs:0x28
0x000000000000119e <+21>: mov QWORD PTR [rbp-0x8],rax
0x00000000000011a2 <+25>: xor eax,eax
0x00000000000011a4 <+27>: lea rdi,[rip+0xe59] # 0x2004
0x00000000000011ab <+34>: mov eax,0x0
0x00000000000011b0 <+39>: call 0x1080 <printf@plt>
0x00000000000011b5 <+44>: lea rax,[rbp-0x10] # name(8) + canary(8)
0x00000000000011b9 <+48>: mov edx,0x40
0x00000000000011be <+53>: mov rsi,rax
0x00000000000011c1 <+56>: mov edi,0x0
0x00000000000011c6 <+61>: call 0x1090 <read@plt>
0x00000000000011cb <+66>: lea rax,[rbp-0x10]
0x00000000000011cf <+70>: mov rsi,rax
0x00000000000011d2 <+73>: lea rdi,[rip+0xe33] # 0x200c
0x00000000000011d9 <+80>: mov eax,0x0
0x00000000000011de <+85>: call 0x1080 <printf@plt>
0x00000000000011e3 <+90>: lea rdi,[rip+0xe2c] # 0x2016
0x00000000000011ea <+97>: mov eax,0x0
0x00000000000011ef <+102>: call 0x1080 <printf@plt>
0x00000000000011f4 <+107>: lea rax,[rbp-0x18] # memo(8) + name(8) + canary(8)
0x00000000000011f8 <+111>: mov edx,0x40
0x00000000000011fd <+116>: mov rsi,rax
0x0000000000001200 <+119>: mov edi,0x0
0x0000000000001205 <+124>: call 0x1090 <read@plt>
0x000000000000120a <+129>: lea rax,[rbp-0x18]
0x000000000000120e <+133>: mov rsi,rax
0x0000000000001211 <+136>: lea rdi,[rip+0xe06] # 0x201e
0x0000000000001218 <+143>: mov eax,0x0
0x000000000000121d <+148>: call 0x1080 <printf@plt>
0x0000000000001222 <+153>: mov eax,0x0
0x0000000000001227 <+158>: mov rcx,QWORD PTR [rbp-0x8]
0x000000000000122b <+162>: xor rcx,QWORD PTR fs:0x28
0x0000000000001234 <+171>: je 0x123b <main+178>
0x0000000000001236 <+173>: call 0x1070 <__stack_chk_fail@plt>
0x000000000000123b <+178>: leave
0x000000000000123c <+179>: ret
End of assembler dump.
main의 disassemble을 확인하면 위와 같다. main + 12에 브레이크 포인트를 설정하고 이를 실행 후 rax를 확인한다.
이 다음 명령을 실행한 후 스택을 확인하면 canary에 동일한 값이 들어가있는 것을 확인할 수 있다.
스택의 구조를 보면 다음과 같다.
카나리의 첫번째 NULL바이트로 인해 name에 'a' 8개, 즉 8바이트만 채우게되면 다음 출력시 카나리의 값은 출력되지 않고 'hello aaaaaaaa"만 출력된다. 따라서 카나리의 첫 바이트까지 덮어야하기 때문에 'a'를 9개 입력해야 카나리 값을 화면에 출력할 수 있다.
name에 'a' 8개 입력
8개 입력 후 스택값의 변화를 보면 'aaaaaaaa\x0a'로 바뀌는 것을 확인할 수 있다. 'aaaaaaaa'의 입력을 하면 엔터(개행)를 통해 입력을 완료한다. read()함수는 'aaaaaaaa'를 입력하면 실제 입력은 'aaaaaaaa\x0a'가 된다. 따라서 카나리의 첫번째 바이트가 \x0a로 바뀌게 된다. 따라서 문자열의 끝을 뜻하는 NULL(\x00)이 없어 카나리 값까지 출력된다.
'CS > system' 카테고리의 다른 글
[System][Dreamhack] Exploit tech : Return to Shellcode (0) | 2022.09.06 |
---|---|
[System][Dreamhack] wargame - ssp_001 스택 순서 (0) | 2022.08.24 |
[System][Dreamhack] 메모리 보호 기법 memory mitigation - Stack Canary (0) | 2022.08.09 |
[System][Dreamhack] stack buffer overflow - 스택 버퍼 오버플로우 (0) | 2022.05.11 |
[System] 레이스 컨디션(Race Condition) (0) | 2022.05.09 |