吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2391|回复: 14
收起左侧

[CTF] [2026]春节红包题解(大部分)

[复制链接]
jasomlyc 发表于 2026-3-4 10:17
本帖最后由 jasomlyc 于 2026-3-4 22:58 编辑

第一次参加,勉勉强强把题目全解了。(昨晚最后才把 Win 高级整出来)

暂时先发一部分,有的题目本地都删了。。。任务那里也不能再下载。


之二

使用工具:IDA v8.3

没壳,直接 IDA 分析。

浏览下找到 start 函数, F5。
image-20260228215822884.png

这个函数是 C 运行时的启动函数,不包括实际的运行逻辑,我们要找的程序入口点在 result 处。
image-20260228220058834.png
(这里直接搜索 String 也能找到下面)
image-20260228220343940.png
进入下面红框区域(显示 Flag 正确)的条件是蓝色处 if 为 1
image-20260228220450572.png
下面的 while 循环在将用户输入与 Block 的每个字符一一比较,最后返回相同的字符个数是否等于 31(也即最终 Flag 长度)。明显蓝色处就是在初始化 Block。(上一个语句应该是 new 一块区域)
image-20260228221007966.png
进去一看很明显的字符串异或解密。
image-20260228221154664.png
模仿写出以下脚本:

def decrypt_sub_401620():
    dwords = [758280311, 1663511336, 1880974179, 494170226, 842146570, 657202491, 658185525]
    word = 12323
    byte = 99

    xor_key = 0x42
    buffer = bytearray()

    for d in dwords:
        buffer.extend(d.to_bytes(4, 'little'))

    buffer.extend(word.to_bytes(2, 'little'))
    buffer.append(byte)

    decrypted = "".join(chr(b ^ xor_key) for b in buffer)

    return decrypted

print(f"Decrypted: {decrypt_sub_401620()}")

(这题挂个调试器上去看应该会更快)


之三

使用工具:JADX 1.5.4

(这题网上搜个解法器会快点。。下面我这个流程要不是在过年真没耐心做完。)

直接找 MainActivity,很明显用 Compose 写的。

找到 setContent 以及内容的来源。
image-20260228223100385.png
image-20260228223338136.png
Switch 分支走的是 default,但是调用的 f6536a 会再调回来走 case 0.
image-20260228223617001.png
指令太多 JADX 直接罢工。(记住这个地方后面还会再来)
image-20260228223853287.png
于是搜索 FLAG,找到了一个 UI 组件。可以推测 f7612q 大概率是 Flag。这个参数是在初始化时引入的,所以要找到是哪里创建了 u1.m.
image-20260228224839639.png
image-20260228224917751.png
回到刚刚的 AbstractC0726k.d,在 Smali 里查询。
image-20260228225653997.png
v23 来源于 v9, 与 w1.g 有关。
image-20260301004829178.png
这三个里明显 l 更可能是 Flag 值。于是接下来找哪个地方会赋值给它。
image-20260301011059894.png
回到之前的 u1.m,可以找到 w1.g 作为参数调用了 F.C. 在这个类我们找到了创建 w1.d 的出现。
image-20260301011515176.png
image-20260301011856719.png
往上翻一翻,下面这部分其实是取密钥和密文的阶段。(这里应该是另外一个 Case)
image-20260301012029190.png
而密文从 r1.a 中获取
image-20260301012431225.png

def decrypt_flag():
    # [0x36, 0x01, 0x16, 0x1c]
    key = [54, 1, 22, 28]

    encrypted_data = [
        [80, 109, 119, 123, 77],
        [97, 116, 34, 45, 105],
        [102, 49, 124, 45, 5, 94],
        [4, 49, 36, 42, 105],
        [101, 113, 100, 45, 88, 102, 73],
        [112, 50, 101, 104, 7, 119, 34, 112, 75]
    ]

    flag = ""

    for row in encrypted_data:
        for i, encrypted_char in enumerate(row):
            decrypted_char = encrypted_char ^ key[i % len(key)]
            flag += chr(decrypted_char)

    return flag

