bpf_probe_read_user 的返回值被丢弃,被标记的指针会静默产出全零数据
bpf_probe_read_user 的返回值被丢弃,被标记的指针会静默产出全零数据
现象
process_SSL_data() 忽略 bpf_probe_read_user() 的返回值,无论成功与否都发送事件。读失败时,
事件里带的是缓冲区原有的内容 —— 也就是零。在 Android 上,堆指针的高字节带有标记
(TBI, Top-Byte-Ignore),于是每一次读取都失败,工具看起来在工作,而每条载荷都是零。
证据
kern/openssl.h 的 process_SSL_data():
event->data_len = (len < MAX_DATA_SIZE_OPENSSL ? ... : MAX_DATA_SIZE_OPENSSL);
bpf_probe_read_user(event->data, event->data_len, buf); /* 返回值被丢弃 */
bpf_perf_event_output(ctx, &tls_events, BPF_F_CURRENT_CPU, event, ...);用 DEBUG=1 构建(打开 debug_bpf_printk)后,读
/sys/kernel/tracing/trace_pipe(需先 echo 1 > /sys/kernel/tracing/tracing_on):
bpf_trace_printk: (OPENSSL) bpf_probe_read ssl_ver_ptr failed, ret: -14
bpf_trace_printk: (OPENSSL) bpf_probe_read ssl_bio_ptr failed, ret: -14
bpf_trace_printk: (OPENSSL) len=42 ptr=b400006e8853ec8a ret=-14
bpf_trace_printk: (OPENSSL) len=90 ptr=b400006f21e3d000 ret=-14
bpf_trace_printk: (OPENSSL) len=166 ptr=b400006f21e3d000 ret=-14-14 是 -EFAULT。注意指针:每一个都以 b40000 开头,那是 Android 的指针标记,
真实地址是低 56 位。bpf_probe_read_user() 不会去掉它。
长度本身是对的(42、90、166),所以失败在地址,不在长度。
影响
同一台设备、同一目标、各跑一次:
| 版本 | 解出的载荷 | 零字节占比 | 可打印字符 |
|---|---|---|---|
| 2.6.0 发布版 | 557,875 B | 100.0% | 0.0% |
| 去掉指针标记后 | 126,667 B | 0.1% | 88.1% |
发布版写出了 2.3 MB、223 条「载荷」记录,没有任何报错,全是零。输出里没有任何东西 表明读取失败了。
建议的修法
读用户内存前掩掉高字节:
#define UNTAG(p) ((void *)((unsigned long long)(p) & 0x00FFFFFFFFFFFFFFULL))用在 kern/openssl.h 中指针的来源处(probe_entry_SSL 的 PT_REGS_PARM1 / PT_REGS_PARM2,
以及 probe_SSL_set_fd 里的 SSL 指针)。对未标记的地址这是空操作,因为其高字节本就是零。
另外,与掩码无关的一点:读失败至少应当上报或跳过。ssl_ver_ptr 与 ssl_bio_ptr 的调试
输出本来就有,只有载荷这一次读没有检查。把静默的错误答案变成可见的错误,本身就值得做。
说明
这也解释了为什么在此类库上 keylog 模式拿不到东西:它通过同一个被标记的指针去读 SSL
结构体。
复现环境
DEBUG=1 CROSS_ARCH=arm64 ANDROID=1 make nocore
ecapture tls -m text --libssl=<库路径>可用改动与构建工作流(基于 master,保留完整历史): https://github.com/YuYuKunKun/ecapture-openssl-android
Source: gojue/ecapture