如何把 QQ 资料卡变得 NB?

真的很 NB 的那种 ( ੭ ˙ᗜ˙ )੭


NB,也就是 non-binary,多指非二元性别。

人尽皆知的 Facebook 五十八种性别(其实大部分只是各人偏好的标签)显得十分麻烦,但 QQ 仍然采取的简单二元分类所带来的困扰也绝对不可忽视 —— 包括奇奇怪怪的好友申请、小窗、广告,无处不在却毫无必要的区别对待,以及非常规性别者早已被科学研究证实存在的各种困扰。

总之,二分类没有想像中那么好。

虽说 QQ 资料卡在注册时确实都是「未填」的状态,可是一旦填写「性别」和「生日」就不能把它们重新改回「未填」了,至少 Android 和 macOS 的官方客户端都不允许这么做。

既然这样,就别怪我动手啦 |•’-’•)و✧

定位目标

本地的客户端是 QQ for Mac V6.6.2 (10090)。(最新版是 6.6.8,莫名好久没有自动检查更新的样子)

1
2
3
4
5
$ cd /Applications/QQ.app/Contents/MacOS
$ md5 QQ
MD5 (QQ) = ab670335a910aad6b6aeb785ced4d87f
$ shasum QQ
35f952a66d87ad163b656f157a9ab9710423850b QQ

首先尝试观察符号表:

1
$ objdump -t QQ | grep [Gg]ender

什么都没有……?看看 strings:

1
$ strings QQ | grep [Gg]ender

跳出的一堆字符串中有几个十分引人注目:

setGender:
setGenderExist:
_gender
_genderExist

看起来有一些 Objective-C 的方法在操作数据,你们跑不掉啦!

1
$ otool -ov QQ | less
0000000102233d40 0x1024c9b78
           isa 0x1024c9ba0
    superclass 0x0 _OBJC_CLASS_$_NSObject
        ...
                      name 0x101d96b8e gender
                     types 0x102046bb0 q16@0:8
                       imp 0x1000a91e9
                      name 0x101d9d9b5 setGender:
                     types 0x102046a0a v24@0:8q16
                       imp 0x1000a91f6 
                        ...
                      name 0x101d96b82 genderExist
                     types 0x1020468b9 c16@0:8
                       imp 0x1000a9315 
                      name 0x101d9da64 setGenderExist:
                     types 0x1020468c1 v20@0:8c16
                       imp 0x1000a9322 

抓到两个属性 gendergenderExist(三单漏掉了喂)的 getter 和 setter!

调试

接着看反汇编:

1
$ lldb QQ
(lldb) dis -s 0x1000a91e9
QQ`___lldb_unnamed_symbol2419$$QQ:    gender
QQ[0x1000a91e9] <+0>:  pushq  %rbp
QQ[0x1000a91ea] <+1>:  movq   %rsp, %rbp
QQ[0x1000a91ed] <+4>:  movq   0xa8(%rdi), %rax
QQ[0x1000a91f4] <+11>: popq   %rbp
QQ[0x1000a91f5] <+12>: retq   

QQ`___lldb_unnamed_symbol2420$$QQ:    setGender:
QQ[0x1000a91f6] <+0>:  pushq  %rbp
QQ[0x1000a91f7] <+1>:  movq   %rsp, %rbp
QQ[0x1000a91fa] <+4>:  movq   %rdx, 0xa8(%rdi)
QQ[0x1000a9201] <+11>: popq   %rbp
QQ[0x1000a9202] <+12>: retq   

(lldb) dis -s 0x1000a9315
QQ`___lldb_unnamed_symbol2443$$QQ:    genderExist
QQ[0x1000a9315] <+0>:  pushq  %rbp
QQ[0x1000a9316] <+1>:  movq   %rsp, %rbp
QQ[0x1000a9319] <+4>:  movsbl 0xd7(%rdi), %eax
QQ[0x1000a9320] <+11>: popq   %rbp
QQ[0x1000a9321] <+12>: retq   

QQ`___lldb_unnamed_symbol2444$$QQ:    setGenderExist:
QQ[0x1000a9322] <+0>:  pushq  %rbp
QQ[0x1000a9323] <+1>:  movq   %rsp, %rbp
QQ[0x1000a9326] <+4>:  movb   %dl, 0xd7(%rdi)
QQ[0x1000a932c] <+10>: popq   %rbp
QQ[0x1000a932d] <+11>: retq   

几个函数都十分简单,不必了解 Objective-C 的调用规范也能很容易看出寄存器 RDI 存放资料卡结构的起始地址,gendergenderExist 两个属性分别位于偏移 0xa8 与 0xd7 处,大小为 gender 8 字节,genderExist 1 字节;对于两个 setters,设置的新值由寄存器 RDX 传入。居然还要动栈帧,这优化也太惨了吧

据此只要设置断点,在命中时覆盖写入的值即可。不过在开始执行之前直接按地址添加断点会导致断点 unresolved,在运行后暂停再设置断点可以绕过此问题。也有一些别的方法,比如用 image lookup 看一下符号名字之类的。

(lldb) r
^C
(lldb) br set -a 0x1000a91f6
Breakpoint 1: where = QQ`___lldb_unnamed_symbol2420$$QQ, address = 0x00000001000a91f6
(lldb) br set -a 0x1000a9322
Breakpoint 2: where = QQ`___lldb_unnamed_symbol2444$$QQ, address = 0x00000001000a9322
(lldb) c

打开自己的资料卡修改性别,就会命中 setGender: 的断点:

Process 829 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x00000001000a91f6 QQ`___lldb_unnamed_symbol2420$$QQ
QQ`___lldb_unnamed_symbol2420$$QQ:
->  0x1000a91f6 <+0>:  pushq  %rbp
    0x1000a91f7 <+1>:  movq   %rsp, %rbp
    0x1000a91fa <+4>:  movq   %rdx, 0xa8(%rdi)
    0x1000a9201 <+11>: popq   %rbp
Target 0: (QQ) stopped.

检查寄存器值:

(lldb) reg read
General Purpose Registers:
       rax = 0x00000000000002a8
       rbx = 0x0000000000003015
       rcx = 0x0000000000000000
       rdx = 0x0000000000000001
       rdi = 0x000000011bd517b0
       ...

这里把性别修改成了♂,所以传入的 RDX = 1。实测用 0 表示未填,1 表示♂,2 表示♀。

步进越过 MOVQ 指令之后检查内存内容,对应 gender 属性的位置确实变成了 1:

(lldb) s
(lldb) s
(lldb) s
(lldb) mem read -a "$rdi + 0xa8"
0x11bd51858: 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x11bd51868: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

此时直接覆盖内存中的内容就可以了。保险起见,顺便把 genderExist 也改成 0:

(lldb) mem write -s 8 "$rdi + 0xa8" 0
(lldb) mem write -s 1 "$rdi + 0xd7" 0
(lldb) mem read -a "$rdi + 0xa8"
0x11bd51858: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x11bd51868: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
(lldb) c

然后回到 QQ 界面提交,目前服务器上并没有做「不能将已填的性别改为未填」这一验证。

Android 客户端上也能看到变化。啊—— 感觉整个人都 NB 起来了呢(划。

还简单试了试把生日改回「未填」,可是没有成功的样子 。°(°¯᷄◠¯᷅°)°。 不过这个倒是无所谓啦,不研究了(

总之做 NB 很难但也很好玩,祝大家玩得开心,早日实现平权 |•’-’•)و✧

作者:Shiqing
链接:https://kawa.moe/2020/07/qq-profile-nb/
版权:文章在 CC BY-NC-SA 4.0 许可证下发布。转载请注明来自 quq