25年浙江省决赛re复现

re1.你是我的天命人吗

先查壳,无壳直接IDA启动!在main函数里看到两个函数,一个是give_you_a_gift(),另一个是where_flag

第一个函数里是VirtualProtect,这是代码自解密。解密的长度是648,开始位置就是where_flag,解密方式是逐字节异或0x33。

1.可以使用idapython,写脚本解密

1
2
3
4
5
6
7
8
start = 0x1400018C5  # encrypt 函数起始地址
length = 648 # 总共需要解密的长度

for i in range(length):
b = ida_bytes.get_byte(start + i)
ida_bytes.patch_byte(start + i, b ^ 0x33)

print("Decrypt done.")

2.可以动态调试,让程序自己运行到where_flag函数处,进行解密。

在give_you_a_gift()处打上断点,然后F9运行

然后F7进入where_flag函数,看到出来了一个弹窗,这个弹窗的意思是 要不要从当前 RIP 开始,把这里重新当成一条新的指令来反汇编?直接点击yes,让ida重新分析就好。

选中这个函数先按U解除定义,再在where_flag函数头按P重新定义一下就行

3.也可以使用x64dbg去dump

dump后可以看到主要逻辑

就是简单的异或,逆向过来就是enc的索引值与enc的值异或

写出解密脚本

这里本人太无知了,python有一个内置库binascii,

功能:做二进制数据各种文本表示形式之间的转换,比如:

  • 十六进制字符串 → 原始字节 binascii.unhexlify()
  • 原始字节 → 十六进制字符串 binascii.hexlify()
  • 二进制 -> base64 形式 binascii.b2a_base64
  • base64 -> 二进制 binascii.a2b_base64
  • CRC 校验 binascii.crc32(data)
1
2
3
4
5
6
7
import binascii
enc=bytearray(binascii.unhexlify('4440514050437D3E386C3A3F3E6F386D232124202D7420742C2F2E282525782D124344471015405A'))
print(enc)
flag=''
for i in range(len(enc)):
flag+=chr(enc[i]^i)
print(flag)

另一种:

1
2
3
4
5
6
7
enc=bytes.fromhex('4440514050437D3E386C3A3F3E6F386D232124202D7420742C2F2E282525782D124344471015405A')
print(enc)
flag=''
for i in range(len(enc)):
flag+=chr(enc[i]^i)
print(flag)
#DASCTF{90e042b6b30639a6c464398f22bfd40f}

re2.androidtest.apk

使用jeb打开,找到MainActivity进行分析

可以看到字符串z是一个静态常量,下面是对字符串z的实现,通过 w.i() 方法来对类似base64的字符串进行一些解密操作。

直接对import androidx.activity.w; 这个类右键交叉引用,找到w类,查找i方法

看到i方法里还调用了j0.a.a方法,直接点进去,发现这是实现了一个base64解码器

那么这些方法的作用都清楚了,就是对那两个字符串先base64解码,然后将后位的字符串作为密钥去异或前位的字符串,这样就可以解出来字符串z。

这个字符串是base32的字母表(其实这里还看不出来什么)

接着往下分析MainActivity

这里可以看到ret_str(String arg1)函数是一个JNI 函数,在 libtest.so 中实现,直接右键解析出来,放到ida里面进行查看

这里只有一个字符串,它看起来像base系列的密文,接着再进行分析。

对**ret_str(String arg1)**函数进行交叉引用,来到了k类,具体代码如下所示

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package com.google.android.material.datepicker;

import android.view.View.OnClickListener;
import android.view.View;
import android.widget.Toast;
import androidx.activity.w;
import androidx.appcompat.widget.Toolbar;
import com.example.myapplication.MainActivity;
import f.e;
import i.b;
import j.o;
import k.Y0;

