2025 ISCC WP

最难崩的一集,头一次见到 web, pwn 是静态 flag,re ,MISC 是动态 flag 的比赛了。

SP

签到题

1
upx -d sp.exe 脱壳

然后在 incorrect 比较函数处下断点就可以得到 flag 了

我爱看小品

pyc 逆向,运行程序出现报错,baidu 一下:

发现是个 pyinstaller 打包的程序。

拿 pyinstxtractor 解包程序,发现有个 something, pyz 加密包

拿 uncompyle 6 梭一下,引用了 mypy, yourpy,这两个文件都在 pyz 加密包里面,因此需要解开

python 3.11 一直解不出 pyc. encrypted 文件,因此在python 3.8 环境用 pyinstxtractor-ng 解包程序,拿到 key (在 pyimod 00_crypto_key. pyc 里面),在 pyinstxtractor-ng github 上可以找到对应版本的解密脚本,解密:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env python3
# For pyinstaller >= 4.0
import tinyaes
import zlib

CRYPT_BLOCK_SIZE = 16

# key obtained from pyimod00_crypto_key
key = bytes('yibaibayibei1801', 'utf-8')

inf = open('mypy.pyc.encrypted', 'rb') # encrypted file input
outf = open('mypy.pyc', 'wb') # output file 

# Initialization vector
iv = inf.read(CRYPT_BLOCK_SIZE)

cipher = tinyaes.AES(key, iv)

# Decrypt and decompress
plaintext = zlib.decompress(cipher.CTR_xcrypt_buffer(inf.read()))

# Write pyc header
# The header below is for Python 3.8
outf.write(b'\x55\x0d\x0d\x0a\0\0\0\0\0\0\0\0\0\0\0\0')

# Write decrypted data
outf.write(plaintext)

inf.close()
outf.close()

然后再梭 mypy. pyc, yourpy. pyc:

字符处理,解密即可。

三进制战争

frida 一把梭,解密脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import frida
import sys

current_char_index = 32


def on_message(message, data):
    if message['type'] == 'send':
        # 获取原始返回值
        payload = message['payload']
        if isinstance(payload, dict) and 'char' in payload and 'result' in payload:
            char = payload['char']
            result = payload['result']
            print(f"[+] 字符: {char}, 方法返回值: {result}")
        else:
            print(f"[+] 方法返回值: {payload}")
    else:
        print(message)


jscode = """
Java.perform(function () {
    var MainActivity = Java.use('com.example.mobile02.MainActivity');

    Java.choose('com.example.mobile02.MainActivity', {
        onMatch: function(instance) {
            try {
                var target = "002102001221012212020121001200001221";
                var a = "";
                var sum = 1;
                var found = false;

                while (!found && (sum * 6) <= target.length) {
                    let matched = false;
                    for (let k = 32; k <= 126; k++) {
                        let str3 = a + String.fromCharCode(k);
                        let str1 = instance.stringFromJNl("da156b6a06bcd826c60f07bfe2136d87", str3);

                        if (str1 == target.substring(0, sum * 6)) {
                            console.log("已匹配: " + str1);
                            a = str3;
                            sum += 1;
                            matched = true;
                            if (str1 == target) {
                                found = true;
                                console.log("最终匹配成功: " + a);
                            }
                            break; // 找到本轮字符,进入下一轮
                        }
                    }
                    if (!matched) {
                        console.error("未找到匹配字符,当前a: " + a);
                        break;
                    }
                }
            } catch (e) {
                console.error("调用失败: " + e);
            }
        },
        onComplete: function() {}
    });
});
"""

device = frida.get_usb_device()
try:
    session = device.attach("mobile02")
except frida.ProcessNotFoundError:
    print("[-] 应用未运行!请先启动应用")
    sys.exit(1)

script = session.create_script(jscode)
script.on('message', on_message)
script.load()
sys.stdin.read()

Encode

apk 损坏且没有签名,把 dex 拿出来反编译:

flag 分为两部分,后半部分在 JNI nativeCheckLast 处,前半部分在 JNI nativeCheckFormat。apk 损坏动调不了,只能拉 so 硬造了。nativeCheckLast 是一个简单的字符置换

将密文加 3 就可以得到后半部分的 flag

前半部分是一个异或加 base 编码:

密文在此处:

解密:

最终 ISCC{Slyth3r! n_L@bYrinth }

冗余的代码

一个没符号的程序,懒得恢复,硬调 简单分析一下主函数,是一个 5 x 5 的迷宫,因此只要让输入最终为迷宫的正确路径即可得到 flag 迷宫:*11110100001010000101111# 正确路径:020202040401010404020202

用 IDA 插件发现有个 xxtea 加密。

在此处打个断点,看看输入的值在此之前有无进行处理:

此处输入的 080808 被转换为 02030405,说明前面对输入字符进行了处理,将 02030405 输入,发现变回了 080808,说明是一个 xor 加密

