抓包改价包拆解:服务端金额校验与防篡改回归
简介面向希望了解移动端抓包与请求调试的初学者及测试人员这份Fiddler专题资料包围绕商品价格请求的抓包分析与修改演示展开配套工具、视频教程和文档说明可用于本地实验环境中的抓包流程学习与问题排查。压缩包共52个文件整体约10.62MB包含exe与dll工具程序、txt配置与说明、wav音频素材、dat数据文件以及少量js、config、ico和png等辅助文件覆盖工具运行、示例素材与教程文档等用途。目前已有9964人学习下载热度较高。读者可据此了解Fiddler会话捕获、请求参数观察和模拟器配合调试等环节并通过教程录屏与文字材料对照理解工具配置、数据定位和结果验证思路适合作为抓包入门练习与调试排错的参考。1. 抓包改价包拆解先看服务端金额校验再谈工具怎么用预发环境里把订单请求里的金额从 199 元改成 1 元接口返回 200订单详情却还是 199 元——这种“客户端改了、服务端没认”的场面是抓包调试里最典型的一课。抓包改价包不是一把万能钥匙它本质上是抓包工具配置、证书安装说明、请求重放脚本和一组金额校验用例的合集。它解决的是在自有系统、授权测试环境里把请求拦下来、改字段、看服务端是否重算金额、是否校验签名、是否留下审计日志。适合后端开发、测试和安全审计人员做支付链路回归不适合拿去碰第三方真实交易。先记住一句话能不能改成功取决于服务端把不把客户端金额当可信输入而不是抓包工具多强。2. 压缩包落地监听端口、证书信任链与订单请求定位2.1 环境隔离与监听端口拿到压缩包后先别急着开抓包。第一件事是把测试边界划出来只连本地或预发测试域名只使用测试账号和沙箱商品数据抓包工具只监听本机回环地址。常见做法是给测试域名配一条 hosts 记录例如把order.test.local指到本地 mock 服务或测试网关再把抓包工具的监听地址设为127.0.0.1、端口设为8080。这样做的原因是订单请求里往往带登录态和业务标识直接对真实交易链路抓包既容易误伤线上数据也会把敏感头写进 HAR 文件。压缩包里的工具配置一般会包含监听地址、端口、过滤规则和脱敏头列表。下面这份配置可以直接照着改重点是只抓目标域名和订单路径不把整机流量都收进来。listen: host: 127.0.0.1 port: 8080 capture: include_hosts: - order.test.local include_paths: - /api/order/* redact_headers: - authorization - cookie export: format: har output: ./capture/order.har这段配置里listen.port是抓包工具在本机开的监听端口工具界面里填同一个值即可include_hosts和include_paths是白名单避免把埋点、静态资源、第三方脚本一起抓进来redact_headers决定导出 HAR 时哪些头要打码authorization和cookie必须列进去。我一般还会把x-request-id保留下来后面查日志时能靠它把一次请求串起来。导出格式选 HAR是因为它既能被浏览器开发者工具打开也能被 Python 脚本解析比截图和手抄参数可靠得多。环境隔离还要注意两件事。第一测试账号不要复用个人账号商品 SKU 用沙箱数据订单金额用分做单位避免出现199和19900混用。第二抓包结束后把工具监听关掉确认系统转发设置已经恢复否则第二天本地服务连不上排查半天才发现是端口还占着。这个动作很小但血泪经验是抓包环境不干净后面所有结论都不可信。2.2 证书安装与 HTTPS 解密订单接口基本都是 HTTPS抓包工具要解密就得让客户端信任它生成的根证书。压缩包里的教程通常会写“安装证书”但真正容易翻车的是信任链位置桌面端要装到系统受信任根证书区移动端测试包要显式允许用户证书发布包不要带这个配置。先在命令行确认目标测试域名握手正常再去看抓包工具里的请求是不是明文。openssl s_client -connect order.test.local:443 -servername order.test.local -showcerts /dev/null 2/dev/null \ | openssl x509 -noout -subject -issuer -dates这条命令只做一件事打印测试域名当前证书的主题、签发者和有效期。-servername不能省否则多域名共用入口时可能拿到默认证书-showcerts把证书链带出来方便判断是自签证书还是测试网关证书。如果这里就报证书过期或域名不匹配先修测试环境不要急着怀疑抓包工具。证书链没问题再导入抓包工具根证书导入后重启客户端让新信任链生效。移动端自有测试包常见做法是通过network_security_config.xml只在 debug 构建里信任用户证书release 构建保持默认。下面这段只适合测试包不能带进发布包。network-security-config debug-overrides trust-anchors certificates srcuser / certificates srcsystem / /trust-anchors /debug-overrides /network-security-configdebug-overrides表示只在 debuggable 构建生效srcuser是允许用户安装的证书srcsystem保留系统根证书。这样测试机上装完抓包工具根证书就能解密正式包不会继承这个信任。注意只对自有测试应用这样做第三方应用不要动证书信任配置测试结束把用户证书移除避免测试机长期处在宽松信任状态。2.3 过滤与定位订单请求抓包工具一开请求列表会刷得很快真正要看的只有下单、改单、支付前校验这几类请求。我的习惯是先按 Host 和 Path 过滤再看 Method 和状态码最后才看请求体字段。订单请求里常见字段包括amount、total、price、quantity、sign、timestamp、nonce但不同系统命名不一样所以不要一上来就全局搜amount先把候选请求缩小到/api/order/*。压缩包里的 HAR 可以直接用脚本过一遍输出候选请求和命中的金额字段。下面这段只读 HAR不修改任何请求适合先做侦察。import json import sys from urllib.parse import urlparse def load_har(path): with open(path, encodingutf-8) as f: return json.load(f) def candidates(har): rows [] for entry in har[log][entries]: req entry[request] url urlparse(req[url]) if url.hostname ! order.test.local: continue if not url.path.startswith(/api/order/): continue if req[method] not in (POST, PUT, PATCH): continue body_text req.get(postData, {}).get(text, ) fields [] try: body json.loads(body_text) if body_text else {} fields [k for k in body if k in ( amount, total, price, quantity, sign, timestamp, nonce )] except json.JSONDecodeError: pass rows.append({ method: req[method], url: req[url], status: entry[response][status], fields: fields }) return rows for row in candidates(load_har(sys.argv[1])): print(row)脚本里的sys.argv[1]是 HAR 路径运行方式就是python3 find_order.py capture/order.har。url.hostname限定测试域名避免把其他环境请求混进来url.path.startswith(/api/order/)是路径白名单fields只打印命中的敏感字段名不打印字段值防止金额和签名进日志。输出里如果看到status是 200 但fields里没有金额说明金额可能在表单、加密体或 header 里下一步再查postData.mimeType和请求头。定位到候选请求后先复制一份原始请求做基线再在测试环境改一个字段做对照。不要同时改金额、数量、签名和时间戳否则返回 409 你都不知道是哪个校验拦的。常见做法是第一轮只改amount第二轮只改quantity第三轮改amount并重算签名。每轮只动一个变量结果才可解释。注意抓包和解密只用于自有系统或明确授权的测试环境测试数据不要包含真实用户信息和真实交易凭证。3. 改金额为什么翻车四条金额信任边界与本地拦截实验3.1 订单金额的四条信任边界很多教程把“改金额”讲成抓包工具里点一下“打断点”、把amount改成 1、放行然后看接口返回。这个演示只能说明请求发出去了说明不了业务真的按 1 元成交。订单金额至少经过四条边界客户端展示金额、下单请求里的金额、服务端重算金额、支付渠道回执金额。客户端展示金额只影响页面下单请求金额是客户端可篡改的输入服务端重算金额才是业务应收支付渠道回执金额是最终对账依据。四条边界里只要有一条把客户端金额当真就可能出现资损反过来服务端重算加签名覆盖加回执对账改请求体基本都会在某一层被拦下来。信任边界常见实现被篡改后的表现正确做法客户端展示金额页面渲染、缓存页面显示 1 元订单仍按原价只做展示不作为下单依据下单请求金额请求体amount接口 200订单金额不变或变 1 元服务端按 SKU 和数量重算服务端重算金额商品表、价格中心返回 409 或按重算价下单金额单位统一重算结果落快照支付渠道回执支付单、对账单回执金额与订单不符挂起回执校验加差异告警这张表里最容易被忽略的是“接口 200 但订单金额不变”。有些系统下单接口只做参数格式校验真正落库是异步任务异步任务又按商品表重算所以抓包改金额看起来“成功”实际订单没变。测试时不能只看接口状态码要查订单快照、支付单、库存流水三处是否一致。还有一种情况是服务端把金额当字符串比较199和199不相等直接返回参数错误这种属于实现粗糙不是安全校验。3.2 用本地 mock 服务验证篡改是否被拦截要判断服务端有没有把客户端金额当可信输入最直接的办法是搭一个本地订单服务故意接收篡改金额看它返回什么。下面这段用 Python 标准库起一个最小服务商品价格写死在服务端客户端传来的amount只用于比对不用于计算应收。from http.server import BaseHTTPRequestHandler, HTTPServer import json PRODUCTS {sku_1001: 19900} # 单位分 class Handler(BaseHTTPRequestHandler): def _reply(self, status, payload): body json.dumps(payload, ensure_asciiFalse).encode(utf-8) self.send_response(status) self.send_header(Content-Type, application/json; charsetutf-8) self.send_header(Content-Length, str(len(body))) self.end_headers() self.wfile.write(body) def do_POST(self): length int(self.headers.get(Content-Length, 0)) raw self.rfile.read(length) if length else b{} try: data json.loads(raw) except json.JSONDecodeError: return self._reply(400, {code: BAD_JSON}) sku data.get(sku) client_amount data.get(amount) server_amount PRODUCTS.get(sku) if server_amount is None: return self._reply(404, {code: SKU_NOT_FOUND}) if not isinstance(client_amount, int): return self._reply(400, {code: AMOUNT_TYPE_INVALID}) if client_amount ! server_amount: return self._reply(409, { code: AMOUNT_MISMATCH, client_amount: client_amount, server_amount: server_amount }) return self._reply(200, { order_id: ord_local_001, sku: sku, amount: server_amount }) if __name__ __main__: HTTPServer((127.0.0.1, 8000), Handler).serve_forever()运行python3 order_guard.py后服务监听127.0.0.1:8000。PRODUCTS是服务端价格表单位用分避免浮点误差client_amount只做整数类型和相等性校验金额不一致返回 409并在响应里带回服务端金额方便测试断言。这里故意不用客户端金额计算订单是因为真实系统里应收金额应该由服务端价格中心、优惠计算、运费规则共同决定客户端传上来的值最多只能作为“用户预期金额”做提示。用 curl 发一个篡改请求观察返回curl -i -X POST http://127.0.0.1:8000/api/order \ -H Content-Type: application/json \ -d {sku:sku_1001,quantity:1,amount:1}预期是HTTP/1.1 409响应体里code为AMOUNT_MISMATCHserver_amount为19900。如果返回 200说明这个 mock 服务缺少金额重算如果返回 500说明类型校验或异常处理没兜住。把amount改成19900再发一次应该返回 200这一步是基线证明服务本身能正常下单。测试用例要同时覆盖 1、0、-1、超大值、字符串金额和缺字段别只测一个 1。3.3 重放、幂等与日志追踪改金额测试经常伴随重放同一个请求连续发两次看会不会生成两笔订单。如果服务端没有幂等键第二次请求可能又下一单测试同学以为“改价成功”实际是重复下单。正确做法是客户端生成Idempotency-Key服务端用唯一索引或 Redis 键做去重重复请求返回同一笔订单。import os import uuid import requests base os.environ[TEST_BASE_URL] key str(uuid.uuid4()) headers { Authorization: fBearer {os.environ[TEST_TOKEN]}, Idempotency-Key: key, Content-Type: application/json } payload {sku: sku_1001, quantity: 1, amount: 19900} first requests.post(f{base}/api/order, headersheaders, jsonpayload, timeout5) second requests.post(f{base}/api/order, headersheaders, jsonpayload, timeout5) assert first.json()[order_id] second.json()[order_id], 重复请求生成了不同订单TEST_BASE_URL只能指向本地或测试域名TEST_TOKEN用测试账号令牌Idempotency-Key每次测试用例生成一个同一个用例的两次请求复用同一个键。断言比较order_id相同才算幂等生效。如果服务端返回 409 表示“请求处理中”也算合理但要有明确状态码约定。日志里至少要记录request_id、client_amount、server_amount、decision、idempotency_key这样出现金额差异时能直接定位是客户端篡改、服务端重算还是异步任务覆盖。4. 授权测试复现从 HAR 导出到断言脚本的闭环4.1 测试用例设计与参数表抓到订单请求后不要随手改一个数字就结束。我一般先列用例矩阵把“改什么、预期什么、证据放哪”写清楚再跑脚本。这样即使换一个人复现也知道每一步在验证哪条边界。用例编号前置条件篡改字段预期结果证据AMT-001测试 SKU 价格 19900 分amount1409 或 400订单不落库响应体、订单查询AMT-002同上amount-1400参数非法响应体、审计日志AMT-003同上amount19900200订单金额 19900订单快照AMT-004同上amount为字符串400类型错误响应体AMT-005同上删除amount400缺参响应体AMT-006同一幂等键重放两次不改金额两次返回同一订单订单号对比表格里的“预期结果”要写成可断言的状态码或字段不要写“应该失败”这种模糊描述。AMT-001 到 AMT-005 都在验证服务端是否重算金额和校验类型AMT-006 验证幂等。每跑完一轮把响应体、订单号、日志片段放进同一个目录文件名带用例编号后面回归时直接对比。4.2 抓包导出为可重放请求HAR 里带 Cookie 和 Authorization不能直接贴进脚本。常见做法是只保留content-type、accept、x-request-id这类必要头令牌从环境变量注入请求体原样保留但只用于测试域名。下面脚本从 HAR 里挑第一条订单 POST 请求生成一个可读的重放模板。import json import os import sys with open(sys.argv[1], encodingutf-8) as f: har json.load(f) target None for entry in har[log][entries]: req entry[request] if req[method] POST and order.test.local in req[url]: target req break if not target: raise SystemExit(没有找到订单请求) keep {content-type, accept, x-request-id} headers { h[name]: h[value] for h in target[headers] if h[name].lower() in keep } headers[Authorization] Bearer os.environ[TEST_TOKEN] body target.get(postData, {}).get(text, {}) print(import os, requests) print(headers %r % headers) print(payload %s % body) print(resp requests.post( os.environ[TEST_BASE_URL] /api/order, headersheaders, jsonpayload, timeout5)) print(print(resp.status_code, resp.text))sys.argv[1]是 HAR 路径keep是允许保留的头白名单authorization和cookie不在里面避免令牌进代码库TEST_TOKEN和TEST_BASE_URL从环境变量读测试脚本里不写死。生成的模板只指向测试域名跑之前先确认TEST_BASE_URL不是生产地址。请求体里的金额字段可以保留因为这是测试用例的一部分但不要把真实用户订单导出到公共目录。4.3 断言脚本与结果记录重放模板只是把请求发出去真正有价值的是断言。下面脚本把篡改金额请求打到本地或测试服务断言服务端必须拦截并把结果写进report.json。import json import os import requests base os.environ[TEST_BASE_URL] token os.environ[TEST_TOKEN] headers { Authorization: fBearer {token}, Content-Type: application/json, X-Request-Id: case-amt-001 } payload {sku: sku_1001, quantity: 1, amount: 1} resp requests.post(f{base}/api/order, headersheaders, jsonpayload, timeout5) assert resp.status_code in (400, 409), ( f服务端没有拦截篡改金额: {resp.status_code} {resp.text} ) report { case: AMT-001, status: resp.status_code, body: resp.json(), decision: blocked } with open(report.json, w, encodingutf-8) as f: json.dump(report, f, ensure_asciiFalse, indent2) print(拦截通过, resp.status_code)base必须是本地或测试域名token用测试账号payload.amount是最小单位1 表示 1 分不是 1 元断言只接受 400 或 409200 一律视为漏洞。report.json里保留状态码和响应体后面做回归时可以直接比对decision字段。脚本跑通后把 AMT-001 到 AMT-006 串成一个循环每个用例单独写报告失败就退出非零状态码方便接到 CI 里。5. 避坑记录证书、重放、幂等与日志的五次翻车5.1 证书装了仍失败信任位置不对现象抓包工具里目标域名一直握手失败浏览器却能打开测试站换一台机器又正常。原因通常不是证书没装而是装错了位置移动端应用只信任系统根证书用户证书对发布包不生效有的测试包还开了证书固定只认指定签发者。解决先确认是自有测试包还是第三方应用。自有测试包用 debug 构建的network-security-config只对测试环境放开用户证书release 包不要带证书固定场景换测试域名或测试签发者别在生产包上动。第三方应用不要改信任配置测试边界到此为止。5.2 返回 200 但订单金额没变异步重算覆盖了现象抓包改amount1下单接口返回 200订单详情还是 199 元再过几秒看支付单金额也没变。原因可能是服务端同步接口只做参数接收真正落库是异步任务异步任务按商品表重算金额也可能是订单缓存没刷新或者查询走了只读库。解决别只看下单接口查订单快照、支付单、库存流水三处在日志里搜request_id看client_amount和server_amount是否都记录了如果异步任务重算测试用例应该断言最终订单金额等于服务端价格而不是接口返回体。这个坑很容易让人误判“改价成功”实际业务没受影响。5.3 重放触发重复下单缺幂等键现象同一个请求连发两次生成两笔订单库存扣了两次测试同学以为第二次是改价成功。原因是没有Idempotency-Key或者服务端收了键但没做唯一约束。解决客户端每次下单生成一个键同一个业务动作复用服务端用数据库唯一索引或缓存占位重复请求返回第一次的订单号测试用例 AMT-006 专门验证两次请求返回同一order_id。注意幂等键要跟用户和业务场景绑定不能全局一个固定值否则不同订单会被误判为重复。5.4 移动端抓包不生效网络栈和证书固定现象测试机 Wi-Fi 里填了监听地址桌面端能抓到请求移动端应用就是不走抓包工具换一个老版本应用又能抓。原因可能是应用走了独立网络库、QUIC 或证书固定也可能是测试包没有开启调试日志。解决只在自有测试构建里处理关闭 QUIC、统一网络库、打开请求日志确认测试包信任用户证书如果是第三方应用不要继续尝试绕过直接放弃。这个边界要写进测试说明不然新人会在这上面耗一整天。5.5 日志里找不到请求过滤规则和直连现象抓包工具里能看到请求服务端日志里搜不到request_id或者反过来日志有记录但 HAR 里没有。原因可能是抓包白名单把路径过滤掉了也可能是客户端对本地地址走了直连或者请求被重试到了另一个实例。解决先把过滤规则临时放宽到只按 Host 过滤确认请求能进来再检查NO_PROXY类直连配置和测试域名解析最后用x-request-id在网关日志、应用日志、异步任务日志里三段串联。不要只看一个日志文件订单链路的请求经常跨服务。6. 把抓包结果变成防御清单三个校验点和一套回归习惯6.1 三个校验点抓包改价测试的终点不是“改成功了”而是把结果变成防御清单。我现在会盯三个校验点服务端是否按 SKU 和数量重算金额签名是否覆盖金额和数量幂等键和支付回执是否对得上。第一点决定客户端金额是不是可信输入第二点决定请求体被改后签名会不会失效第三点决定重放和异步回调会不会造成重复扣款。三个点都过才敢说这条支付链路能扛住抓包篡改。校验点实现位置失败响应回归断言服务端重算下单服务、价格中心409AMOUNT_MISMATCH篡改金额必须非 200签名覆盖网关、签名服务401 或 400SIGN_INVALID改金额后签名校验失败幂等与回执订单库、支付回调重复请求返回同一单两次请求order_id相同6.2 回归脚本落点回归脚本不要写得太聪明固定用例矩阵就够了。下面这段只做一件事把金额边界值跑一遍失败就退出。import os import requests base os.environ[TEST_BASE_URL] token os.environ[TEST_TOKEN] cases [ {amount: 1, expect: (400, 409)}, {amount: -1, expect: (400,)}, {amount: 19900, expect: (200,)}, ] for case in cases: resp requests.post( f{base}/api/order, headers{Authorization: fBearer {token}}, json{sku: sku_1001, quantity: 1, amount: case[amount]}, timeout5 ) assert resp.status_code in case[expect], (case, resp.status_code, resp.text)TEST_BASE_URL只指向测试环境cases里每个金额都对应一个明确预期断言失败时把用例、状态码和响应体一起打出来排查不用猜。这个脚本我一般放在订单服务的tests/目录下改支付相关代码就先跑一遍。从那以后我每次动支付相关逻辑都强制先跑一轮本地篡改回归把金额改成 1、负数、超大值看服务端是不是一律拒绝再看日志里有没有留下client_amount和server_amount。抓包工具只是放大镜真正要修的是服务端的信任边界。希望帮到你。本文还有配套的精品资源点击获取