Windows客户端测试

前言

有一个windows客户端的渗透要做,该客户端是一个视频会议系统。测试过程中发现原来该客户端只是一个http套了一个QT5UI。但数据交互过程中,请求包和响应包全是加密的字节流,解密字节流后,发现请求包还有签名signature需要绕过,因此记录一下自己对windows客户端的测试和分析过程。

开始工作

抓包

最开始用系统代理发现流量没有转发到burpsuite,然后就换到了proxifer,这样还能指定进程去抓包。

先设置一个代理服务器,代理地址是127.0.0.1,代理端口是8080(burpsuite的默认端口),代理类型是https,如下图所示:

image

然后再新建一个代理规则

应用程序这里选择可能会发起请求的exeaction这里选择刚刚添加的代理服务器。

我这里是把安装目录下的exe都放进来了。

image

随便输入一个账号密码,流量到burpsuite后,可以看见请求和响应全是加密的字节流。

不过单看请求url,就可以判断出该系统框架为struts2

image

加解密分析

查找接口找算法

先把主程序直接丢到ida进行反编译。

通过快捷键shift+f12查找字符串login.action

image

双击跟入,按快捷键X查找交叉引用

image

继续跟进,并按下f5,得到伪代码。

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
char __thiscall sub_5BAE40(int this, char a2, char a3, char a4, char a5, char a6, int a7, int *a8, int a9, int a10)
{
...

v147 = 4;
sub_410D20(v137, &unk_82DCD4, "bussIntfAction!login.action");
LOBYTE(v147) = 5;
sub_5B8600(v135);
LOBYTE(v147) = 6;
QJsonObject::QJsonObject((QJsonObject *)v151);
...

QJsonValue::QJsonValue((QJsonValue *)v154, (const struct QString *)&a2);
v162 = v158("userCode", 8);
QJsonObject::insert(v151, v142, &v162, v154);
QString::~QString((QString *)&v162);
...

QJsonValue::QJsonValue((QJsonValue *)v154, (const struct QJsonObject *)v151);
v162 = v11("data", 4);
QJsonObject::insert(v135, v142, &v162, v154);
QString::~QString((QString *)&v162);
...

QJsonDocument::QJsonDocument((QJsonDocument *)v143);
QJsonDocument::setObject((QJsonDocument *)v143, (const struct QJsonObject *)v135);
QJsonDocument::toJson(v143, v138, 1);
QString::QString((QString *)v144, (const struct QByteArray *)v138);
...

sub_54A9A0(0);
LOBYTE(this) = sub_54B040(v123, v124, v125, (char)v126);
...

v16 = QMessageLogger::QMessageLogger((QMessageLogger *)v133, 0, 0, 0);
v17 = QMessageLogger::debug(v16);
v18 = QDebug::operator<<(v17, "login send data", v144);
QDebug::operator<<(v18);
...

v19 = QMessageLogger::QMessageLogger((QMessageLogger *)v133, 0, 0, 0);
v20 = QMessageLogger::debug(v19);
v22 = QDebug::operator<<(v20, "login receive data", v21);
...

if ( !(_BYTE)this || (LOBYTE(v147) = 32, v157 = qstrcmp, v26 = qstrcmp(v13, Directory), LOBYTE(v147) = 29, !v26) )
{
QByteArray::operator=(a9, "0");
goto LABEL_20;
}
...

QJsonDocument::fromJson(v149, v13, v131);
if ( v132 )
{
QByteArray::operator=(a9, "0");
QJsonDocument::~QJsonDocument((QJsonDocument *)v149);
goto LABEL_20;
}
...

v162 = v158("resultCode", 10);
v28 = QJsonObject::take(v136, v154, &v162);
v29 = (QVariant *)QJsonValue::toVariant(v28, v141);
v153 = (const char *)QVariant::toInt(v29, 0);
...

if ( v153 )
{
...
goto LABEL_20;
}
...

v156 = ((int (__cdecl *)(const char *, int))v27)("userInfo", 8);
v140 = ((int (__cdecl *)(const char *, int))v27)("data", 4);
...

if ( QJsonObject::isEmpty((QJsonObject *)v160) )
{
QByteArray::operator=(a9, "0");
v25((QJsonObject *)v160);
QString::~QString((QString *)v152);
v25((QJsonObject *)v136);
QJsonDocument::~QJsonDocument((QJsonDocument *)v149);
LABEL_20:
v103 = 0;
goto LABEL_21;
}
...

if ( v159((QString *)&a5, 0, 10) == 3 || QByteArray::toInt((QByteArray *)(v99 + 112), 0, 10) )
{
v103 = 1;
}
else
{
...
v103 = 0;
}
...

LABEL_21:
sub_54AB10(v127);
...
return v103;
}

