x2658y's Blog

杂七杂八的记事本

Linux可执行文件, 直接拖进IDA, F5查看伪代码, 但是有些复杂抓不住关键, 打开String窗口改由字符串入手.

image-20220128151231261

跟到字符串所在位置, 交叉引用确定程序语句再F5看伪代码, 直接找到关键点

image-20220128151403433

分析程序

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
__int64 sub_4009C6()
{
__int64 result; // rax
int i; // [rsp+Ch] [rbp-114h]
__int64 v2; // [rsp+10h] [rbp-110h]
__int64 v3; // [rsp+18h] [rbp-108h]
__int64 v4; // [rsp+20h] [rbp-100h]
__int64 v5; // [rsp+28h] [rbp-F8h]
__int64 v6; // [rsp+30h] [rbp-F0h]
__int64 v7; // [rsp+38h] [rbp-E8h]
__int64 v8; // [rsp+40h] [rbp-E0h]
__int64 v9; // [rsp+48h] [rbp-D8h]
__int64 v10; // [rsp+50h] [rbp-D0h]
__int64 v11; // [rsp+58h] [rbp-C8h]
char v12[13]; // [rsp+60h] [rbp-C0h] BYREF
char v13[4]; // [rsp+6Dh] [rbp-B3h] BYREF
char v14[19]; // [rsp+71h] [rbp-AFh] BYREF
char v15[32]; // [rsp+90h] [rbp-90h] BYREF
int v16; // [rsp+B0h] [rbp-70h]
char v17; // [rsp+B4h] [rbp-6Ch]
char v18[72]; // [rsp+C0h] [rbp-60h] BYREF
unsigned __int64 v19; // [rsp+108h] [rbp-18h]

v19 = __readfsqword(0x28u);
qmemcpy(v12, "Iodl>Qnb(ocy", 12);
v12[12] = 127;
qmemcpy(v13, "y.i", 3);
v13[3] = 127;
qmemcpy(v14, "d`3w}wek9{iy=~yL@EC", sizeof(v14));
//上面这几行代码将v12~v14填充为"Iodl>Qnb(ocy\x7fy.i\x7fd`3w}wek9{iy=~yL@EC"
//这一长度为36的字符, 与后续v15的比较做准备
memset(v15, 0, sizeof(v15));
v16 = 0;
v17 = 0;
sub_4406E0(0LL, v15, 37LL);
//根据经验,sub_4406E0()应该是某种输入函数
//感觉类似scanf_s(),后面的37应该是限制长度
v17 = 0;
if ( sub_424BA0(v15) == 36 )
//可见v15的长度应该为36,但是v15数组长度只有32,这说明v16的值被输入函数越界覆盖
{
for ( i = 0; i < (unsigned __int64)sub_424BA0(v15); ++i )
//从这里可以猜出sub_424BA0()就是strlen()
{
if ( (unsigned __int8)(v15[i] ^ i) != v12[i] )
//将输入字符串与下标异或后与v12对比.已知v12很容易推出v15的值
{
result = 4294967294LL;
goto LABEL_13;
}
}
sub_410CC0("continue!");
memset(v18, 0, 0x40uLL);
v18[64] = 0;
sub_4406E0(0LL, v18, 64LL);
v18[39] = 0;
if ( sub_424BA0(v18) == 39 )
//v18长度应为39
{
v2 = sub_400E44(v18);
v3 = sub_400E44(v2);
v4 = sub_400E44(v3);
v5 = sub_400E44(v4);
v6 = sub_400E44(v5);
v7 = sub_400E44(v6);
v8 = sub_400E44(v7);
v9 = sub_400E44(v8);
v10 = sub_400E44(v9);
v11 = sub_400E44(v10);
//进到sub_400E44()里可以看到它用了base64码表,应该是base64编码函数
//上面对v18进行了10次base64编码
if ( !(unsigned int)sub_400360(v11, off_6CC090) )
//sub_400360()应该是strcmp()
//对比编码10次后的值是否与off_6CC090这个字符串相等
//将off_6CC090解码10次就可以得到v18
{
sub_410CC0("You found me!!!");
sub_410CC0("bye bye~");
}
result = 0LL;
}
else
{
result = 4294967293LL;
}
}
else
{
result = 0xFFFFFFFFLL;
}
LABEL_13:
if ( __readfsqword(0x28u) != v19 )
sub_444020();
return result;
}

