Memory Corruption: Use After Free
Use After Free
- 메모리 참조에 사용한 포인터를 메모리 해제 후에 적절히 초기화하지 않아서 발생하는 취약점.
- 또는 해제한 메모리를 초기화하지 않고 다음 청크에 재할당해주면서 발생하는 취약점이다.
- 다른 취약점에 비해 익스플로잇 성공률이 높은 편이라 상당히 위험한 취약점이다.
실습 환경 Dockerfile
Ubuntu 18.04 64-bit(Glibc 2.27) 실습 환경 Dockerfile
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 -v 1.6.2
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
Dangling Pointer
- 유효하지 않은 메모리 영역을 가리키는 포인터를 말한다.
- 메모리를 동적 할당할 때, 포인터를 선언하고 그 포인터에
malloc함수가 할당한 메모리의 주소를 저장한다. 그리고 그 포인터를 참조하여 할당한 메모리에 접근한다. - 메모리를 해제할 때는
free함수를 호출한다. free함수는 청크를ptmalloc에 반환하기만 하고, 청크의 주소를 담고 있던 포인터를 초기화하지는 않는다.
→free호출 이후 따로 포인터를 해제해주지 않으면, 포인터는 해제된 청크를 가리키는 Dangling Pointer가 된다.- Dangling Pointer는 보안적으로 취약하다기 보단 오동장 가능성을 키우고, 이는 공격 수단이 될 가능성이 있다.
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");
}
free(ptr);
break;
default:
break;
}
}
}
실행 결과
결과 분석
위 예제를 보면 ptr = malloc(256)을 통해 256바이트 청크를 할당한다.
그러나 free(ptr)을 통해 ptr이 가리키고 있는 힙 메모리 블록을 힙 관리자에 반환하기는 하지만, 메모리 자체의 값이 변하는건 아니다.
조금 더 자세히 살펴보면,
ptr은ptr = malloc(256)을 통해 256 바이트 청크가 할당되어 있는 상태이다.free(ptr)을 통해 메모리를 반납한다.- in-use 상태에서 free 상태로 들어가게 된다. ← bin 에 들어간다. (단순히 청크 헤더의 inuse 플래그가 꺼지고, bin 에 들어간거지, 내부 값 사라지는건 아님. 청크 헤더만 변경된 상태)
따라서 위와 같은 경우에는 Dangling Pointer 위험성이 존재한다.
동적 분석
우선 diassem 을 통해 구조를 파악해준다.
main +67에 scanf가 호출되므로 브레이크를 걸고 1을 먼저 넣어줬다.
그리고 malloc을 호출할 때 까지 쭉 넘어가주면,
mov edi, 0x100 으로 256바이트 크기 동적 할당을 위한 인자를 설정해주고,call malloc@plt를 통해 0x100 (256) 바이트 크기의 청크를 할당해준다.
그리고 반환된 주소를,mov qword ptr [rbp - 0x10], rax을 통해 rbp - 0x10 위치에 저장해두었다는 것을 알아낼 수 있다.
또한, ptr인 $rbp-0x10는 0x0000555555602a80 주소를 가르키고 있음을 기억해두자.
이후에 scanf 호출 뒤 2를 입력해준 뒤free(ptr)을 실행 이후 ptr의 변화를 살펴보면,
여전히 ptr인 $rbp-0x10는 0x0000555555602a80 주소를 가르키고 있음을 알 수 있다.
즉, ptr이 가르키는 값인 주소는 변화가 없다는 것을 확인했다.
이는 UAF 취약점이 발생할 수 있기 때문에 새로운 청크를 할당할 시에는 명시적으로 초기화를 해주어야함을 잊어선 안된다.
또한, 실행결과에 보이는 double free bug 도 프로그램에 심각한 보안 위협이 되는 소프트웨어 취약점이다.
이 강의에서는 다루지 않고, week2에서 자세히 다뤄볼 예정이다.
Use After Free
이제 해제된 메모리에 접근할 수 있을 때 발생하는 Use After Free 취약점 예제를 살펴보자.
이전 예제처럼, Dangling Pointer로 인해 발생하기도 하지만, 새롭게 할당한 영역을 초기화하지 않고 사용할 때도 발생한다.
malloc과 free 함수에는 메모리의 데이터 초기화 기능이 없기 때문에, 새롭게 할당한 청크를 명시적으로 초기화해주는 작업을 해줘야 한다.
아래 예제를 통해 자세히 알아보자.
Use After Free 위험성 예제
// 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();
}
}
코드 분석
위 코드를 먼저 살펴보면, 한 가지 취약점이 보인다.
free(secret);secret = NULL;
위 코드를 통해 secret 포인터를 NULL로 만들었지만, secret 포인터가 가리키는 (해제된 청크의) 메모리는 그대로 존재하기 때문에 취약점이 발생한다.
구조체의 크기같고, 해제 후 바로 사용하기 때문에 같은 bin 에서 재사용할 것이다.
자세한 분석은 실행 결과와 동적 분석을 통해 해보겠다.
실행 결과
동적 분석
pwndbg> disassem main
Dump of assembler code for function main:
0x00000000004005e7 <+0>: push rbp
0x00000000004005e8 <+1>: mov rbp,rsp
0x00000000004005eb <+4>: sub rsp,0x10
0x00000000004005ef <+8>: mov edi,0x38
0x00000000004005f4 <+13>: call 0x4004f0 <malloc@plt>
0x00000000004005f9 <+18>: mov QWORD PTR [rbp-0x10],rax
0x00000000004005fd <+22>: mov rax,QWORD PTR [rbp-0x10]
0x0000000000400601 <+26>: movabs rcx,0x4150204e494d4441
0x000000000040060b <+36>: mov QWORD PTR [rax],rcx
0x000000000040060e <+39>: mov DWORD PTR [rax+0x8],0x4f575353
0x0000000000400615 <+46>: mov WORD PTR [rax+0xc],0x4452
0x000000000040061b <+52>: mov BYTE PTR [rax+0xe],0x0
0x000000000040061f <+56>: mov rax,QWORD PTR [rbp-0x10]
0x0000000000400623 <+60>: add rax,0x10
0x0000000000400627 <+64>: movabs rcx,0x6472307773734050
0x0000000000400631 <+74>: mov QWORD PTR [rax],rcx
0x0000000000400634 <+77>: mov DWORD PTR [rax+0x8],0x234021
0x000000000040063b <+84>: mov rax,QWORD PTR [rbp-0x10]
0x000000000040063f <+88>: mov QWORD PTR [rax+0x30],0x1337
0x0000000000400647 <+96>: mov rax,QWORD PTR [rbp-0x10]
0x000000000040064b <+100>: mov rdi,rax
0x000000000040064e <+103>: call 0x4004c0 <free@plt>
0x0000000000400653 <+108>: mov QWORD PTR [rbp-0x10],0x0
0x000000000040065b <+116>: mov edi,0x38
0x0000000000400660 <+121>: call 0x4004f0 <malloc@plt>
0x0000000000400665 <+126>: mov QWORD PTR [rbp-0x8],rax
0x0000000000400669 <+130>: mov rax,QWORD PTR [rbp-0x8]
0x000000000040066d <+134>: movabs rdx,0x7974697275636573
0x0000000000400677 <+144>: mov QWORD PTR [rax],rdx
0x000000000040067a <+147>: mov DWORD PTR [rax+0x8],0x61657420
0x0000000000400681 <+154>: mov WORD PTR [rax+0xc],0x6d
0x0000000000400687 <+160>: mov rax,QWORD PTR [rbp-0x8]
0x000000000040068b <+164>: add rax,0x10
0x000000000040068f <+168>: mov edx,0x1
0x0000000000400694 <+173>: lea rsi,[rip+0x109] # 0x4007a4
0x000000000040069b <+180>: mov rdi,rax
0x000000000040069e <+183>: call 0x4004e0 <memcpy@plt>
0x00000000004006a3 <+188>: mov rax,QWORD PTR [rbp-0x8]
0x00000000004006a7 <+192>: mov rsi,rax
0x00000000004006aa <+195>: lea rdi,[rip+0xf5] # 0x4007a6
0x00000000004006b1 <+202>: mov eax,0x0
0x00000000004006b6 <+207>: call 0x4004d0 <printf@plt>
0x00000000004006bb <+212>: mov rax,QWORD PTR [rbp-0x8]
0x00000000004006bf <+216>: add rax,0x10
0x00000000004006c3 <+220>: mov rsi,rax
0x00000000004006c6 <+223>: lea rdi,[rip+0xe8] # 0x4007b5
0x00000000004006cd <+230>: mov eax,0x0
0x00000000004006d2 <+235>: call 0x4004d0 <printf@plt>
0x00000000004006d7 <+240>: mov rax,QWORD PTR [rbp-0x8]
0x00000000004006db <+244>: mov rax,QWORD PTR [rax+0x30]
0x00000000004006df <+248>: test rax,rax
0x00000000004006e2 <+251>: je 0x40070f <main+296>
0x00000000004006e4 <+253>: mov rax,QWORD PTR [rbp-0x8]
0x00000000004006e8 <+257>: mov rax,QWORD PTR [rax+0x30]
0x00000000004006ec <+261>: mov rsi,rax
0x00000000004006ef <+264>: lea rdi,[rip+0xc9] # 0x4007bf
0x00000000004006f6 <+271>: mov eax,0x0
0x00000000004006fb <+276>: call 0x4004d0 <printf@plt>
0x0000000000400700 <+281>: mov rax,QWORD PTR [rbp-0x8]
0x0000000000400704 <+285>: mov rdx,QWORD PTR [rax+0x30]
0x0000000000400708 <+289>: mov eax,0x0
0x000000000040070d <+294>: call rdx
0x000000000040070f <+296>: mov eax,0x0
0x0000000000400714 <+301>: leave
0x0000000000400715 <+302>: ret
우선 disassem을 해준 뒤 코드를 살펴보자.
main+13 에서 브레이크를 걸고 천천히 코드를 따라가보면서 메모리 할당과 재사용을 살펴보면 취약점을 정확히 볼 수 있을 것 같다.
0x38 크기의 구조체 크기만큼 인자를 설정해주고,
청크를 할당해주는 것을 볼 수 있다.
secret인 $rbp-0x10은 0xffffe6a0을 가르키고 있음을 기억하자.
0x000000000040063f <+88>: mov QWORD PTR [rax+0x30],0x1337
0x0000000000400647 <+96>: mov rax,QWORD PTR [rbp-0x10]
0x000000000040064b <+100>: mov rdi,rax
0x000000000040064e <+103>: call 0x4004c0 <free@plt>
그리고 free 이전에 해당 힙 주소에 어떤 값이 있는지 알기 위해,
main+88 에서 브레이크를 걸고 확인해준다.
0x602260: "ADMIN PASSWORD"
0x60226f: ""
0x602270: "P@ssw0rd!@#"
0x60227c: ""
이후 heap 명령어를 통해 청크 정보를 조회해보면,
두 번째 결과가 secret에 해당하는 청크임을 알 수 있다.
그러고 secret 초기화를 지나,nametag = malloc(sizeof(struct NameTag)); 부분에 가보면,
secret은 0x0 으로 잘 바뀌었으나 0x602270 주소의 P@ssw0rd!@# 는 여전히 남아있는 것을 볼 수 있다.
이는 inuse 상태였던 청크가 free 상태가 되면서 청크 헤더의 일부가 변경되는 과정에서 secret의 일부는 지워지고 일부는 남아있는 모습으로 이해할 수 있다.
다시 말해 일부 fd/bk 요소가 기존 헤더에서, data 영역 직전 위치에 추가로 16바이트를 덮어서, 앞 부분은 날라가고 뒷 부분은 남아있는 모습이다.
그렇게 진행된 결과로, 결국Name: S@ssw0rd!@# 이라는 다른 결과가 나오게 된 것이다.
그리고 조금 더 자세히 살펴보면,
secret->code 로 대입했던 0x1337 또한 남아있는 것을 알 수 있다.
이 값이 0 이 아니므로 마지막 코드에서 오류가 생겨 Segmentation Fault 가 발생한다.
이렇게 초기화되지 않은 메모리의 값을 읽어내거나, 새로운 객체가 악의적인 값을 사용하도록 유도하려 프로그램의 정상적인 실행을 방해할 수 있다.
'Dreamhack > Pwnable 문제 풀이' 카테고리의 다른 글
| [Pwnable] Exploit Tech: Return to Library (0) | 2026.04.15 |
|---|---|
| [Pwnable] basic_exploitation_000 (0) | 2026.04.15 |
| [Pwnable Advanced] Exercise: tcache_dup (0) | 2026.04.15 |
| [Pwnable Advanced] Exploit Tech: Tcache Poisoning (0) | 2026.04.15 |
| [Pwnable Advanced] Exploit Tech: Use After Free (1) | 2026.04.14 |