最难崩的一集,头一次见到 web, pwn 是静态 flag,re ,MISC 是动态 flag 的比赛了。
SP
签到题
然后在 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}