留校的时候打的,感觉特别高脂,复现一下

realme

这道题当时用ida写的,现在看到很多wp都是用dbg

首先有调试,用dbg的插件去调试,真的很方便●﹏●

可以看到密钥和输入都被push了进入加密,过加密之后直接dump

image-20250908204433297

然后再ida打开,会发现加密过程变成了真的不是原来的假的

image-20250908204642427

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
#include <stdio.h>
#include <stdint.h>
#include <string.h>

uint8_t *ini(const uint8_t *key, int keylen, uint8_t *S) {
uint8_t v5[256];
int i, v6 = 0;
for (i = 0; i < 256; i++) {
S[i] = i ^ 0xCF;
v5[i] = key[i % keylen];
}
for (i = 0; i < 256; i++) {
v6 = (v6 + v5[i] + S[i]) & 0xFF;
uint8_t tmp = S[i];
S[i] = S[v6];
S[v6] = tmp ^ 0xAD;
}
return S;
}

void decrypt(uint8_t *S, const uint8_t *in, uint8_t *out, int len) {
int v9 = 0, v8 = 0;
for (int i = 0; i < len; i++) {
v9 = (v9 + 1) & 0xFF;
v8 = (v8 + v9 * S[v9]) & 0xFF;
uint8_t tmp = S[v9]; S[v9] = S[v8]; S[v8] = tmp;
int v4 = (S[v8] + S[v9]) & 0xFF;
out[i] = (i % 2) ? (in[i] - S[v4]) & 0xFF : (in[i] + S[v4]) & 0xFF;
}
}

int main() {
uint8_t key[] = {0x59, 0x30, 0x75, 0x5F, 0x43, 0x61, 0x6E, 0x27, 0x74, 0x5F, 0x46, 0x31, 0x6E, 0x64, 0x5F, 0x4D, 0x65, 0x21};
uint8_t data[] = {
0x50, 0x59, 0xA2, 0x94, 0x2E, 0x8E, 0x5C, 0x95, 0x79, 0x16, 0xE5, 0x36, 0x60, 0xC7, 0xE8, 0x06,0x33, 0x78, 0xF0, 0xD0, 0x36, 0xC8, 0x73, 0x1B, 0x65, 0x40, 0xB5, 0xD4, 0xE8, 0x9C, 0x65, 0xF4,0xBA, 0x62, 0xD0
};
int len = sizeof(data);
uint8_t S[256], out[256];
ini(key, sizeof(key), S);
decrypt(S, data, out, len);
printf("Plaintext:\n");
for (int i = 0; i < len; i++)
putchar(out[i]);
return 0;
}

SMC

其实我对它的理解一直停留在新生赛,VirtualProtect一函数为标志,现在有了新的理解

它是可执行文件中的指定区段实施加密处理,SMC 技术会将那些需要加密的代码部分,诸如特定函数或代码块等,单独编译成独立的 section(段)。

所以这个程序中的section可以看见rc4在自定义段

image-20250908213320084

代码段改为可读可写可执行的,此时代码段就可以被视作数据段,即能够进行正常数据操作,从而对代码的硬编码做一个修改

Crack me

主函数挺容易找的,输入32位十六进制变成了16位的字节

image-20250911153456794

动调可以发现sign函数和AES加密

两次sign里输入的不一样,第一次是用户名,第二次是用户名+Showmaker11

然后获得两个签名

image-20250911153756409

大概的验证意思就是

用户输入的 16 字节和 sign(username + “Showmaker11”) 结合,用 AES 处理

如果 sign(username) 对应就是正确的

输入是16个字节,AES-128,仔细分析一下AES可以发现

字节代换没有变化,s盒没被魔改,

image-20250911162412984

行位移

image-20250911162331516

列混淆 多了一个异或0x55

image-20250911162533573

密钥扩展

image-20250911162057743

轮密钥加

image-20250911162504697

轮数任然是AES-128的10轮

image-20250911161410107

法一 动调出密文密钥

断点在AES加密的函数上,取出密钥

image-20250913155251354

密文直接再v58上取

image-20250913163126506

写个脚本

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
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "AES.h"

int aesDecrypt(const uint8_t *key, uint32_t keyLen,const uint8_t *ct, uint8_t *pt, uint32_t len);