public final class k implements View.OnClickListener {
public final int a;
public final Object b;

public k(int v, Object object0) {
this.a = v;
this.b = object0;
super();
}

@Override // android.view.View$OnClickListener
public final void onClick(View view0) {
String s3;
String s2;
int v6;
String s1;
switch(this.a) {
case 0: {
l l0 = (l)this.b;
int v = l0.X;
if(v == 2) {
l0.H(1);
return;
}

if(v == 1) {
l0.H(2);
}

return;
}
case 1: {
((e)this.b).w.obtainMessage(1, ((e)this.b).b).sendToTarget();
return;
}
case 2: { //主要加密逻辑
MainActivity mainActivity0 = (MainActivity)this.b;
String s = mainActivity0.y.getText().toString();
if(s.length() == 44) { //判断长度等于44
int v1 = (byte)s.length(); //v1=44
byte[] arr_b = s.getBytes();//输入的字符串赋值给arr_b
byte[] arr_b1 = new byte[arr_b.length];
for(int v3 = 0; v3 < arr_b.length; ++v3) {
arr_b1[v3] = (byte)(arr_b[v3] ^ v1);
}

//base32加密
StringBuilder stringBuilder0 = new StringBuilder(); //新建一个对象
int v4 = 0;
int v5 = 0;
for(int v2 = 0; true; ++v2) {
s1 = MainActivity.z; //z字符串,base32的字母表
if(v2 >= arr_b.length) {
break;
}

v5 = v5 << 8 | arr_b1[v2] & 0xFF;
v4 += 8;
while(v4 >= 5) {
stringBuilder0.append(s1.charAt(v5 >>> v4 - 5 & 0x1F));
v4 += -5;
}
}

if(v4 > 0) {
v6 = s1.charAt(v5 << 5 - v4 & 0x1F);
stringBuilder0.append(((char)v6));
}

label_44:
if(stringBuilder0.length() % 8 != 0) {
v6 = 61;
stringBuilder0.append(((char)v6));
goto label_44;
}

if(mainActivity0.ret_str(stringBuilder0.toString())) { //与ret_str的字符串进行判断
s2 = "7F+v030i4Hk=\n";
s3 = "vyrMsBhRk1g=\n";
}
else {
s2 = "mrVcl8J8\n";
s3 = "/8cu+LBdMiY=\n";
}

Toast.makeText(mainActivity0, w.i(s2, s3), 1).show();
return;
}

throw new ArithmeticException(w.i("4nC+/ChD4PbvbaC1NkOy7g==\n", "hhnIlUwmwIE=\n"));
}
case 3: {
((b)this.b).a();
return;
}
default: {
Y0 y00 = ((Toolbar)this.b).L;
o o0 = y00 == null ? null : y00.b;
if(o0 != null) {
o0.collapseActionView();
}

return;
}
}
}
}

OnClickListener,被复用在好几个地方,在构造函数里传了一个 int a 指示“这是第几种用法”。switch(this.a) 根据不同的值执行不同逻辑。

再联系一下MainActivity的 ((Button)this.findViewById(0x7F08012F)).setOnClickListener(new k(2, this)); // id:mybutton1

这里传参是2,意味着加密逻辑是调用的case2,接着细看case2的逻辑:

就是先判断一下传入的字符串长度是不是44,如果是44的话,就让字符串逐字节与44进行异或,异或后进行base32加密,最后与ret_str传入的字符串进行对比验证。

逆向思路就是将”NBWX633YNJLU2QSILZBUKSDTJVBFQRLTJVBEQ42YJFPVQ42FL5ZUKQSYJFPESX2YIVBEWUI=”字符串先base32解密,再xor 44

或者写脚本

1
2
3
4
5
6
import base64

a = list(base64.b32decode('NBWX633YNJLU2QSILZBUKSDTJVBFQRLTJVBEQ42YJFPVQ42FL5ZUKQSYJFPESX2YIVBEWUI='))

for i in a:
print(chr(i ^ 44), end='')

re3 vm

本题有两个附件,一个是program,一个是Wraning,ELF文件。对它查壳,无壳64位,依旧IDA启动。

