l3hctf_part_wp

Posted by marginal on 2021-11-17
Estimated Reading Time 7 Minutes
Words 1.5k In Total
Viewed Times

double-joy 和 load

double-joy

找到具体加密函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__int64 __fastcall sub_558A3CFB3D90(__int64 *a1)
{
__int64 v1; // rcx
__int64 i; // rsi
__int64 v3; // rax

v1 = *a1;
for ( i = *((int *)a1 + 4); ; ++i )
{
v3 = *(unsigned __int8 *)(v1 + i);
*((_DWORD *)a1 + 4) = i + 1;
if ( (unsigned __int8)v3 <= 0x12u )
break;
}
return ((__int64 (*)(void))((char *)dword_558A3CFB5004 + dword_558A3CFB5004[v3]))();
}

这里是主要是后面的比较关键, 前面的就是取一下指令.

传入的参数我把它改成一个结构体, 具体结构体的定义在Structures里面.

1
2
3
4
5
6
00000000 vm              struc ; (sizeof=0x18, mappedto_16)
00000000 opcode dq ?
00000008 mem dq ?
00000010 index dd ?
00000014 point dd ? //指向内存的索引, 总是指向最后一个值的下一位置
00000018 vm ends

查看流程图窗口, 是有非常多的分支的, 具体跳转位于

1
2
3
4
text:000055E187261DBD 49 63 04 81 movsxd  rax, dword ptr [r9+rax*4]
.text:000055E187261DC1 4C 01 C8 add rax, r9
.text:000055E187261DC4 db 3Eh
.text:000055E187261DC4 3E FF E0 jmp rax

这里我们将每一个rax改为对应的硬编码, 就可以查看微代码了.

这里我大概总结了一下要用到的指令作用(具体细节记录得很丑陋, 就不放出来了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0 加法 
1 减法
2 乘法
3 除法(带符号) 这里要注意一定要带符号
5
7 异或
8 2字节内存赋值 mem[mem[point-2]] = mem[point-1]
9 mem[point-1]作为索引, 再对mem[point-1]赋值
a
b 判断是否<0
c 对最后两个值交换
f 跳转指令
10 判断跳转
11 改变索引
12 return

然后逐一分析指令集(一共有两套), 可以发现, 两套加密是轮换来的, 并且第一次加密都会有异或,(这也是为什么后面加密其他字节都不改变, 但是头两次加密其他字节都发生变化的原因) 对两个字节的加密, 都需要加密20次才会轮到下两个字节加密. 两套指令分别使用两个结构体和两段内存, 每个内存中都一直存在一个字节, 表示加密次数.

解密脚本: (加密过程写在里面了)

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
#include<iostream>

uint32_t key = 448431333;
uint32_t key2 = 3848427296;

int encode1(uint32_t flag[2])
{
int key1[4] = {0x494C, 0x6F76, 0x6520, 0x4355};
uint32_t b = (flag[1] * 0x10);
uint32_t a = ((signed)flag[1] / 0x20);
uint32_t c = ((a ^ b) + flag[1]);
uint32_t d = (c ^ (key + key1[key & 3]));// 6f76
flag[0] = d + flag[0];
key += 0x75BCD15;
uint32_t e = (flag[0] * 0x10);
uint32_t f = ((signed)flag[0] / 0x20);
uint32_t g = (e ^ f);
uint32_t h = g + flag[0];
uint32_t i = h ^ (key + key1[((signed)key / 0x800) & 3]);
flag[1] = i + flag[1];
return 0;
}
void decode1(uint32_t flag[2])
{
int key1[4] = {0x494C, 0x6F76, 0x6520, 0x4355};
uint32_t e = (flag[0] * 0x10);
uint32_t f = ((signed)flag[0] / 0x20);
uint32_t g = ( e^f );
uint32_t h = g + flag[0];
uint32_t i = h ^ (key + key1[((signed)key / 0x800) & 3]);
flag[1] = flag[1] - i;
key -= 0x75BCD15;
uint32_t b = (flag[1] * 0x10);
uint32_t a = ((signed)flag[1] / 0x20);
uint32_t c = ((a ^ b) + flag[1]);
uint32_t d = (c ^ (key + key1[key & 3]));// 6f76
flag[0] -= d;
return;
}
int decode2(uint32_t flag[2])
{
key2 -= 0x154CBF7;
uint32_t b1 = (flag[0] * 0x10 + 0x2074);
uint32_t c1 = (key2 + flag[0]);
uint32_t a1 = ((signed)flag[0] / 0x20 + 0x6561);
uint32_t d1 = (c1 ^ a1);
uint32_t e1 = d1 ^ b1;
flag[1] -= e1;
uint32_t b = (flag[1] * 0x10 + 0x5354);
uint32_t c = (key2 + flag[1]);
uint32_t a = ((signed)flag[1] / 0x20 + 0x4f4d);
uint32_t d = (c ^ a);
uint32_t e = d ^ b;
flag[0] -= e;
return 0;
}
int encode2(uint32_t flag[2])
{
key2 += 0x154CBF7;
uint32_t b = (flag[1] * 0x10 + 0x5354);
uint32_t c = (key2 + flag[1]);
uint32_t a = ((signed)flag[1] / 0x20 + 0x4f4d);
uint32_t d = (c ^ a);
uint32_t e = d ^ b;
uint32_t f = e + flag[0];
flag[0] = f;
uint32_t b1 = (flag[0] * 0x10 + 0x2074);
uint32_t c1 = (key2 + flag[0]);
uint32_t a1 = ((signed)flag[0] / 0x20 + 0x6561);
uint32_t d1 = (c1 ^ a1);
uint32_t e1 = d1 ^ b1;
uint32_t f1 = e1 + flag[1];
flag[1] = f1;
return 0;
}
int main()
{
uint32_t flag[10] = { 0xAEE0FAE8, 0xFC3E4101, 0x167CAD92, 0x51EA6CBE, 0x242A0100, 0x01511A1B, 0x514D6694, 0x2F5FBFEB,
0x46D36398, 0x79EEE3F0};

for (int i = 8; i >= 0; i-=2)
{
for (int j = 19; j >= 0; j--)
{
decode2(flag + i);
if (i == 0 && j == 0)
{
flag[0] ^= 0x1010101;
flag[1] ^= 0x2020202;
}
decode1(flag + i);
if (i == 0 && j == 0)
{
flag[0] ^= 0x1010101;
flag[1] ^= 0x2020202;
}
int m = 3;
}
int n = 3;
}
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 4; j++)
printf("%c", *((char *)&flag[i] + j));
}
return 0;
}