int main() {
uint8_t ct[16] = {
0x22, 0xEB, 0xB5, 0x40,
0x91, 0x62, 0x9C, 0xF7,
0xE2, 0x13, 0xFF, 0xA8,
0x8C, 0x54, 0xD9, 0x80
};

uint8_t key[16] = {
0x24, 0x25, 0x8A, 0x5B,
0x6A, 0x20, 0x62, 0x5D,
0xD2, 0x71, 0x64, 0x32,
0xFD, 0xE7, 0x5E, 0xC4
};

uint8_t pt[16] = {0};

if (aesDecrypt(key, 16, ct, pt, 16) != 0) {
printf("解密失败!\n");
return 1;
}

printf("Decrypted: ");
for (int i = 0; i < 16; i++) {
printf("%02x", pt[i]);
}
printf("\n");

return 0;
}

法二 调用dll

可以看出这两函数都是在libcrypto.dll中,可以直接调用

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
#include <iostream>
#include <windows.h>
#include <string>
#include <cstdint>
#include "D:\test\AES-master\AES-master\AES.h"

int aesDecrypt(const uint8_t* key, uint32_t keyLen, const uint8_t* ct, uint8_t* pt, uint32_t len);
std::string GetPassword(std::string UserName)
{
HMODULE hLibCrypto = LoadLibraryA("D:\\test\\nep\\crackme\\bin\\libcrypto.dll");
if (!hLibCrypto) {
std::cerr << "Failed to load libcrypto.dll" << std::endl;
return "";
}

typedef DWORD* (*MD5_Func)(const char*, int, uint8_t*, int);
MD5_Func MD5_ = (MD5_Func)GetProcAddress(hLibCrypto, "sign");
if (!MD5_) {
std::cerr << "Failed to get function address" << std::endl;
FreeLibrary(hLibCrypto);
return "";
}

uint8_t Key[16]{};
MD5_(UserName.c_str(), UserName.length(), Key, 16);
uint8_t Cipher[16]{};
auto Str1 = UserName + "Showmaker11";
MD5_(Str1.c_str(), Str1.length(), Cipher, 16);
uint8_t Password_bytes[16]{};
aesDecrypt(Key, 16, Cipher, Password_bytes, 16);
std::string Password;
for (int i = 0; i < 16; i++)
{
char Buffer[10]{};
sprintf_s(Buffer, "%02x", Password_bytes[i]);
Password += Buffer;
}

FreeLibrary(hLibCrypto);
return Password;
}

int main()
{
std::cout << GetPassword("inkey") << std::endl;
return 0;
}

image-20250913163532717

QRS

这个运行起来熟悉感扑面而来,有点像暑假的一道题

依旧打开恢复符号表,可以看出来有一些带rust的系统函数

在main函数的一开头下断点,调试不了,在GetCurrentThread引用找到了反调试的地方nop掉重新调试,但是在这里往后一直调的话找不到输入的地方,最后还是找了missing field函数引用在这下了断点

image-20250916101733043

然后就调试在1970断下的地方输入,是get传参,往后调

1
http://127.0.0.1:8887/interface?input=AAAAAAAAAAAAAAAAAA

这里其实当时调了很久,很好奇为什么是get不是post。

暑假那道题其实题目里给了就是要求get请求。

朋友说因为post传参不会被记录到地址中,get上传的数据会被缓存,嘶很有道理

后面会碰到一个大跳那是加密的地方,在汇编可以看到我刚刚的输入,下面那个是加密函数,可以看见里面是xtea加密

image-20250916122525972

魔数可以动调看见定值,密钥和密文拿到写脚本

image-20250916140803671

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

void main(){
uint32_t enc[] = {0x83EA621, 0xC745973C, 0xE3B77AE8, 0xCDEE8146, 0x7DC86B96, 0x6B8C9D3B, 0x79B14342, 0x2ECF0F0D};
uint32_t key[] = {0x1234567, 0x89ABCDEF, 0xFEDCBA98, 0x76543210};
for(int i = 0; i < 8; i += 2){
uint32_t delta = 0x68547369, round = 48, sum = delta * round, v0 = enc[i], v1 = enc[i + 1];
for(int j = 0; j < round; j++){
sum -= delta;
v1 -= (v0 + ((16 * v0) ^ (v0 >> 5))) ^ (sum + delta + *(uint32_t *)((char *)key + (((sum + delta) >> 9) & 0xC)));
v0 -= (v1 + ((16 * v1) ^ (v1 >> 5))) ^ (key[sum & 3] + sum);
}
enc[i] = v0;
enc[i + 1] = v1;
}
for(int i = 0; i < 32; i++){
printf("%02X ", ((uint8_t *)&enc[i / 4])[(i % 4)]);
}
}

axum框架

Axum是Rust生态里的一个 异步Web 框架,类似 Go 的 net/http

