MiniLctf2022

题目质量很高,我也是真的菜

题目和WP

https://github.com/XDSEC/miniLCTF_2022

Twin

考点:TLS_CallBack函数、SMC、XXTEA+XXTEA魔改、反调试、花指令

前置知识

TLS_CALLBACK函数

TLS设计的本意,是为了解决多线程程序中变量同步的问题,是Thread Local Storage的缩写,意为线程本地存储。线程本身有独立于其他线程的栈空间,因此线程中的局部变量不用考虑同步问题。多线程同步问题在于对全局变量的访问,TLS在操作系统的支持下,通过把全局变量打包到一个特殊的节,当每次创建线程时把这个节中的数据当做副本,拷贝到进程空闲的地址空间中。以后线程可以像访问局部变量一样访问该异于其他线程的全局变量的副本,而不用加同步控制。

TLS回调函数常用于反调试

每当创建或终止进程的线程时会自动调用执行的函数。当然,创建进程的主线程的时候也会自动调用回调函数,且其执行先于EP代码。反调试技术就是利用的TLS回调函数的这一特征。它是各线程独立的数据存储空间,可修改进程的全局/静态数据

而它可以注册多个TLS_CALLBACK函数

进程通信-共享内存CreateFileMapping+MapViewOfFile

https://www.cnblogs.com/endenvor/p/9753135.html

把文件映像到内存,首先必须调用CreateFileMapping()函数,然后再调用MapViewOfFile函数,把文件视映像到进程地址空间上。这样多个子进程都可以访问该内存

分析

这道题所有的字符串都进行了异或加密

在main函数中逆出来得到的是fake_flag,然后去Export发现了TLS函数

https://blog.csdn.net/CSNN2019/article/details/113094488

点进去发现里面什么代码都没有,很奇怪,查看汇编

这里加了一处花指令导致ida无法正常分析

这段花指令就是将返回地址(call后压入栈的值)加上插入字符串的长度,以跳过这段无用代码

直接改为jmp到原本指令的地址

然后F5

要注意的是这里面还有一处花指令很坑

去除之后,这里就是将我们的函数注册为TLS_CALLBACK函数

将文件映射到内存中

继续往下,这里的reason是TLS_CALLBACK函数的第二个参数,因为还处于主线程,所以是1,所以不会进入这里的if

先在注册的TLS_CALLBACK第二个函数下断点,然后F9

Hook掉WriteFile

Hook函数内部

可以看到这里是遍历,然后只要地址等于WriteFile就将final函数的地址赋值过去,实现Hook

Hook完成

Hook之后ExitProcess

结束所有进程和线程

所以不会进入到main函数中

第二次进入TLS_CALLBACK函数

此时第二个参数变成了0,进入第二段

加载资源文件

加载EXERES文件,然后xor_0x5F后创建tmp文件

注意这里的WriteFile已经被Hook成我们的函数

修改值为6,其他正常写入,我们手动去文件修改

下面这个函数是将取消Hook,也就是将WriteFile函数恢复

在Resources Hacker也可以找到资源文件

生成tmp文件

CreateProcessA创建新进程,这个新进程运行指定的可执行文件tmp文件,也就是创建子进程

https://baike.baidu.com/item/CreateProcess/11050419

这里的3代表的是新进程会被当做被调试的进程。系统把被调试程序发生的所有调试事件通知给调试器。

此时也可以通过工具查看

DEBUG模式

进入该函数

等待调试

子进程

去tmp文件查看

注册一个异常处理函数

异常处理函数

当出现地址访问异常时,EIP+2

但是这里要注意因为子进程是DEBUG模式,所以优先让调试器也就是父进程处理

因为子进程是调试状态,所以这里要执行,也就是xxtea的常量进行异或,key[1]改为0x90

异常地址

内存访问出错

此时再去父进程看

而EAX的值恰好是XXTEA的常量经过处理

常量的处理

子进程通过文件映射通信

xxtea

最后就是xxtea,不过要注意这里的>>5都改成了>>6,也就是前面WirteFile时候改的数值

脚本

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