找到main函数进行分析

在解题前先补充一个知识点,就是命令行参数

在 C 里,main 的这三个参数通常是:

1
int main(int argc, char **argv, char **envp)

a1 对应 argc 命令行参数的个数,最少是1,argv[0] 永远是程序本身的路径(比如本题 ./wraning)。
a2 对应 argv 命令行参数字符串数组,argv 是一个“指针数组”,每个元素是一个 char *,指向一个以 \0 结尾的 C 字符串。
a3 对应 envp 环境变量字符串数组,跟 argv 类似,也是“字符串指针数组”,不过内容是环境变量。

对应main函数中

1
2
if ( a1 <= 1 )
return 0xFFFFFFFFLL;

a1 <= 1 等价于 “命令行参数个数小于等于 1”,如果没有额外的命令行参数,就会返回0xFFFFFFFFLL,也就是 -1,作为进程退出码,通常会表现为退出码 255(取低 8 位)。

动态调试时,调试器启动程序时,相当于在命令行只执行了: ./program

这时 argc == 1,也就是 a1 == 1,所以要再额外输入参数

1
2
3
v5 = a2[1];
if ( !*v5 )
return 0xFFFFFFFFLL;

取命令行参数 argv[1],如果第一个字符就是 ‘\0’,即空字符串就退出。

1
2
3
4
v6 = strlen(a2[1]);
v7 = v6;
if ( v6 > 0x40 )
return 0xFFFFFFFFLL;

获取 argv[1] 的长度,限制长度最长是 0x40 = 64 字节,超过就会退出。

大概逻辑是这样的,它还会读取文件program的内容到v10,并把文件内容复制到 dest 开头,然后把传入的参数字符串放到 dest + 2048 的位置,接着把这个大缓冲区复制到 v9,然后把参数长度传给 sub_1640 做进一步处理。

接着分析sub_1640函数,这有好多case,一眼vm,一般方法是分析每个opcode的作用,自己写一个解释器,但这有些耗费时间和精力,霍雅师傅闪亮登场,教我本题可以使用trace来看。记录一下hhh🤗

做法:1.先静态分析一下,看大致的opcode的作用

2.在关键指令(进行一些异或,左移,右移之类的)上打断点

3.写出对应的idc脚本,在 VM 的“取指/dispatch”位置做循环 trace,每步记录 opcode、寄存器、内存变化

对应的idc脚本

1
2
3
4
5
6
7
8
9
10
import idc

r9 = idc.get_reg_value('r9')
rdx = idc.get_reg_value('rdx')
rax = idc.get_reg_value('rax')

op1 = idc.read_dbg_qword(r9+rdx*8)
op2 = rax

print(f'xor {hex(op1)} , {hex(op2)} == {hex(op1^op2)}')

在汇编视图下的xor处打上断点,右键选择Edit breakpoint settings

然后把脚本粘贴上去

然后动态调试,循环结束后,就会退出,并在output窗口打印出 寄存器所做的操作

如下所示