if __name__ == "__main__":
    print(f"Result: {decrypt_flag()}")

之四

使用工具:
https://github.com/extremecoders-re/pyinstxtractor
https://github.com/greenozon/pycdc/tree/py314 (原版只支持到 3.12 所以用了别人的 Fork)(需要自行编译)

(看图标就知道用了什么)用 PyInstaller 打了包,用 Extractor 拆出来。
有其他别的依赖之类的东西,但下面这个文件名就很明显。
image-20260301013445408.png
对上面的 pyc 用 pycdas 解出操作码。
image-20260301013536455.png
image-20260301013738208.png
照着以上两个函数写个脚本解密即可

import base64

def get_encrypted_flag():
    enc_data = 'e3w+fiRvfW18fnx4ZAZ6Pj43YwB9OWMXfXo8Dg4O'
    return base64.b64decode(enc_data)

def generate_flag():
    encrypted = get_encrypted_flag()

    key = 78

    result = bytearray()

    for _, byte in enumerate(encrypted):
        result.append(byte ^ key)

    return result

if __name__ == '__main__':
    print(generate_flag())

之五

使用工具:

IDA v8.3
https://github.com/extremecoders-re/nuitka-extractor
https://www.angusj.com/resourcehacker/
参考:
https://www.52pojie.cn/forum.php?mod=viewthread&tid=2063208

用 DIE 可以看到用了 Nuitka 打包,用工具解出来。不像 PyInstaller 可以解出 pyc,这次只能得到 dll.

之后分析过程照抄上面的论坛帖子即可,不过由于版本更替,相应的分析脚本需要更新。

总之最终能得到主函数里下面类似的输出

--- Blob: __main__ | Size: 0x62b | Objects: 83 ---

  [0]: [b'dc!a;`b', '<Runtime_Identifier_17>', b'cacg', '<Runtime_Identifier_47>', b'\x19e!!(', '<Runtime_Identifier_14>', b'\x1fb&', '<Runtime_Identifier_14>', b'\x08be#', b'ppp']
  [1]: '_parts'
  [2]: 81
  [3]: '_key'
  [4]: 30
  [5]: '_total_len'
  ... 以下省略

刚刚好第一部分里有 30 个字符,相对应的 81 就是密钥。猜测是异或解密(也确实是),所以用以下脚本解决。

def decrypt_flag():
    key = 81

    encrypted_parts = [
        b'dc!a;`b',
        bytes([17]),
        b'cacg',
        bytes([47]),
        b'\x19e!!(',
        bytes([14]),
        b'\x1fb&',
        bytes([14]),
        b'\x08be#',
        b'ppp'
    ]

    ciphertext = b"".join(encrypted_parts)

    decrypted_chars = [chr(b ^ key) for b in ciphertext]
    flag = "".join(decrypted_chars)

    print(f"Result: {flag}")

if __name__ == "__main__":
    decrypt_flag()

之六

使用工具:binwalk

Credit 和 License 两个文件把用的什么技术全交代了。
用 binwalk 把 exe 里藏的东西解出来。
image-20260304115537812.png
main.lua 里给出了资产的解密逻辑。
image-20260304115713548.png
于是

def extract_flag(file_path):
    key = b"52pojie"

    with open(file_path, "rb") as f:
        encrypted_content = f.read()

    decrypted_bytes = bytearray()

    for i, byte in enumerate(encrypted_content):
        key_byte = key[i % len(key)]
        decrypted_bytes.append(byte ^ key_byte)

    flag = decrypted_bytes.decode('utf-8')
    print(f"Decrypted Flag: {flag}")

extract_flag("assets/flag.dat")

之七

使用工具:IDA v8.3

