最近腾讯推出了基于 Electron 的 Linux QQ 3.0 版本,总算是可以正常收发消息了,但似乎 QQ 在 Windows 平台上曾有过一些 流氓行为,还是不太放心让它直接在我的机器上裸奔

但好在这里是 Linux,我们可以玩许多魔法,不是么。

bwrap 沙盒

bubblewrap 是一个基于用户命名空间的非特权沙盒,本质上就是 CLONE_NEWUSER 的同时分离一些其它的命名空间,然后就可以在其中做一些非特权挂载,外加一些权限的处理。参考依云的 这篇博客,里面介绍了 bwrap 的一些简单用法

因为现阶段暂未发现 QQ 有什么越权的流氓行为,我只限制了 QQ 在家目录下乱写文件,没有对访问设备文件做太多约束:

bwrap --unshare-all --share-net \
  --dev-bind / / \
  --proc /proc \
  --tmpfs $HOME \
  --bind $HOME/.config/QQ $HOME/.config/QQ \
  --chdir ~ \
  /opt/QQ/qq

打包脚本

使用 Ubuntu 自带的 dpkg 工具就能很方便地对 deb 文件进行解包和打包,配合一些 shell 命令,就能制作出一个简易的自动打包工具,脚本已经上传到 GitHub:

处理链接

某日打开链接的时候:

常年使用第三方 QQ 的我,甚至已经快忘了还存在这种页面,不由得想起早年在 Windows 上用 TIM 的时候可是被这玩意恶心坏了,最后弄了个 Chrome 扩展来自动重定向这些链接才总算是解决问题

但好在这里是 Linux,我们可以玩许多魔法,不是么。

其实我们并不需要写 Chrome 扩展或是把 Chrome 怎么样,既然是 QQ 的问题,那就从 QQ 上来解决

  • 附一个完整的 strace 命令
strace -e trace=execve -f -s 1024 -v -o syscall.txt qq
# 参数说明:
# -e trace=execve : 过滤 execve
# -f : 追踪 fork
# -s 1024 : 字符串最大长度
# -v : 打印完整参数列表
# -o strace.txt : 输出文件

strace 一下它的 execve 系统调用,可以看到它会试图在 PATH 中搜索 xdg-open,那么只需要在 bwrap 启动前给它的 PATH 塞上一些东西,由我们来代理这个 xdg-open 就好了

#!/usr/bin/env python3
import os
import sys
from sys import argv
from urllib import parse


def my_exec(*args):
    env = set(os.environ['PATH'].split(':'))
    env.remove('/opt/QQ/__patch__')
    os.environ['PATH'] = ':'.join(env)
    os.execvp('xdg-open', ['xdg-open', *args])
    exit(1)


if len(argv) == 1:
    my_exec(*argv[1:])

result = parse.urlsplit(argv[1])

if result.netloc == 'c.pc.qq.com':
    my_exec(parse.parse_qs(result.query)['pfurl'][0])

my_exec(*argv[1:])

似乎是 bwrap 隔离的原因,在 QQ 中启动 Chrome 会出现在独立的窗口中,而不是在已打开的 Chrome 新增一个标签页,暂时没想到什么合适的解决办法(或许可以用管道弄个事件总线把消息传出来?)

意外发现

使用下面的命令跟踪 Linux QQ:

strace -e trace=openat /opt/QQ/qq |& grep /opt/QQ

可以发现它打开了 /opt/QQ/resources/app 底下的一些 js 文件,这里我们选择 background.js,向这个文件开头添加一些内容:

(() => {    
    const { app, BrowserWindow } = require('electron')    
    try {    
        app.on('ready', () => {    
            setTimeout(() => {    
                console.log(BrowserWindow.getAllWindows())    
            }, 1000)    
        })    
    } catch(err) {    
        console.error(err)    
    }    
})();   

然后启动 QQ,我们写入的 js 代码已经被正确加载并执行了:

我们现在拥有了对其注入代码的能力,然而没法调试可不行!首先尝试像其它 eletron 应用那样为它添加 --remote-debugging-port=9222 参数,然而并没有起到什么作用

于是 strace 分析,对比同为 electron 的 Icalingua++,可以发现 Icalingua 在启动 renderer 进程时把我们的参数顺带传递了过去,而 Linux QQ 并没有:

  • Icalingua++

  • Linux QQ

于是用 C++ 搓了个库来 Hook 它的 execvp 调用:

#include <cstdio>
#include <cstdlib>
#include <dlfcn.h>
#include <memory.h>

extern "C"
int execvp(const char *path, char *argv[]) {
    typedef decltype(&execvp) Self;

    static void *handle = nullptr;
    static Self backup_old = nullptr;

    if (!handle) {
        handle = dlopen("libc.so.6", RTLD_LAZY);
        backup_old = (Self) dlsym(handle, "execvp");
    }

    int argc = 0;
    for (char **ptr = argv; *ptr; ptr++) {
        argc++;
    }

    char **argv_new = (char **) malloc(sizeof(char *) * (argc + 2));
    memcpy(argv_new, argv, sizeof(char *) * argc);
    argv_new[argc] = "--remote-debugging-port=9222";
    argv_new[argc + 1] = nullptr;

    printf("execvp: %s\n", path);
    printf("argv:\n");
    for (char **ptr = argv_new; *ptr; ptr++) {
        printf("    %s\n", *ptr);
    }

    int result = backup_old(path, argv_new);
    
    free(argv_new);
    return result;
}

[[maybe_unused]]
[[gnu::constructor]] void on_inject() {
    printf("Hack: injected!\n");
}

设置 LD_PRELOAD 让我们的代码注入进去,给每一个 execvp 出来的进程都加上 --remote-debugging-port=9222 参数:

可以看到我们的代码确实注入进去了,然而转到 Chrome 查看,还是没办法访问到调试器

– 未完待续 –