[포너블] Memory Corruption: Use After Free

Chris Kim·2024년 10월 24일

시스템해킹

목록 보기
24/33

출처:드림핵 강의

0. 서론

Use-After-Free는 메모리 참조에 사용한 포인터를 메뫃리 해제 후에 적절히 초기화하지 않아서, 또는 해제한 메모리를 초기화하지 않고 다음 청크에 재할당해주면서 발생하는 취약점이다.

Docker file

FROM ubuntu:18.04

ENV PATH="${PATH}:/usr/local/lib/python3.6/dist-packages/bin"
ENV LC_CTYPE=C.UTF-8

RUN apt update
RUN apt install -y \
    gcc \
    git \
    python3 \
    python3-pip \
    ruby \
    sudo \
    tmux \
    vim \
    wget

# install pwndbg
WORKDIR /root
RUN git clone https://github.com/pwndbg/pwndbg
WORKDIR /root/pwndbg
RUN git checkout 2023.03.19
RUN ./setup.sh

# install pwntools
RUN pip3 install --upgrade pip
RUN pip3 install pwntools

# install one_gadget command
RUN gem install elftools -v 1.1.3
RUN gem install one_gadget -v 1.9.0

WORKDIR /root
$ IMAGE_NAME=ubuntu1804 CONTAINER_NAME=my_container; \
docker build . -t $IMAGE_NAME; \
docker run -d -t --privileged --name=$CONTAINER_NAME $IMAGE_NAME; \
docker exec -it -u root $CONTAINER_NAME bash

1. Dangling Pointer

이 글에서 잠깐 언급한 적이 있다.
free 함수는 청크를 ptmalloc에 반환하기만 하지, 청크 주소를 담고있던 포인터를 초기화 하지는 않는다. Dangling Pointer가 있다고 프로그램이 보안적으로 취약한 것은 아니다. 그러나 이는 프로그램이 예상치 못한 동작을 할 가능성을 키운다.
예를 들어 이미 해제된 청크 주소를 가리키는 포인터를 다시 free로 해제하면 버그가 발생한다. 이를 Double Free Bug라고 한다.

2. Use After Free

위에서 본 Dangling Pointer 외에도 새로 할당된 영역을 초기화하지 않고 사용하면 UAF가 발생할 수 있다. 따라서 명시적으로 할당/해제된 메모리를 초기화 하지 않으면 남아있는 데이터가 유출되거나 사용될 수 있다. 아래 예시는 UAF 취약점이 있는 예제 코드다. NameTagSecret이 저으이되어있는데, Secret이 할당되고, secret_name,secret_info, code에 값을 입력하고 이를 해제한다.

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

실행 결과는 다음과 같다.

$ gcc -o uaf uaf.c -no-pie
$ ./uaf
Team Name: security team
Name: S@ssw0rd!@#
Nametag function: 0x1337
Segmentation fault (core dumped)

3. 동적 분석

NametagSecret은 같은 크기의 구조체다. 따라서 secret을 해제해고 nametag를 할당하면 nametag는 같은 메모리 영역을 사용하게 된다. 당연히 메모리 데이터가 초기화 되지 않았으므로, secret의 값 일부가 남아있는 상태다.
heap으로 할당/해제된 청크 정보 조회가 가능하다.

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x602000
Size: 0x251

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x602250
Size: 0x41
fd: 0x00

Top chunk | PREV_INUSE
Addr: 0x602290
Size: 0x20d71
}

Free chunk에 secret에 해당하는 청크가 있다. 0x602000tchae와 관련된 공간으로 tchae_perthread_struct 구조체에 해당하며, libc 단에서 힘 영역을 초기화할 때 할당하는 청크다. secret가 사용하던 메모리 영역을 출력해보자.

pwndbg> x/10gx 0x602250
0x602250:	0x0000000000000000	0x0000000000000041
0x602260:	0x0000000000000000	0x0000000000602010
0x602270:	0x6472307773734050	0x0000000000234021
0x602280:	0x0000000000000000	0x0000000000000000
0x602290:	0x0000000000001337	0x0000000000020d71
pwndbg> x/s 0x602270
0x602270:	"P@ssw0rd!@#"
pwndbg>

자 이제 nametag를 할당하고 printf 함수 호출 시점에서 nametag 멤버들을 살펴보자.

Breakpoint *main+207
pwndbg> x/10gx 0x602250
0x602250:   0x0000000000000000  0x0000000000000041
0x602260:   0x7974697275636573  0x0000006d61657420
0x602270:   0x6472307773734053  0x0000000000234021
0x602280:   0x0000000000000000  0x0000000000000000
0x602290:   0x0000000000001337  0x0000000000020d71
pwndbg> x/s 0x602260
0x602260:   "security team"
pwndbg> x/s 0x602270
0x602270:   "S@ssw0rd!@#"
pwndbg> x/gx 0x602290
0x602290:   0x0000000000001337
pwndbg>

입력이 이뤄진 멤버에는 그대로 입력이 이뤄졌으나, 초기화 되지 않은 멤버의 값이 존재하는 것을 확인 할 수 있다.

이러한 특징을 이용해, 초기화되지 않은 메모리의 값을 읽어내거나, 새로운 객체가 악의적인 값을 사용하도록 유도하여 프로그램의 정상적인 실행을 방해할 수 있다.

profile
회계+IT=???

0개의 댓글