#define xxtea_DELTA ((((0x9E3779B9 ^0x90909090^ 0x12345678 ^ 0x7b)+12345))^0x1b207)
#define xxtea_MX (((xxtea_z>>6^xxtea_y<<2) + (xxtea_y>>3^xxtea_z<<4)) ^ ((xxtea_sum^xxtea_y) + (xxtea_key[(xxtea_p&3)^xxtea_e] ^ xxtea_z)))
void xxtea(DWORD* xxtea_origin, int xxtea_n, int const xxtea_key[4])
{
uint32_t xxtea_y, xxtea_z, xxtea_sum;
unsigned xxtea_p, xxtea_rounds, xxtea_e;
if (xxtea_n > 1) /* Coding Part */
{
xxtea_rounds = 6 + 52 / xxtea_n;
xxtea_sum = 0;
xxtea_z = xxtea_origin[xxtea_n - 1];
do
{
xxtea_sum += xxtea_DELTA;
xxtea_e = (xxtea_sum >> 2) & 3;
for (xxtea_p = 0; xxtea_p < xxtea_n - 1; xxtea_p++)
{
xxtea_y = xxtea_origin[xxtea_p + 1];
xxtea_z = xxtea_origin[xxtea_p] += xxtea_MX;
}
xxtea_y = xxtea_origin[0];
xxtea_z = xxtea_origin[xxtea_n - 1] += xxtea_MX;
} while (--xxtea_rounds);
}
else if (xxtea_n < -1) /* Decoding Part */
{
xxtea_n = -xxtea_n;
xxtea_rounds = 6 + 52 / xxtea_n;
xxtea_sum = xxtea_rounds * xxtea_DELTA;
xxtea_y = xxtea_origin[0];
do
{
xxtea_e = (xxtea_sum >> 2) & 3;
for (xxtea_p = xxtea_n - 1; xxtea_p > 0; xxtea_p--)
{
xxtea_z = xxtea_origin[xxtea_p - 1];
xxtea_y = xxtea_origin[xxtea_p] -= xxtea_MX;
}
xxtea_z = xxtea_origin[xxtea_n - 1];
xxtea_y = xxtea_origin[0] -= xxtea_MX;
xxtea_sum -= xxtea_DELTA;
} while (--xxtea_rounds);
}
}

int main()
{
//0x40CEA5BC,0xE7B2B2F4,0x129D12A9,0x5BC810AE,0x1D06D73D,0xDCF800DC
//unsigned int enc[155] = { 3532577106, 1472742623, 3642468664, 4193500461, 2398676029, 617653972, 1474514999, 1471783658, 1012864704, 3615627536, 993855884, 438456717, 3358938551, 3906991208, 198959101, 3317190635, 3656923078, 613157871, 2398768861, 97286225, 2336972940, 1471645170, 3233163154, 583597118, 2863776301, 3183067750, 1384330715, 2929694742, 3522431804, 2181488067, 3303062236, 3825712422, 145643141, 2148976293, 2940910035, 506798154, 994590281, 2231904779, 3389770074, 2814269052, 1105937096, 1789727804, 3757028753, 2469686072, 1162286478, 680814033, 2934024098, 2162521262, 4048876895, 2121620700, 4240287315, 2391811140, 3396611602, 3091349617, 3031523010, 2486958601, 3164065171, 1285603712, 798920280, 2337813135, 4186055520, 3523024366, 1077514121, 1436444106, 2731983230, 1507202797, 500756149, 198754565, 2382448647, 880454148, 1970517398, 3217485349, 1161840191, 560498076, 1782600856, 2643721918, 1285196205, 788797746, 1195724574, 4061612551, 103427523, 2502688387, 4147162188, 617564657, 978211984, 1781482121, 2205798970, 3939973102, 3826603515, 659557668, 2582884932, 1561884856, 2217488804, 1189296962, 169145316, 2781742156, 1323893433, 824667876, 408202876, 3759637634, 4094868412, 1508996065, 162419237, 3732146944, 3083560189, 3955940127, 2393776934, 2470191468, 3620861513, 481927014, 2756226070, 3154651143, 1261069441, 2063238535, 2222237213, 101459755, 3159774417, 1721190841, 1078395785, 176506553, 3552913423, 1566142515, 1938949000, 1499289517, 3315102456, 829714860, 3843359394, 952932374, 1283577465, 2045007203, 3957761944, 3767891405, 2917089623, 3296133521, 482297421, 1734231412, 3670478932, 2575334979, 2827842737, 3413631016, 1533519803, 4008428470, 3890643173, 272960248, 317508587, 3299937500, 2440520601, 27470488, 1666674386, 1737927609, 750987808, 2385923471, 2694339191, 562925334, 2206035395 };
DWORD enc[18] = { 0x6B7CE328, 0x4841D5DD, 0x963784DC, 0xEF8A3226, 0x0776B226 };
//unsigned int key[4] = {0x12,0x90,0x56,0x78};
int key[4] = { 0x12,0x90,0x56,0x78 };
xxtea(enc, -5, key);//长度为多少,解密时第二个参数就改为多长
for (int i = 0; i < 5; ++i)
{
printf("%c%c%c%c", ((char*)&enc[i])[0], ((char*)&enc[i])[1], ((char*)&enc[i])[2], ((char*)&enc[i])[3]);
}

/* char s[] = "3e90c91c02e9b40b78b}";
printf("%d", strlen(s));*/
return 0;
}

