最近尝试给一台 Pixel 4a 备用机配 Samba 服务,目标很简单:让手机作为文件共享服务端,能在 WiFi 和热点网络上运行。本来安装配置什么都顺利,但客户端就是连不上。
一开始我以为又是用户名、密码或者协议版本的问题。结果顺着 Samba 客户端和服务端两边的 NTLMSSP parser 一路追进去,最后发现这次还真不是 Samba 配置写错了,而是 Android 的 tagged pointer 和 Samba 的指针边界检查撞在了一起。
0x00 先看症状
客户端的报错很典型:
Failed to parse the NTLMSSP Challenge: (#1)
gensec_spnego_client_negTokenTarg_step: SPNEGO(ntlmssp) login failed: NT_STATUS_INVALID_PARAMETER
session setup failed: NT_STATUS_INVALID_PARAMETER
这个错误很容易把人带到错误的方向上。比如先检查密码有没有输错,再检查 Samba 用户有没有创建成功,接着换几个用户名、协议版本、workgroup,最后甚至开始怀疑 Windows 客户端的兼容性。
我做的第一件有用的事情,是换一个完全独立的客户端测试。手机上的 smbclient 无法解析服务端发回来的 Challenge;换成另一台 Linux 环境里的 Samba 客户端后,Challenge 可以正常解析,但服务端在收到 Authenticate 包时又报了同样的 NT_STATUS_INVALID_PARAMETER。
这说明问题不是某个客户端实现,也不像是密码验证失败。通信双方都已经走到了 NTLMSSP,但在真正检查密码之前,某一段 packet parser 就把合法的数据判成了非法数据。
0x01 Samba 在检查什么
Samba 4.24.6 的 NTLMSSP 解析最终会走到 msrpc_parse()。这里会读取一系列带有 offset 和 length 的安全相关缓冲区,例如用户名、域名、工作站名以及 NTLM response。
相关代码里的边界检查大致是这样:
#define offset_outside_range(base, end, offset) (((end) - (base)) < (offset))
#define ptr_overflow(ptr, offset, type) \
offset_outside_range((ptr), ((type *)INTPTR_MAX), (offset))
这些检查本意是防止 offset 把指针带到缓冲区之外。逻辑看起来很朴素:从当前指针到地址空间上限还有多少空间,如果剩余空间小于 offset,就说明发生了溢出。
问题在于,它默认拿到的是一个普通的、没有额外信息的指针。Samba 这里使用了指针减法,而不是把地址转换成整数后再做明确的范围判断。
完整的相关实现可以直接看 Samba 的源码:
0x02 Android 的指针为什么不普通
Android 在 64 位进程上可能启用原生堆指针 tagging:分配出来堆指针的最高字节会带一个实现相关的 tag。硬件和内核支持 ARM Top-byte Ignore,所以正常的内存访问可以忽略这个字节。
Android 官方文档对这个机制有详细说明,也特别提到了 Pixel 4 这一代内核具备所需的 TBI 支持:
https://source.android.com/docs/security/test/tagged-pointers
在这台手机上实际打印出来的 heap 地址是:
base=0xb4000077414fc960 max=0x7fffffffffffffff
最关键的不是地址长什么样,而是把 Samba 原本的 ptr_overflow 宏原样拿出来跑了一遍:
offsets: 0=1 12=1 56=1 88=1 112=1 346=1 422=1
连 offset 为 0 都被报告成了 overflow。
这就把问题钉死了:0xb4 是 Android 放进指针最高字节的 tag,而 Samba 用这个带 tag 的指针去和 INTPTR_MAX 做范围计算。对于正常的 NTLMSSP 安全缓冲区,检查结果自然会变成“越界”。
所以之前看到的两个错误其实是一回事:
- 手机上的 Samba 客户端去解析服务端 Challenge 时失败;
- 手机上的 Samba 服务端去解析客户端 Authenticate 时失败。
数据包本身没有坏,密码也还没有来得及验证,parser 就先退出了。
0x03 解决方案:只对 smbd 关闭 pointer tagging
最干净的长期方案,当然是给 Samba 打补丁:不要直接拿带 tag 的 pointer 做这种范围计算,而是先转换成整数地址,并且在 Android 上去掉 top byte,再做溢出检查。然后重新编译 Termux 的 Samba 包。
不过我现在需要的是一个可用、影响范围尽可能小的修复。因此没有去全局关闭 Termux 的 heap tagging,也没有修改整个 Android 应用的 manifest,而是利用 Bionic 提供的 allocator 控制,只在 smbd 进程启动时关闭它。
具体做法是编译一个非常小的 LD_PRELOAD 注入库:
#define _GNU_SOURCE
#include <malloc.h>
/* Termux 的 malloc.h 没有声明这个 Android-specific 选项。 */
extern int mallopt(int option, int value);
__attribute__((constructor))
static void disable_android_heap_pointer_tags(void)
{
(void)mallopt(-204, 0);
}
在手机自己的 Termux 环境中编译:
cc -O2 -fPIC -shared -Wl,-z,relro,-z,now \
-o "$PREFIX/lib/samba/libandroid-no-heap-tags.so" \
android-no-heap-tags.c
然后只把它放到 Samba 的启动命令上:
LD_PRELOAD="$smbd_preload" "$smbd" \
--foreground \
--debug-stdout \
--configfile="$runtime_config" &
这里有几个细节比较重要:
- 只注入
smbd,不影响其他进程; - 需要改写 runit wrapper,启动前检查注入库是否存在,避免丢失;
-204是 Android/Bionic 的实现细节,不是可移植的 Samba API,Android 或 Termux 升级后需要重新验证。
这不是凭感觉乱试的。临时 Samba 实例加载这个注入库后,客户端可以完成 NTLMSSP 认证;去掉它则立即恢复之前的 parser 错误。
0x04 验证结果
重启经过注入的 Samba 服务,使用独立的 Samba 客户端尝试连接并执行:
smbclient //127.0.0.1/Internal \
-p 14445 \
-U 'pixel4a%<password>' \
-m SMB3 \
-c ls
结果常用的 SMB2, SMB2_10, SMB3, SMB3_11 协议均能正常使用。并且都能正常列出手机共享目录,所以认证和实际访问都走通了。
0x05 结论:这算不算一个好的 hack?
严格来说,这仍然是一个 dirty hack,而不是 Samba 上游意义上的修复。
它的优点是影响面很小:不改 Samba 协议、不降低 SMB 安全设置、也不需要重打整个 Termux APK,对于当前这台手机,通过注入库能直接解决问题。
它的缺点也很明确:mallopt(-204, 0) 是 Android/Bionic 相关的实现细节,不应该假设在所有 Android 版本上都有效。未来最理想的处理方式,还是给 Samba 上游提 Android 兼容补丁
另外,这个修复针对的是 Samba 服务端。若以后需要在手机上直接运行 Termux 自带的 smbclient 客户端,它本身也有同样的 bug,因此也需要用同样注入库去启动;外部 Windows、Linux 等客户端则不需要这个额外处理。所以我觉得注入库的修复方式对我已经够好够用了。
这次排查最有意思的地方,是整个问题表面上看起来非常像密码不对,但真正失败的位置却在密码验证之前。Android 的 pointer tagging 对普通应用、甚至 C++、Python 等高级语言基本是透明的,Samba 也确实可以在常规 Linux 上正常工作。两者单独看都没有问题,组合到 Android/Termux 的 aarch64 进程里,就刚好触发了一个兼容 bug。
最后的修复也没有什么神秘之处:找出是哪个环节需要兼容处理,把影响范围控制在 samba 进程里,然后用独立客户端验证完整协议路径。所谓 hack,至少要先把它 hack 在正确的地方。
0x06 结语
此次 hack 完全由 GPT 5.6 Luna Max 独立调查完成并总结,经由我整理发布为本文。
回顾上次更新博文,都已经快两年前了。短短两年间我见证了 LLM 的迅速崛起,不由得感叹如今计算机软件业已经被彻底颠覆。光拿本篇的问题来说,如果用古法着手解决,没一两个周末恐怕完全搞不定,但如今我用相当便宜的 5.6 Luna Max,在短短四五十分钟内就做完了全套 debug 分析验证修复,这在以前是不可想象的。所以我觉得以后应该越来越没有机会、也没有必要再撰写这类短篇技术博文,甚至连我们这种技术博客都会加速消失,只留下些许古法非遗技术的情怀慢慢消失😢。谁知道未来会怎么样呢?
但我还保持谨慎乐观——以往那些不可想象的软件工程实践,要么是人力时间成本问题,要么是复杂度问题,现在因为 LLM 的出现而有了无限可能——也许天花板只在于我们的想象力,只有想不到的,没有做不到的?
未来这个博客应该会慢慢转型(如果还存在的话),这两年间我也积累了不少 Vibe、Agent 编程的心得经验,将来有机会写出来记录一下。