2025 UCSC CTF WriteUp
2025 UCSC CTF wp
Reverse
easy_re-ucsc
简单异或
1 | enc = '''n=<;:h2<'?8:?'9hl9'h:l>'2>>2>hk=>;:?''' |
flag{d7610b86-5205-3bf3-b0f4-84484ba74105}
EZ_debug-ucsc
debug能看结果,下断点道最后就行,直接看
1 | for i in range(0,len(enc)): |
flag{709e9bdd-0858-9750-8c37-9b135b31f16d}
simplere-ucsc
需要手动脱壳,偷了个懒
直接脱
得到逻辑经过base58+反转异或
1 | enc = [ |
mPWV7et2RTxobH5Tn8iqGSdFWc5vYzps1jHuynpvpfmsmxeL9K28H1L1xs
re_ez-ucsc
- 程序逻辑:程序模拟一个状态机,用户输入的每个字符决定移动方向。初始位置为 1,目标位置为 3。每个字符处理后得到方向值 n4,用于调整当前位置 n0x18。
- n4 的计算:输入的字符
c
经过(c - 32) ^ 3
得到 n4 的值,n4 必须为 0、1、2、3 中的一个。对应的字符为:- n4=0 →
#
(ASCII 35) - n4=1 →
"
(ASCII 34) - n4=2 →
!
(ASCII 33) - n4=3 → 空格(ASCII 32)
- n4=0 →
- 移动步长:根据数组
qword_140020498
,每个 n4 对应步长:- 0: -5
- 1: +5
- 2: -1
- 3: +1
- 迷宫结构:数据段
dword_14002A000
定义了各位置的初始访问状态。合法路径需避开已标记的位置(值为 1)。 - 路径规划:从位置 1 出发,合法路径示例:
- 1 → 6(输入
"
,n4=1,+5) - 6 → 11(输入
"
,+5) - 11 → 16(输入
"
,+5) - 16 → 17(输入空格,n4=3,+1)
- 17 → 18(输入空格,+1)
- 18 → 13(输入
#
,n4=0,-5) - 13 → 8(输入
#
,-5) - 8 → 3(输入
#
,-5)
- 1 → 6(输入
一共五个试了第一个,其它没试过
flag{c4eb11b0e0a3cbeed7df057deaec18aa}
WEB
ezLaravel-ucsc
dirsearch直接扫
PWN
BoFido-ucsc
考察伪随机+栈溢出,栈溢出可以溢出到seed
1 | from pwn import * |
flag{b175e604-f4ad-4bbe-9c8a-f12aadc42178}
userlogin-ucsc
泄露密码,然后getshell,注意栈对齐
1 | from pwn import * |
flag{c77cdcc3-ed2d-4bad-9e00-0a943460f49c}
heap[复现]
此exp来自Hexo2εr00иe师傅,做了一些自己的注解和理解,如有错误请指正
这个知道是off-by-one,由于第一次做跟tcache相关的题目不太清楚怎么释放和申请到unsortedbin,时间有限所以没做,赛后复现下,同时在这记录下tcache
首先需要申请7个堆块
1 | for i in range(7): |
这是为了确保之后释放的 chunk 不进入 tcache,而是进入 unsorted bin。
tcache
(thread-cache)是 glibc 从 2.26 开始引入,2.27正式启用的一种 线程本地缓存机制,目的是优化 malloc/free 性能。
- 每个线程有自己的 tcache。
- tcache 维护多个“bin”(缓存桶),每个 bin 管理某一个 size class 的 chunk。
- 每个 bin 最多缓存 7 个 chunk(默认)。
free(ptr)
会先尝试将 chunk 加入 tcache:
- 判断 chunk 的 size 是否 ≤ 0x408(tcache 支持的最大 chunk size)。
- 如果这个 size 的 bin 没满(即 < 7 个 chunk),直接插入到 tcache 中,不进 fastbin,也不会进 unsorted bin。
- 插入顺序是 LIFO(后进先出),就像个栈。
malloc(size)
会先尝试从 tcache 中拿 chunk:
- 如果 tcache bin 中有合适大小的 chunk,直接返回,不用找 fastbin、smallbin、unsorted bin。
- 没有的话,才去 main_arena(fastbin、unsortedbin、小bin、大bin)找。
必须保证该大小的 tcache bin 已满(7个),这样下一个 free 才会进入 unsorted bin。
看一下此时的chunk8(0x38)
1 | for i in range(8): |
由于本题存在off-by-nullbyte(one)漏洞(最好手动尝试下)
1 | void __fastcall setinput(_BYTE *a1, int a2) |
Chunk 8 的大小是 0x38,但实际可写的数据区域只有 0x30 字节
因此我们可以控制写入0x140
我们可以这样
1 | edit(8,b'\x00' * 0x30 + p64(0x140) + b'\n') |
edit(8)
溢出 chunk[8],修改 chunk[9] 的 prev_size
字段(伪造)为 0x140,从而破坏 chunk9 的 metadata。
接着释放 chunk[9],因为 chunk[9] 的 prev_size
不匹配,会被合并到 chunk[8] 形成大 chunk 进入 unsorted bin。
此时再 show(8)
,由于 chunk[8] 被合并成大 chunk,其中包含 libc 的 unsorted bin fd 指针(指向 main_arena
),所以可以泄露 libc 地址。
因此造成了
然后就是经典的泄露环节了
1 | for i in range(7): |
1 | create(0, 0xe0) |
这个执行前后我们发现
这里变成了ed0为什么?不应该指向main_arena嘛?
2025 UCSCCTF Pwn-wp(含附件)-CSDN博客
里解释
这是因为我们申请add(0x??)之后堆块里面没有找到合适的chunk,然后原本在unsortedbin中的chunk就会先放入smallbin中,然后进行切割,切割出的0x??给用户,剩余的部分放回unsortedbin中,这个过程看起来很像是直接从unsortedbin中切割,其实并不是
GPT解释:
你对比的地址内容,其实位于两个不同的 chunk:
- 你第一次观察到的是 原始 unsortedbin 中一个 freed chunk 的 fd/bk
- 第二次看到的,是 你新建的堆块,它在调用 malloc 后又分配了 top chunk 的一部分,这部分也会进入 unsortedbin 临时区域
当你调用 malloc
时:
- 如果当前 top chunk 足够大,会将其切割;
- malloc 的剩余部分(top chunk 被切完后多余的部分)如果满足要求,会被放入 unsorted bin;
- 这时候新的 chunk 可能就会被插入到
main_arena
的链表中,并使用main_arena+0x60
/main_arena+0x68
设置其 fd/bk。
所以:这不是 main_arena 自己的地址变了,而是不同 chunk 的 fd/bk 链接到了不同位置的 main_arena+offset!
这下面应该是一个unlink操作,往free里写入binsh和system地址
1 | edit(1, p64(0x100) + p64(0x41) + b'\n') |
真的很容易搞错,就是造成堆块重叠,然后释放
完整Exp
1 | from pwn import * |
Crypto
XR4-ucsc
GPT一把梭
1 | import base64 |
essential-ucsc
RSA
part1是chatgpt出的
1 | from Crypto.Util.number import long_to_bytes |
flag{75811c6d95770d
part2很难解,一直调教deepseek
1 | from Crypto.Util.number import long_to_bytes |
56092817b75f15df05}