剩下的一部分也是xxtea,没魔改就不说了

What assembly

考点:WASM逆向

https://www.52pojie.cn/thread-962068-1-1.html

https://github.com/WebAssembly/wabt

http://www.dwenzhao.cn/profession/netbuild/webassemblyfunc.html

wasm动态调试

利用python3在与.html.js.wasm文件同目录下开启http服务。

1
python -m http.server 8888

然后在浏览器输入http://127.0.0.1:8888/MiniLctf-2022.html

下断点动态调试

过程

1
./wasm2c flag.wasm -o flag.c

生成flag.c和flag.h

将之前反编译出来的wasm.c,wasm.h,以及wabt项目内的wasm-rt.h,wasm-rt-impl.c,wasm-rt-impl.h三个文件放到同一个文件夹。

1
gcc -c flag.c -o final.o

生成.o文件, 因为很多wasm的函数没有具体的实现,我们不需要编译链接生成elf文件

因为wasm是基于栈的,编译后的C的结构也模拟对栈的操作,所以可以模拟栈进行分析

基础指令

https://juejin.cn/post/6844904069186715656

1
2
3
4
5
6
7
8
9
10
11
12
13
load(memory,a2)//出栈指令
//将栈顶元素弹出至a1
store(memory,a1,a2)//入栈指令
//a1一般是栈顶,也就是目标地址,而a2是要入栈的值
//当然上面的指令也是可以指定不同长度的
i32_load8_u//8位无符号数
i32_load8_s//8位有符号数

//数组索引取值,通过地址+索引
i32_store(&w2c_memory, Stack_Pointer + 28LL, 0);// 初始化为索引为0
key_addr = i32_load(&w2c_memory, Stack_Pointer + 100LL);
tmp_key_addr = i32_load(&w2c_memory, Stack_Pointer + 28LL) + key_addr;// 存储key的地址+索引

分析

就一步一步分析