FlutterPro

这道题,没办法正常解出来,报错了

ai说是libapp.so 里找不到符号 _kDartVmSnapshotData,里面是Dart 的一些东西。

image-20250921211736288

ida打开在字符串里面意外发现了 _kDartVmSnapshotData,因为到0x346的时候突然出现了一堆0,所以报错了

image-20250925214249763

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def extract_snapshot_hash_flags(libapp_file):
with open(libapp_file, 'rb') as f:
elf = ELFFile(f)
# find "_kDartVmSnapshotData" symbol
dynsym = elf.get_section_by_name('.dynsym')
sym = dynsym.get_symbol_by_name('_kDartVmSnapshotData')[0]
#section = elf.get_section(sym['st_shndx'])
assert sym['st_size'] > 128
f.seek(sym['st_value']+20)
snapshot_hash = f.read(32).decode()
data = f.read(256) # should be enough
flags = data[:data.index(b'\0')].decode().strip().split(' ')

return snapshot_hash, flags

可以看到extract_dart_info里面的源码,是连续读取的我们修改一下改成从0x354开始读,然后继续尝试一下

image-20250926090856780

image-20250926091514270

蒽依旧报错这次是因为**.rodata 段**没找到,分析一下so

image-20250926091548962

看了一下正常的.rodata、.text、.bss段都被删去了,嘶难搞再跑到源码那分析了一下找.rodata 段的代码,代码的作用是找flutter的版本信息和Dart SDK的版本号

image-20250926092426162

出问题的地方应该是这边

image-20250926115057962

image-20250926115758729

修一下,经过好久的编译错误终于成功了,在官方wp里面找到了ida9还原函数名的方法

1
python blutter.py --dart-version 3.8.1_android_arm64 D:\test\nep\flutterpro\flutterPro\lib\arm64-v8a\libapp.so .\output

image-20250927181514296

在output文件夹里面asm/flutterpro/main.dart可以看到有两个回调函数其中ontap点击事件在ida里面分析,找到主要事务函数分析

不断ai

image-20250927210052046

大概就是指这一部分构成一个8*8的样子,构成了之后应该会有一点操作,点进这部分下面的一个sub_203624函数继续分析,一点一点分析实在是太慢了,然后就想hook去看看,结果很久很久一直不成功,后来在一个文章里面看到要用真机😊

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
while ( 1 )
{
*(_QWORD *)(v10 - 24) = v17;
if ( (unsigned __int64)v16 <= *(_QWORD *)(v5 + 56) )
ArrayStub_32d270 = StackOverflowSharedWithoutFPURegsStub_32d378(ArrayStub_32d270, v15);
if ( v17 >= 8 )
break;
v18 = AllocateArrayStub_32d270();
for ( i = 0LL; ; ++i )
{
if ( (unsigned __int64)v16 <= *(_QWORD *)(v5 + 56) )
v18 = StackOverflowSharedWithoutFPURegsStub_32d378(v18, i);
if ( i >= 8 )
break;
*(_DWORD *)(v18 + 4 * i + 15) = 0;
}
v15 = *(_QWORD *)(v10 - 24);
v20 = *(_QWORD *)(v10 - 32);
*(_DWORD *)(v20 + 4 * v15 + 15) = v18;
v17 = v15 + 1;
ArrayStub_32d270 = v20;
}
v21 = ArrayStub_32d270;
for ( j = 0LL; ; ++j )
{
v23 = *(_QWORD *)(v10 - 8);
*(_QWORD *)(v10 - 56) = j;
if ( (unsigned __int64)v16 <= *(_QWORD *)(v5 + 56) )
ArrayStub_32d270 = StackOverflowSharedWithoutFPURegsStub_32d378(ArrayStub_32d270, v15);
if ( j >= 8 )
break;
v24 = 8 * j;
*(_QWORD *)(v10 - 48) = 8 * j;
v25 = 0LL;
while ( 1 )
{
*(_QWORD *)(v10 - 24) = v25;
if ( (unsigned __int64)v16 <= *(_QWORD *)(v5 + 56) )
StackOverflowSharedWithoutFPURegsStub_32d378(ArrayStub_32d270, v15);
if ( v25 >= 8 )
break;
*(_QWORD *)(v10 - 40) = *(unsigned int *)(v21 + 4 * j + 15) + (v7 << 32);
v26 = *(unsigned int *)(v23 + 19) + (v7 << 32);
MintSharedWithoutFPURegsStub_32d4f8 = 2 * ((int)v24 + (int)v25);
if ( v24 + v25 != MintSharedWithoutFPURegsStub_32d4f8 >> 1 )
{
MintSharedWithoutFPURegsStub_32d4f8 = AllocateMintSharedWithoutFPURegsStub_32d4f8();
*(_QWORD *)(MintSharedWithoutFPURegsStub_32d4f8 + 7) = v28;
}
*v16 = MintSharedWithoutFPURegsStub_32d4f8;
v16[1] = v26;
v29 = dart_core__StringBase::op_at_3090f4();
v30 = sub_272F98(v29, v6->Obj_0x142d0, v29);
if ( !((__int64)*(int *)(v30 + 19) >> 1) )
{
v42 = RangeErrorSharedWithoutFPURegsStub_32d7c0();
goto LABEL_46;
}
v32 = *(unsigned __int8 *)(v30 + 23);
v33 = *(_QWORD *)(v10 - 24);
v34 = 2 * (int)v33;
if ( v33 != v34 >> 1 )
{
v34 = AllocateMintSharedWithoutFPURegsStub_32d4f8();
*(_QWORD *)(v34 + 7) = v35;
}
v36 = 2 * v32;
v37 = *(_QWORD *)(v10 - 40);
v38 = (unsigned int)*(_QWORD *)(v37 - 1) >> 12;
v31[1] = v34;
v31[2] = v37;
*v31 = v36;
(*(void (**)(void))(v3 + 8 * (v38 - 1377)))();
ArrayStub_32d270 = *(_QWORD *)(v10 - 24);
v25 = ArrayStub_32d270 + 1;
v23 = *(_QWORD *)(v10 - 8);
j = *(_QWORD *)(v10 - 56);
v21 = *(_QWORD *)(v10 - 32);
v24 = *(_QWORD *)(v10 - 48);
}
ArrayStub_32d270 = j;
v21 = *(_QWORD *)(v10 - 32);
}

