[Pwnable] UAF (Use-After-Free)
오늘은 UAF 취약점에 대해 알아보겠습니다.
UAF 취약점은 Use After Free의 약자로 메모리 관리에서 발생하는 취약점 중 하나입니다.
동적할당을 한 뒤 Free한 메모리 영역을 초기화하지 않았을 때, 메모리에 남아있던 데이터가 유출되거나 사용될 수 있습니다.
이러한 현상은 ptmalloc2가 새로운 할당 요청이 들어왔을 때, 최근에 사용했고, 크기가 비슷한 청크가 있을 때 먼저 꺼내서 사용하기 때문에 발생합니다.
먼저 도커를 이용해 Ubuntu 18.04 glibc 2.27버전으로 셸을 실행해주겠습니다.
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 one_gadget
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
위 아래 명령어를 순서대로 입력하면 됩니다.
Ubuntu 18.04/glibc 2.27로 실습하는 이유는 먼저 tcache가 도입되었으면서 safe-linking은 적용이 안되어있어 UAF로 임의 주소 쓰기를 하는게 더 쉽고, tcache 중복 삽입 방지가 쉬워서 double free와 같은 기법이 가능하기 때문이라고 합니다.
먼저 UAF에 대해 알아보기 전에 Dangling Pointer 개념에 대해 살펴보겠습니다.
Dangling Pointer는 해제된 메모리 영역을 가리키고 있는 포인터를 말합니다.
이러한 포인터는 메모리 접근시 예측 불가능한 동작을 유발하고, segment fault를 발생시킬 수 있습니다.
간단한 코드 예제를 보겠습니다.
#include <stdlib.h>
#include <stdio.h>
int main() {
int *p = malloc(sizeof(int));
*p = 42;
free(p);
return 0;
}
이렇게 동적할당을 하고, p를 free했는데, p = NULL;이 없습니다.
지금처럼 p를 NULL로 초기화하지 않은 경우에서 p를 dangling pointer라고 하고 이러한 포인터에 임의의 값을 씌운다거나 임의 코드를 실행할 수도 있습니다.
이제 UAF 관련 코드 예제를 보겠습니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Poirier {
char team_name[16];
char token[32];
void (*func)();
};
struct Holloway {
char team_name[16];
char token[32];
long id;
};
void safe_action(void) {
puts("[*] safe_action(): harmless callback");
}
int main(void) {
struct Holloway *holloway = NULL;
struct Poirier *poirier = NULL;
holloway = malloc(sizeof(*holloway));
if (!holloway) { perror("malloc(holloway)"); return 1; }
strncpy(holloway->team_name, "team holloway", sizeof(holloway->team_name) - 1);
holloway->team_name[sizeof(holloway->team_name) - 1] = '\0';
strncpy(holloway->token, "max", sizeof(holloway->token) - 1);
holloway->token[sizeof(holloway->token) - 1] = '\0';
holloway->id = 0x41414141UL;
printf("\n[before free] holloway@%p\n team=%s\n token=%s\n id=0x%lx\n",
(void*)holloway, holloway->team_name, holloway->token, holloway->id);
free(holloway);
poirier = malloc(sizeof(*poirier));
if (!poirier) { perror("malloc(poirier)"); return 1; }
strncpy(poirier->team_name, "team poiri34", sizeof(poirier->team_name) - 1);
poirier->team_name[sizeof(poirier->team_name) - 1] = '\0';
strncpy(poirier->token, "dustin", sizeof(poirier->token) - 1);
poirier->token[sizeof(poirier->token) - 1] = '\0';
poirier->func = safe_action;
printf("\n[poirier@%p]\n team=%s\n token=%s\n func=%p\n",
(void*)poirier, poirier->team_name, poirier->token, (void*)poirier->func);
volatile struct Holloway *dangling = holloway;
printf("\n[via dangling]\n team=%s\n token=%s\n id=0x%lx\n",
dangling->team_name, dangling->token, dangling->id);
if (poirier->func) poirier->func();
free(poirier);
return 0;
}
코드가 길기 때문에 조금씩 끊어서 보겠습니다.
struct Poirier {
char team_name[16];
char token[32];
void (*func)();
};
struct Holloway {
char team_name[16];
char token[32];
long id;
};
먼저 구조체 Poirier랑 Holloway구조체를 만드는데 크기를 똑같이 맞춰주어야합니다.
holloway = malloc(sizeof(*holloway));
strncpy(holloway->team_name, "team holloway", sizeof(holloway->team_name) - 1);
holloway->team_name[sizeof(holloway->team_name) - 1] = '\0';
strncpy(holloway->token, "max", sizeof(holloway->token) - 1);
holloway->token[sizeof(holloway->token) - 1] = '\0';
holloway->id = 0x41414141UL;
출력했을 때 다음과 같이 나오도록 holloway구조체를 선언하고, 값을 넣은 뒤, free로 청크를 해제합니다.

poirier = malloc(sizeof(*poirier));
strncpy(poirier->team_name, "team poiri34", sizeof(poirier->team_name) - 1);
poirier->team_name[sizeof(poirier->team_name) - 1] = '\0';
strncpy(poirier->token, "dustin", sizeof(poirier->token) - 1);
poirier->token[sizeof(poirier->token) - 1] = '\0';
poirier->func = safe_action;
마찬가지로 poiri3r도 똑같이 출력되도록 선언해줍니다.

여기서 주의깊게 보셔야할 점이 poirier의 포인터 주소가 방금 해제됐던 holloway의 주소와 같다는 점 입니다.
즉 holloway의 메모리가 써져있던 청크가 해제된 뒤 다시 poirier의 메모리가 작성됨을 알 수 있습니다.
만약에 위의 strncpy부분을 다 지운 뒤, 출력만 하게된다면 다음과 같이 출력됩니다.

이게 Use After Free로 holloway의 청크가 해제됐음에도 메모리가 남아있어서 다음 동적할당 시 출력을 하면 이전 청크의 데이터가 일부 출력되게 됩니다.
volatile struct Holloway *dangling = holloway;
printf("\n[via dangling]\n team=%s\n token=%s\n id=0x%lx\n",
dangling->team_name, dangling->token, dangling->id);
여기 코드는 제일 핵심적인 부분인데, holloway 구조체의 포인터를 하나 만들어서 holloway변수의 값을 저장됩니다.
이 때 holloway는 NULL처리되지 않았기 때문에 아까 설명했던 dangling pointer입니다.
이러면 dangling포인터에 아까 해제했던 holloway의 주소가 담기게 되고, 이 주소에는 그 이후 할당한 poirier의 메모리가 담기게 됩니다.
즉 dangling을 통해 값을 읽는게 poirier 구조체의 값을 읽는것과 동일한 역할을 하게 되고, 이것이 메모리 누수를 일으킵니다.

출력결과를 보면 via dangling과 poirier부분의 출력값이 똑같습니다.
이렇게 해제된 메모리 영역에 데이터가 남는걸 이용하면 초기화되지 않은 값을 읽거나, 새로운 객체가 악의적인 코드를 사용하도록 유도할 수 있습니다
일단 이상으로 UAF에 대한 기본 포스팅을 마치겠습니다.
이제 시험기간이라 당분간은 시험관련된 공부를 하면서 포스팅을 할 듯 하네요
읽어주셔서 감사합니다