小技巧:复制在vscode里面可以使用列块选择来一次性截取同一纵向位置的内容,Shift + Alt 按住,再用鼠标左键拖出一个“竖长的矩形”。

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
xor 0x31 , 0x0 == 0x31
xor 0x31 , 0x1 == 0x30
xor 0x31 , 0x2 == 0x33
xor 0x31 , 0x3 == 0x32
xor 0x31 , 0x4 == 0x35
xor 0x31 , 0x5 == 0x34
xor 0x31 , 0x6 == 0x37
xor 0x31 , 0x7 == 0x36
xor 0x31 , 0x8 == 0x39
xor 0x31 , 0x9 == 0x38
xor 0x31 , 0xa == 0x3b
xor 0x31 , 0xb == 0x3a
xor 0x31 , 0xc == 0x3d
xor 0x31 , 0xd == 0x3c
xor 0x31 , 0x50 == 0x61
xor 0x30 , 0x67 == 0x57
xor 0x33 , 0x21 == 0x12
xor 0x32 , 0x2b == 0x19
xor 0x35 , 0xce == 0xfb
xor 0x34 , 0xd7 == 0xe3
xor 0x37 , 0x84 == 0xb3
xor 0x36 , 0x3a == 0xc
xor 0x39 , 0xf5 == 0xcc
xor 0x38 , 0xc2 == 0xfa
xor 0x3b , 0xc2 == 0xf9
xor 0x3a , 0x22 == 0x18
xor 0x3d , 0x48 == 0x75
xor 0x3c , 0x1d == 0x21
xor 0x0 , 0x14 == 0x14
xor 0x0 , 0x2a == 0x2a
xor 0x0 , 0x71 == 0x71
xor 0x0 , 0x25 == 0x25
xor 0x0 , 0xa7 == 0xa7
xor 0x0 , 0x73 == 0x73
xor 0x0 , 0x3c == 0x3c
xor 0x0 , 0x9c == 0x9c
xor 0x0 , 0x72 == 0x72
xor 0x0 , 0xfe == 0xfe
xor 0x0 , 0x3c == 0x3c
xor 0x0 , 0xb7 == 0xb7
xor 0x0 , 0xad == 0xad
xor 0x0 , 0x80 == 0x80
xor 0x0 , 0xa9 == 0xa9
xor 0x0 , 0x6f == 0x6f
xor 0x0 , 0x37 == 0x37
xor 0x0 , 0x46 == 0x46
xor 0x0 , 0x91 == 0x91
xor 0x0 , 0x32 == 0x32
xor 0x0 , 0xb4 == 0xb4
xor 0x0 , 0xf7 == 0xf7
xor 0x0 , 0xa5 == 0xa5
xor 0x0 , 0xad == 0xad
xor 0x0 , 0xd8 == 0xd8
xor 0x0 , 0x6b == 0x6b
xor 0x0 , 0x35 == 0x35
xor 0x0 , 0x8c == 0x8c
xor 0x0 , 0xe4 == 0xe4
xor 0x0 , 0x0 == 0x0
xor 0x0 , 0x20 == 0x20
xor 0x0 , 0x2e == 0x2e

进行仔细分析这些操作指令,会发现,前14行是我们输入的参数与它的下标索引值进行异或,后面的是我们输入的内容与索引异或后的结果再和一个类似于S盒的东西进行异或。那我们要反推出flag(就是我们输入的字符串)需要知道密文,直接密文与这个S盒异或再异或索引值就行了。

现在来提取密文

静态提取有问题,提数据最好是动态提取,这样大小端序也不会出问题

断点打在第13行,就可以直接读取 si128这个变量里面的值

提取后就可以写出解密脚本了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enc=[0x14, 0x27, 0x70, 0x6B, 0x9E, 0x94, 0xF9, 0x51, 0x8E, 0x9F, 
0xB2, 0x51, 0x69, 0x73, 0x2F, 0x46, 0x53, 0x05, 0xDC, 0x36,
0x69, 0xA4, 0x03, 0x86, 0x4B, 0xC4, 0xC6, 0xD5, 0xE6, 0x5F,
0x50, 0x37, 0xF7, 0x5C, 0xC6, 0x86, 0xF9, 0xA5, 0xCA, 0x74,
0x24, 0xCC, 0xBA, 0x7E, 0x64, 0x7E]

s=[0x50,0x67,0x21,0x2b,0xce,0xd7,0x84,0x3a,0xf5,0xc2,0xc2,0x22,
0x48,0x1d,0x14,0x2a,0x71,0x25,0xa7,0x73,0x3c,0x9c,0x72,0xfe,
0x3c,0xb7,0xad,0x80,0xa9,0x6f,0x37,0x46,0x91,0x32,0xb4,0xf7,
0xa5,0xad,0xd8,0x6b,0x35,0x8c,0xe4,0x00,0x20,0x2e]