得出最终的加密过程

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
#include<stdio.h>
#include<windows.h>
#include<string.h>
#include"defs.h"
#define ROL(a,b) (((a) << (b)) | ((a) >> (8 - (b))))
void qua_rou(unsigned char* s, int a, int b, int c, int d)
{
//直接异或即可
//逆着来解密就行了
s[b] ^= ROL((s[a] + s[d]) & 0xff, 4);
s[d] ^= ROL((s[c] + s[b]) & 0xff, 2);
s[c] ^= ROL((s[b] + s[a]) & 0xff, 3);
s[a] ^= ROL((s[d] + s[c]) & 0xff, 1);
return;
}
int main()
{
char key[] = "D33.B4T0";
char table[] = "0123456789abcdef";
char flag[] = {0};
unsigned char enc[] = { 0x05,0x77,0x9c,0x24,0xd9,0x24,0x9e,0x69,0x3f,0xa7,0xac,0x4a,0x10,0xc6,0x8d,0xfb,0xd3,0x52,0x00,0x83,0xb3,0x3f,0x56,0xe9,0x0f,0xd8,0x49,0x78,0xb6,0xa1,0x5c,0x97,0x0b,0x97,0x67,0x79,0xa8,0xfe,0xfe,0x91,0xfb,0x87,0xd2,0x22,0x1c,0x9a,0x1f,0x87,0xed,0x7e,0xad,0xdb,0x8a,0xe6,0x37,0x0f,0x9d,0xe6,0x9e,0x3a,0x7a,0x5c,0x5c,0x48,0x8c,0xde,0x79,0x75,0x6b,0x0b,0x9f,0x17,0x13,0xe7,0x49,0xed,0xd4,0x1c,0xff,0x04 };
unsigned char s[] = { 0 };
//key赋值
for (int i = 0; i < 8; ++i)
{
s[i] = key[i];
}
for (int i = 0; i < 80; i+=8)
{
for (int j = 0; j < 8; ++j)
{
s[j + 8] = flag[i + j];//每次取出flag的8个字符存储到s中,而前八个是上一轮的加密结果
}
for (int j = 0; j < 42; ++j)//进行42轮加密
{
qua_rou(s, 12, 8, 4, 0);
qua_rou(s, 13, 9, 5, 1);//根据0-7下标的值对8-15下标的加密(也就是我们的flag)
qua_rou(s, 14, 10, 6, 2);
qua_rou(s, 15, 11, 7, 3);
qua_rou(s, 15, 10, 5, 0);
qua_rou(s, 12, 11, 6, 1);
qua_rou(s, 13, 8, 7, 2);
qua_rou(s, 14, 9, 4, 3);
}
int correct = 0;
for (int j = 0; j < 16; j++)
{
correct |= (enc[i * 4 + j * 2 + 0] != table[s[j] / 0x10]);//加密一次之后,此时s数组长度为16,然后偶数取模
correct |= (enc[i * 4 + j * 2 + 1] != table[s[j] % 0x10]);//奇数取余
}
}

return 0;
}

可以知道第一轮先把key赋值给s的前八个,将flag的前八个字符赋值给s数组,然后进行42轮的加密

求解时只需要逆着进行,每次取出密文的16个字符,因为异或过程就只有异或,但是有先后的关系,比如s[12]已经被重新赋值,再次被使用时就会出错,所以要逆着来

脚本

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
#include<stdio.h>
#include<windows.h>
#include<string.h>
#include"defs.h"
#define ROL(a,b) (((a) << (b)) | ((a) >> (8 - (b))))
void qua_rou(unsigned char* s, int a, int b, int c, int d)
{
//直接异或即可
//逆着来解密就行了
s[a] ^= ROL((s[d] + s[c]) & 0xff, 1);
s[c] ^= ROL((s[b] + s[a]) & 0xff, 3);
s[d] ^= ROL((s[c] + s[b]) & 0xff, 2);
s[b] ^= ROL((s[a] + s[d]) & 0xff, 4);
return;
}
int main()
{
char key[] = "D33.B4T0";
char table[] = "0123456789abcdef";
unsigned char enc[] = { 0x05,0x77,0x9c,0x24,0xd9,0x24,0x9e,0x69,0x3f,0xa7,0xac,0x4a,0x10,0xc6,0x8d,0xfb,0xd3,0x52,0x00,0x83,0xb3,0x3f,0x56,0xe9,0x0f,0xd8,0x49,0x78,0xb6,0xa1,0x5c,0x97,0x0b,0x97,0x67,0x79,0xa8,0xfe,0xfe,0x91,0xfb,0x87,0xd2,0x22,0x1c,0x9a,0x1f,0x87,0xed,0x7e,0xad,0xdb,0x8a,0xe6,0x37,0x0f,0x9d,0xe6,0x9e,0x3a,0x7a,0x5c,0x5c,0x48,0x8c,0xde,0x79,0x75,0x6b,0x0b,0x9f,0x17,0x13,0xe7,0x49,0xed,0xd4,0x1c,0xff,0x04 };
for (int i = 0; i < 5; ++i)
{
unsigned char buf[17] = { 0 };
buf[16] = 0;
memmove(buf, enc + i * 16, 16);
for (int j = 0; j < 42; ++j)
{
qua_rou(buf, 14, 9, 4, 3);
qua_rou(buf, 13, 8, 7, 2);
qua_rou(buf, 12, 11, 6, 1);
qua_rou(buf, 15, 10, 5, 0);
qua_rou(buf, 15, 11, 7, 3);
qua_rou(buf, 14, 10, 6, 2);
qua_rou(buf, 13, 9, 5, 1);
qua_rou(buf, 12, 8, 4, 0);
}

printf("%s", buf+8);
}
return 0;
}

