ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

浮点数精度泄露:从Claude额度异常看系统安全设计缺陷

浮点数精度泄露:从Claude额度异常看系统安全设计缺陷 1. 从一次“意外”的发现说起浮点数精度泄露的隐秘角落前几天我在一个技术社群里潜水看到有人分享了一个关于Claude订阅的截图上面显示了一个奇怪的数字比如“剩余额度999999.999999”。起初大家以为是P图或者显示Bug一笑而过。但我的职业病犯了——作为一个常年和二进制、内存、协议逆向打交道的人这种“过于规整”的异常值往往意味着背后有故事。它不像是一个简单的整数更像是一个浮点数在特定条件下的溢出或精度极限表现。这让我想起了早年游戏修改、软件破解中一个经典的手法内存扫描与浮点数锁定。很多程序内部尤其是涉及资源、货币、经验值等数值计算时出于性能或设计习惯会使用单精度float或双精度double浮点数来存储。而浮点数在计算机中的表示是有精度限制和特定格式的。Claude作为一个AI服务其订阅额度管理系统无论是前端展示还是后端校验逻辑有没有可能也存在类似的“缝隙”呢所谓“浮点数逆向破解”并不是指去攻击OpenAI的服务器或者破解其加密算法——那是不现实且不合规的。这里的“破解”更多是指一种技术探究通过客户端如网页、API返回数据暴露的有限信息结合对浮点数在计算机中存储、传输、解析全流程的理解去推测、验证甚至复现其内部额度计算或校验逻辑中的潜在缺陷。这是一种纯粹本地的、基于数据格式的分析技术其核心价值在于理解系统设计的薄弱环节提升自身对数据安全与完整性的认知。注意本文讨论的所有技术细节均基于公开的、可观察的客户端行为和数据格式进行原理性分析旨在分享计算机科学中浮点数处理的知识点及其在安全领域的启示。严禁用于任何实际攻击、欺诈或违反服务条款的行为。技术是把双刃剑请务必用于合法合规的学习与研究。2. 浮点数额度存储的“阿喀琉斯之踵”为什么浮点数会成为这类系统的一个潜在风险点要理解这一点我们需要深入浮点数的本质。2.1 浮点数的内存表示与精度陷阱计算机中的浮点数遵循IEEE 754标准。以最常见的双精度double64位为例它由三部分组成1位符号位S、11位指数位E和52位尾数位M。一个数字的实际值大致等于(-1)^S * 1.M * 2^(E-1023)。这个设计带来了两个关键特性也是“漏洞”的来源精度有限52位尾数决定了其有效数字位数大约是15-17位十进制数。对于额度这种通常被认为是“整数”的概念当数值非常大比如超过9万亿时浮点数无法精确表示每一个整数。例如数字9007199254740993即2^531在双精度浮点数中就无法与90071992547409922^53区分开来它们会被存储为同一个值。这就是著名的“大整数精度丢失”问题。特殊的边界值浮点数定义了一些特殊的位模式比如正无穷大Infinity、负无穷大-Infinity和非数字NaN。这些值在算术运算中会产生特定的传播效果。例如任何数除以0.0在浮点数运算中会得到Infinity。想象一下Claude的额度系统假设后端用Java的double或Python的float来存储用户剩余额度。前端通过API请求获得这个值然后展示。如果因为某个逻辑错误如边界条件未处理、数值运算溢出导致后端意外地将一个超大整数或一个非法运算结果如1.0/0.0赋给了这个额度字段那么通过API传输到前端的就是一个特殊的浮点数值。2.2 数据在传输链路上的“变形”问题往往不止发生在存储环节。一个额度值从后端数据库到用户屏幕可能经历这样的旅程数据库整数/浮点数 - 后端业务逻辑浮点数运算 - 序列化为JSON数字 - 网络传输 - 前端JavaScript解析 - 前端显示逻辑。这其中每一步都可能引入问题JSON序列化JSON本身不区分整数和浮点数只有一个“数字”类型。一个非常大的整数超过2^53在序列化成JSON时如果语言如Python的json模块默认使用浮点数来序列化大数字就会导致精度丢失。反序列化到JavaScript其数字只有基于IEEE 754的双精度浮点数一种类型时这个精度丢失的值就被固定下来了。前端JavaScript的“宽容”JavaScript是动态类型语言它对数字的解析非常“努力”。当你尝试显示一个Infinity时它可能会显示为“Infinity”字符串也可能在参与某些计算后被转换成其他形式。如果前端显示逻辑没有对这类特殊值进行过滤和格式化就可能直接把Infinity或一个极其长的浮点数字符串扔到页面上形成我们开头看到的“999999.999999”之类的怪异显示。这个显示值正是我们进行逆向分析的起点。3. 逆向分析实战从怪异显示到逻辑推测当我们看到一个异常的额度显示时如何一步步逆向推测其背后发生了什么这不是直接修改内存而是一个逻辑推理和假设验证的过程。3.1 信息收集与模式识别假设我们在浏览器中通过开发者工具的网络Network选项卡捕获到了调用额度查询的API响应。响应体可能是一个JSON{ remaining_quota: 1.8446744073709556e19, subscription_tier: plus }或者更糟糕的情况{ remaining_quota: Infinity, subscription_tier: plus }第一步分析数值本身。1.8446744073709556e19这个数字看起来很眼熟。让我们计算一下2^64 ≈ 1.8446744e19。这强烈暗示后端可能使用了一个64位无符号整数uint64_t来存储额度但在某个环节被当作有符号整数处理或者发生了溢出然后被转换/解释为了一个浮点数。Infinity则直接指向了除零等非法运算。第二步尝试本地复现。我们可以在本地用Python或JavaScript快速写个脚本模拟这种转换# Python 模拟 import struct import json # 假设一个巨大的uint64值比如 0xFFFFFFFFFFFFFFFF (即2^64-1) big_int 2**64 - 1 print(f原始大整数: {big_int}) # 尝试直接放入Python的float双精度 as_float float(big_int) print(f转换为float: {as_float}) print(f以科学计数法显示: {as_float:.16e}) # 模拟JSON序列化与反序列化 data {quota: big_int} json_str json.dumps(data) # 默认情况下Python的json会将超过一定范围的int转为float print(fJSON字符串: {json_str}) parsed_data json.loads(json_str) print(f解析后quota的值和类型: {parsed_data[quota]}, {type(parsed_data[quota])})运行这段代码你会看到big_int在转换成float和经过JSON一圈后精度已经丢失并且值发生了变化。这个变化后的值如果和API返回的值接近那就验证了我们的猜想。3.2 构造假设与边界测试基于收集到的信息我们可以形成几个假设假设A整数溢出额度计算逻辑中存在整数溢出漏洞。例如剩余额度 总额度 - 已用额度如果“已用额度”在某些情况下被错误地计算为一个负数由于有符号/无符号混淆那么减法可能变成加法导致结果超过最大值发生环绕wrap-around或溢出。假设B浮点数特殊值注入某个API参数或内部状态被意外设置为了NaN或Infinity并在后续的算术运算中传播到了额度字段。假设C序列化/反序列化Bug后端在将数据库中的数值准备给API时使用的序列化库存在缺陷错误地处理了某些边界数值。如何测试这些假设我们无法直接测试Claude的后端但可以在本地构建一个模拟环境。例如用Node.js写一个简单的额度计算服务故意引入整数溢出// 模拟一个有整数溢出风险的额度计算使用JavaScript的BigInt避免实际溢出但模拟逻辑 function calculateRemainingQuota(total, used) { // 假设total和used是64位无符号整数范围的值 // 在C/C中如果used被错误地当作有符号负数且total很小那么 total - (-used) 会变成巨大的数 // 这里我们模拟一个错误当used是特定值时我们错误地将其取反 let simulatedUsed used; if (used SOME_TRIGGER_VALUE) { // 假设的触发条件 simulatedUsed -used; // 错误的操作本意可能是其他计算 } // 模拟溢出如果结果超过64位最大值取其低位模拟环绕 const MAX_UINT64 (1n 64n) - 1n; let result BigInt(total) - BigInt(simulatedUsed); result result MAX_UINT64; // 按位与模拟64位截断 // 将这个可能很大的整数转换为Number即JS的浮点数返回模拟API响应 return Number(result); } // 测试 const total 1000; const used 18446744073709551615n - 500n; // 一个巨大的“负数”当被解释为无符号时 console.log(calculateRemainingQuota(total, used)); // 可能会输出一个巨大的浮点数通过这样的模拟我们可以观察在不同输入下输出是否会出现类似1.8446744e19这样的“魔数”。如果模式匹配那么假设A的可能性就大大增加。3.3 前端解析与显示逻辑的探查除了API数据前端如何显示也至关重要。打开浏览器开发者工具在控制台Console里可以直接查询显示额度那个DOM元素的数据来源。是直接innerText了API返回的数字吗还是经过了某个格式函数// 在包含额度页面的浏览器控制台执行 const quotaElement document.querySelector([包含额度数据的元素选择器]); console.log(元素内容:, quotaElement.textContent); console.log(元素数据属性:, quotaElement.dataset); // 追踪可能存在的格式化函数 // 1. 在Sources面板搜索 remaining_quota, quota, formatNumber 等关键词。 // 2. 查看Network响应在Preview或Response里看原始数据。有时前端为了“美化”显示会对过大的数字进行格式化比如转换成“999.9k”、“1.0M”等。如果这个格式化函数没有处理好Infinity或超出其处理范围的超大数字就可能原样输出科学计数法字符串或“Infinity”。找到这个格式化函数就找到了怪异显示的直接原因。4. 漏洞的根源与安全设计启示通过上面的逆向分析我们实际上是在进行一场“数字考古”和“逻辑推理”。那么从工程角度看这类问题的根源是什么又该如何避免4.1 常见根源剖析类型混淆这是最经典的错误。后端用uint64_t存储但在某个RPC框架序列化、某个中间件计算、或某个ORM映射时被隐式转换成了int64_t、double或float。不同语言、不同库之间的类型边界非常模糊。缺乏输入验证与边界检查额度计算相关的API参数如扣减额度值如果没有被严格限制在合理范围内如正数、小于当前余额恶意或异常的请求可能导致内部状态出现非法值。异常处理不完整在进行除法、开方等可能产生Infinity或NaN的运算时没有用try-catch或条件判断进行保护让这些特殊值污染了业务数据流。序列化/反序列化库的默认行为许多JSON库为了兼容性会默认将无法精确表示为JS Number的大整数转为浮点数。开发者如果没有意识到这一点或者没有启用库的“大整数序列化为字符串”选项就会埋下隐患。前端对数据的盲目信任前端直接显示后端返回的数值没有进行有效性校验和防御性格式化。对于金融、额度等关键数据前端至少应该判断是否为有限数isFinite()并对异常值进行降级UI显示如“额度计算中”或“--”。4.2 防御性编程实践如何构建健壮的额度系统以下是一些具体建议后端数据源头核心模型使用整数额度、金额等业务核心数据在数据库和内存中永远使用整数类型如BIGINT,int64并以最小单位存储例如美元以美分存储Token数以整数个存储。这从根源上避免了浮点数精度问题。定义清晰的数据边界在业务逻辑层为所有数值型参数和字段定义明确的有效范围最小值、最大值并在入口处进行严格校验。使用高精度计算库如果确实需要复杂的小数运算如按比例分配使用专门的高精度计算库如Java的BigDecimalPython的decimal.Decimal并在最终存储前转换为整数。谨慎处理序列化在API序列化时对于可能超过JS安全整数范围Number.MAX_SAFE_INTEGER即2^53-1的整数字段强制序列化为字符串。这是现代API设计尤其是金融科技领域的最佳实践。完整的异常处理在所有数值运算周围包裹异常捕获确保任何算术异常如除零都能被优雅处理返回明确的错误码而不是让特殊值渗透。前端数据消费端防御性解析解析API响应时对数值字段进行类型和有效性检查。function safeParseQuota(apiResponse) { const value apiResponse.remaining_quota; // 如果是字符串形式的数字先转换 const num typeof value string ? parseFloat(value) : value; // 检查是否为有效数字且非无穷大 if (typeof num ! number || !isFinite(num) || num 0) { console.error(Invalid quota value received:, value); return 0; // 或显示一个默认错误状态 } // 如果数字过大进行友好格式化 return formatLargeNumber(num); }友好的UI格式化使用成熟的库如Intl.NumberFormat来格式化大数字和货币这些库通常能更好地处理边界情况。5. 从技术探究到安全思维的转变这次对“浮点数额度破解”的探究其价值远不止于理解一个潜在的显示Bug。它更像是一个切入点引导我们审视整个软件数据流中的信任链条。在分布式系统、前后端分离的架构下一个数据从产生到消费路径漫长环节众多。每个环节对数据的假设、处理方式都可能不同。安全往往就崩塌在这些不一致的假设和薄弱的环节连接处。浮点数精度问题只是一个具体的表现形式其背后是“数据一致性”、“类型安全”和“边界守卫”这些更根本的工程命题。对于开发者而言应当养成“数据溯源”和“不信任原则”的习惯。对于关键业务数据要能清晰地回答它从哪里来源头类型经过哪些处理转换逻辑到哪里去最终展示每个环节是否可能改变其语义对于外部输入包括来自其他模块甚至同一系统内其他服务的输入都要进行验证和清洗。对于安全研究人员或爱好者这种分析训练的是“敏感度”和“联想能力”。一个异常的显示、一个奇怪的网络包、一个超出预期的返回值都可能是通往系统深层逻辑的一扇窗。通过合法的、本地的分析手段去推测其背后的实现不仅能满足技术好奇心更能极大地提升在代码审计、安全评估中发现潜在风险的能力。最后必须再次强调所有技术探索都应在法律与道德的红线之内。本文所揭示的原理和思路目的是为了加固我们自己的系统理解防御之道而非提供攻击之矛。在数字世界里真正的“破解”是破解我们对技术复杂性的无知构建起更安全、更可靠的软件基石。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进