得到v15

1
2
3
4
5
6
7
string = "Iodl>Qnb(ocy\x7fy.i\x7fd`3w}wek9{iy=~yL@EC"

for i in range(0, len(string)):
print(chr(i ^ ord(string[i])), end="")

#Info:The first four chars are `flag`
#一个提示, 但是不知道它指的前4个字符是谁的前4个

得到v18

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import base64

string = """
Vm0wd2VHUXhTWGhpUm1SWVYwZDRWVll3Wkc5WFJsbDNXa1pPVlUxV2NIcFhhMk0\
xVmpKS1NHVkdXbFpOYmtKVVZtcEtTMUl5VGtsaVJtUk9ZV3hhZVZadGVHdFRNVT\
VYVW01T2FGSnRVbGhhVjNoaFZWWmtWMXBFVWxSTmJFcElWbTAxVDJGV1NuTlhia\
0pXWWxob1dGUnJXbXRXTVZaeVdrWm9hVlpyV1hwV1IzaGhXVmRHVjFOdVVsWmlh\
MHBZV1ZSR1lWZEdVbFZTYlhSWFRWWndNRlZ0TVc5VWJGcFZWbXR3VjJKSFVYZFd\
ha1pXWlZaT2NtRkhhRk5pVjJoWVYxZDBhMVV3TlhOalJscFlZbGhTY1ZsclduZG\
xiR1J5VmxSR1ZXSlZjRWhaTUZKaFZqSktWVkZZYUZkV1JWcFlWV3BHYTFkWFRrZ\
FRiV3hvVFVoQ1dsWXhaRFJpTWtsM1RVaG9hbEpYYUhOVmJUVkRZekZhY1ZKcmRG\
Tk5Wa3A2VjJ0U1ExWlhTbFpqUldoYVRVWndkbFpxUmtwbGJVWklZVVprYUdFeGN\
HOVhXSEJIWkRGS2RGSnJhR2hTYXpWdlZGVm9RMlJzV25STldHUlZUVlpXTlZadE\
5VOVdiVXBJVld4c1dtSllUWGhXTUZwell6RmFkRkpzVWxOaVNFSktWa1phVTFFe\
FduUlRhMlJxVWxad1YxWnRlRXRXTVZaSFVsUnNVVlZVTURrPQ==
"""

for i in range(0, 10):
string = base64.b64decode(string)
print(string.decode())

#https://bbs.pediy.com/thread-254172.htm
#怎么是个网站????我的flag呢?!

打开网站, 很好, 上当了...

image-20220128165228452
搞到这里了却发现是个假的, 好尴尬😅

看了别人的WP才知道答案原来从我眼前溜过了

off_6CC090这个字符串附近有点东西

image-20220128230344597
image-20220128230404288

交叉引用找过去

image-20220128230447817