flag=''
for i in range(len(enc)):
flag+=chr(enc[i]^s[i]^i)
print(flag)
##DASCTF{lsTzx-c5c21iVA-goojqNS-ynFOPRx-489itUh}

re4 U.hap

这题给了一个hap后缀名文件,这是一道鸿蒙逆向题目。

上网搜查hap文件给的解释:.hap 文件是华为鸿蒙系统HarmonyOS的应用程序包格式,全称为 Harmony Ability Package。它是鸿蒙原生应用安装和运行的基本单元,类似于 Android 的 APK 文件或 iOS 的 IPA 文件,包含了代码、资源、第三方库和配置文件。

鸿蒙应用文件概览:参考文献

https://shell.virbox.com/2025/11/14/%E9%B8%BF%E8%92%99%E5%BA%94%E7%94%A8%E6%96%87%E4%BB%B6%E6%A6%82%E8%A7%88%EF%BC%9Aabc%E3%80%81hap%E3%80%81har%E3%80%81hsp-%E5%92%8C-app/

hap文件就和安卓的apk文件一样,安卓的apk文件可以当成一个zip文件,去解压缩,这个同样也可以。因为它就是一个模块包,里面包含了代码,资源,第三方库等其他配置文件。

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
entry_default
├─ets
│ ├─modules.abc
│ └─sourceMaps.map
├─libs
│ └─arm64-v8a
│ └─libentry.so
├─resources
│ └─base
│ ├─media
│ │ ├─app_background.png
│ │ ├─app_foreground.png
│ │ ├─app_layered_image.json
│ │ ├─background.png
│ │ ├─foreground.png
│ │ ├─layered_image.json
│ │ └─startIcon.png
│ └─profile
│ ├─backup_config.json
│ └─main_pages.json
├─.pages.info
├─module.json
├─pack.info
├─pkgContextInfo.json
└─resources.index

ets目录包含应用的核心代码,modules.abc就是ArkTS 源代码编译后的字节码,sourceMaps.map 则是各个源文件的一些信息。

libs 目录放置应用引入的 so 库,支持多架构,每个架构放到对应的目录下。

resources 目录放置应用的资源文件,图片、配置、页面信息、音频等等文件都在这个目录下面。

对于逆向选手来说,改后缀为zip文件,解压缩,重点应该放在ets目录里的modules.abc文件。

ABC(Ark Bytecode,方舟字节码)是由鸿蒙的方舟编译器编译 ArkTS/TS/JS 生成的字节码文件,也就是可被解析执行的二进制文件,以 .abc 作为后缀名。ABC 文件包含应用所有的逻辑,类、方法、字段、调试信息、字符串、字面量等数据都在其中,通过分析这个文件,就可以逆向鸿蒙应用。

那么,我们知道以后遇到hap文件应该怎么去逆向了,但是如果没有好的工具也只能望眼欲穿秋水。没关系,大佬会出手,一位大佬把鸿蒙逆向所用的工具整理的很到位。这里引用一下他的博客。

https://github.com/Sciencekex/-wp-OHapp_re?tab=readme-ov-file

俺就不多赘述了,我是用的jadx-dev-all.jar工具去反编译了modules.abc文件。

image-20260317202434430

注意到Index这个类,能够知道这个是画出了程序的页面。

我们能从构造函数 Index() 知道一些有用的信息。

arc4Key:加密密钥, "HarMonyOS_S3cur3_K3y!2025"

theSecondKey:二级密钥, [90, 60, 231, 145, 47]

MatrixCrypto:3x3矩阵数组,用于加密,

cipherBase64:目标密文,"37L9UF8uNl1TSgYMLIW/RosGPMxVYXNcUoTTQXihX8ZyaQVgxY9Ywz/0fIwRzI4H"

继续往下分析,我们能看到校验逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Object #1873697688084456659#(Object functionObject, Object newTarget, Index this, Object arg0, Object arg1) {
Button.createWithLabel("加密并比较");
Button.id("encryptButton");
Button.width("80%");
Button.height(50);
Button.margin(createobjectwithbuffer(["bottom", 20]));
Button.onClick(#4441499115937063224#);
return null;
}

