文章总结: 本文是2026年春节解题红包活动中部分题目的解题思路分享。作者使用IDA、JEB等工具,通过动态调试、静态分析及AI辅助等方式,详细解析了第二题(Windows程序)、第三题(Android应用)和第四题(Python程序)的加密逻辑并给出了解密代码。其中,第二题通过分析核心函数发现数据异或加密;第三题在逆向分析后找到关键的flag字符串;第四题则通过反编译和AI翻译字节码来还原加密算法。 综合评分: 85 文章分类: CTF,WEB安全,二进制安全,移动安全,安全工具
AES S-Box
S_BOX = [ 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, 0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, 0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75, 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, 0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, 0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8, 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, 0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73, 0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB, 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, 0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, 0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A, 0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, 0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, 0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16 ]
标准PNG文件头
PNG_HEADER = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
defbytestouint64le(array): returnint.frombytes(array, byteorder=”little”)
defuint64tobytesle(value): value &= 0xFFFFFFFFFFFFFFFF array = value.tobytes(8, byteorder=”little”, signed=False) return array
defrol64(value, shift): return ((value << shift) & 0xFFFFFFFFFFFFFFFF) | (value >> (64 – shift))
defror64(value, shift): return (value >> shift) | ((value << (64 – shift)) & 0xFFFFFFFFFFFFFFFF)
反推key
defreversekey(lastkey): invSBox = [0] * len(SBOX) for i inrange(len(SBOX)): invSBox[S_BOX[i]] = i
key = lastkey for inrange(8): low = key & 0xFF hi = invS_Box[low] key = (key >> 8) | ((hi << 56) & 0xFFFFFFFFFFFFFFFF)
return ror64(key, 3)
数据非线性混合
defmix_qword(buf, mixdata, offset):
v5 = bytestouint64le(buf[0:8]) v7 = rol64(v5, 3) for inrange(8): hi = (v7 >> 56) & 0xFF low = SBOX[hi] v7 = ((v7 << 8) & 0xFFFFFFFFFFFFFFFF) | low buf[0:8] = uint64tobytesle(v7) tmp = mixdata[offset : offset + 8]
for i inrange(8): mixdata[offset + i] = buf[i] ^ buf[i + 16] ^ mixdata[offset + i]
buf[16:24] = tmp
逆向求解
definverse(): premix = PNGHEADER[0:8] curmix = FILEDATA[8:16] nexmix = FILEDATA[16:24] curkey = [premix[i] ^ curmix[i] ^ nexmix[i] for i inrange(8)] prekey = bytestouint64le(curkey) return reversekey(pre_key)
解密整个文件,并提取flag
defdecryptfile(key): buf = [0] * 32 buf[0:8] = list(uint64tobytesle(key)) buf[16:24] = FILEDATA[8:16] filelen = len(FILEDATA) – 16 filelen &= 0xFFF8 filedata = FILEDATA[16 : 16 + filelen] for i inrange(0, filelen, 8): mixqword(buf, filedata, i)
withopen(“flag_decrypted.png”, “wb”) as f: f.write(bytes(filedata))
str = bytes(filedata).decode(“utf-8″, errors=”ignore”)
result = re.findall(r”flag{\S+?}”, str) print(result)
defmain(): checknum = inverse() print(“checknum=” + hex(checknum)) decryptfile(checknum)
if name == “main“: main()
* 最后展示一下王铁锤的logo,奇怪好像在哪里见过呢
### 第九题(Web中级)
* “如呼吸一般轻松,是产品的设计目标。”,这才是本题的精华
复制代码 隐藏代码 // 如此之校验,让我不能呼吸,心疼我电脑 asyncfunctioncheckCode(code, expectedHash) { const enc = newTextEncoder() let current = enc.encode(code)
for (let i = 0; i < 0x2026; i++) { current = await crypto.subtle.digest(‘SHA-256’, current) }
const hashArray = Array.from(newUint8Array(current)) const hashHex = hashArray.map(b => b.toString(16).padStart(2, ‘0’)).join(”)
return hashHex === expectedHash }
* 此函数用来重新产生flag
复制代码 隐藏代码
checkboxText.addEventListener(‘click’, async () => { const uidInput = document.getElementById(‘uid’)
if (!uidInput.value) { uidInput.focus() return }
const uid = parseInt(uidInput.value) || 0 const voice = document.getElementById(‘voice’).value
try { const challenge = wasm_bindgen.gen(uid, voice) // 此函数是产生语音的地方 currentHash = challenge.h
audio.src = URL.createObjectURL(newBlob([challenge.a], { type: ‘audio/wav’ }))
challengeView.style.display = ‘block’
checkboxText.classList.remove(‘btn-important’) document.getElementById(‘verifyBtn’).classList.add(‘btn-important’)
while (n = w.nextNode()) n.data.includes) && (n.remove(), i = 0x2026)
audio.play().catch(e =>console.warn(“Auto-play blocked:”, e))
document.getElementById(‘verifyInput’).focus()
checkboxText.innerText = “重新生成语音验证码”
} catch (e) { console.error(e) } })
* 继续跟进,会跟到wasm中的gen函数,函数非常的长,这里就不贴了
* 核心的流程大概是把uid与一些特定的随机数,然后进行HMAC计算hash,最后再进行base64产生字符
* base64码表在内存中的地址是固定的1295903,可以在内存中直接查看a-zA-Z0-9?!
* 最后算出来的这个base64值就是语音播报的内容,后面应该是合成语言的过程,没有分析
* 所以,只需要将这个base64的值给dump出来就可以了
复制代码 隐藏代码
i32.load8_u offset=1295903 // 这个值是base64码表的位置 local.set $var8 local.get $var3 i32.load offset=416 local.get $var0 i32.eq if local.get $var3 i32.const 416 i32.add call $func39 end local.get $var3 //var3中保存了内存的基址,抓包发现是定值1047920 i32.load offset=420 //这个是地址偏移量 local.get $var6 // var6是索引后的偏移,地址的计算法方是[[var3+420] + var6] i32.add local.get $var8 //此处是查完了base64表的值 i32.store // 此处保存回内存 local.get $var3 local.get $var0 i32.const 1 i32.add local.tee $var0 i32.store offset=424 local.get $var2 i32.const 6
复制代码 隐藏代码 local.get $var0 i32.const 1 i32.add local.tee $var0 i32.store offset=424 br $label15 end i32.const 4 i32.const 200 call $func65 unreachable end i32.const 1 i32.const 37 call $func65 unreachable end $label1 local.get $var3 // 在此处下断比较合适,这地方刚好计算完50个字符 i32.load offset=420 local.set $var6 local.get $var0 if (result i32)
* 计算内存偏移的核心部分,用AI给翻译了一下,这块注意一下,不要给AI太多的数据,要不AI会直接崩溃
复制代码 隐藏代码 // 这是一个代码块,用于错误处理或提前退出 { // 分配内存并检查分配结果 ptr = allocate(37, 1); if (ptr != NULL) { // 第一部分:数据准备和初始化
// 对var0进行字节拆分和异或操作 // 从var3+83到var3+80读取4个字节,分别与var0的4个字节异或 byte0 = var0 ^ memory[var3+80]; byte1 = (var0 >> 8) ^ memory[var3+81]; byte2 = (var0 >> 16) ^ memory[var3+82]; byte3 = (var0 >> 24) ^ memory[var3+83];
// 将结果存入分配的内存 ptr[0] = byte0; ptr[1] = byte1; ptr[2] = byte2; ptr[3] = byte3;
// 复制var3+80处的数据到ptr+4 memcpy(ptr+4, var3+80, 8); // 复制8字节 memcpy(ptr+12, var3+88, 8); // 复制8字节 ptr[20] = memory[var3+96]; // 复制1字节
// 将异或结果存回var3+100到var3+103 memory[var3+100] = byte3; memory[var3+101] = byte2; memory[var3+102] = byte1; memory[var3+103] = byte0;
// 分配栈空间 stack = malloc(352);
// 初始化栈空间为0 memset(stack, 0, 352);
// 从内存地址1295967复制14字节到栈空间 memcpy(stack, 1295967, 14);
// 将栈空间的数据复制到var3+416处 memcpy(var3+416, stack, 64);
// 释放栈空间 free(stack);
// 对var3+416开始的64字节进行异或操作(常数54) for(i=0; i<64; i+=4) { memory[var3+416+i] ^= 54; memory[var3+416+i+1] ^= 54; memory[var3+416+i+2] ^= 54; memory[var3+416+i+3] ^= 54; }
// 初始化哈希状态和调用哈希函数 memcpy(var3+480, 1295984, 32); // 复制32字节 var3+512 = 1; // 设置计数器 hash_function(var3+480, var3+416, 1); // 调用哈希函数
// 再次对var3+416开始的64字节进行异或操作(常数106) for(i=0; i<64; i+=4) { memory[var3+416+i] ^= 106; memory[var3+416+i+1] ^= 106; memory[var3+416+i+2] ^= 106; memory[var3+416+i+3] ^= 106; }
// 初始化另一个哈希状态并调用哈希函数 memcpy(var3+520, 1295984, 32); // 复制32字节 var3+552 = 1; // 设置计数器 hash_function(var3+520, var3+416, 1); // 调用哈希函数
// 复制哈希结果 memcpy(var3+560, var3+480, 80); // 复制80字节 memcpy(var3+600, var3+520, 80); // 复制80字节
// 从var3+560复制152字节到var3+256 memcpy(var3+256, var3+560, 152);
// 初始化var3+336处的65字节为0 memset(var3+336, 0, 65);
// 从var3+256复制152字节到var3+104 memcpy(var3+104, var3+256, 152);
// 处理数据块 offset = memory[var3+248]; // 获取当前偏移 if(offset >= 43) { // 如果偏移>=43,处理剩余数据 remaining = 64 – offset; if(remaining != 0) { memcpy(var3+184+offset, ptr, remaining); } var3+136 += 1; // 增加计数器 hash_function(var3+104, var3+184, 1); // 调用哈希函数
if(offset != 43) { memcpy(var3+184, ptr+remaining, offset-43); } newoffset = 0; } else { // 如果偏移<43,直接复制数据 memcpy(var3+184+offset, ptr, 21); newoffset = offset + 21; }
// 更新偏移 memory[var3+248] = new_offset;
// 从var3+104复制152字节到var3+256 memcpy(var3+256, var3+104, 152);
// 在缓冲区的适当位置添加0x80 bufferoffset = memory[var3+400]; buffer[var3+336+bufferoffset] = 0x80;
// 计算消息长度并进行字节序转换 length = bufferoffset * 8; // 转换为比特数 // 进行复杂的字节序转换操作 lengthbytes = converttobigendian64(length);
// 处理填充 if(bufferoffset != 63) { // 如果缓冲区不满,填充0 fillcount = 63 – bufferoffset; if(fillcount != 0) { memset(var3+336+bufferoffset+1, 0, fillcount); }
if(bufferoffset <= 56) { // 如果长度可以放在当前块 hashfunction(var3+256, var3+336, 1); // 准备最终块 memset(var3+560, 0, 80); var3+616 = lengthbytes; // 存储长度 hashfunction(var3+256, var3+560, 1); } else { // 如果长度需要下一个块 var3+392 = lengthbytes; // 存储长度 hashfunction(var3+256, var3+336, 1); } } else { // 缓冲区已满的情况 var3+392 = lengthbytes; // 存储长度 hashfunction(var3+256, var3+336, 1); }
// 重置缓冲区偏移 memory[var3+400] = 32;
// 对哈希状态进行字节序转换 for(i=0; i<8; i++) { word = (int)(var3+256+i*4); // 转换为大端序 bigendianword = ((word << 24) & 0xFF000000) | ((word << 8) & 0x00FF0000) | ((word >> 8) & 0x0000FF00) | ((word >> 24) & 0x000000FF); *(int)(var3+336+i4) = bigendianword; }
// 处理最终的哈希块 length = (longlong)(var3+328) * 8; // 转换为比特数 lengthbytes = converttobigendian_64(length | 0x100);
// 准备填充 memset(var3+368, 0, 17); memory[var3+368] = 0x80; var3+392 = length_bytes;
// 调用最终的哈希函数 hash_function(var3+296, var3+336, 1);
// 获取哈希结果并转换为大端序 hash0 = (int)(var3+296); hash1 = (int)(var3+300); hash2 = (int)(var3+304); hash3 = (int)(var3+308);
// 将结果存回分配的内存 (int)(ptr+21) = converttobigendian32(hash0); (int)(ptr+25) = converttobigendian32(hash1); (int)(ptr+29) = converttobigendian32(hash2); (int)(ptr+33) = converttobigendian32(hash3);
// 第二部分:类似Base64编码
// 分配输出缓冲区 output = allocate(200, 4); if(output != NULL) { var3+416 = 50; // 设置块大小 var3+420 = output; // 设置输出指针 var3+424 = 0; // 设置输出索引
inputindex = 0; bitbuffer = 0; bitcount = 0; inputptr = ptr;
// 处理输入数据 for(byteindex=0; byteindex<37; byteindex++) { // 读取一个字节 byte = inputptr[byte_index];
// 添加到位缓冲区 bitbuffer = (bitbuffer << 8) | byte; bit_count += 8;
// 处理完整的6位组 while(bitcount >= 6) { // 提取6位 index = (bitbuffer >> (bit_count – 6)) & 0x3F;
// 检查是否需要调用函数 if(var3+416 == inputindex) { callfunction(var3+416); }
// 从表中查找并存储 tablevalue = lookuptable[1295903 + index]; output[inputindex] = tablevalue; inputindex++; var3+424 = inputindex;
bit_count -= 6; } }
// 处理剩余的位 if(bitcount > 0) { // 提取剩余的位 index = (bitbuffer << (6 – bit_count)) & 0x3F;
// 检查是否需要调用函数 if(var3+416 == inputindex) { callfunction(var3+416); }
// 从表中查找并存储 tablevalue = lookuptable[1295903 + index]; output[inputindex] = tablevalue; inputindex++; var3+424 = inputindex; }
// 块结束 } else { // 内存分配失败 error(4, 200); } } else { // 内存分配失败 error(1, 37); } }
* 直接查内存就可以定位到flag了,顺便写了个dump的函数
复制代码 隐藏代码 __exports.Dump = function(){ ptr = 1048340; len = 4; buffer = getArrayU8FromWasm0(ptr,len); ptr = 0; for (let i = 0; i < buffer.length; i++) { console.log(“buffer[“+i+”]=”+buffer[i]); ptr = (ptr << 8) | buffer[buffer.length – i – 1]; } console.log(“Address:”+ptr); strbuf = getStringFromWasm0(ptr,200); const codestr = strbuf.replace(/[^a-zA-Z0-9?!]/g, ”); console.log(“CodeLen:”+codestr.length); console.log(“CheckCode:”+codestr); } “`
- 断点断下来后,就可以直接调试窗口中运行此函数,即可得到50字节的flag
- 总之做这道题有点费劲,不太懂JS,完全现学现卖
- 也不知道用啥工具来进行分析,此题全作仍赖AI的翻译,解的题的方法也比较笨,欢迎指点
总结
- 今年中级题比较多,而且题出的也比较有意思,作者们都是有故事的人啊
- 今年开始学习了python,但是不是很熟,题解也尽量都用python开写的,写的不好,多多指点
- 个人来讲中级题免强能做,但是做出来还是比较费劲,由其代码中加入一些混淆以后,对我的干扰还是很大的,不知道有没有大佬讲讲如何在IDA中去混淆,让代码可读性更好一些
- 解第七题的时候,一开始一直想着是不是需要暴力破解,写题解的时候,我才发现,作者有提醒,暴力不可取,哎,作者用心良苦啊,耐何我才看到,呵呵
- 还有那个Android代码的混淆,对于我来说直的是无从下手,目前也搜不到好的方法可以反混淆,不知道有没有大佬能提点一二
- 今年中级题遇到的最大的挑战就是,Android中的反调试,一点都不会破,IDA一附加上去程序就崩,我也泪崩
- 总之,通过跟着论坛的贴子一点一点的积累,从啥也不懂的小白,到今年,自我感觉成绩还不错,明年若有机会,我会继续加油努力
- 最后,希望吾爱论坛越做越好,吾爱百年,百年吾爱!
-官方论坛
www.52pojie.cn
👆👆👆
公众号设置“星标”,您不会错过新的消息通知
如开放注册、精华文章和周边活动等公告
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:吾爱破解论坛 吾爱pojie 吾爱pojie《【2026春节解题红包】WriteUP(2、3、4、5、6、7、9)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论