可以看见伪代码里面包含了userCodeuserPwd

image

在触发登录请求的时候,会构造一个json,包含userCodeuserPwd,通过某个加密算法加密后,将字节流发送给服务器,服务器解析,并返回加密字节流。

但是整个代码逻辑里面没有发现疑似加密和解密的

只有在253行处,发现了一个封装,将请求参数和请求接口传给了sub_54B040方法。

sub_54B040((char)v116, v117, HIDWORD(v117), v118);

其中v117就是被封装的json对象。

image

在封装前,可以看见在第242行还有一个用来初始化的函数。但具体初始化了什么,得分析一下。

算法分析

分析sub_54A9A0

该函数的主要作用是用来构建http请求所需的全部核心组件并完成初始化,为后续发送网络请求(如登录请求)。

并还初始化了一个疑似用来加解密的密钥2ebf6b694f4ad1f5b33072df4b602743

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
_DWORD *__thiscall sub_54A9A0(_DWORD *this, struct QObject *a2)
{
QNetworkAccessManager *v3; // esi
QTimer *v4; // esi
QEventLoop *v5; // esi

QObject::QObject((QObject *)this, a2);
*this = &CHttpHandle::`vftable';
QUrl::QUrl((QUrl *)(this + 2));
QByteArray::QByteArray((QByteArray *)(this + 8));
QString::QString(this + 9);
v3 = (QNetworkAccessManager *)operator new(8u);
a2 = v3;
QNetworkAccessManager::QNetworkAccessManager(v3, (struct QObject *)this);
*(_DWORD *)v3 = &QNetworkAccessManager::`vftable';
this[3] = v3;
sub_54C4A0(this);
QObject::connect(&a2, this[3], "2finished(QNetworkReply*)", this, "1OnHttpFinished(QNetworkReply*)", 0);
QMetaObject::Connection::~Connection((QMetaObject::Connection *)&a2);
v4 = (QTimer *)operator new(0x18u);
a2 = v4;
QTimer::QTimer(v4, (struct QObject *)this);
*(_DWORD *)v4 = &QTimer::`vftable';
this[7] = v4;
QObject::connect(&a2, v4, "2timeout()", this, "1handleTimeOut()", 0);
QMetaObject::Connection::~Connection((QMetaObject::Connection *)&a2);
v5 = (QEventLoop *)operator new(8u);
a2 = v5;
QEventLoop::QEventLoop(v5, 0);
*(_DWORD *)v5 = &QEventLoop::`vftable';
this[6] = v5;
this[4] = 0;
this[5] = 0;
QString::operator=(this + 9, "2ebf6b694f4ad1f5b33072df4b602743");
return this;
}

image

分析sub_54B040

双击跟进sub_54B040,得到伪代码如下

