最近腾讯推出了基于 Electron 的 Linux QQ 3.0 版本,总算是可以正常收发消息了,但似乎 QQ 在 Windows 平台上曾有过一些 流氓行为,还是不太放心让它直接在我的机器上裸奔。
但好在这里是 Linux,我们可以玩许多魔法,不是么。
bwrap 沙盒
bubblewrap 是一个基于用户命名空间的非特权沙盒,本质上就是 CLONE_NEWUSER 的同时分离一些其它的命名空间,然后就可以在其中做一些非特权挂载,外加一些权限的处理。参考依云的 这篇博客,里面介绍了 bwrap 的一些简单用法
因为现阶段暂未发现 QQ 有什么越权的流氓行为,我只限制了 QQ 在家目录下乱写文件,没有对访问设备文件做太多约束:
1 2 3 4 5 6 7
| 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 上来解决

1 2 3 4 5 6 7
| strace -e trace=execve -f -s 1024 -v -o syscall.txt qq
|
strace 一下它的 execve 系统调用,可以看到它会试图在 PATH 中搜索 xdg-open,那么只需要在 bwrap 启动前给它的 PATH 塞上一些东西,由我们来代理这个 xdg-open 就好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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:
1
| strace -e trace=openat /opt/QQ/qq |& grep /opt/QQ
|
可以发现它打开了 /opt/QQ/resources/app 底下的一些 js 文件,这里我们选择 background.js,向这个文件开头添加一些内容:
1 2 3 4 5 6 7 8 9 10 11 12
| (() => { 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 并没有:


于是用 C++ 搓了个库来 Hook 它的 execvp 调用:
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
| #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 查看,还是没办法访问到调试器。
– 未完待续 –