Dreamhack/Pwnable 문제 풀이

[Pwnable Advanced] Memory Corruption: Use After Free

seobangwool 2026. 4. 14. 19:16

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이 가리키고 있는 힙 메모리 블록을 힙 관리자에 반환하기는 하지만, 메모리 자체의 값이 변하는건 아니다.

조금 더 자세히 살펴보면,

  1. ptrptr = malloc(256)을 통해 256 바이트 청크가 할당되어 있는 상태이다.
  2. free(ptr)을 통해 메모리를 반납한다.
  3. in-use 상태에서 free 상태로 들어가게 된다. ← bin 에 들어간다. (단순히 청크 헤더의 inuse 플래그가 꺼지고, bin 에 들어간거지, 내부 값 사라지는건 아님. 청크 헤더만 변경된 상태)

따라서 위와 같은 경우에는 Dangling Pointer 위험성이 존재한다.

 

동적 분석

image

우선 diassem 을 통해 구조를 파악해준다.

main +67에 scanf가 호출되므로 브레이크를 걸고 1을 먼저 넣어줬다.
그리고 malloc을 호출할 때 까지 쭉 넘어가주면,

 

image

mov edi, 0x100 으로 256바이트 크기 동적 할당을 위한 인자를 설정해주고,
call malloc@plt를 통해 0x100 (256) 바이트 크기의 청크를 할당해준다.

그리고 반환된 주소를,
mov qword ptr [rbp - 0x10], rax을 통해 rbp - 0x10 위치에 저장해두었다는 것을 알아낼 수 있다.

또한, ptr$rbp-0x100x0000555555602a80 주소를 가르키고 있음을 기억해두자.

 

image

이후에 scanf 호출 뒤 2를 입력해준 뒤
free(ptr)을 실행 이후 ptr의 변화를 살펴보면,
여전히 ptr$rbp-0x100x0000555555602a80 주소를 가르키고 있음을 알 수 있다.

즉, ptr이 가르키는 값인 주소는 변화가 없다는 것을 확인했다.

이는 UAF 취약점이 발생할 수 있기 때문에 새로운 청크를 할당할 시에는 명시적으로 초기화를 해주어야함을 잊어선 안된다.

또한, 실행결과에 보이는 double free bug 도 프로그램에 심각한 보안 위협이 되는 소프트웨어 취약점이다.
이 강의에서는 다루지 않고, week2에서 자세히 다뤄볼 예정이다.

 

Use After Free

이제 해제된 메모리에 접근할 수 있을 때 발생하는 Use After Free 취약점 예제를 살펴보자.

이전 예제처럼, Dangling Pointer로 인해 발생하기도 하지만, 새롭게 할당한 영역을 초기화하지 않고 사용할 때도 발생한다.

mallocfree 함수에는 메모리의 데이터 초기화 기능이 없기 때문에, 새롭게 할당한 청크를 명시적으로 초기화해주는 작업을 해줘야 한다.

아래 예제를 통해 자세히 알아보자.

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 에서 브레이크를 걸고 천천히 코드를 따라가보면서 메모리 할당과 재사용을 살펴보면 취약점을 정확히 볼 수 있을 것 같다.

 

image

0x38 크기의 구조체 크기만큼 인자를 설정해주고,
청크를 할당해주는 것을 볼 수 있다.

secret$rbp-0x100xffffe6a0을 가르키고 있음을 기억하자.

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 에서 브레이크를 걸고 확인해준다.

 

image

0x602260:       "ADMIN PASSWORD"
0x60226f:       ""
0x602270:       "P@ssw0rd!@#"
0x60227c:       ""


이후 heap 명령어를 통해 청크 정보를 조회해보면,

두 번째 결과가 secret에 해당하는 청크임을 알 수 있다.

그러고 secret 초기화를 지나,
nametag = malloc(sizeof(struct NameTag)); 부분에 가보면,

 

image

secret0x0 으로 잘 바뀌었으나 0x602270 주소의 P@ssw0rd!@# 는 여전히 남아있는 것을 볼 수 있다.

이는 inuse 상태였던 청크가 free 상태가 되면서 청크 헤더의 일부가 변경되는 과정에서 secret의 일부는 지워지고 일부는 남아있는 모습으로 이해할 수 있다.

다시 말해 일부 fd/bk 요소가 기존 헤더에서, data 영역 직전 위치에 추가로 16바이트를 덮어서, 앞 부분은 날라가고 뒷 부분은 남아있는 모습이다.

 

image

그렇게 진행된 결과로, 결국
Name: S@ssw0rd!@# 이라는 다른 결과가 나오게 된 것이다.

그리고 조금 더 자세히 살펴보면,

 

secret->code 로 대입했던 0x1337 또한 남아있는 것을 알 수 있다.

이 값이 0 이 아니므로 마지막 코드에서 오류가 생겨 Segmentation Fault 가 발생한다.

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