在这个函数里面,可以发现是有矩阵在相乘的

我看 不论是官方的还是其它大师的wp都是hook出了矩阵的key,在blutter给的frida脚本上修改

1
2
3
4
5
6
7
8
9
10
11
Interceptor.attach(libapp.add(0x203844), {
onEnter: function () {
console.log(this.context['x2'])
}
});

Interceptor.attach(libapp.add(0x203850), {
onEnter: function () {
console.log(this.context['x0'])
}
});

所以第一次加密这边是一个矩阵相乘

image-20250928170401932

再往下分析一下

image-20250927220959191

创建另一个DartObjectPool的结构体,然把v6修改成池子,变成了这样

image-20250927220440296

image-20250927220458428

点进去就会发现又一长串的字符串,可能是密文

从相乘后往下可以发现还有一个sub_2030D8函数,点进去里面也有很多类似于v6的东西,会发现里面有写莫名其妙的字符串和密文差不多

image-20250928170602142

image-20250928170658419

image-20250928170749249

所以可能逻辑就是

input -> len -> 矩阵相乘 -> RCNB -> compare

1
2
3
4
import rcnb

data = rcnb.decodeBytes("RƇǹþRȻňBRÇȠƀŔćnBŔCÑƁRĈȵþRƇƞƀŔȼNBŔCŇÞŔĊņßŔĈŃƀŔĊȠƃŔċȵƄRčņßŔcȠbŕĊNƄŔcǹƃŔĆŅƁŔĆŇƅŔĊƞƁŔĉȵƁRčńƁRȻƝƅŕĊņþRȼȠƁŔćƞbŔćŅƀŔĉŅƁŔĊÑBRĊȵƁRȼȵƄŕćȵƀRȻņBŔćņƁŔCƞƁŔĈÑƀŔĉƞƄRĊnƃRȼņƃŕĆŃþRƇƝƄŔcnƁRÇnBRȻŅßŔcńƃRćǸƄRčŅÞŔċȠBŔcÑBŔĊńƀŔĈńƀŔĊńƃŔċnƃRƇņƀŔcňƁŕċÑƃŔCÑƀŔĈǹƁŔĉņßŔČŇƀŔČǹþRƇƝÞŔCňÞŕĊňƁ")
print([int.from_bytes(data[i:i+2], byteorder='big') for i in range(0, len(data), 2)])
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
from z3 import Solver, Int, sat

key = [0x4, 0x6, 0x6, 0x7, 0x7, 0x6, 0x4, 0x4, 0xe, 0xf, 0x1, 0x5, 0x6, 0x5, 0x1, 0x3, 0x6, 0x5, 0x4, 0x7, 0x6, 0x7, 0x6, 0x7, 0x5, 0x3, 0x9, 0x4, 0x9, 0x5, 0xc, 0x5, 0x7, 0x6, 0x7, 0x6, 0x6, 0x7, 0x7, 0x7, 0x5, 0x1, 0x3, 0x5, 0xc, 0x2, 0x3, 0x4, 0x7, 0x6, 0x4, 0x4, 0x4, 0x6, 0x6, 0x6, 0x2, 0xd, 0x3, 0x5, 0xe, 0xf, 0xf, 0x5, 0x4, 0x6, 0x6, 0x7, 0x7, 0x6, 0x4, 0x4, 0xe, 0xf, 0x1, 0x5, 0x6, 0x5, 0x1, 0x3, 0x6, 0x5, 0x4, 0x7, 0x6, 0x7, 0x6, 0x7, 0x5, 0x3, 0x9, 0x4, 0x9, 0x5, 0xc, 0x5, 0x7, 0x6, 0x7, 0x6, 0x6, 0x7, 0x7, 0x7, 0x5, 0x1, 0x3, 0x5, 0xc, 0x2, 0x3, 0x4, 0x7, 0x6, 0x4, 0x4, 0x4, 0x6, 0x6, 0x6, 0x2, 0xd, 0x3, 0x5, 0xe, 0xf, 0xf, 0x5, 0x4, 0x6, 0x6, 0x7, 0x7, 0x6, 0x4, 0x4, 0xe, 0xf, 0x1, 0x5, 0x6, 0x5, 0x1, 0x3, 0x6, 0x5, 0x4, 0x7, 0x6, 0x7, 0x6, 0x7, 0x5, 0x3, 0x9, 0x4, 0x9, 0x5, 0xc, 0x5, 0x7, 0x6, 0x7, 0x6, 0x6, 0x7, 0x7, 0x7, 0x5, 0x1, 0x3, 0x5, 0xc, 0x2, 0x3, 0x4, 0x7, 0x6, 0x4, 0x4, 0x4, 0x6, 0x6, 0x6, 0x2, 0xd, 0x3, 0x5, 0xe, 0xf, 0xf, 0x5]
enc = [3879, 4271, 4182, 4951, 4753, 2999, 3842, 6611, 4718, 5457, 5122, 5534, 5695, 3657, 4630, 7665, 4624, 4843, 4866, 5493, 5393, 3633, 4286, 7709, 4483, 5040, 4992, 5293, 5501, 3293, 4495, 7342, 4251, 5003, 4743, 5202, 5345, 3154, 4404, 7079, 3835, 4503, 4051, 4247, 4534, 2815, 3648, 5681, 4601, 5432, 5132, 5434, 5554, 3802, 4573, 7904, 4752, 5223, 5307, 5762, 5829, 3838, 4728, 7723]

# --- 参数 ---
BLOCKS = 8 # 8 blocks of 8 equations each
BLOCK_SIZE = 8 # 8 variables per block

ASCII_LO = 32 # 可打印字符下限(空格)
ASCII_HI = 126 # 可打印字符上限(~)

plaintext = [] # 存储每个 block 解出的字符串

for block_idx in range(BLOCKS):
s = Solver()
vars_ = [Int(f"m_{block_idx}_{k}") for k in range(BLOCK_SIZE)]

for v in vars_:
s.add(v >= ASCII_LO, v <= ASCII_HI)
for row in range(BLOCK_SIZE):
total = 0
for col in range(BLOCK_SIZE):
# 修正 key_index 计算:确保不会超出 key 数组的范围
key_index = (64 * block_idx + 8 * row + col) % len(key)
total += vars_[col] * key[key_index]
enc_index = 8 * block_idx + row
s.add(total == enc[enc_index])

if s.check() == sat:
m = s.model()
chars = []
for k in range(BLOCK_SIZE):
val = m[vars_[k]].as_long()
# 如果在 ASCII 可打印范围则转为字符,否则显示数字(以便排查)
if ASCII_LO <= val <= ASCII_HI:
chars.append(chr(val))
else:
chars.append(f"[{val}]")
block_str = ''.join(chars)
print(f"Block {block_idx}: {block_str}")
plaintext.append(block_str)
else:
print(f"Block {block_idx}: no solution (unsat)")
plaintext.append(None)

# 合并并打印最终字符串(如果每块都解出)
if all(p is not None for p in plaintext):
full = ''.join(plaintext)
print("\nFull plaintext:")
print(full)
else:
print("\nNot all blocks solved; partial output above.")