2023安洵杯第六届网络安全挑战赛 Re 部分WriteUp

本文最后更新于 2024年1月4日 上午

题目地址:i-SOON_CTF_2023/re at main · D0g3-Lab/i-SOON_CTF_2023
(github.com)

WriteUp + 复现

感觉有点简单

0x0 初探


64位的驱动

0x1 静态分析

驱动文件的入口点是DriverEntry 主要逻辑看起来还是比较清晰的

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
__int64 sub_1400016F0()
{
__int64 v1; // [rsp+20h] [rbp-78h] BYREF
PVOID NumberOfBytes_4; // [rsp+28h] [rbp-70h]
PVOID P; // [rsp+30h] [rbp-68h]
__int64 v4; // [rsp+38h] [rbp-60h]
__int64 v5; // [rsp+40h] [rbp-58h]
__int64 v6; // [rsp+48h] [rbp-50h] BYREF
const char *v7; // [rsp+50h] [rbp-48h]
const char *v8; // [rsp+58h] [rbp-40h]
struct _UNICODE_STRING DestinationString; // [rsp+60h] [rbp-38h] BYREF
char v10[40]; // [rsp+70h] [rbp-28h] BYREF

HIDWORD(v1) = 4096;
memset(&v6, 0, sizeof(v6));
RtlInitUnicodeString(&DestinationString, L"\\??\\C:\\Users\\Public\\flag.txt");
NumberOfBytes_4 = ExAllocatePool(NonPagedPool, 0x1000ui64);
P = ExAllocatePool(NonPagedPool, 0x1000ui64);
if ( NumberOfBytes_4 && P )
{
v4 = HIDWORD(v1);
memset(P, 0, HIDWORD(v1));
v5 = HIDWORD(v1);
memset(NumberOfBytes_4, 0, HIDWORD(v1));
qmemcpy(v10, &DestinationString, 0x10ui64);
LOBYTE(v1) = sub_140001040(v10, v6, NumberOfBytes_4, (char *)&v1 + 4);
if ( (_BYTE)v1 )
{
if ( HIDWORD(v1) <= 0xC00 )
{
sub_1400011F0(NumberOfBytes_4, HIDWORD(v1), "the_key_", 8i64, v1);
sub_140001360(P, NumberOfBytes_4, HIDWORD(v1));
LOBYTE(v1) = sub_140001560(P, 56i64);
v8 = "tips: YES, RIGHT FLAG. you got it!";
v7 = "tips: NO , WRONG ANSWER. try again !";
if ( (_BYTE)v1 )
DbgPrint("tips: %s\n", v8);
else
DbgPrint("tips: %s\n", v7);
}
...
}

flag是从 flag.txt文件中读取,经过加密后在sub_140001560
函数中进行对比,其中sub_1400011F0是RC4被魔改了的,sub_140001360
是魔改的base64 魔改的RC4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 __fastcall RC4(_BYTE *buf, unsigned int len, _BYTE *a3, int a4)
{
__int64 result; // rax
unsigned __int8 ta; // [rsp+20h] [rbp-18h]
unsigned __int8 tb; // [rsp+21h] [rbp-17h]
unsigned int i; // [rsp+24h] [rbp-14h]

ta = 0;
tb = 0;
init(a3, a4);
for ( i = 0; ; ++i )
{
result = len;
if ( i >= len )
break;
ta = (ta + 1) % 64;
tb = (BOX[ta] + tb) % 64;
swap(&BOX[ta], &BOX[tb]);
buf[i] ^= (tb ^ ta) & BOX[(((tb ^ ta) + BOX[tb] + BOX[ta]) % 64)];
}
return result;
}

魔改的base64

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
__int64 __fastcall Base64(char *Base64Out, char *buf, int len)
{
int ta; // [rsp+0h] [rbp-88h]
int tb; // [rsp+4h] [rbp-84h]
char table[64]; // [rsp+10h] [rbp-78h] BYREF

strcpy(table, "4KBbSzwWClkZ2gsr1qA+Qu0FtxOm6/iVcJHPY9GNp7EaRoDf8UvIjnL5MydTX3eh");
ta = 0;
tb = 0;
while ( ta < len )
{
Base64Out[tb] = table[buf[ta] & 0x3F]; // 6bits
Base64Out[tb + 1] = table[(4 * (buf[ta + 1] & 0xF)) | ((buf[ta] & 0xC0) >> 6)];// 6bits
Base64Out[tb + 2] = table[(16 * (buf[ta + 2] & 3)) | ((buf[ta + 1] & 0xF0) >> 4)];// 6bits
Base64Out[tb + 3] = table[(buf[ta + 2] & 0xFC) >> 2];// 6bits
ta += 3;
tb += 4;
}
if ( len % 3 == 1 )
{
Base64Out[tb - 2] = '=';
Base64Out[tb - 1] = '=';
}
else if ( len % 3 == 2 )
{
Base64Out[tb - 1] = '=';
}
return 0i64;
}

这个base64与标准不一样的是,他是相当于三个字节为一组,按小端排序,从低位到高位取,标准的base64就相当于把数据直接转化为二进制,然后逐一取6bits

0x2 求解

先还原base64,然后解RC4异或回去

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
import struct
byte1400 = [i for i in range(64)]
def keyinit():
global byte1400
key = 'the_key_'
for i in range(64):
byte1400[i] = i

keybox = [0 for i in range(64)]
for i in range(64):
keybox[i] = ord(key[i%8])
temp = 0
for i in range(64):
temp = (keybox[i] + byte1400[i] + temp) % 64
byte1400[i], byte1400[temp] = byte1400[temp], byte1400[i]

def enFunc(buf):
global byte1400
temp = 0
ta = 0
for i in range(len(buf)):
temp = (temp + 1) % 64
ta = (byte1400[temp]+ta) % 64
byte1400[temp], byte1400[ta] = byte1400[ta], byte1400[temp]
buf[i] ^= ((ta ^ temp) & byte1400[(((ta ^ temp) + byte1400[temp] + byte1400[ta]) % 64)])

def deBase64(buf):
res = []
table = '4KBbSzwWClkZ2gsr1qA+Qu0FtxOm6/iVcJHPY9GNp7EaRoDf8UvIjnL5MydTX3eh'
if len(buf) % 4 == 0:
for i in range(0, len(buf), 4):
strbin = ''
for k in range(3, -1, -1):
if buf[i+k] == '=':
continue
strbin += bin(table.index(buf[i+k]))[2:].zfill(6)
dwordValue = int(strbin, 2)
res.append(dwordValue)
return res


def main():
buf = '6zviISn2McHsa4b108v29tbKMtQQXQHA+2+sTYLlg9v2Q2Pq8SP24Uw='
endata = deBase64(buf)
endata = b''.join(struct.pack("<I", i) for i in endata)
endata = struct.unpack('<{}B'.format(len(endata)), endata)
endata = list(endata)
# 去除数组中的所有0,我们是3个字符一组,第四个字符多余了
endata = [value for index, value in enumerate(endata) if (index + 1) % 4 != 0]
keyinit()
enFunc(endata)
print(''.join(chr(i) for i in endata)) #D0g3{608292C4-15400BA4-B3299A5C-704C292D}

if __name__ == '__main__':
main()

牢大想你了

0x0 初探

1
2
3
4
5
6
7
8
Mode          Length Name
---- ------ ----
d---- GalgamePro_BackUpThisFolder_ButDontShipItWithYourGame
d---- GalgamePro_Data
-a--- 640000 GalgamePro.exe
-a--- 7363584 GameAssembly.dll
-a--- 909312 UnityCrashHandler32.exe
-a--- 20324352 UnityPlayer.dll

Unity编写的

运行后的画面有点抽象,就不放了

0x1 分析

通过检索资料,从Assembly-CSharp.dll 入手

用dnSpy32打开,找到关键函数

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
// GameManager
// Token: 0x06000005 RID: 5 RVA: 0x00002240 File Offset: 0x00000440
public void OnValueChanged(string ABBAAAABBBBAAABABBBABAAABAABAABBABBBBABAABAABAB)
{
uint[] str = new uint[]
{
286331153U,
286331153U,
286331153U,
286331153U
};
byte[] strBytes = Encoding.UTF8.GetBytes(ABBAAAABBBBAAABABBBABAAABAABAABBABBBBABAABAABAB);
int paddingCount = 8 - strBytes.Length % 8;
byte[] paddedArray = new byte[strBytes.Length + paddingCount];
Array.Copy(strBytes, paddedArray, strBytes.Length);
uint[] uintArray = new uint[paddedArray.Length / 4];
Buffer.BlockCopy(paddedArray, 0, uintArray, 0, paddedArray.Length);
uint[] encryptedData = new uint[0];
AAABAAABABABAAABBABBABAAAABBAABBAABABBBBBABAAAB str2 = new AAABAAABABABAAABBABBABAAAABBAABBAABABBBBBABAAAB(str);
for (int i = 0; i < uintArray.Length; i += 2)
{
encryptedData = encryptedData.Concat(str2.BABBBBBBAAAAAABABBBAAAABBABBBAABABAAABABBAAABBA(uintArray[i], uintArray[i + 1])).ToArray<uint>();
}
uint[] array = new uint[]
{
3363017039U,
1247970816U,
549943836U,
445086378U,
3606751618U,
1624361316U,
3112717362U,
705210466U,
3343515702U,
2402214294U,
4010321577U,
2743404694U
};
MonoBehaviour.print(array);
if (array.SequenceEqual(encryptedData))
{
this.BBBAAAAABABABABBABAAAAABBABBAABBABABABABBBABAAB = 5;
this.ABAABAAABABABABABBBBBAAABBAABBBBBAABAAAABBABABB("port");
this.BAABAABBABABABABBBABBBBABBBBBBBABABBAABBABABABB("牢大");
this.AAAABBABAAAABBAABAABAABAABBBAAABBBABBBBBAABABBA("哈哈,我没有变成耐摔王");
return;
}
this.BBBAAAAABABABABBABAAAAABBABBAABBABABABABBBABAAB = 5;
this.ABAABAAABABABABABBBBBAAABBAABBBBBAABAAAABBABABB("耐摔王");
this.BAABAABBABABABABBBABBBBABBBBBBBABABBAABBABABABB("狂暴牢大");
this.AAAABBABAAAABBAABAABAABAABBBAAABBBABBBBBAABABBA("获得成就“耐摔王”");
}

查看 AAABAAABABABAAABBABBABAAAABBAABBAABABBBBBABAAAB 函数

1
2
3
4
public AAABAAABABABAAABBABBABAAAABBAABBAABABBBBBABAAAB(uint[] BBABABBBABBABABAAABBBAABBAAAAAAABBBBBAABBAAAAAA)
{
this.BBABABBBABBABABAAABBBAABBAAAAAAABBBBBAABBAAAAAA = BBABABBBABBABABAAABBBAABBAAAAAAABBBBBAABBAAAAAA;
}

进行了赋值,而这个this.BBABABBBABBABABAAABBBAABBAAAAAAABBBBBAABBAAAAAA被引用的地方全是tea算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	public uint[] BABBBBBBAAAAAABABBBAAAABBABBBAABABAAABABBAAABBA(uint ABBAABAAAAAABAAAABBBBBBABAABAAAABBBABBBAABBABBA, uint BAABBAAAAABABBAABBABBAABABABABABABAAABABBBABABA)
{
uint v0 = ABBAABAAAAAABAAAABBBBBBABAABAAAABBBABBBAABBABBA;
uint v = BAABBAAAAABABBAABBABBAABABABABABABAAABABBBABABA;
uint sum = 0U;
uint delta = 2654435769U;
uint[] str2 = this.BBABABBBABBABABAAABBBAABBAAAAAAABBBBBAABBAAAAAA;
for (int i = 0; i < 32; i++)
{
sum += delta;
v0 += ((v << 4) + str2[0] ^ v + sum ^ (v >> 5) + str2[1]);
v += ((v0 << 4) + str2[2] ^ v0 + sum ^ (v0 >> 5) + str2[3]);
}
return new uint[]
{
v0,
v
};
}

直接进行解密

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
#include <iostream>
using namespace std;
// 加密函数
void encrypt(uint32_t* v, uint32_t* k, unsigned int len)
{
for (int n = 0; n < len; n += 2)
{
// 数据初始化
uint32_t v0 = v[n], v1 = v[n + 1], sum = 0;
uint32_t delta = 0x9e3779b9;
uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];
// 加密
for (int i = 0; i < 32; i++) {
sum += delta;
v0 += ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
v1 += ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
}
v[n] = v0; v[n + 1] = v1;
}
}

void decrypt(uint32_t* v, uint32_t* k, unsigned int len)
{
for (int n = 0; n < len; n += 2)
{
uint32_t v0 = v[n], v1 = v[n + 1], sum = 0xC6EF3720;
uint32_t delta = 0x9e3779b9;
uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];
for (int i = 0; i < 32; i++) {
v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
sum -= delta;
}
v[n] = v0; v[n + 1] = v1;
}
}
// 输出十六进制
void out_hex(uint32_t* v, unsigned int len)
{
for (int i = 0; i < len; i++)
{
cout << hex << v[i] << " ";
}
cout << endl;
}
// 输出字符
void out_str(uint32_t* v, unsigned int len)
{
uint8_t word;
for (int i = 0; i < len; i++)
{
for (int k = 0; k < 4; k++)
{
word = v[i] >> (k * 8);
cout << word;
}
}
cout << endl;
}
int main(int argc, char** argv)
{
// 输入
uint32_t input[12] = { 3363017039U,
1247970816U,
549943836U,
445086378U,
3606751618U,
1624361316U,
3112717362U,
705210466U,
3343515702U,
2402214294U,
4010321577U,
2743404694U };
uint32_t key[4] = { 286331153U,
286331153U,
286331153U,
286331153U };
// len 为了input 的长度
unsigned int len = 12;

// 解密
// 这里的input 可更换为需要解密的数据
decrypt(input, key, len);
out_hex(input, len);
out_str(input, len);
}