public Object #4441499115937063224#(Object functionObject, Object newTarget, Index this) {
MatrixCrypto = import { default as MatrixCrypto } from "@normalized:N&&&entry/src/main/ets/EUtils&";
if ((_lexenv_0_0_.cipherBase64 == MatrixCrypto.encrypt(_lexenv_0_0_.inputValue, _lexenv_0_0_.MatrixCrypto, _lexenv_0_0_.theSecondKey, _lexenv_0_0_.arc4Key) ? 1 : 0) != 0) {
ldlexvar = _lexenv_0_0_;
ldlexvar.showResultDialog("you are right!");
return null;
}
ldlexvar2 = _lexenv_0_0_;
ldlexvar2.showResultDialog("try again!");
return null;
}

大概逻辑就是,我们点击按钮后,就会去调用MatrixCrypto.encrypt()加密函数对输入的内容进行加密。然后与cipherBase64进行对比,如果一样的话就会弹出” you are right! “

接着就要去分析这个encrypt函数了,看到底执行的什么加密。这个加密逻辑是在EUtils类里。

image-20260317204504031

结合ai的分析,能知道这个加密流程:

第一步:字符串转UTF-8

1
stringToUtf8Bytes = EncodeUtils.stringToUtf8Bytes(arg0);

第二步:前置 4 字节长度

1
2
3
4
5
6
7
r32 = stringToUtf8Bytes.length;
newobjrange = Uint8Array((4 + r32));
newobjrange[0] = (r32 >> 24) & 255;
newobjrange[1] = (r32 >> 16) & 255;
newobjrange[2] = (r32 >> 8) & 255;
newobjrange[3] = r32 & 255;
newobjrange.set(stringToUtf8Bytes, 4);

就是把长度按照大端序塞进前4个字节里面。

第三步:按矩阵阶数补齐

1
2
obj = arg1.length;
padToBlock = padToBlock(newobjrange, obj);

arg1 就是矩阵 MatrixCrypto,是 3x3,所以 arg1.length == 3。根据上面的那个校验函数里去调用了加密函数,看它的参数就知道此处arg1是MatrixCrypto。

arg0就是输入的明文inputValue。

arg2就是theSecondKey。

arg3是arc4Key,rc4加密的key。

接着看 padToBlock()

1
2
3
4
5
6
7
8
9
public Object padToBlock(..., Object arg0, Object arg1) {
i = arg0.length % arg1;
if ((0 == i ? 1 : 0) != 0) {
return arg0;
}
newobjrange = Uint8Array(((arg0.length + arg1) - i));
newobjrange.set(arg0);
return newobjrange;
}

arg1对arg1取余,arg1是3,余数是0,1,2。如果余数是0的话,就会return arg0。如果不是就会放到一个更长的字节数组里面。

newobjrange.set(arg0);只把旧数据复制进去,没有给后面的新位置赋别的值。而 Uint8Array 新建时,剩余位置默认全是 0

所以它会把数据补到 3 的倍数长度,补的是 0。

第四步:矩阵分组加密

1
matrixEncryptBlocks = matrixEncryptBlocks(padToBlock, arg1);

接着往下看看这个加密内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Object matrixEncryptBlocks(Object functionObject, Object newTarget, EUtils this, Object arg0, Object arg1) {
r24 = arg1.length;
newobjrange = Uint8Array(arg0.length);
i = 0;
while (true) {
i2 = i;
if ((i2 < arg0.length ? 1 : 0) == 0) {
return newobjrange;
}
slice = arg0.slice(i2, i2 + r24);
ldlexvar = _lexenv_0_0_;
mulMatrixVectorMod256 = ldlexvar.mulMatrixVectorMod256(arg1, Array.from(slice));
for (i3 = 0; (i3 < r24 ? 1 : 0) != 0; i3 = tonumer(i3) + 1) {
newobjrange[i2 + i3] = mulMatrixVectorMod256[i3] & 255;
}
i = i2 + r24;
}
}

