NepCTF wp

NepCTF-wp by lpp
NepBotEvent
first blood 嘻嘻
拿到附件发现是keylogger的日志文件,手写map映射一下字符,自己写的map可能有点问题
010editor里面可以看出
24字节分组
键盘日志的二进制文件有数据结构,先记录一下
输出
但能看出来ueseNENepCTF-20250725-114514;
1 | import struct |
提交flag
SpeedMino
根据love2d引擎的规则,直接解包exe得到main.lua,更改获得flag的分数,再重新打包为游戏
7z直接解包得到
打开main.lua
能看到有RC4 的 KSA 部分
CalcData() 是 RC4 PRGA 变种 + 加法加密
这一行是初始密文
然后有
for i = 1, 2600 do
flagArray = calcData(flagArray)
end
对其执行 2600 次 calcData(),每次都基于当前的 secretBox 和 secret_i/j 状态。
但是如果让我做2600次的解密操作那我就要崩溃了
但是继续往下看
超过2600分自动打印flag
那我们直接修改main.lua文件,在游戏开始时就进行变换到系统剪贴板上,然后显示flag
1 | local Array = {187,24,5,131,58,243,176,235,179,159,170,155,201,23,6,3,210,27,113,11,161,94,245,41,29,43,199,8,200,252,86,17,72,177,52,252,20,74,111,53,28,6,190,108,47,16,237,148,82,253,148,6} |
然后重新用7z打包游戏,具体打包方法
1 | 7z a -tzip mygame.love .\game* |
拿到flag
easyGooGooVVVY
题目提示考察Groovy 表达式注入
思路:利用 Groovy 的动态特性和反射,调用Java 的 Runtime 执行系统命令
构造exp
1 | "".class.forName("java.lang.Runtime") |
完成命令执行
flag在env里面
RevengeGooGooVVVY
exp
1 | def pb = new ProcessBuilder('env') |
思路:创建一个 ProcessBuilder
实例,来执行命令,启动该命令对应的进程,返回一个 Process
对象,命令执行完毕,阻塞当前线程直到 printenv
命令结束,读取进程的标准输出流,从而绕过沙箱
在java里面
1 | private static final List<String> BLOCKED_TRANSFORMS = Collections.unmodifiableList(Arrays.asList( |
加了黑名单,限制了这些注解
拿到flag
JavaSeri
界面很像vuln靶场的shiro。抓包看到
存在特征,deleteme
shiro一把梭
flag在env里面
canutrytry
拿到附件放ida分析
选项 1:
- 遍历两格槽位(
i = 0,1
),如果当前槽位指针为空且对应的size[i] > 0
,就调用malloc(size[i])
,保存到ptrs[i]
,并打印 “malloc success/failed”。 - 若两格都已被分配(
ptrs[0]
、ptrs[1]
都非 0),会打印 “no free chunks”。 - 若对应的
size[i]
不合法(<=0
),则抛出 C++ 异常 “invalid size”。
- 遍历两格槽位(
选项 2:
- 遍历两格
size[i]
(i=0,1
),对第一个值为 0 的槽位,提示 “size:”,用scanf("%d",&size[i])
读入用户输入,结束循环。 - 若两格都已被设置过(都非 0),打印 “no more size”。
- 提示 “index:”,用
scanf("%d",&idx)
读入用户选择的槽位idx
。 - 如果
ptrs[idx]
(即*((_QWORD*)&unk_405440 + idx)
)非 0,则提示 “content:”,调用read(0, ptrs[idx], size[idx])
从 stdin 读入内容写入该 chunk。然后打印 “success”。 - 否则打印 “invalid index”。
- 遍历两格
:
在 scanf("%d",&idx)
之后,用 ptrs[idx]
和 size[idx]
直接做数组下标访问,但并未检查 idx
的合法范围(>=0 && <2
)。
因此,你可以传入任意整数,比如 -1
、2
、100
等,来达到对 ptrs
数组外的任意地址进行读写。
利用思路:
任意地址写
read(0, ptrs[idx], size[idx])
,在ptrs[idx]
对应指针 任意可控 的情况下,就成了write
原语:往任意地址写任意长度(受size[idx]
限制)的数据。- 只要我们事先把
ptrs[idx]
设置成目标地址,就可以写任意数据到任意地址。
泄露 libc 基址
- 可以先用同样的越界读(
idx = -1
、idx = 2
等)读出 GOT 表的地址,计算 libc 基址。
- 可以先用同样的越界读(
构造 ROP / 劫持流程
把flagwrite到堆上再去读出
找一下会用到的gadgets的地址
1 | 0x0000000000045eb0: pop rax; ret; |
exp
1 |
|
realme
变种rc4
能够发现有smc
在汇编中找到未识别的代码块后,手动标记为函数,发现其中两处对某地址的字节进行异或处理。通过 IDAPython 脚本还原该地址内容,确认是一种改版的 RC4 加密。随后编写脚本逆向解密,成功还原出 flag。
加密流程
patch掉反调试后动调查看rc4,发现s盒多了两次异或
根据加密逻辑编写exp
1 | def ksa_custom(key): |
结果
客服小美
题目附件给了一段raw内存,还有流量分析
先看raw内存,用r-studio打开内存
在user里面的JohnDoe用户的Desktop里面发现了题目描述里面的文件,推测被控机器的用户名为JohnDoe
内存取证少不了lovelymem大人(tokeii0 666),加载镜像,因为是木马外联,在netscan里面看到
有2025的字样(估计不支持中文),猜测就是桌面上的木马文件的dump,有外联地址192.168.27.132:12580
剩下一个敏感文件,在内存上没看出来什么东西(试了好多都不对,果然流量是有用的),看给的流量附件,是一段cs流量,需要密钥
把刚刚的木马进程dump下来
利用这个进程的dump提取aeskey和hmackey
项目地址https://github.com/DidierStevens/Beta/blob/master/cs-extract-key.py
1 | python cs-extract-key.py -c 00000040ad0baebef6f64a60ecffdb0594a6f24f90e979d9b5cdecc44649cb939883b77fb1b5e021d59f9234d97ba8384f808f631e6acf73a17b1cb579651454a261ecac 6492.dmp |
得到
1 | AES Key: a6f4a04f8a6aa5ff27a5bcdd5ef3b9a7 |
然后
1 | import base64 |
然后就是CBC 解密 + HMAC 校验也就是(反向cs解密),交给ai搓个脚本
1 | import hmac |
得到
也就是最后一段flag
拼接NepCTF{JohnDoe_192.168.27.132:12580_5c1eb2c4-0b85-491f-8d50-4e965b9d8a43}
smallbox
ida打开,程序要求输入一段shellcode,然后执行该shellcode。但是存在沙箱
唯一允许的系统调用是 ptrace
,所有常用的文件读写、执行 shell 等都被禁止。任何直接的 execve
、read
、write
都会被 seccomp 杀死,利用ptrace注入shellcode
思路
由于只有 ptrace
被允许,我们可以让自身进程通过 ptrace(PTRACE_TRACEME)
让一个外部 tracer 进程附着,并由 tracer 进程帮我们完成被禁止的系统调用。
整体思路:
- ptrace 调用
先执行一次ptrace(PTRACE_PEEKUSER, …)
,检测自己是否正在被调试(反调试),失败则直接退出。 - 空循环
简单延时,进一步反调试,绕过沙箱/动态分析。 - mmap 匿名页
mmap(NULL, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
申请一块可读/写/执行的匿名内存,用于后续存 flag 数据。 - 写入 flag
立即数异或+mov 的方式,把字符串"flag"
写进刚 mmap 的内存。 - orw 系统调用
以O_RDONLY
打开文件"flag"
去读
写汇编
1 | _start: |
转shellcode直接发送Online x86 and x64 Intel Instruction Assembler
exp:
1 |
|
Time
拿到附件,放到ida分析,函数主逻辑
此处存在格式化字符串
测试一下程序,随便输入一些东西就会显示ls -l的内容
然后循环访问flag,直至触发格式化字符串的内容
多试几次可以读到
1 | 0xa7d3763640x36393333626132630x382d333236312d300x3262372d646239640x2d633866613063650x657b46544370654e |
小端序倒序拆分出来
1 | 6436377d0a000000 |
exp
1 |
|
1 |
|
Nepsign
分析附件server.py
漏洞根源:迭代哈希签名中 “零次迭代” 导致私钥直接泄露
1 | from gmssl import sm3 |
服务端提供两种操作:
- 对任意消息(除了
b"happy for NepCTF 2025"
)返回签名列表qq
(长度 48,每项是十六进制字符串)。 - 接收用户提交的
qq
,验证它是否为b"happy for NepCTF 2025"
的合法签名,验证通过则打印 flag。
- 对任意消息(除了
签名算法核心流程:
- 先对消息做一次 SM3,得到 64 字节(256 bit)的哈希值。
- 将哈希二进制拆成前 32 个字节各自的值(记作
a[0..31]
),再统计每种十六进制字符在哈希文本中出现位置的加权和(记作sum[0..15]
),共 48 维 “step” 向量。 - 对私钥数组
sk[0..47]
的每个分量,执行SM3_n(sk[i], step[i])
,得到签名分量qq[i]
。
公钥
pk[i]
=SM3_n(sk[i], 255)
(全部迭代次数固定为 255)。思路是伪造对目标消息
b"happy for NepCTF 2025"
的签名,从而拿到 flag
重点关注
当step[i] == 0
1 | qq[i] = SM3_n(sk[i], 0) == sk[i] |
这一维签名分量直接泄露了私钥 sk[i]
所以我们只需要不断对随机消息请求签名,就能刷出不同消息对应的 48 维 step
向量,筛选那些在某些下标 i
上恰好等于零的消息,每次就能输出出一个或多个 sk[i]
。
利用思路
循环生成并提交随机消息,计算其本地
step
向量(方法同服务端签名)。找到所有
step[i] == 0
且对应的sk[i]
尚未知的下标集合needed_indices
。向服务器请求该消息的签名,所返回的
qq[i]
则直接等于sk[i]
,将其保存。重复直至收集到全量的
sk[0..47]
对目标消息
b"happy for NepCTF 2025"
本地计算完整的target_step[0..47]
。逐项用已知的
sk[i]
调用SM3_n(sk[i], target_step[i])
,拼成fake_qq[0..47]
。用菜单选项 2 提交
fake_qq
,服务器执行验签:
1 | # 验签实现等价于: |
exp
1 | from gmssl import sm3 |
结果
- Title: NepCTF wp
- Author: luyanpei
- Created at : 2025-07-29 08:07:05
- Updated at : 2025-07-29 09:12:28
- Link: https://redefine.ohevan.com/posts/36779.html
- License: All Rights Reserved © luyanpei