分析一下

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
unsigned __int64 sub_400D35()
{
unsigned __int64 result; // rax
unsigned int v1; // [rsp+Ch] [rbp-24h]
int i; // [rsp+10h] [rbp-20h]
int j; // [rsp+14h] [rbp-1Ch]
unsigned int v4; // [rsp+24h] [rbp-Ch]
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
v1 = sub_43FD20(0LL) - qword_6CEE38;
for ( i = 0; i <= 1233; ++i )
{
sub_40F790(v1);
sub_40FE60();
sub_40FE60();
v1 = sub_40FE60() ^ 0x98765432;
}
//上面这一坨不是很懂
v4 = v1;
if ( ((unsigned __int8)v1 ^ byte_6CC0A0[0]) == 102 && (HIBYTE(v4) ^ (unsigned __int8)byte_6CC0A3) == 103 )
//这个判断无关紧要,直接看下面
{
for ( j = 0; j <= 24; ++j )
sub_410E90((unsigned __int8)(byte_6CC0A0[j] ^ *((_BYTE *)&v4 + j % 4)));
//关键点应该就在这里了,byte_6CC0A0这个字符串的内容已知,异或的密钥只有4位,
//结合前面给出的提示,前面4位是"flag",我们就可以得出密钥,从而解密出flag
}
result = __readfsqword(0x28u) ^ v5;
if ( result )
sub_444020();
return result;
}

解密代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <cstdio>

using namespace std;

int main()
{
char ciphertext[25] =
{
64, 53, 32, 86, 93, 24, 34, 69, 23, 47,
36, 110, 98, 60, 39, 84, 72, 108, 36, 110,
114, 60, 50, 69, 91
};
char hint[4] = { 'f','l','a','g' };
char key[4] = { 0 };
for (int i = 0; i < 4; i++)
for (int j = -128; j <= 127; j++)
if ((ciphertext[i] ^ j) == hint[i])
key[i] = j;
for (int i = 0; i < 25; i++)
putchar(ciphertext[i] ^ key[i % 4]);
}
//flag{Act1ve_Defen5e_Test}

无壳, 拖进IDA, F5查看伪代码

可以看到两个关键函数

image-20220126164811383

两个函数结构区别不大, 里面调用了很多WidnwosCryptoAPI

这里以sub_40100A()为例, 通过它间接调用sub_401230()

查询微软文档, CryptCreateHash()函数的第二个参数指明算法类型

image-20220126165402916

查表可以得知这个函数是进行SHA-1哈希

微软文档: MSDN

image-20220126170805262

同理可以得知sub_401019()是进行MD5哈希的函数

通过这一段可以得知第一段密码长度是6位, 且为6位数字, 可以直接穷举

1
2
3
4
5
6
7
8
if ( strlen(Destination) != 6 )
{
printf("Must be 6 characters!\n");
ExitProcess(0);
}
v7 = atoi(Destination);
if ( v7 < 100000 )
ExitProcess(0);

代码如下

1
2
3
4
5
6
7
8
9
import hashlib

for i in range(100001,1000000):
hasher = hashlib.sha1()
hasher.update((str(i)+"@DBApp").encode("utf-8"))
if hasher.hexdigest().upper() == "6E32D0943418C2C33385BC35A1470250DD8923A9":
print(i)
break
#123321

所以第一段密码就是123321

第二段密码, 同样长度为6位, 但是第二段密码并没有限制是纯数字.

第二次验证的算法改成了MD5, 进行哈希的内容就是第二段密码+第一段密码+@DBApp组成的18字节字符.

方法一: 真·暴力破解法

所有ASCII可打印字符从32~126共95个, 第二段密码长6位, 那么可能性就有\(95^6\)种, 大约7350亿种可能, 写脚本穷举好像不太可能...但是, 显卡进行MD5哈希的速度可是相当快的, CPU则要慢得多多多多多...

放张hashcatbenchmark感受一下, 这还只是个3050Ti, 换做高端一点的那更是快得多.image-20220126193440960

换算出来差不多就是164亿次哈希每秒, 照这个速度7350亿种可能也不是很多.

确定可行性之后, 就可以用hashcat来进行穷举

hashcat: Github

N卡安装好CUDA套件之后, 在hashcat目录下打开命令行执行

1
hashcat -a 3 27019e688a4e62a649fd99cadaafdb4e -m 0 ?a?a?a?a?a?a123321@DBApp

-a 3 指定攻击方式, 掩码攻击

-m 0 指定哈希类型, MD5

?a?a?a?a?a?a123321@DBApp 是掩码, 前面6位代表所有可打印ASCII字符