分析可知,这是一个基于Qt网络模块实现的带加密和解密的 HTTP POST 请求处理函数。

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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
char __thiscall sub_54B040(void *this, char a2, char a3, int a4, char a5)
{
...

v5 = (int)this;
v96 = (int)this;
v6 = 0;
HIDWORD(v83) = a4;
v98 = 0;
v84 = 4;
v79 = (double)clock();
sub_40DFF0(
"..\\..\\QMeeting_IM_new_5_15_2MSVC\\Src\\Model\\WSThreadModel\\httphandle.cpp",
"CHttpHandle::PostDownLoadUrl",
68);
QUrl::QUrl(v87, &a2, 0);
if ( QUrl::isValid((QUrl *)v87) )
{
QByteArray::QByteArray((QByteArray *)&v95);
v100 = operator new(0x180330u);
v8 = (void *)sub_526E90(v100);
LOBYTE(v84) = 6;
v100 = v8;
Block[0] = 1;
sub_5303C0();
if ( !(unsigned __int8)sub_40DC10("IsEncrypt", (int)Block)
|| (v101 = QString::fromAscii_helper("NotEncrypt", 10),
v6 = 1,
LOBYTE(v84) = 8,
v98 = 1,
v9 = QString::indexOf(&a2, &v101, 0, 1),
Block[0] = 0,
v9 != -1) )
{
Block[0] = 1;
}
v84 = 6;
if ( (v6 & 1) != 0 )
{
v6 &= ~1u;
QString::~QString(&v101);
}
if ( Block[0] )
{
v10 = (QByteArray *)QString::toUtf8(&a3, &v101);
v11 = QByteArray::data(v10);
QByteArray::operator=(&v95, v11);
LOBYTE(v84) = 6;
QByteArray::~QByteArray((QByteArray *)&v101);
}
else
{
...
while ( v15 < *(_DWORD *)(v99 + 4) / 2 )
{
...
++v15;
}
...
while ( v22 != (const struct QString *)v21 )
{
...
v22 = (const struct QString *)(v92 + 1);
v26 = v94 == 1;
v94 ^= 1u;
++v92;
if ( v26 )
break;
v21 = v93;
}
...
QString::~QString(&v99);
}
v101 = (int)operator new(4u);
LOBYTE(v84) = 19;
v27 = QNetworkRequest::QNetworkRequest((QNetworkRequest *)v101);
LOBYTE(v84) = 6;
v74 = (int)v90;
*(_DWORD *)(v5 + 20) = v27;
QNetworkRequest::sslConfiguration(v27, v74);
LOBYTE(v84) = 20;
QSslConfiguration::setPeerVerifyMode(v90, 0);
QSslConfiguration::setProtocol(v90, 6);
QNetworkRequest::setSslConfiguration(*(QNetworkRequest **)(v5 + 20), (const struct QSslConfiguration *)v90);
QNetworkRequest::setUrl(*(QNetworkRequest **)(v5 + 20), (const struct QUrl *)v87);
...
sub_410D20(v82, &v101, "c9dee2aa124845cfb35c3b222339f9bb");
...
v36 = QNetworkAccessManager::post(
*(QNetworkAccessManager **)(v96 + 12),
*(const struct QNetworkRequest **)(v96 + 20),
(const struct QByteArray *)&v95);
...
QObject::connect(&v102, LODWORD(v71), HIDWORD(v71), v72, v73, v74);
...
QObject::connect(
&v102,
*(_DWORD *)(v96 + 16),
"2downloadProgress(qint64,qint64)",
v96,
"1OnDownloadProgress(qint64,qint64)",
0);
...
if ( v39 )
v74 = 15000;
else
v74 = 35000;
QTimer::start(v41, v74);
QEventLoop::exec(*(_DWORD *)(v40 + 24), 0);
...
Block[0] = 1;
v74 = (int)Block;
v73 = "IsEncrypt";
v88 = (double)v44;
sub_5303C0();
if ( !(unsigned __int8)sub_40DC10(v73, v74)
|| (v102 = QString::fromAscii_helper("NotEncrypt", 10),
LOBYTE(v84) = 31,
v30 |= 2u,
v98 = v30,
v45 = QString::indexOf(&a2, &v102, 0, 1),
Block[0] = 0,
v45 != -1) )
{
Block[0] = 1;
}
v84 = 24;
if ( (v30 & 2) != 0 )
{
v30 &= ~2u;
QString::~QString(&v102);
}
if ( Block[0] )
{
QByteArray::operator=(HIDWORD(v83), v40 + 32);
v46 = (char *)v100;
}
else
{
...
if ( *(int *)(v99 + 4) <= 0 )
{
...
v7 = 0;
LABEL_62:
QString::~QString(v82);
QSslConfiguration::~QSslConfiguration((QSslConfiguration *)v90);
QByteArray::~QByteArray((QByteArray *)&v95);
goto LABEL_63;
}
...
QString::~QString(&v99);
}
if ( v46 )
{
sub_526EC0(v46);
v74 = 1573680;
sub_61462E(v46);
}
v7 = 1;
goto LABEL_62;
}
v7 = 0;
LABEL_63:
QUrl::~QUrl((QUrl *)v87);
sub_41A220(v80);
QString::~QString(&a2);
QString::~QString(&a3);
QString::~QString(&a5);
return v7;
}

在第125行开始读取配置文件,判断是否需要加密。如果加密就走到153行sub_529540代码逻辑处。

image

还在265行这里硬编码了请求头appKey的值

image

sub_527540是负责响应解密的。

image

借助豆包,弄了一个表格

方法名 核心作用 关键细节/补充说明
sub_54A9A0 初始化Http对象 + 硬编码初始化密码 ① 作为CHttpHandle类构造函数,创建QNetworkAccessManager/QTimer等核心网络组件;
② 硬编码存储DES加解密密钥(2ebf6b694f4ad1f5b33072df4b602743);
③ 绑定“请求完成/超时”回调,搭建请求基础环境
sub_5BAE40 构造HTTP请求对象(请求URL、请求体、请求内容) ① 组装登录请求核心要素:目标接口URL、JSON格式请求体、原始明文参数(如账号/密码);
② 仅做请求对象构造,无加密逻辑;
③ 为后续加密/发送提供原始数据载体
sub_529540 加密请求参数 ① DES对称加密算法底层核心实现,仅处理8字节(64位)固定分组数据;
② 执行DES关键步骤:64位位拆分、IP初始置换、16轮轮函数(S盒/P盒);
③ 依赖sub_54A9A0的硬编码密钥,输出密文参数
sub_54B040 带加密和解密的 HTTP POST 请求处理函数 ① 接收加密参数,构建QNetworkRequest(配置SSL、请求头如appKey);
② 发送POST请求,设置15s/35s超时并等待响应;
③ 接收服务端密文响应,触发解密流程;
④ 包含内存安全校验,防止数据越界
sub_527540 解密服务端响应数据 ① DES解密上层封装:将响应密文按8字节分块、去除加密补位;
② 复用sub_529540(反向执行16轮轮函数)完成单块解密;
③ 拼接解密结果,还原明文业务数据(如用户信息/token);
④ 校验响应内存边界,避免解密越界

数据解密

这里换到wireshark抓包

设置过滤规则如下:

1
ip.addr == 127.0.0.1 &&http

请求解密

复制请求包的请求数据为hex格式的字符串

image

解密成功

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"appId": "c9dee2aa124845cfb35c3b222339f9bb",
"data": {
"clientType": "3",
"custCode": "",
"userCode": "111",
"userPwd": "3b018a6265b6439dfe80eca21773f5da",
"userType": "PC",
"versionCode": "14731",
"versionKey": "",
"versionType": "0"
},
"method": "",
"nonce": "1ce14fb5cb1a49a0a04b6f947d52fbf9",
"signature": "0021e124a650d3b6596aa94981313a072a338a0477507113eebf5643e6a05137",
"timestamp": "707741804021548485",
"tokenId": "",
"userCode": ""
}

响应解密

一样的流程操作一遍

image

响应也解密成功

image

1
2
3
4
5
{
"data": null,
"resultCode": "11001",
"resultMsg": "用户名或者密码错误,请重新输入"
}

防重放分析

看到这个json的键值,就知道肯定是有一个防重放了。

通过删改json的键值,发现登录接口似乎不受signature影响,只有appId会产生一点微不足道的影响(不影响一直重放数据包)

image

image

注:上面两个截图是借助了mitmproxy框架实现了自动请求响应加解密脚本。

在获取当前用户信息的接口处就很明显是存在了防重放。分别有invalid.noncetimestamp is not allowed参数可能被修改

image

image

image

其实大概也知道这个signature的生成逻辑,就是noncetimestampappIddatauserCode经过一定的组合后,通过sha256之类的hash算法生成的值。

分析方式也很简单,一样的思路,查找交叉引用。

image

分析sub_5B8600

请求头的appKeyuserCode-appId
请求体的appId是固定值c9dee2aa124845cfb35c3b222339f9bb
请求体的nonce是删除-uuid字符串
请求体的timestamp是时间戳

signature的生成逻辑是sub_5A80A0 初始化摘要上下文,随后将userCode、 appId、 nonce 和 timestamp 依次复制后传给 sub_5A80B0

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
QJsonObject *__thiscall sub_5B8600(char *this, QJsonObject *a2)
{
// ...
v46 = this;
v37 = a2;

// 1. 初始化QJsonObject对象
QJsonObject::QJsonObject(a2);

// 2. 生成时间戳(timestamp)
v2 = (QDateTime *)QDateTime::currentDateTime(v47);
v3 = QDateTime::toMSecsSinceEpoch(v2);
QString::number(v43, *(_DWORD *)(v4 + 472) + v3, (unsigned __int64)(*(_QWORD *)(v4 + 472) + v3) >> 32);
QJsonValue::QJsonValue((QJsonValue *)v41, (const char *)v5);
v49 = QString::fromAscii_helper("timestamp", 9);
QJsonObject::insert(a2, v40, &v49, v41);
// ...

// 3. 设置appId(固定值:c9dee2aa124845cfb35c3b222339f9bb)
v44 = QString::fromAscii_helper("c9dee2aa124845cfb35c3b222339f9bb", 32);
QJsonValue::QJsonValue((QJsonValue *)v41, (const char *)v9);
v49 = QString::fromAscii_helper("appId", 5);
QJsonObject::insert(a2, v40, &v49, v41);
// 析构临时对象省略...

// 4. 设置nonce随机数
sub_5A8810(&v48, v45);
QJsonValue::QJsonValue((QJsonValue *)v41, (const char *)v12);
v49 = QString::fromAscii_helper("nonce", 5);
QJsonObject::insert(a2, v40, &v49, v41);
// ...

// 5. 计算并设置签名(signature)
QString::QString((QString *)&v34, (const struct QString *)(v46 + 8));
QString::QString((QString *)&v33, (const struct QString *)&v44);
QString::QString((QString *)&v31, (const struct QString *)v45);
QString::QString((QString *)&v31, (const struct QString *)v43);
v19 = sub_5A80B0(v47, v32, v33, v34, (char)v35); // 签名计算核心
QJsonValue::QJsonValue((QJsonValue *)v39, (const char *)v20);
v49 = QString::fromAscii_helper("signature", 9);
QJsonObject::insert(a2, v42, &v49, v39);
// ...

// 6. 设置method字段
QJsonValue::QJsonValue((QJsonValue *)v39, Directory);
v49 = QString::fromAscii_helper("method", 6);
QJsonObject::insert(a2, v42, &v49, v39);
// ...

// 7. 设置tokenId字段
v23 = sub_51B5F0(v36);
QString::QString((QString *)v47, (const struct QString *)(v23 + 456));
QJsonValue::QJsonValue((QJsonValue *)v39, (const char *)v24);
v49 = QString::fromAscii_helper("tokenId", 7);
QJsonObject::insert(a2, v42, &v49, v39);
// ...

// 8. 设置userCode字段
v27 = QString::toStdString(v46 + 8, Block);
QJsonValue::QJsonValue((QJsonValue *)v39, (const char *)v27);
v46 = (char *)QString::fromAscii_helper("userCode", 8);
QJsonObject::insert(a2, v42, &v46, v39);
// ...

// 9. 清理资源并返回构造完成的QJsonObject
QString::~QString(v45);
QString::~QString(&v44);
QString::~QString(v43);
nullsub_1(&v48);
return a2;
}

代码sub_5A80A0

不用分析

1
2
3
4
void *__thiscall sub_5A80A0(void *this)
{
return this;
}

代码sub_5A80B0

在这个代码处,可以看见userCode、 appId、 nonce 和 timestamp 是先放进QMap,然后按字典序(升序)依次取出、append,再做 HMAC-SHA256签名,其中HMAC-SHA256算法的密钥是5fddffdf05aa480ab9e7bb48e658db99

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
int __stdcall sub_5A80B0(int a1, char a2, char a3, char a4, char a5)
{
// ...
v50 = 0;
v60 = 1;

// 1. 初始化入参对应的QString对象
QString::QString((QString *)v41, (const struct QString *)&a2);
QString::QString((QString *)v42, (const struct QString *)&a3);
QString::QString((QString *)v43, (const struct QString *)&a4);
QString::QString((QString *)v44, (const struct QString *)&a5);
QString::QString(v45);

// 2. 处理QMap/QList数据结构,拼接待签名字符串
sub_5A8920(&v51, v41, v41);
sub_5A8920(&v51, v42, v42);
sub_5A8920(&v51, v43, v43);
sub_5A8920(&v51, v44, v44);
// ...
QString::append((QString *)v45, v15);

// 3. HMAC-SHA256计算签名(核心逻辑)
// 固定密钥:5fddffdf05aa480ab9e7bb48e658db99
strcpy(v59, "5fddffdf05aa480ab9e7bb48e658db99");
v19 = (QByteArray *)QString::toUtf8(v45, v39);
v20 = QByteArray::data(v19);

// 初始化HMAC-SHA256上下文
v51 = (volatile signed __int32 *)EVP_sha256();
HMAC_CTX_init(v53);
HMAC_Init_ex(v53, v59, strlen(v59), v51, 0);

// 计算签名
HMAC_Update(v53, v20, strlen(v20));
HMAC_Final(v53, v21, &v38);
HMAC_CTX_cleanup(v53);

// 4. 签名结果转十六进制字符串
// ...
for ( k = 0; k < v22; Src[v33 + 1] = v32 )
{
v28 = (*((_DWORD *)v57 + k) >> 4) & 0xF;
v29 = *((_DWORD *)v57 + k) & 0xF;
v30 = v28 + 87 + (v28 < 0xA ? 0xD9 : 0);
Src[v48] = v30;
++k;
v32 = v29 + 87 + (v29 < 0xA ? 0xD9 : 0);
v48 = v31 + 2;
}

// 5. 资源清理
QByteArray::~QByteArray((QByteArray *)v39);
QString::~QString(v45);
QString::~QString(v44);
QString::~QString(v43);
QString::~QString(v42);
QString::~QString(v41);
// 其他临时字符串析构省略...

// 6. 返回结果
return a1;
}

以之前解密的数据进行尝试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"appId": "c9dee2aa124845cfb35c3b222339f9bb",
"data": {
"clientType": "3",
"custCode": "",
"userCode": "111",
"userPwd": "3b018a6265b6439dfe80eca21773f5da",
"userType": "PC",
"versionCode": "14731",
"versionKey": "",
"versionType": "0"
},
"method": "",
"nonce": "1ce14fb5cb1a49a0a04b6f947d52fbf9",
"signature": "0021e124a650d3b6596aa94981313a072a338a0477507113eebf5643e6a05137",
"timestamp": "707741804021548485",
"tokenId": "",
"userCode": ""
}

userCode、 appId、 nonce 和 timestamp经过排序再拼接后得到字符串1ce14fb5cb1a49a0a04b6f947d52fbf9707741804021548485c9dee2aa124845cfb35c3b222339f9bb

经过算法签名后,值和数据包里的signature能完全对应。也就分析成功了

image

最后效果

实现了一个mitmproxy脚本

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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
from mitmproxy import http
from Crypto.Cipher import DES
from Crypto.Util.Padding import pad, unpad
import json
import time
import uuid
import hmac
import hashlib

# -------------------------- 核心配置(按你的要求修改) --------------------------
# DES加密配置
ORIGINAL_KEY_STR = "2ebf6b694f4ad1f5b33072df4b602743"
DES_KEY = ORIGINAL_KEY_STR[:8].encode("ascii") # 最终密钥字节:b'2ebf6b69'
BLOCK_SIZE = DES.block_size # DES固定8字节块
# 【关键】指定需要加解密的主机和端口
TARGET_HOSTS = ["127.0.0.1"] # 目标主机列表
TARGET_PORT = 8001 # 目标端口

# HMAC签名配置
APP_ID = "c9dee2aa124845cfb35c3b222339f9bb"
HMAC_KEY = "5fddffdf05aa480ab9e7bb48e658db99"

# -------------------------- HMAC签名生成函数 --------------------------
def build_payload(user_code: str):
"""
生成包含新nonce、timestamp、signature的字典
:param user_code: 用户编码(如safetest10_677)
:return: 包含nonce、timestamp、signature的字典
"""
timestamp = str(int(time.time() * 1000))
nonce = uuid.uuid4().hex
parts = sorted([user_code, APP_ID, nonce, timestamp])
payload_str = "".join(parts)
signature = hmac.new(
HMAC_KEY.encode(),
payload_str.encode(),
hashlib.sha256
).hexdigest()
return {
"nonce": nonce,
"signature": signature,
"timestamp": timestamp,
}

# -------------------------- 字节流版加解密函数(适配Burp字节流) --------------------------
def des_ecb_encrypt_bytes(plain_bytes: bytes) -> bytes:
"""
DES ECB加密(输入/输出都是字节流,适配Burp)
:param plain_bytes: 明文字节流
:return: 密文字节流
"""
try:
# PKCS7填充(8字节对齐)
padded_plain = pad(plain_bytes, BLOCK_SIZE, style="pkcs7")
# DES ECB加密
cipher = DES.new(DES_KEY, DES.MODE_ECB)
cipher_bytes = cipher.encrypt(padded_plain)
return cipher_bytes
except Exception as e:
print(f"加密失败:{str(e)}")
return plain_bytes # 失败时返回原数据,避免断连

def des_ecb_decrypt_bytes(cipher_bytes: bytes) -> bytes:
"""
DES ECB解密(输入/输出都是字节流,适配Burp)
:param cipher_bytes: 密文字节流
:return: 明文字节流
"""
try:
# DES ECB解密
cipher = DES.new(DES_KEY, DES.MODE_ECB)
decrypted_bytes = cipher.decrypt(cipher_bytes)
# PKCS7去填充(自动去掉末尾0x01等填充字节)
plain_bytes = unpad(decrypted_bytes, BLOCK_SIZE, style="pkcs7")
return plain_bytes
except Exception as e:
print(f"解密失败:{str(e)}")
return cipher_bytes # 失败时返回原数据,避免断连

# -------------------------- 过滤判断函数(主机+端口) --------------------------
def is_target_flow(flow: http.HTTPFlow) -> bool:
"""判断当前流量是否是目标主机+端口"""
# 匹配主机(忽略大小写)+ 匹配端口
return (flow.request.host.lower() in [h.lower() for h in TARGET_HOSTS]) and (flow.request.port == TARGET_PORT)

# -------------------------- mitmproxy 请求处理(Burp→服务器) --------------------------
def request(flow: http.HTTPFlow) -> None:
"""
拦截请求:
1. Burp发送的明文 → 替换nonce/timestamp/signature → DES加密 → 发给服务器
"""
if is_target_flow(flow):
try:
# 1. 提取Burp发送的请求体(字节流)
request_body = flow.request.content
if not request_body:
return

# 2. 解析请求体为JSON,替换指定字段
try:
# 解析JSON
req_json = json.loads(request_body.decode("utf-8"))
# 提取userCode(优先从JSON取,备用从appKey请求头取)
user_code = req_json.get("userCode")
if not user_code and "appKey" in flow.request.headers:
app_key = flow.request.headers["appKey"]
user_code = app_key.split("-")[0] # 从appKey拆分出userCode

if user_code:
# 生成新的签名相关字段
new_sign_info = build_payload(user_code)
# 替换JSON中的字段
req_json["nonce"] = new_sign_info["nonce"]
req_json["timestamp"] = new_sign_info["timestamp"]
req_json["signature"] = new_sign_info["signature"]
# 转回字节流
request_body = json.dumps(req_json, ensure_ascii=False).encode("utf-8")
print(f"✅ 已替换请求体字段 | userCode: {user_code}")
print(f" 新nonce: {new_sign_info['nonce']}")
print(f" 新timestamp: {new_sign_info['timestamp']}")
print(f" 新signature: {new_sign_info['signature']}")
else:
print("⚠️ 未找到userCode,跳过字段替换")
except json.JSONDecodeError as e:
print(f"❌ JSON解析失败:{str(e)},跳过字段替换")

# 3. 加密处理后的请求体(明文→密文)
encrypted_body = des_ecb_encrypt_bytes(request_body)

# 4. 替换请求体为密文,发给服务器
flow.request.content = encrypted_body
print(f"✅ 请求加密完成 | 主机: {flow.request.host}:{flow.request.port}")
print(f" 明文长度:{len(request_body)}字节 | 密文长度:{len(encrypted_body)}字节")
except Exception as e:
print(f"❌ 请求处理失败:{str(e)}")

# -------------------------- mitmproxy 响应处理(服务器→Burp) --------------------------
def response(flow: http.HTTPFlow) -> None:
"""
拦截响应:服务器返回的密文 → mitmproxy解密 → 给Burp显示明文
"""
if is_target_flow(flow):
try:
# 1. 提取服务器返回的响应体(字节流)
response_body = flow.response.content
if not response_body:
return

# 2. 解密响应体(密文→明文)
decrypted_body = des_ecb_decrypt_bytes(response_body)

# 3. 替换响应体为明文,给Burp显示
flow.response.content = decrypted_body
print(f"✅ 响应解密完成 | 主机: {flow.request.host}:{flow.request.port}")
print(f" 密文长度:{len(response_body)}字节 | 明文长度:{len(decrypted_body)}字节")
except Exception as e:
print(f"❌ 响应处理失败:{str(e)}")

# -------------------------- 辅助函数(启动提示) --------------------------
def start():
"""mitmproxy启动时执行"""
print("🚀 DES ECB 加解密+签名替换代理已启动")
print(f" DES密钥(ASCII前8字符):{ORIGINAL_KEY_STR[:8]}")
print(f" DES密钥字节(Hex):{DES_KEY.hex()}")
print(f" HMAC APP_ID:{APP_ID}")
print(f" 目标主机:{TARGET_HOSTS} | 目标端口:{TARGET_PORT}")

image

image

image

Author: jdr
Link: https://jdr2021.github.io/2026/01/14/Windows客户端测试/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.