得到flag

你好PE

0x0 初探

1
2
3
4
 ⚡Administrator ❯❯ .\re4.exe
[out]: PLZ Input FLag
[in ]: aaaaaaaaaaaaaaaa
[out]: len error

依然是
PE32 并且打开IDA无法搜索到相关的字符串

0x1 分析 & 解题

在IDA中发现了加载资源的代码

1
2
3
4
5
6
7
8
9
__CheckForDebuggerJustMyCode(&unk_51E018);
hModule = GetModuleHandleA(0);
hResInfo = FindResourceA(hModule, (LPCSTR)101, "com");
if ( hResInfo )
{
dwSize = SizeofResource(hModule, hResInfo);
if ( dwSize )
{
hResData = LoadResource(hModule, hResInfo);

对flag的检验是运行时加载运行的,导致我们无法搜索到相关字符串
通过调试,加载的地方是

1
2
3
4
5
6
7
8
re4.exe:00E101D0 db 2Eh
re4.exe:00E101D1 db 47h ; G
re4.exe:00E101D2 db 4Bh ; K
re4.exe:00E101D3 db 52h ; R
re4.exe:00E101D4 db 0
re4.exe:00E101D5 db 0
re4.exe:00E101D6 db 23h ; #
re4.exe:00E101D7 db 44h ; D

大小为 0x000EBE00 下面的代码可以通过反编译,似乎是跳转表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
re4.exe:00E101DA
re4.exe:00E101DA sub_E101DA proc near
re4.exe:00E101DA jmp sub_E12EA0
re4.exe:00E101DA sub_E101DA endp
re4.exe:00E101DA
re4.exe:00E101DF
re4.exe:00E101DF ; =============== S U B R O U T I N E =======================================
re4.exe:00E101DF
re4.exe:00E101DF ; Attributes: thunk
re4.exe:00E101DF
re4.exe:00E101DF sub_E101DF proc near
re4.exe:00E101DF jmp sub_E1466F
re4.exe:00E101DF sub_E101DF endp
re4.exe:00E101DF
re4.exe:00E101DF ; ---------------------------------------------------------------------------
re4.exe:00E101E4 db 0E9h
re4.exe:00E101E5 db 0F6h
re4.exe:00E101E6 db 43h ; C

把这一部分dump出来,用IDA打开,可以看到字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
seg000:000BC141 aOutPlzInputFla db 'out]: PLZ Input FLag ',0Ah,0
seg000:000BC158 dd 0
seg000:000BC15C db 5Bh ; [
seg000:000BC15D aIn db 'in ]: ',0
seg000:000BC164 align 8
seg000:000BC168 db 25h ; %
seg000:000BC169 db 73h ; s
seg000:000BC16A align 4
seg000:000BC16C db 5Bh ; [
seg000:000BC16D aOutLenError db 'out]: len error',0Ah,0
seg000:000BC17E align 10h
seg000:000BC180 dd 0
seg000:000BC184 db 5Bh ; [
seg000:000BC185 aOutRightFlag db 'out]: RIGHT FLAG',0Ah,0
seg000:000BC197 align 4
seg000:000BC198 dd 0
seg000:000BC19C db 5Bh ; [
seg000:000BC19D aOutWrongFlag db 'out]: WRONG FLAG',0Ah,0

根据这里面PLZ Input Flag的偏移地址,结合起始地址,计算出此时字符串的地址

1
2
Python>0xE101D0 + 0xBC141
0xecc311

在此处下一个硬件断点,F9,程序运行到了这里

1
2
3
4
5
if ( _bittest(&dword_E0B368, 1u) )
{
qmemcpy(a1, Src, Size);
return a1;
}

这里对资源赋值到了 0x01010000处,在这里重新找到字符串的地址,下访问断点

1
2
Python>0x1010000 + 0xBC141
0x10cc141

通过断点的命中,发现下面这段代码其实在对资源进行处理

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
int __cdecl sub_D45870(unsigned __int8 **a1, int a2, int a3)
{
__CheckForDebuggerJustMyCode(&unk_E0E02A);
if ( !a1 || !a1[3] || !a2 )
return 0;
a1[6] = 0;
if ( !sub_D3FED7((int)a1, a2) )
return 0;
if ( !sub_D3EB77((int)a1, a2) )
return 0;
if ( !sub_D3F478((int)a1) )
goto LABEL_17;
if ( !sub_D41057(a1) )
goto LABEL_17;
a1[2] = (unsigned __int8 *)sub_D40D69(0, *a1, (int)a1[1]);
if ( !sub_D4016B(a1) )
goto LABEL_17;
if ( !sub_D409DB(a1) )
return 0;
if ( !a3 || sub_D3ED5C((int)a1, 1) )
return 1;
LABEL_17:
sub_D3EFD2(a1);
return 0;
}

sub_D3ED5C函数中的

1
2
3
4
5
6
7
8
9
10
11
.text:00D44B9D
.text:00D44B9D loc_D44B9D:
.text:00D44B9D push 0
.text:00D44B9F mov eax, [ebp+arg_4]
.text:00D44BA2 push eax
.text:00D44BA3 mov ecx, [ebp+arg_0]
.text:00D44BA6 mov edx, [ecx]
.text:00D44BA8 push edx
.text:00D44BA9 call [ebp+var_8]
.text:00D44BAC add esp, 0Ch
.text:00D44BAF mov eax, 1

代码块call了资源中的内容,调试这部分,可以调到关键函数

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
int sub_1005F820()
{
char v1; // [esp+0h] [ebp-D8h]
char v2; // [esp+0h] [ebp-D8h]
char v3; // [esp+0h] [ebp-D8h]
_DWORD *v4; // [esp+D0h] [ebp-8h]

sub_1005B16F(byte_1014000F);
((void (__stdcall *)(_DWORD, int, int, int))kernel32_VirtualAlloc)(0, 65548, 12288, 4);
v4 = (_DWORD *)sub_1005A260();
if ( !v4 )
return -1;
v4[1] = 0x10000;
*v4 = 0;
v4[2] = v4 + 3;
sub_10059572(v4[2], 0, v4[1]);
print((int)"[out]: PLZ Input FLag \n", v1);
print((int)"[in ]: ", v2);
scanf(&aS_1, v4[2]);
*v4 = sub_1005B5BB(v4[2]);
if ( *v4 == 41 ) // flag长度为41
{
*v4 = 48;
sub_1005A242(v4); // 加密
if ( sub_10058AA0(v4[2], &unk_1013C008, 48) )// 对比
print((int)"[out]: WRONG FLAG\n", v3);
else
print((int)"[out]: RIGHT FLAG\n", v3);
((void (__stdcall *)(_DWORD *, _DWORD, int))kernel32_VirtualFree)(v4, 0, 49152);
sub_1005A260();
return 0;
}
else
{
print((int)"[out]: len error\n", v3);
((void (__stdcall *)(_DWORD *, _DWORD, int))kernel32_VirtualFree)(v4, 0, 49152);
sub_1005A260();
return -1;
}
}

虽然调到了,但是还是一步一步调过来的…

根据调试情况,写出正向脚本和逆向脚本

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
import struct
def shld(a1, a2):
return ((a1 << 1) | (a2 >> 31)) & 0xffffffff
def shl(a1):
return (a1 << 1) & 0xffffffff

def shr(a1):
return (a1 >> 1) & 0xffffffff

def en(buf):
for i in range(0, 12, 2):
ta = buf[i]
tb = buf[i + 1]
for k in range(64):
if (tb & 0x80000000 == 0x80000000):
tb = shld(tb, ta)
ta = shl(ta)
ta = (0x54AA4A9 ^ ta) & 0xffffffff
else:
tb = shld(tb, ta)
ta = shl(ta)
buf[i] = ta
buf[i + 1] = tb

def de(buf):
for i in range(0, 12, 2):
ta = buf[i]
tb = buf[i+1]
for k in range(64):
# 判断是否异或: 左移之后最后一位必然是0,如果和9进行异或,那么最后一位必然是1,以此为标准
if (ta & 1):
ta = (0x54AA4A9 ^ ta) & 0xffffffff
ta = shr(ta) | ((tb & 1) << 31)
# 在此情况下,tb最高位必然为1
tb = shr(tb) | (1 << 31)
else:
ta = shr(ta) | ((tb & 1) << 31)
# 在此情况下 tb 最高位必然为 0
tb = shr(tb)
buf[i] = ta
buf[i + 1] = tb

def main():
cmp = [0x2976B84D, 0x599EA9F5, 0xC4B15655, 0x302C212F, 0x177879B3, 0xDBF7EDA8, 0xDBF053E1, 0x5E5103E9, 0xDF00C109, 0xC1FC96F0, 0x9562E6B5, 0x00000001]
de(cmp)
for i in range(len(cmp)):
print(hex(cmp[i]), end=', ')
print()
flag = b''.join(struct.pack("<I", i) for i in cmp)
print(flag)


if __name__ == '__main__':
main()

得到flag

你见过蓝色的小鲸鱼

0x0 初探

32位PE

在readme中,给了User:UzBtZTBuZV9EMGcz

0x1 分析

使用IDA打开,一时还找不到关键函数在哪,由于该程序使用了Win32API,可以尝试从这里下手,打MessageBoxA的断点

成功断下来,回溯堆栈

1
0019F398  00457018  sub_456EE0+138

这个函数里面我们看到了正确或错误的相关字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int __thiscall sub_456EE0(void *this)
{
char v2[28]; // [esp+D0h] [ebp-54h] BYREF
CHAR Text[20]; // [esp+ECh] [ebp-38h] BYREF
CHAR Caption[24]; // [esp+100h] [ebp-24h] BYREF
void *v5; // [esp+118h] [ebp-Ch]

v5 = this;
__CheckForDebuggerJustMyCode(&unk_52102F);
strcpy(Caption, "tip");
strcpy(Text, "You Get It!");
strcpy(v2, "Wrong user/passwd");
if ( *((_DWORD *)v5 + 2) != *((_DWORD *)v5 + 3)
|| j__memcmp(*(const void **)v5, *((const void **)v5 + 1), *((_DWORD *)v5 + 3)) )
{
return MessageBoxA(0, v2, Caption, 0);
}
else
{
return MessageBoxA(0, Text, Caption, 0);
}
}

该函数只有一个交叉引用,引用的函数即为主要处理函数

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
int __cdecl Func(int argc, const char **argv, const char **envp)
{
int result; // eax
void *v4; // [esp+10h] [ebp-154h]
void *v5; // [esp+24h] [ebp-140h]
CHAR *Passwd; // [esp+114h] [ebp-50h]
CHAR *Name; // [esp+120h] [ebp-44h]
HWND DlgItem; // [esp+12Ch] [ebp-38h]
HWND hWnd; // [esp+138h] [ebp-2Ch]
int PasswdLen; // [esp+144h] [ebp-20h]
int NameLen; // [esp+150h] [ebp-14h]

__CheckForDebuggerJustMyCode(&unk_52105E);
hWnd = GetDlgItem((HWND)argc, 1003);
DlgItem = GetDlgItem((HWND)argc, 1004);
NameLen = GetWindowTextLengthA(hWnd);
PasswdLen = GetWindowTextLengthA(DlgItem);
Name = (CHAR *)j__malloc(__CFADD__(NameLen, 16) ? -1 : NameLen + 16);
result = (int)j__malloc(__CFADD__(PasswdLen, 16) ? -1 : PasswdLen + 16);
Passwd = (CHAR *)result;
if ( Name && result )
{
GetWindowTextA(hWnd, Name, NameLen + 16);
GetWindowTextA(DlgItem, Passwd, PasswdLen + 16);
v5 = operator new(0x10u);
if ( v5 )
{
sub_451B43(0x10u);
v4 = (void *)sub_450CE3(v5);
}
else
{
v4 = 0;
}
sub_44FC2B(&unk_51D38C, 0x10u);
sub_45126F(Name, NameLen, (int)Passwd, PasswdLen);
sub_450199(v4); // right/wrong
j__free(Name);
j__free(Passwd);
result = (int)v4;
if ( v4 )
return sub_44F77B(1);
}
return result;
}

在加密函数中发现了算法关键字

1
2
3
4
5
6
7
8
_DWORD *__thiscall sub_456C20(_DWORD *this, void *Src, size_t a3)
{
__CheckForDebuggerJustMyCode(&unk_52102D);
sub_45176A(Src, a3);
*this = &Bl0wFish::`vftable';
this[9] = 1;
return this;
}

故去Github搜索BlowFish,发现和加密过程中用到的数组数据一致,猜测是未进行魔改算法,通过对比算法,猜测是以Nmae为密钥,对Password进行加密,

0x2 求解

使用python进行求解

1
2
3
4
5
6
7
8
9
10
11
from Crypto.Cipher import Blowfish  

# Blowfish
key = bytes([0x55, 0x7A, 0x42, 0x74, 0x5A, 0x54, 0x42, 0x75, 0x5A, 0x56, 0x39, 0x45, 0x4D, 0x47, 0x63, 0x7A])
enc_b = bytes([0x11, 0xA5, 0x1F, 0x04, 0x95, 0x50, 0xE2, 0x50, 0x8F, 0x17, 0xE1, 0x6C, 0xF1, 0x63, 0x2B, 0x47])

cipher = Blowfish.new(key, Blowfish.MODE_ECB)
dec = cipher.decrypt(enc_b)
for i in dec:
print(chr(i), end='')
print()

得到密码为

1
QHRoZWJsdWVmMXNo

检验一下

mobilego

0x0 初探

这是一个Android逆向,通过jeb查看,主要逻辑主要是go编写的so文件中

1
2
3
4
5
6
7
public /* synthetic */ void m44lambda$onCreate$0$comexamplemobilegoMainActivity(View v) {
if (Game.checkflag(this.editText.getText().toString()).equals(getResources().getString(R.string.cmp))) {
Toast.makeText(this, "yes your flag is right", 0).show();
} else {
Toast.makeText(this, "No No No", 0).show();
}
}

0x1 动态调试

使用jadx直接进行调试 在check_flag调用处打断点

1
2
3
4
5
6
000003c0: 5430 0200               0000: iget-object         v0, p0, Lcom/example/mobilego/MainActivity;->editText:Landroid/widget/EditText; # field@0002
000003c4: 6e10 0200 0000 0002: invoke-virtual {v0}, Landroid/widget/EditText;->getText()Landroid/text/Editable; # method@0002
000003ca: 0c00 0005: move-result-object v0
000003cc: 6e10 1100 0000 0006: invoke-virtual {v0}, Ljava/lang/Object;->toString()Ljava/lang/String; # method@0011
000003d2: 0c00 0009: move-result-object v0
000003d4: 7110 0f00 0000 000a: invoke-static {v0}, Lgame/Game;->checkflag(Ljava/lang/String;)Ljava/lang/String; # method@000f

断下来之后,单步调试,在变量窗口可以看到我们的输入经过加密后的结果

1
1k38a274jf56hlcbd09eg

还有要对比的字符串

1
49021}5f919038b440139g74b7Dc88330e5d{6

可以看到,其实就是打乱了次序的,我们再构建一次输入,检验一下打乱的次序是不是固定的

1
1K38A274JF56HLCBD09EG

局部换成大写之后,依然是一致的,现在我们构建一个没有重复的输入

1
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijkl

经过加密后

1
ViLdOlJTePKcMYZFQBSHUCXWIaAGkfbDghjNER

写出解密脚本

1
2
3
4
5
6
7
ta = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijkl'
tb = 'ViLdOlJTePKcMYZFQBSHUCXWIaAGkfbDghjNER'
order = [tb.index(i) for i in ta]
enflag = '49021}5f919038b440139g74b7Dc88330e5d{6'
flag = [enflag[i] for i in order]
flag = ''.join(flag)
print(flag)#D0g3{4c3b5903d11461f94478b7302980e958}

0x2 静态分析

使用IDA分析so文件,打开后发现没有去除符号,直接搜索
check,找到mobile_go_Checkflag

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
// mobile_go.Checkflag
char *__usercall mobile_go_Checkflag@<X0>(unsigned __int8 *a1@<X0>, unsigned __int64 a2@<X1>)
{
__int64 v2; // x28
__int64 *v3; // x29
__int64 v4; // x30
_QWORD *v5; // x0
_QWORD *v6; // x1
unsigned __int8 *buffer; // x0
__int64 Now; // x1
__int64 numA; // x2
unsigned __int64 RandB; // x0
unsigned __int8 *temp; // x3
unsigned __int8 ta; // w4
__int64 *v14; // [xsp+0h] [xbp-C8h] BYREF
__int64 v15; // [xsp+8h] [xbp-C0h]
unsigned __int8 *v16; // [xsp+10h] [xbp-B8h]
unsigned __int64 v17; // [xsp+18h] [xbp-B0h]
int64 SeedNum; // [xsp+40h] [xbp-88h]
unsigned __int64 RandA; // [xsp+48h] [xbp-80h]
__int64 NumB; // [xsp+50h] [xbp-78h]
unsigned __int64 length; // [xsp+58h] [xbp-70h]
unsigned __int8 *buf; // [xsp+80h] [xbp-48h]
_QWORD *object[8]; // [xsp+88h] [xbp-40h] BYREF

while ( (unsigned __int64)object <= *(_QWORD *)(v2 + 16) )
{
LABEL_10:
v16 = a1;
v17 = a2;
runtime_morestack_noctxt(); // 处理栈的扩展
a1 = v16;
a2 = v17;
}
v15 = v4;
v14 = v3;
v3 = (__int64 *)&v14;
SeedNum = Num2023;
object[0] = (_QWORD *)runtime_newobject(); // 用于分配新的内存以创建一个对象
math_rand__ptr_rngSource_Seed(object[0], SeedNum);// 种子
v5 = runtime_assertI2I2((__int64)&RTYPE_rand_Source64, off_EE580, (__int64)object[0], v15, v16, v17);
object[5] = 0LL;
object[6] = 0LL;
object[1] = off_EE580;
object[2] = object[0];
object[3] = v5;
object[4] = v6;
buffer = (unsigned __int8 *)runtime_stringtoslicebyte();
buf = buffer;
length = Now;
numA = 0LL;
while ( Now > numA )
{
NumB = numA;
RandA = math_rand__ptr_Rand_Intn(Now); // 生成随机数
RandB = math_rand__ptr_Rand_Intn(length);
Now = length;
if ( RandA >= length )
goto LABEL_9;
temp = buf;
ta = buf[RandA];
if ( RandB >= length )
{
runtime_panicIndex();
LABEL_9:
a1 = (unsigned __int8 *)runtime_panicIndex();
goto LABEL_10;
}
buf[RandA] = buf[RandB];
temp[RandB] = ta;
numA = NumB + 1;
buffer = temp;
}
return runtime_slicebytetostring(0LL, buffer, Now, v15, v16, v17);
}

其中这个种子是在mobile_go_init_0 中进行定义的

1
2
3
4
5
6
7
8
9
// mobile_go.init.0
__int64 __usercall mobile_go_init_0@<X0>()
{
__int64 result; // x0

result = 2023LL;
Num2023 = 2023LL;
return result;
}

大概意思就是通过生成伪随机数来达到打乱顺序的作用,而这个伪随机数的种子就是
2023 写一个go程序打印出这个过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"math/rand"
)

func main() {
// 设置随机数种子为2023
rand.Seed(2023)

// 循环38次,每次生成两个随机数并打印
for i := 0; i < 38; i++ {
randomNumber1 := rand.Intn(38) // 生成0到38的随机整数
randomNumber2 := rand.Intn(38) // 生成0到38的随机整数

fmt.Printf("[%d,%d],", randomNumber1, randomNumber2)
}
}

得到

1
[11,14],[15,37],[24,18],[8,30],[6,9],[30,3],[29,9],[4,13],[13,24],[37,1],[28,28],[3,1],[23,22],[21,26],[7,19],[1,34],[37,17],[27,29],[31,30],[14,2],[35,34],[4,27],[9,3],[3,24],[30,29],[3,27],[14,25],[26,0],[4,28],[5,15],[9,9],[13,18],[24,3],[35,24],[36,27],[25,21],[11,4],[27,28],

然后再JEB中找到对比的字符串

1
2
3
4
5
public static final class string {
public static int app_name = 0x7F0F001C; // string:app_name "mobilego"
public static int cmp = 0x7F0F0028; // string:cmp "49021}5f919038b440139g74b7Dc88330e5d{6"

}

进行还原

1
2
3
4
5
6
7
order = [[11,14],[15,37],[24,18],[8,30],[6,9],[30,3],[29,9],[4,13],[13,24],[37,1],[28,28],[3,1],[23,22],[21,26],[7,19],[1,34],[37,17],[27,29],[31,30],[14,2],[35,34],[4,27],[9,3],[3,24],[30,29],[3,27],[14,25],[26,0],[4,28],[5,15],[9,9],[13,18],[24,3],[35,24],[36,27],[25,21],[11,4],[27,28]]
cmp = bytearray(b'49021}5f919038b440139g74b7Dc88330e5d{6')
for i in range(len(cmp)):
ta = cmp[order[len(cmp)-1-i][0]]
cmp[order[len(cmp)-1-i][0]] = cmp[order[len(cmp)-1-i][1]]
cmp[order[len(cmp)-1-i][1]] = ta
print(cmp) # bytearray(b'D0g3{4c3b5903d11461f94478b7302980e958}')

2023安洵杯第六届网络安全挑战赛 Re 部分WriteUp
https://l3isu7e.github.io/2023/12/30/AnXun2023/
作者
L3iSu7e
发布于
2023年12月30日
更新于
2024年1月4日
许可协议