都到这题就不讲开头怎么找了。核心函数是 sub_140008720
sub_140008640 不需要返回值,大概率是初始化函数。(0xC96C5795D7870F42, ECMA 182 的多项式)
image-20260304132833879.png
sub_140008500 是 Hash Update.

以下代码将 52pojie_2026_ 与用户输入组合后送入 sub_140008580 (Hash Final)
再从文件流读 0x10 的文件头一起送进 sub_140008310
image-20260304130121960.png
函数先读取文件的 Magic Number,确认无误后再拿出 IV 送进 sub_140008360
image-20260304130720140.png
image-20260304130708355.png
do while 块将文件的再后 16 字节赋值给了 a1 + 16 。最下 return 处也复制了 unk_14000A270 开始的 256 字节给 a1 + 4(注意此处 a1 是 QWORD*)
image-20260304131945459.png

接下回主循环看是怎么解密文件的。
image-20260304133705761.png

sub_1400081E0 以 8 个字节为单位循环将文件流交给 sub_140008080 (解密)
image-20260304133806993.png

image-20260304134032657.png
这里有点难看,但关注下

v7 = __ROL8__(v5, 3);
do
{
  v7 = (v7 << 8) | *((unsigned __int8 *)a1 + (HIBYTE(v7) | 0x221300) - 2233056);
  --v2;
}
while ( v2 );

其中 (HIBYTE(v7) | 0x221300) - 2233056) 可以简化成 HIBYTE(v7) + 32,等价于在 a1 + 32 后查表(即先前 memcpy 的部分)

再关注下方

result = v6[21568] ^ v6[21584] ^ v9;

事实上在做的是
$Plaintext[i] = Keystream[i] \oplus Previous\_Ciphertext[i] \oplus Ciphertext[i];$
即块密码工作模式中的密文反馈(CFB)。这种工作方式要求 IV 是随机的以抗破解。

整合以上内容,再加上明文内容为 PNG 文件,即文件头(89 50 4E 47 0D 0A 1A 0A)已知。
上方公式可重写为

$$Keystream_0 = Plaintext_0 \oplus IV \oplus Ciphertext_0$$

再模仿上方 PRNG (v7)的运算方法,就能恢复出完整文件。

代码如下:

import struct

# (unk_14000A270)
SBOX = [
    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
]

def rol8(val, shift):
    return ((val << shift) & 0xFFFFFFFFFFFFFFFF) | (val >> (64 - shift))

def get_next_state(state):
    v7 = rol8(state, 3)
    for _ in range(8):
        idx = (v7 >> 56) & 0xFF
        v7 = ((v7 << 8) & 0xFFFFFFFFFFFFFFFF) | SBOX[idx]
    return v7

def xor_bytes(b1, b2, b3):
    return bytes(x ^ y ^ z for x, y, z in zip(b1, b2, b3))

def decrypt_file(file_path):
    with open(file_path, "rb") as f:
        data = f.read()

    iv = data[8:16]
    ciphertext = data[16:]

    png_magic = b"\x89PNG\r\n\x1A\n"
    c_0 = ciphertext[0:8]

    keystream_0 = xor_bytes(png_magic, iv, c_0)
    state = struct.unpack("<Q", keystream_0)[0]
    print(f"[+] Initial State Recovered: {hex(state)}")

    decrypted_blocks = [png_magic]
    prev_c = c_0

    for i in range(8, len(ciphertext), 8):
        c_i = ciphertext[i:i+8]

        state = get_next_state(state)
        keystream_bytes = struct.pack("<Q", state)

        p_i = xor_bytes(keystream_bytes, prev_c, c_i)

        decrypted_blocks.append(p_i)
        prev_c = c_i 

    plaintext = b"".join(decrypted_blocks)

    pad_len = plaintext[-1]
    if 0 < pad_len <= 8:
        plaintext = plaintext[:-pad_len]

    out_path = file_path.replace(".encrypted", ".decrypted.png")
    with open(out_path, "wb") as f:
        f.write(plaintext)
    print(f"[+] Success! {out_path}")