它每次切出 r24 = arg1.length 个字节,也就是 3 字节一组,然后调用 mulMatrixVectorMod256。

1
2
3
4
5
6
7
8
9
10
11
12
13
public Object mulMatrixVectorMod256(Object functionObject, Object newTarget, EUtils this, Object arg0, Object arg1) {
obj = arg0.length;
newobjrange = Array(obj);
for (i = 0; (i < obj ? 1 : 0) != 0; i = tonumer(i) + 1) {
i2 = 0;
for (i3 = 0; (i3 < obj ? 1 : 0) != 0; i3 = tonumer(i3) + 1) {
i2 += arg0[i][i3] * arg1[i3];
}
newobjrange[i] = i2 & 255;
}
return newobjrange;
}

这就是标准的:输出向量 = 矩阵 × 输入向量 mod 256。

因为 & 255 就等价于模 256,所以这一层可以准确写成:

按3字节分组 -> 3x3矩阵乘法 mod 256

第五步:XOR

1
2
XorUtils = import { default as XorUtils } from "@normalized:N&&&entry/src/main/ets/XorUtils&";
xorBytes = XorUtils.xorBytes(matrixEncryptBlocks, arg2);

去看XorUtils类

image-20260317211553708

arg0是矩阵加密后的字节流,arg1是[90, 60, 231, 145, 47],i % arg1.length 表示循环使用 key。

这一层就是将矩阵加密结果与第二把 key 循环异或。

第六步:RC4

继续看encrypt()

1
2
3
4
5
6
7
obj2 = import { default as CryptoJS } from "@normalized:N&&&@ohos/crypto-js/index&2.0.5".RC4;
obj3 = obj2.encrypt;
obj4 = import { default as CryptoJS } from "@normalized:N&&&@ohos/crypto-js/index&2.0.5".lib.WordArray;
create = obj4.create(xorBytes);
obj5 = import { default as CryptoJS } from "@normalized:N&&&@ohos/crypto-js/index&2.0.5".enc.Utf8;
callthisN = obj3(create, obj5.parse(arg3));
toString = callthisN.toString();

xor后的结果放到WordArray里面,然后与密钥arg3进行RC4加密。

第七步:自定义Base64

1
2
CustomBase64 = import { default as CustomBase64 } from "@normalized:N&&&entry/src/main/ets/CustomBase64&";
return CustomBase64.fromStandard(toString);

这个需要看CustomBase64类了

image-20260317212837577

能知道变表,也就能解了。这个就是rc4加密后的结果用base64变表加密了。

解密的顺序就是

自定义Base64 -> RC4解密 -> XOR -> 逆矩阵解密 -> 去掉4字节长度 -> UTF-8

那进行解密,下面的脚本ai给的,ai还是太强了。

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import base64

cipher_custom = "37L9UF8uNl1TSgYMLIW/RosGPMxVYXNcUoTTQXihX8ZyaQVgxY9Ywz/0fIwRzI4H"
arc4_key = b"HarMonyOS_S3cur3_K3y!2025"
xor_key = [90, 60, 231, 145, 47]

M = [
[1, 2, 3],
[0, 1, 4],
[0, 0, 1],
]

CUSTOM_CHARS = "3GHIJKLMzxy01245PQRSTUFabcdefghijklmnopqrstuv6789+/NOVWXYZABCDEw"
STANDARD_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

# 逆矩阵(对这题这个矩阵直接写出来即可)
M_INV = [
[1, 254, 5], # -2 mod 256 = 254
[0, 1, 252], # -4 mod 256 = 252
[0, 0, 1],
]


# 自定义 Base64 -> 标准 Base64
def custom_to_standard(s: str) -> str:
out = []
for ch in s:
if ch == "=":
out.append("=")
else:
idx = CUSTOM_CHARS.index(ch)
out.append(STANDARD_CHARS[idx])
return "".join(out)


