LLDB实战之导出Mac微信备份聊天记录的SQLite密码(SQLCipher加密)

涉及到的LLDB命令

  • br: 设置断点
  • memory read: 读取内存原始值
  • po: 打印变量,也可以执行函数并且获得返回值
  • bt: 打印当前调用栈
  • thread step over/in/out: 单步跳过/进入/跳出
  • register: 寄存器操作
  • next/ni/n/step/si: 同上

参考链接

0x00 准备工作

查看WCDB所用的SQLite的加密方式,直接在WCDB的README里写了:

Encryption Support: WCDB supports database encryption via SQLCipher.

于是查看SQLCipher的API,看到用的是sqlite3_key()sqlite3_key_v2()这2个函数,在源码里搜索,找到调用,一共有两处,在WCTDatabase+Database.mm文件里

- (void)setCipherKey:(NSData *)cipherKey
{
    _database->setCipher(cipherKey.bytes, (int) cipherKey.length);
}

- (void)setCipherKey:(NSData *)cipherKey andCipherPageSize:(int)cipherPageSize
{
    _database->setCipher(cipherKey.bytes, (int) cipherKey.length, cipherPageSize);
}

然后是下面的C寄存器的含义对照表。其中函数参数也可以不用寄存器表示,可以直接$arg1/$arg2/$arg3来表示 。

0x01获取保存目录

开始用LLDB动态调试,首先Hook微信进程

$ ps aux | grep WeChat

// 25132   6.7  1.0  7341704 170876   ??  S    Fri04PM   8:36.19 /Applications/WeChat.app/Contents/MacOS/WeChat  进程id是25132

$ lldb -p 25132 //开始hook进程

(lldb) process attach --pid 25132
Process 25132 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
    frame #0: 0x00007fff6162722a libsystem_kernel.dylib`mach_msg_trap + 10
libsystem_kernel.dylib`mach_msg_trap:
->  0x7fff6162722a <+10>: retq
    0x7fff6162722b <+11>: nop

libsystem_kernel.dylib`mach_msg_overwrite_trap:
    0x7fff6162722c <+0>:  movq   %rcx, %r10
    0x7fff6162722f <+3>:  movl   $0x1000020, %eax          ; imm = 0x1000020
Target 0: (WeChat) stopped.

Executable module set to "/Applications/WeChat.app/Contents/MacOS/WeChat".
Architecture set to: x86_64h-apple-macosx.
(lldb)

进入LLDB命令行模式

打断点获取微信的数据库目录,看WCDB的初始化接口,[WCTDatabase [alloc] initWithPath:path];我们要获取path

(lldb) br set -n '[WCTDatabase initWithPath:]'
//输出
Breakpoint 1: where = WCDB`-[WCTDatabase(Database) initWithPath:], address = 0x0000000110b38676
(lldb) br list //查看断点
Current breakpoints:
1: names = {'[WCTDatabase initWithPath:]', '[WCTDatabase initWithPath:]'}, locations = 1, resolved = 1, hit count = 0
  1.1: where = WCDB`-[WCTDatabase(Database) initWithPath:], address = 0x0000000110b38676, resolved, hit count = 0

点击微信的备份文件,恢复聊天记录至手机或者管理备份文件来触发断点。

Process 25132 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000110b38676 WCDB`-[WCTDatabase(Database) initWithPath:]
WCDB`-[WCTDatabase(Database) initWithPath:]:
->  0x110b38676 <+0>: pushq  %rbp
    0x110b38677 <+1>: movq   %rsp, %rbp
    0x110b3867a <+4>: pushq  %r15
    0x110b3867c <+6>: pushq  %r14
Target 0: (WeChat) stopped.
(lldb) po $arg3 //或者po $rdx 打印第三个参数
/Users/xxxx/Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9/Backup/91f05ea8dc79f929613c0beb267b789a/75563957-B5FD-4CC5-90F4-0F36D91DD190/Backup.db

获取到备份的数据库位置,直接在finder中打开,发现一共有三个文件

  • Backup.db
  • BAK_0_MEDIA
  • BAK_0_TEXT

为什么是第三个参数呢?

OC的对象方法调用,实际调用的是底层的objc_msgSend($arg1,$arg2,...),其中$arg1为调用者本身,$arg2为方法名,后面的参数表示传递的实际参数,因此是从$arg3开始的,可以打印整个寄存器和$arg1,$arg2出来看看

(lldb) register read
General Purpose Registers:
       rax = 0x00006000027b98e0
       rbx = 0x00007fff5fd16680  libobjc.A.dylib`objc_msgSend
       rcx = 0x0000000000000000
       rdx = 0x00006000012e63a0
       rdi = 0x00006000027b98e0
       rsi = 0x00007fff337c64f7  "initWithPath:"
       rbp = 0x00007ffee1a3f060
       rsp = 0x00007ffee1a3efb8
        r8 = 0x0000cc851f0d98e0
        r9 = 0x00000000000007fb
       r10 = 0x0000000110c78ee8  (void *)0x001d800110c78ec1
       r11 = 0x00007fcb9badab70
       r12 = 0x00006000037ca9c0
       r13 = 0x0000000000000000
       r14 = 0x00006000027b9a40
       r15 = 0x00006000037ca9c0
       rip = 0x0000000110b38676  WCDB`-[WCTDatabase(Database) initWithPath:]
    rflags = 0x0000000000000246
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000
(lldb) po $arg1 //po $rdi
<WCTDatabase: 0x6000027b98e0>
(lldb) po $arg2 //po $rsi 
140734057178359 //字符串打印出被转成数字了,可以用memory read打印
(lldb) memory read $rsi
0x7fff337c64f7: 69 6e 69 74 57 69 74 68 50 61 74 68 3a 00 73 65  initWithPath:.se
0x7fff337c6507: 74 46 69 6c 65 6e 61 6d 65 3a 00 69 6d 61 67 65  tFilename:.image

因此实际的函数参数会从第三个$arg3开始。

sqlitebrowser打开这个db文件,发现是SQLCipher加密,要输入密码。

0x02 获取sqlite3_key

继续加断点,如果加在sqlite3_key上,会发现拿不到PageSize,查看源码看调用链,pageSize是在void Database::setCipher(const void *key, int keySize, int pageSize)的时候接收的,断点打在setCipher

(lldb) br set -n setCipher
(lldb) c //继续执行

触发到sqlite3_key的断点,

获取key和pageSize

(lldb) memory read $arg2
0x600003f8fa90: 64 64 30 36 33 35 65 63 65 62 35 37 39 35 32 66  dd0635eceb57952f
0x600003f8faa0: 31 62 35 65 63 31 65 64 33 37 38 30 36 65 31 30  1b5ec1ed37806e10

(lldb) po $arg3
32

(lldb) po $arg4
4096

key是dd0635eceb57952f1b5ec1ed37806e10,取32位,也就是整个字符串,页长度是4096.



打开数据库。

分析一下表,发现文本内容存在BAK_0_TEXT,媒体内容存在BAK_0_MEDIA,以偏移量记录某条消息,简单查看一下这2个文件,都是写二进制数据,看来还用了某种加密方式。

0x03 Todo

  • 静态分析WeChat.app,获取打开两个加密文件的方法。

0x04 Update

有网友反馈key错误,跟了下发现是sqlitebrowser更新了,支持了最新的SQLCipher4算法,但是微信依旧使用的WCDB1.0.3,还是SQLCipher3,操作的时候先选择SQLCipher3,之后再选择custom,把pageSize改成响应大小即可。