if __name__ == "__main__":
    target_file = "flag.png.encrypted" 
    decrypt_file(target_file)

打开原图仅是图标并没有 Flag,猜测是某种隐写术:

image-20260304144256581.png


之九

使用工具:
随便找个浏览器按 F12
https://github.com/WebAssembly/wabt

验证逻辑在 verify.js 里,将我们的输入 SHA256 0x2026 次后跟预期结果比对,因此完全不可能爆破出。
单靠听力也是不可行的,因为区分了大小写。
image-20260302103021307.png
可以看到 UID 到 Hash 的生成逻辑在 WASM 的 gen 函数里。
image-20260302104007579.png
使用 wasm-decompile 将 WASM 转为略接近 C 的语句。
image-20260304221012312.png
听了下生成的语音,感觉上像是拼接合成。因此代码里应该有用明文字符去查表找音频片段的代码。
因此搜索 1179011410(= RIFF .wav 文件的 Magic Number)
image-20260304221359629.png
下方的 1163280727 (= WAVE)也再次验证我们找到了合成音频的开始,由此往上找。

不远处的大 f 函数是一个解码器( UTF-8 )而 a 是对应的字符。
再加上 f(d + 560, a = (a & 255) ^ 204);,很大概率 a 就是要找的明文字符。
image-20260304222122156.png
在控制台找到相应位置下断点。
image-20260304224054166.png
听一下录音核对就知道找对了地方,接下来写个 Logpoint 收集完然后集合即可。
image-20260304224959850.png


免费评分

参与人数 9威望 +2 吾爱币 +106 热心值 +8 收起 理由
AngelUser0225 + 1 谢谢@Thanks!
111mz + 1 + 1 谢谢@Thanks!
Hmily + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
cherzsh + 1 + 1 用心讨论,共获提升!
循环水 + 1 + 1 谢谢@Thanks!
gufangkai + 1 + 1 我很赞同!
Coxxs + 1 用心讨论,共获提升!
aniuxyz + 1 我很赞同!
llurry + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

Hmily 发表于 2026-3-4 11:23
题目还没来得及打包,没考虑到结束后无法查看题目(后台已经打开即使完成题目也可以看到),我暂时把所有题目地址更新到https://www.52pojie.cn/thread-2092302-1-1.html 活动帖中,可以直接下载了,后面我统一打包一个大包。

@jasomlyc 文章补充完请回复我,我再一起加分哈~
Poner 发表于 2026-3-4 11:36
Command 发表于 2026-3-4 11:26
Windows高级能给个源码吗, 很好奇这个CM是怎么实现的

其实现在的高级题还是个阉割版   之前的完整版因为内部评估难度过高  给他减弱了还换了算法。。

你可以在继续分析看看   后续可能会开源
LittleK 发表于 2026-3-4 11:13
Poner 发表于 2026-3-4 11:20
稍后题目文件都会打包上传爱盘  可以关注下爱盘下载
Command 发表于 2026-3-4 11:26
Poner 发表于 2026-3-4 11:20
稍后题目文件都会打包上传爱盘  可以关注下爱盘下载

Windows高级能给个源码吗, 很好奇这个CM是怎么实现的

点评

其实现在的高级题还是个阉割版 之前的完整版因为内部评估难度过高 给他减弱了还换了算法。。 你可以在继续分析看看 后续可能会开源  详情 回复 发表于 2026-3-4 11:36
aniuxyz 发表于 2026-3-4 11:31
我只解出来31位这个,密码解不出来,摸到门,还没有进去。。。
wushishen 发表于 2026-3-4 11:42
学习一下了
fdk0305 发表于 2026-3-4 12:17
学习一下下
liulangdeshi 发表于 2026-3-4 12:21
支持下,感谢分享~~
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-8-29 21:59

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表