# RC4
def rc4_crypt(data: bytes, key: bytes) -> bytes:
S = list(range(256))
j = 0

# KSA
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]

# PRGA
i = 0
j = 0
out = bytearray()
for byte in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
k = S[(S[i] + S[j]) % 256]
out.append(byte ^ k)

return bytes(out)


# XOR
def xor_bytes(data: bytes, key) -> bytes:
return bytes((data[i] ^ key[i % len(key)]) & 0xFF for i in range(len(data)))


# 矩阵 * 向量 mod 256
def mul_matrix_vector_mod256(mat, vec):
n = len(mat)
out = []
for i in range(n):
s = 0
for j in range(n):
s += mat[i][j] * vec[j]
out.append(s & 0xFF)
return out

# 按块做逆矩阵解密
def matrix_decrypt_blocks(data: bytes, inv_mat) -> bytes:
n = len(inv_mat)
assert len(data) % n == 0
out = bytearray(len(data))
for i in range(0, len(data), n):
block = list(data[i:i+n])
dec = mul_matrix_vector_mod256(inv_mat, block)
for j in range(n):
out[i + j] = dec[j]
return bytes(out)



def decrypt():
# 1. 自定义 Base64 -> 标准 Base64
std_b64 = custom_to_standard(cipher_custom)
print("[+] Standard Base64:", std_b64)

# 2. Base64 解码
rc4_cipher = base64.b64decode(std_b64)
print("[+] RC4 ciphertext hex:", rc4_cipher.hex())

# 3. RC4 解密
xored = rc4_crypt(rc4_cipher, arc4_key)
print("[+] After RC4 decrypt:", xored.hex())

# 4. XOR 还原
mat_enc = xor_bytes(xored, xor_key)
print("[+] After XOR restore:", mat_enc.hex())

# 5. 逆矩阵解密
padded_plain = matrix_decrypt_blocks(mat_enc, M_INV)
print("[+] After matrix decrypt:", padded_plain.hex())

# 6. 取前4字节长度
msg_len = int.from_bytes(padded_plain[:4], "big")
print("[+] Message length:", msg_len)

# 7. 取正文并转 UTF-8
plaintext = padded_plain[4:4+msg_len].decode("utf-8")
print("[+] Plaintext:", plaintext)

return plaintext


if __name__ == "__main__":
decrypt()

DASCTF{H4rmOny0S_Mult1_L4y3r_Crypt0_M4st3r!}

或者将前面两步直接用在线求解

image-20260317214407322

后面的异或和逆矩阵解密写个脚本

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

data = bytes.fromhex("5a 3c e7 e6 67 1b e9 74 c5 3b c1 74 b8 b7 42 cc 6e 9e 62 e0 05 47 c2 fd dc f7 63 f8 89 56 6e d2 b8 03 79 23 d4 d3 a1 ba 47 08 13 d1 1c 71 29 9a")
xor_key = [0x5a, 0x3c, 0xe7, 0x91, 0x2f]

M_INV = [
[1, 254, 5],
[0, 1, 252],
[0, 0, 1],
]

def xor_restore(data, key):
return bytes(data[i] ^ key[i % len(key)] for i in range(len(data)))

def mul(mat, vec):
out = []
for i in range(3):
s = 0
for j in range(3):
s += mat[i][j] * vec[j]
out.append(s & 0xff)
return out

def matrix_decrypt_blocks(data, inv):
out = bytearray()
for i in range(0, len(data), 3):
block = list(data[i:i+3])
out.extend(mul(inv, block))
return bytes(out)

x = xor_restore(data, xor_key)
print("xor后:", x.hex())

p = matrix_decrypt_blocks(x, M_INV)
print("矩阵解密后:", p.hex())

n = int.from_bytes(p[:4], "big")
print("长度:", n)
print("明文:", p[4:4+n].decode())