实际的哈希速度要慢一些, 但依然是极快的, 不到半分钟就完成了工作

image-20220126195352794

得出第二段密码是~!3a@0

方法二: 常规方法

老实说我是上来就考虑用hashcat尝试穷举的, 没想到还有方法, 看了别人的WP才知道.

关键点在这个函数image-20220126203837733

跟进去找到sub_4014D0()这个函数, WindowsAPI有点多, 结合微软的文档分析其用意

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
char __cdecl sub_4014D0(LPCSTR lpString)
{
LPCVOID lpBuffer; // [esp+50h] [ebp-1Ch]
DWORD NumberOfBytesWritten; // [esp+58h] [ebp-14h] BYREF
DWORD nNumberOfBytesToWrite; // [esp+5Ch] [ebp-10h]
HGLOBAL hResData; // [esp+60h] [ebp-Ch]
HRSRC hResInfo; // [esp+64h] [ebp-8h]
HANDLE hFile; // [esp+68h] [ebp-4h]

hFile = 0;
hResData = 0;
nNumberOfBytesToWrite = 0;
NumberOfBytesWritten = 0;
hResInfo = FindResourceA(0, (LPCSTR)0x65, "AAA");
//从载入为当前进程的PE文件,找到一个名为"e"(0x65)且资源类型为"AAA"的资源
if ( !hResInfo )
return 0;
nNumberOfBytesToWrite = SizeofResource(0, hResInfo);
//计算资源大小
hResData = LoadResource(0, hResInfo);
//载入资源
if ( !hResData )
return 0;
lpBuffer = LockResource(hResData);
//锁定资源,返回资源在内存中的指针
sub_401005(lpString, (int)lpBuffer, nNumberOfBytesToWrite);
//下一个关注点,此函数处理资源,对其进行异或解密
hFile = CreateFileA("dbapp.rtf", 0x10000000u, 0, 0, 2u, 0x80u, 0);
//创建名为"dbapp.rtf"的文件,储存解密后的rtf格式flag文件
if ( hFile == (HANDLE)-1 )
return 0;
if ( !WriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, &NumberOfBytesWritten, 0) )
//将解密后的资源写入到文件中
return 0;
CloseHandle(hFile);
return 1;
}

sub_401005()调用了sub_401420(), 接着分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned int __cdecl sub_401420(LPCSTR lpString, int a2, int a3)
{
unsigned int result; // eax
unsigned int i; // [esp+4Ch] [ebp-Ch]
unsigned int v5; // [esp+54h] [ebp-4h]

v5 = lstrlenA(lpString);
for ( i = 0; ; ++i )
{
result = i;
if ( i >= a3 )
break;
*(_BYTE *)(i + a2) ^= lpString[i % v5];
//循环异或
}
return result;
}

这个函数将资源文件每个字节与第二段密码+第一段密码+@DBApp组成密钥进行循环异或从而解密出flag文件. 突破点就在flag文件的前6个字节上, 要知道, RTF格式不像txt这种文本格式, 它是有文件头的, 而文件头往往是固定的! 既然flag是个RTF格式的文件, 那么它一定符合RTF的文件头规范. 我们可以通过这已知的6个字节, 倒推出密钥前6字节, 即第二段密码.

找个RTF文件看看前面6字节

image-20220126211735455

前6字节是0x7B, 0x5C, 0x72, 0x74, 0x66, 0x31

写出解密代码

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

using namespace std;

int main()
{
HMODULE hModule = LoadLibraryA("crack_rtf.exe");
//将程序载入获取模块句柄
if (!hModule)
return -1;
HRSRC hResInfo = FindResourceA(hModule, (LPCSTR)0x65, "AAA");
if (!hResInfo)
return -1;
HGLOBAL hResData = LoadResource(hModule, hResInfo);
if (!hResData)
return -1;
BYTE* lpRes = (BYTE*)LockResource(hResData);
if (!lpRes)
return -1;
BYTE lpHeaderByte[6] = { 0x7B, 0x5C, 0x72, 0x74, 0x66, 0x31 };
//RTF文件头前6字节
for (DWORD i = 0; i < 6; i++)
for (BYTE c = 32; c < 127; c++) //穷举所有ASCII可打印字符
if ((lpRes[i] ^ c) == lpHeaderByte[i]) //验证字符
putchar(c);
}
//~!3a@0

