把运维脚本搬上 Linux:五处协议逆向的隐藏坑
一、先定位脚本的真实入口
很多人拿到一份运维脚本,第一反应是看 shebang,然后直接 python xxx.py 就开跑,跑不通就怀疑环境。真正常见的坑是,你根本没在执行你以为的那个文件。举一个真实例子:某备份脚本目录下有 main.py 和 bin/run.py,crontab 里写的是 /opt/ops/bin/run.sh,而 run.sh 第一行是 exec python3 "$(dirname "$0")/../main.py" "$@"。你在终端里改 main.py 改半天没反应,因为实际执行的是 run.sh 转手过去的另一个副本,藏在 /usr/local/lib/ops/main.py。定位入口最可靠的办法不是读脚本,是看进程。先跑
ps -ef | grep -i run.sh
ls -l /proc//cwd
readlink -f /proc//exe
tr '\0' ' ' < /proc//cmdline
/proc//exe 指向解释器,cmdline 里第一段是 python3,第二段才是脚本真实路径,cwd 则暴露了相对路径解析的基准目录。这一招在脚本被 wrapper 套了三层时尤其管用。第二步用 strace 看它到底打开了哪些文件:
strace -f -e trace=openat,execve -o /tmp/tr.log python3 main.py
日志里 execve 那条记录就是最终入口,后面紧跟的一串 openat 会把你没意识到的 import 路径全抖出来。我遇到过 Python 3.6.9 环境下,一个脚本因为 sys.path 里残留了旧的 site-packages,加载了同名但不同版本的 requests,报错是 AttributeError: module 'requests' has no attribute 'Session',版本其实是 2.0.1,而项目要求 2.25.1。整个排查花了 47 分钟,最后靠 python3 -c "import requests; print(requests.file)" 一行定位。入口这关过了,后面谈阻塞点才有意义。
二、用 syscall 还原阻塞点
入口确认后,卡顿到底卡在哪一层,只能靠 syscall 时间线说话。先上带时间戳的跟踪,重点盯 read、write、connect、recvfrom 这几个会真挂住的调用:
strace -f -tt -T -e trace=read,write,connect,recvfrom,select,poll -o /tmp/blk.log python3 main.py
-T 打出每个调用的耗时,一行形如 `connect(3, {sa_family=AF_INET, sin_port=htons(5432), sin_addr=inet_addr("10.20.3.17")}, 16) = -1 EINPROGRESS (Operation now in progress) <0.000021>`,紧跟的 select 上挂着 `<30.001142>`,三十秒整,这就是超时源,不是脚本逻辑慢。我手上那台 CentOS 7 的机器,Python 3.6.9 连着 PostgreSQL,wrapper 里没设 connect_timeout,驱动 psycopg2 缺省直接顶到 30 秒才抛 `psycopg2.OperationalError: could not connect to server: Connection timed out`,运维以为是批处理本身重,实际每次都在等这个连不上的库。定位手法是把 strace 的 -T 输出按耗时排序:
awk -F'<' '//,"",t); if(t+0>1) print t, $0}' /tmp/blk.log | sort -rn | head -20
耗时超过 1 秒的调用全浮到顶,一眼就能把"慢"和"等"分开。另一次是 DNS,`sendto(4, "\24\177\1\0\0\1\0\0\0\0\0\0\3api\7example\3com\0\0\1\0\1", 32, ...)` 之后 recvfrom 卡了 5.002 秒,`/etc/resolv.conf` 里配了两个不可达 nameserver,脚本每个请求都在重复解析。还有日志文件写满磁盘时 write 返回 `ENOSPC`,循环重试把 CPU 打满,看起来像死循环,其实是 strace 里成排的 `write(1, ..., ) = -1 ENOSPC`。把这些挂起点按调用类型归类,阻塞就有了明确归属:网络超时、名字解析、磁盘满、锁等待,而不是一句"脚本很慢"。锁等待看 `flock` 或 `fcntl(F_SETLKW)` 的长时间返回,配合 `lsof -p ` 或 `/proc//stack` 更直观。还原完阻塞点,才谈得上协议层签名为什么对不上。
三、协议签名离线对齐验证
阻塞点定位完,签名对不上往往不是算法写错,而是输入压根不干净。把线上请求的原始字节落盘,离线重算,逐字段对拍,是唯一可靠的对齐手段。先抓样本:在网关侧用 tcpdump 截一包 `tcpdump -i eth0 -s 0 -w /tmp/sig.pcap port 8443`,再从 pcap 里剥出 application 层数据存成 `/tmp/sig.bin`,同时把当时的密钥、时间戳、nonce 从服务端日志里抄出来,三件东西缺一不可。接着离线跑一遍签名函数,把中间量打印出来比对。最常见的坑是编码:Python 里 `json.dumps(payload)` 默认带空格、非 ASCII 会转 `\uXXXX`,而客户端用紧凑序列化,两边 JSON 差一个字节,HMAC 就全错。验证脚本这样写最省事:
raw = json.dumps(payload, separators=(',', ':'), ensure_ascii=False).encode()
print(raw, hmac.new(key, raw, hashlib.sha256).hexdigest())
第二个坑是时间戳单位。服务端记的是 `1699999999` 秒级,移动端发的是 `1699999999123` 毫秒,离线对齐时若拿秒去算,签名永远差一截,对拍时把差值打印出来,差 1000 倍一眼就能看出。第三个坑是参数排序,很多客户端按 key 字典序拼 `k=v&`,但大小写敏感,`AppKey` 和 `appkey` 排序结果不同,得用 `sorted(d.items())` 实际验证。最后是二进制字段:文件上传接口的签名把文件 MD5 拼进去,而离线复现时读的是文本模式,`\r\n` 被转成 `\n`,摘要自然不符,得用 `open(p,'rb')`。对拍通过的标准是离线算出的签名与抓包里的 `X-Sign` 头逐字符相等,若不相等,二分掉 payload 的字段区间,缩小到具体哪个键参与签名、哪个被排除,坑就现形了。
- 点赞
- 收藏
- 关注作者
评论(0)