NotRC4

考点:RISV架构

使用readelf可以知道是RISV架构

过程

ida无法反编译,jeb可以反编译但是效果不太好,最好的还是Ghidra

就是一个VM题

分析完大概就是这样

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

int main()
{
unsigned long long final[4] = {
0x4BC21DBB95EF82CA, 0xF57BECAE71B547BE, 0x80A1BDAB15E7F6CD, 0xA3C793D7E1776385
};
unsigned char opcode[23] = {
0xF3, 0x00, 0xF4, 0xE1, 0xF4, 0xE2, 0xF2, 0x04, 0x0B, 0xF5, 0xF3, 0x02, 0xF4, 0xE1, 0xF4, 0xE2,
0xF2, 0x04, 0x0B, 0xF5, 0xF1, 0xFF, 0x00
};
//F1对应的指令
//compare,long型的判断


//F2
//循环的操作,和0x0B作比较,作为循环的次数,而4是指回到前四个指令的位置,实现循环


//F3,取出输入然后加上yyds和dbt!(小端序)


//F4加密操作

//F5,存储
int key[2] = { 0x64627421, 0x79796473 };

for (int i = 0; i < 4; i += 2)
{
unsigned long tmp1 = final[i] + key[0];
unsigned long tmp2 = final[i + 1] + key[1];
for (int j = 0; j < 0x0C; ++j)
{
tmp1 = ((tmp1 ^ tmp2) >> (-(char)tmp2) & 0x3f) | ((tmp1 ^ tmp2) << ((char)tmp2) & 0x3f) + key[0];
tmp2 = ((tmp1 ^ tmp2) >> (-(char)tmp1) & 0x3f) | ((tmp1 ^ tmp2) << ((char)tmp1) & 0x3f) + key[1];
}
final[i] = tmp1;
final[i + 1] = tmp2;
}
return 0;
}

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (int i = 0; i < 4; i += 2)
{
unsigned long long tmp1 = final[i];
unsigned long long tmp2 = final[i + 1];
for (int j = 0; j < 0x0C; ++j)
{
tmp2 = ((((tmp2 - key[1]) & 0xffffffffffffffff) << ((-(char)tmp1) & 0x3f)) | (((tmp2 - key[1]) & 0xffffffffffffffff) >> (((char)tmp1) & 0x3f))) ^ tmp1;
tmp1 = ((((tmp1 - key[0]) & 0xffffffffffffffff) << ((-(char)tmp2) & 0x3f)) | (((tmp1 - key[0]) & 0xffffffffffffffff) >> (((char)tmp2) & 0x3f))) ^ tmp2;
}
final[i] = tmp1 - key[0];
final[i+1] = tmp2 - key[1];
printf("%c%c%c%c%c%c%c%c", ((char*)&final[i])[0], ((char*)&final[i])[1], ((char*)&final[i])[2], ((char*)&final[i])[3], ((char*)&final[i])[4], ((char*)&final[i])[5], ((char*)&final[i])[6], ((char*)&final[i])[7]);
printf("%c%c%c%c%c%c%c%c", ((char*)&final[i+1])[0], ((char*)&final[i+1])[1], ((char*)&final[i+1])[2], ((char*)&final[i+1])[3], ((char*)&final[i+1])[4], ((char*)&final[i+1])[5], ((char*)&final[i+1])[6], ((char*)&final[i+1])[7]);
}