最终, flagflag{N0_M0re_Free_Bugs}

无壳, 拖进IDA, F5查看伪代码, 主函数里面主要就func()函数, 跟到func()

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
int func()
{
int result; // eax
int v1[4]; // [esp+14h] [ebp-44h]
unsigned __int8 v2; // [esp+24h] [ebp-34h] BYREF
unsigned __int8 v3; // [esp+25h] [ebp-33h]
unsigned __int8 v4; // [esp+26h] [ebp-32h]
unsigned __int8 v5; // [esp+27h] [ebp-31h]
unsigned __int8 v6; // [esp+28h] [ebp-30h]
int v7; // [esp+29h] [ebp-2Fh]
int v8; // [esp+2Dh] [ebp-2Bh]
int v9; // [esp+31h] [ebp-27h]
int v10; // [esp+35h] [ebp-23h]
unsigned __int8 v11; // [esp+39h] [ebp-1Fh]
char v12[29]; // [esp+3Bh] [ebp-1Dh] BYREF

strcpy(v12, "Qsw3sj_lz4_Ujw@l");
printf("Please input:");
scanf("%s", &v2);
result = v2;
if ( v2 == 'A' )
{
result = v3;
if ( v3 == 'C' )
{
result = v4;
if ( v4 == 'T' )
{
result = v5;
if ( v5 == 'F' )
{
result = v6;
if ( v6 == '{' )
{
result = v11;
if ( v11 == '}' )
{
v1[0] = v7;
v1[1] = v8;
v1[2] = v9;
v1[3] = v10;
*(_DWORD *)&v12[17] = 0;
while ( *(int *)&v12[17] <= 15 )
{
if ( *((char *)v1 + *(_DWORD *)&v12[17]) > 64 && *((char *)v1 + *(_DWORD *)&v12[17]) <= 90 )
*((_BYTE *)v1 + *(_DWORD *)&v12[17]) = (*((char *)v1 + *(_DWORD *)&v12[17]) - 51) % 26 + 65;
if ( *((char *)v1 + *(_DWORD *)&v12[17]) > 96 && *((char *)v1 + *(_DWORD *)&v12[17]) <= 122 )
*((_BYTE *)v1 + *(_DWORD *)&v12[17]) = (*((char *)v1 + *(_DWORD *)&v12[17]) - 79) % 26 + 97;
++*(_DWORD *)&v12[17];
}
*(_DWORD *)&v12[17] = 0;
while ( *(int *)&v12[17] <= 15 )
{
result = (unsigned __int8)v12[*(_DWORD *)&v12[17]];
if ( *((_BYTE *)v1 + *(_DWORD *)&v12[17]) != (_BYTE)result )
return result;
++*(_DWORD *)&v12[17];
}
result = printf("You are correct!");
}
}
}
}
}
}
return result;
}

解法很常规, 分析算法, 写出解密程序

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

using namespace std;

int main()
{

char ciphertext[32] = "Qsw3sj_lz4_Ujw@l";
char plaintext[32] = { 0 };
for (int i = 0; i < 16; i++)
{
for (char c = 32; c < 127; c++) //逐位穷举所有ASCII可打印字符
{
char temp = c;
if (c > 64 && c <= 90)
temp = (c - 51) % 26 + 65;
else if (c > 96 && c <= 122)
temp = (c - 79) % 26 + 97;
if (temp == ciphertext[i])
plaintext[i] = c;
}
}
printf("ACTF{%s}", plaintext);
}
//ACTF{Cae3ar_th4_Gre@t}
0%