load

这里打开了一个文件映射, 并且把输入复制到文件中:

1
2
3
FileMappingW = CreateFileMappingW(0xFFFFFFFF, 0, 4u, 0, 0x400u, Name);
v7 = MapViewOfFile(FileMappingW, 0xF001Fu, 0, 0, 0x400u);
strncpy(v7, Source, 0x3FFu);

接下来创建了一个进程, 进程对输入进行处理.

这里的进程我没有去dump, 我dump也发生了错误, 所以我直接用ida去附加这个进程, 不管具体子进程如何变化基本上都能分析.在恢复线程执行的函数下好断点, 保证主进程的处理都执行过了. 然后附加好了之后, 选中所有的load.exe段, 让ida分析, 就可以找到关键函数.下好断点, 直接运行, 转过来单步主进程, 就可以在断下来了(注意, 这里会有一个软件断点, 直接pass给进程就可以了)

读取从文件中读取输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
v0 = kernel32_OpenFileMappingW(983071, 0, aL3hsec);
vcruntime140_memset(v26, 0, 1024);
if ( v0 )
{
v1 = kernel32_MapViewOfFile(v0, 983071, 0, 0, 0);
v2 = (char *)v1;
do
{
v3 = *v2++;
v26[(_DWORD)v2 - v1 - 1] = v3;
}
while ( v3 );
kernel32_UnmapViewOfFile(v1);
kernel32_CloseHandle(v0);
}

字符串每两字节转换为16进制,类似于atoi函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
do
{
v6 = *((_BYTE *)&v30 + v5);
if ( v6 >= '0' && v6 <= '9' )
{
v6 -= '0';
*((_BYTE *)&v30 + v5) = v6;
}
if ( (unsigned __int8)(v6 - 'a') <= 5u )
*((_BYTE *)&v30 + v5) = v6 - 'W';
++v5;
}
while ( v5 < v4 );

接下来大概就是通过伴随矩阵求逆矩阵

A* / |A| = A-1

这里需要用到一个代数余子式的东西, 具体都是线代里面的内容

a1 a2 a3

a4 a5 a6

a7 a8 a9

tmp = a1 * (a5 * a9 - a8 * a6) - a2 * (a4 * a9 - a7 * a6) + a3 * (a4 * a8 - a7 * a5)

m1 = (a5 * a9 - a8 * a6) / tmp

m2 = -(a4 * a9 - a7 * a6) / tmp (符号分别正负顺序(根据位置决定))

1
2
3
4
sub_401070(3, (int)v34, 0);                   // v34赋值
sub_401070(2, (int)v25, 9); // v25赋值
sub_401370(v34, 3); // 矩阵运算
sub_401370(v25, 2);

解密: 矩阵的逆矩阵只需要再求一次逆矩阵就是本身了.

1
2
3
4
5
6
7
8
9
10
11
import numpy as np

a = np.array([
[1,0,-9],
[0,-1,-6],
[-1,-2,-4]]) # 初始化一个非奇异矩阵(数组)
# print(np.linalg.inv(a)) # 对应于MATLAB中 inv() 函数

# # 矩阵对象可以通过 .I 更方便的求逆
A = np.matrix(a)
print(A.I)

flag:

flag{f812f706f306ff02ff0dfde207}


如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !