背景
在使用Welink 的时候发现Welink 可以在免登录的情况下打开程序,到用户的IM界面。之后发现是在登录界面勾选了记住密码和自动登录,当勾选了记住密码后就会把密码保存在用户目录下的sys.db 文件中,以下是分析过程。
而且在调试过程中,内存保存着welink 的明文密码
可以实现一个搜索内存的程序获取明文密码。
解析sys.db 配置文件
调试进程
welink 是 electron 编译的Windows可执行文件,将Chromium和Node.js嵌入到二进制文件中。查看app文件夹下的JS 文件,由于经过了js混淆,分析js文件比较困难。最终选择直接调试的方法。
Electron 分为主进程和渲染进程
下CreateProcessW 断点,当启动进程如下是,则启动了主进程
修改启动参数为挂起0x80004
,启动x32DBG 附加新启动的进程
修改X32DBG 增加DLL 入口断点,恢复线程。
sys.db 文件解析
查看welink 用户目录,发现保存着挺多db 后缀的文件,如sys.db、user.db、history.db。这些文件都有一个类似的结构。即前八个字节同一目录下是一样的,0x10 出的8个字节不同目录下的文件也一致。
使用调试器下打开文件断点
strstr( utf16([esp+4]) ,"sys.db")
查看调用堆栈发现存在初始化数据库的函数名,CreateFileW 函数调用发生在wxsqlite.dll 动态链接库,偏移为145BD
sqlite_plugin.tup_sqlite_init_db 断点 wxsqlite+0x145BD
下读取文件ReadFile 断点,当读取sys.db 数据库时停下来查看堆栈。偏移为 14BB1
向上回溯调试,在函数off_14BD8(使用off_表示偏移)调用off_5BA70 函数会解密数据库文件部分数据,有参数决定解密的大小,经调试发现在正式解密中每次解密0x1000 字节大小的数据。
在off_14BD8 下断点调试分析
[esp+8]==0x1000
多次断下来对比源文件数据发现
函数off_5BA70 参数一表示一个加密使用的对象 参数二表述数据的大小 参数三表示数据在源文件中的偏移
手动解析db 文件,需要在真正解密对应文件的时候断下来,然后通过修改反汇编的方式使解密函数进行,不同的文件参数一的值有所不同
1.x32 dbg 申请文件大小的内存 alloc size 2. 修改反汇编 add dword ptr ss:[ebp+0x14], 0x1000 add esi, 0x1000 jmp 0x5E134BCF/. 3.修改 esi 寄存器的值 4.复制源文件数据到申请的用户内存空间
解析部分文件
im_userdb
sys_db
解密db 文件
函数off_5BA70调用off_8DC0 解密
off_8DC0 函数首先判断数据偏移,如果是首页,则进行替换。将off_8 处的8字节数据复制到off_0x10 处,之后才进程解密
构建一个计算sha256 的缓冲区
off_5BA70 参数一中确定的32字节数据,为 [arg+1c]+14,这部分称为cipher_salt 四字节页偏移,如如果为首页,则为1 四字节固定值, 0x546C4173 “sAlT”
计算得到sha256值,作为aes 解密的密钥
四字节页偏移经过一定转换生成16字节数据,计算得到MD5 值,作为aes 解密的IV
解密之后如果是第一页则替换解密数据首部的前16字节的数据,替换为Sqlite的标头
通过向上追踪和内存硬件断点分析,都没有找到赋值时候的代码。观察cipher_salt前面部分的数据发现一个关键数值。0xFA1
搜索自己定位到函数off_8AB0
调试确定,就是该函数计算得到了cipher_salt的值
查看调用,发现在sqlite3_key 函数中调用
分析sqlite3_key 函数,其参数为对应的db 文件路径和一段字符串数据
后续的cipher_salt 为参数3,就是传入的字符串值取前一半多次计算sha256 所得。
最终的函数也是sqlite_plugin.tup_sqlite_init_db,继续回溯发现为处理接收消息。
sysdb sqlite_key 的生成
初始化kmc 解密 kmc.dll
WsecInitializeEx 函数
1.初始化全局变量随机数
2.解析kmcStore.dat 文件,路径为传入参数
kmc Rootkey 头部的结构如下
调用off_10170 函数读取头部获取RootKey,并校验,之后传入off_C5A0用于 派生密钥
根密钥异或一个硬编码数据作为pbkdf2_derive 派生密钥的key
硬编码为
经派生后生成0x40字节的数据Root_pbkdf2_derive_key,这部分数据用于kmc 数据部分(MKDATA)解析
判断ksf 文件判断,然后解密。版本几位头部的前两个字节,这里版本为2
版本为1或2,则调用off_E8A0 函数解密
读取MKDATA 数据,使用Root_pbkdf2_derive_key 的前0x20字节数据作为AES的key,MKDATA 数据中0x48偏移的16字节数据作为IV,解密0x60偏移处的0x30大小的数据。解密数据的大小具体由偏移为0x58的数据决定,解密数据的大小是0x5c 数据,计算得到MK
之后解密后的数据异或开头的随机全局变量
解密过程
解密函数为SdpDecryptEx 函数,解密数据为appdata/IM config.db 文件中的第一部分数据转为二进制数据,如下的内存部分。
解密时先获取初始化生成的主密钥MK,然后使用 pbkdf2_derive 派生获得WorkKey
pbkdf2_derive 派生密钥的salt 值为config_data的0x1c 偏移的8字节数据
WorkKey 经过AES 算法解密
config 结构为
将这部分与config 的第二部分数据作为参数传入 tup_pkcs5_derivekey_ex 函数计算得到derive salt,derive salt即为sqlite3_key的参数。
解析sys 数据解密密码
在 TUP_CPYPT_Encrypt 函数下断点
在tup_cvt_hex 下断点记录
esp+0xc {ansi([esp+0xc])} esp+18 {ansi([esp+0x18])} 27A6+wk_tup_commonlib
线程 6084 已创建,入口:mswsock.6EE91B50,参数:1BACC968 esp+0xc 30654ac2b3fae18b61b94892d508d07a6f3d13579323ec21bcc241a4171d3aff esp+18 {\"description\":\"tup_commonlib_crypt_pkcs5_derive_key_new\",\"cmd\":262153,\"param\":{\"salt\":\"3b5ff508ca97d9c7834f6e322661bd81\",\"saltLen\":32,\"dklen\":32,\"iv\":\"82a4b6a9e3eabbead6d44c8a7094a134\"},\"sno\":35} 线程 5448 已创建,入口:ucrtbase.0D6B2450,参数:1B9BC088 线程 2192 已创建,入口:ucrtbase.0D6B2450,参数:1BA87EA0 线程 5448 退出 线程 5800 已创建,入口:ucrtbase.0D6B2450,参数:1BB1CB60 线程 5800 退出 线程 8136 退出 esp+0xc 30654ac2b3fae18b61b94892d508d07a6f3d13579323ec21bcc241a4171d3aff esp+18 {\"description\":\"tup_commonlib_crypt_pkcs5_derive_key_new\",\"cmd\":262153,\"param\":{\"salt\":\"3b5ff508ca97d9c7834f6e322661bd81\",\"saltLen\":32,\"dklen\":32,\"iv\":\"82a4b6a9e3eabbead6d44c8a7094a134\"},\"sno\":75} esp+0xc c3fdc09d0e2e8f95e81f4e90428edcbb esp+18 {\"description\":\"tup_commonlib_crypt_realrandom\",\"cmd\":262148,\"param\":{\"RandLen\":16},\"sno\":78} esp+0xc e3f0986d04ed56df16a53e8b3b14f17f esp+18 {\"description\":\"tup_commonlib_crypt_realrandom\",\"cmd\":262148,\"param\":{\"RandLen\":16},\"sno\":79} esp+0xc 00000002000000000000000000000001000000074f9486c9aae79cfa8ff031ecd16db6121e536eeb602afcf187a949d0057496ee000000000000000000000030d6d9001e1be9fda077d3bd1f9f06f700e7cb4a1c0690d8f8babeb856490c0f62671aa1f787b5f227e9feb9927a10ef70 esp+18 esp+0xc 3af0f12e9927dd4d518aa1c9ce34860729c7e6cef79b518ea43fe39119a08b06 esp+18 {\"description\":\"tup_commonlib_crypt_pkcs5_derive_key_new\",\"cmd\":262153,\"param\":{\"salt\":\"c3fdc09d0e2e8f95e81f4e90428edcbb\",\"saltLen\":32,\"dklen\":32,\"iv\":\"e3f0986d04ed56df16a53e8b3b14f17f\"},\"sno\":83} esp+0xc 4d24b8706d8c427c4c844f893f061f98 esp+18 {\"description\":\"tup_commonlib_crypt_encrypt\",\"cmd\":262146,\"param\":{\"Key\":\"3af0f12e9927dd4d518aa1c9ce34860729c7e6cef79b518ea43fe39119a08b06\",\"IV\":\"c3fdc09d0e2e8f95e81f4e90428edcbb\",\"PlainText\":\"plain\"},\"sno\":85} 线程 3268 已创建,入口:ucrtbase.0D6B2450,参数:1BA46818 0F96DEC8 348373D0 "{\"description\":\"tup_commonlib_crypt_pkcs5_derive_key_new\",\"cmd\":262153,\"param\":{\"salt\":\"4999e9982e8083c92d5e2b1434711c5c\",\"saltLen\":32,\"dklen\":32,\"iv\":\"26b0d63de8d69e424b4f86eff60a765b\"},\"sno\":58}"
分析上面的日志
程序生成两个随机数
其中一个随机数加密后保存为sys里面的IV
一个数据保存为sys的随机数数据
这两个数据作为参数tup_commonlib_crypt_pkcs5_derive_key_new 使用这个命令加密,得到加密的密钥
c++ 测试
char test_user_name1[] = "\x49\x99\xe9\x98\x2e\x80\x83\xc9\x2d\x5e\x2b\x14\x34\x71\x1c\x5c"; char test_user_name2[] = "\x26\xb0\xd6\x3d\xe8\xd6\x9e\x42\x4b\x4f\x86\xef\xf6\x0a\x76\x5b"; ((pfn_PKCS5_deriveKey)func)(0x31, 0x1, (char*)test_user_name2, 0x10, (char*)test_user_name1, 0x10, 0, 0x20, cipher_salt); typedef void (*pfn_CRYPT_decrypt)(int,char* key,int key_len,char* iv,int iv_len,char* text,int text_len,char* out,int* out_len,int ); HMODULE libipsi_crypto = LoadLibraryA("libipsi_crypto.dll"); auto CRYPT_decrypt = GetProcAddress(libipsi_crypto, "CRYPT_decrypt"); char text[0x10] = { 0x55, 0xC0, 0x3F, 0x61, 0xA0, 0x72, 0x68, 0xE6, 0xBF, 0x92, 0xE0, 0xED, 0xB2, 0x7D, 0xF5, 0x00 }; char out_text[0x100] = {}; int out_text_len = 0x100; //最后一个参数为密文长度+1 ((pfn_CRYPT_decrypt)CRYPT_decrypt)(0x1001b, cipher_salt, 0x20, test_user_name1, 0x10, text, 0x10, out_text, &out_text_len, 0x11);
完整代码
流程图
解析程序开发
im /appdata 下文件解密失败
手动解析出user.db 文件,与开发程序解密出的文件做对比。发现每页末尾有16个字节不同。解密程序的数据都是0
调试发现,aes 解密时得到的大小比输入文本小 。输入0x1000 输出0xff0
aes 解密时设置禁止填充
所需dll