这里是一个迷宫路径移动的逻辑代码,需要让输入字符最终得到正确走完迷宫,迷宫已知,因此输入字符处理后的字符串为 020202040401010404020202, 也就是 xxtea 后的密文。

然后到字符最终检验处看输入值是否还有变化

没有变化,至此该程序已分析完毕。

睡蕉小猴

安装 apk 运行后发现有个视频一直进不去,需要 hook 对应的函数进行跳过。

接着分析主函数发现有个 getsnowkey () 函数,调用它返回 snow key(我一直以为这玩意是自定义的函数,原来是一个隐写,在这卡了半天)

frida 脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import frida
import sys

rdev = frida.get_remote_device()
pid = rdev.spawn(["com.example.mobile03"])
session = rdev.attach(pid)

scr = """
Java.perform(function () {
        // Hook MainActivity.showAd 方法
        var MainActivity = Java.use("com.example.mobile03.MainActivity");
        var showAd = MainActivity.showAd;
        showAd.implementation = function () {
            // 修改 showAd 方法的实现,不执行原方法
        };

        var MainActivity = Java.use("com.example.mobile03.MainActivity");
        var check = MainActivity.check;
        check.implementation = function(str){
            console.log(str);
            var result = this.check(str);
            console.log(result);
            return result;
        }

        // Hook MainActivity.Jformat 方法
        var MainActivity = Java.use("com.example.mobile03.MainActivity");
        var Jformat = MainActivity.Jformat;
        Jformat.implementation = function (str1, str2) {
            console.log(str1, str2);
            var result = this.Jformat(str1, str2);
            console.log(result);
            return result;
        };
    });
"""
script = session.create_script(scr)


def on_message(message, data):
    print(message, data)


script.on("message", on_message)
script.load()
rdev.resume(pid)
sys.stdin.read()

hook 返回的 key:

在资源文件中找到 snow 隐写的 txt,解密得到一串 16 进制数。

接着分析 Jformat 方法,发现它调用了一个 isFlag 方法,全局字符串搜索,定位到相关函数:

有公钥和私钥,初步判断为 rsa 加密,target 就是要解密的文本,既然要解密,因此我们只需要拿到私钥就可以了,同样定位字符串找到初始定义。

frida Hook 不住,应该缺少了环境,拿 unidbg 补环境去跑一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package ISCC;

import com.alibaba.fastjson.util.IOUtils;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.io.FileNotFoundException;


public class mobile extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    private final DvmClass swan, pig;
    private final boolean logging;

    public mobile(boolean logging) throws FileNotFoundException {
        this.logging = logging;

        // 创建模拟器实例
        emulator = AndroidEmulatorBuilder.for64Bit()
                .setProcessName("com.example.test")
                .addBackendFactory(new Unicorn2Factory(true))
                .build();

        // 模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析

        // 创建 Android 虚拟机
        vm = emulator.createDalvikVM(new File(""));//你的文件路径
        vm.setVerbose(logging); // 设置是否打印 Jni 调用细节
        vm.setJni(this);

        DalvikModule dm = vm.loadLibrary(new File(""), false);//你的文件路径
        module = dm.getModule(); 


        Debugger attach = emulator.attach();
        attach.addBreakPoint(module.base + 0x0000000000021B04);
        dm.callJNI_OnLoad(emulator); // 手动执行 JNI_OnLoad 函数

        swan = vm.resolveClass("com/example/mobile03/swan");
        pig = vm.resolveClass("pig");
    }

    public mobile(AndroidEmulator emulator, VM vm, Module module, DvmClass mMainActivity, DvmClass swan, DvmClass pig, boolean logging) {
        this.emulator = emulator;
        this.vm = vm;
        this.module = module;
        this.swan = swan;
        this.pig = pig;
        this.logging = logging;
    }

    void destroy() {
        IOUtils.close(emulator);
        if (logging) {
            System.out.println("destroy");
        }
    }

    public static void main(String[] args) throws Exception {
        mobile test = new mobile(true);
        test.encrypt();
        test.destroy();
    }

    boolean encrypt() {
        String text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +
                "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +
                "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +
                "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +
                "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +
                "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

        String res = String.valueOf(swan.callStaticJniMethodObject(emulator, "getPvKey()Ljava/lang/String;", text));
        return true;
    }
}

将得到密钥流 xor 回输入的字符,然后再与 snow 隐写的数据进行解密,得到一串 rsa 私钥

拿去 rsa 解密

晕,这玩意是 11 位后面的 flag,还有一段,源代码看不出有啥,我直接字符串大法(😀)。

一个简单字符替换,最终 ISCC{Z5$uK7w2#JkQ}

Licensed under CC BY-NC-SA 4.0
最后更新于 May 09, 2025 16:02 CST
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计