[{"content":"整理 SCUM 相关研究与实践，具体结论以文章标明的版本和验证环境为准。\n","date":"2026-09-17","externalUrl":null,"permalink":"/scum/","section":"SCUM","summary":"整理 SCUM 相关研究与实践，具体结论以文章标明的版本和验证环境为准。\n","title":"SCUM","type":"scum"},{"content":"","date":"2026-09-17","externalUrl":null,"permalink":"/tags/scum/","section":"Tags","summary":"","title":"SCUM","type":"tags"},{"content":"在分析 SCUM 人物创建流程时，我发现了两项技能校验缺陷：负经验值可以抵消正常技能消耗，客户端提交的技能分组也可以决定从哪一项属性预算中扣除点数。\n前者破坏了技能消耗应当非负的计算前提，后者让客户端影响了本应由服务端规则确定的技能归属。两项问题可以独立触发，也可以组合，使正常人物创建界面无法生成的技能配置通过校验。这次漏洞于 8 月 25 日修复，文末记录了提交、确认、修复及测试账号的后续经过。\n漏洞表现 # 正常创建人物时，力量、体质、敏捷和智力分别对应一组技能预算。同一分组内的技能需要共用这部分点数，因此即使人物还有其他属性的剩余预算，也不能随意挪用。\n下面是测试过程中记录的异常表现：\n图：四项基础属性均为 3.0，多项技能显示 100%；选中的高级技能经验值为 9,999,999，接近 10,000,000 的等级上限。\n截图展示了异常技能数据在界面上的表现。判断它为什么能够通过人物创建校验，还需要结合请求字段、技能消耗公式和服务端的实际判断分支。\n人物创建数据如何进入服务端 # 与这次问题有关的数据流可以整理为：\n1客户端填写人物模板 2 ↓ 3序列化人物创建请求（消息类型 9） 4 ↓ 5服务端反序列化人物模板 6 ↓ 7检查技能等级、经验值与预算 8 ↓ 9创建角色并写入档案 人物创建请求的高层结构如下。代码保留必要的字段名称，注释说明其含义：\n1struct CreateUserProfileRequest 2{ 3 FString UserDisplayName; // 用户显示名 4 FCharacterTemplate CharacterTemplate; // 人物模板，包含技能数组 5 uint8_t HasUsedNewPlayerProtection; // 新手保护标记 6 uint8_t EnableDigitalDeluxeBiomeSuit; // 特定服装选项标记 7}; 每项技能与本漏洞相关的序列化字段为：\n1struct SerializedSkillTemplate 2{ 3 uint8_t Attribute; // 客户端声明的技能分组 4 FString ClassName; // 技能标识 5 uint8_t Level; // 技能等级 6 float Experience; // 当前等级内的经验值 7}; 这里描述的是字段的序列化顺序。内存中的技能对象大小为 38h，网络数据按归档规则逐项读写，不能直接用整个内存结构的复制结果代替。\n其中，ClassName 用于标识技能，Attribute 决定预算槽位。二者之间的对应关系本应由服务端技能定义确定。预算槽位的含义如下：\n分组值 对应属性 初始技能预算 0 体质 体质 × 2 1 敏捷 敏捷 × 2 2 智力 智力 × 2 3 力量 力量 × 2 服务端原有校验逻辑 # 以下为还原后的核心伪代码，变量名作了可读性整理。它聚焦人物基础属性、技能数据和预算检查；其他模板检查仍属于完整创建流程的一部分。\n1bool ValidateSkills(const FCharacterTemplate\u0026amp; character) 2{ 3 // 原有逻辑仍检查四项基础属性及总分配值。 4 const float strength = character.Strength; 5 const float constitution = character.Constitution; 6 const float dexterity = character.Dexterity; 7 const float intelligence = character.Intelligence; 8 9 if (strength \u0026lt; 0.9999f || strength \u0026gt; 5.0001f) return false; 10 if (constitution \u0026lt; 0.9999f || constitution \u0026gt; 5.0001f) return false; 11 if (dexterity \u0026lt; 0.9999f || dexterity \u0026gt; 5.0001f) return false; 12 if (intelligence \u0026lt; 0.9999f || intelligence \u0026gt; 5.0001f) return false; 13 14 if ((constitution + strength) + 15 (intelligence + dexterity) \u0026gt; 12.1f) 16 { 17 return false; 18 } 19 20 float budget[4] = 21 { 22 character.Constitution * 2.0f, // 体质 23 character.Dexterity * 2.0f, // 敏捷 24 character.Intelligence * 2.0f, // 智力 25 character.Strength * 2.0f // 力量 26 }; 27 28 float totalBudget = 29 (budget[2] + budget[1]) + (budget[3] + budget[0]); 30 31 for (const FSkillTemplate\u0026amp; skill : character.Skills) 32 { 33 const uint8_t level = skill.Level; 34 if (level \u0026gt;= 4) 35 return false; 36 37 const float experienceLimit = 38 powf(10.0f, level + 4.0f); 39 40 // 只比较上界，未先检查非负性与有限性。 41 if (skill.Experience \u0026gt;= experienceLimit) 42 return false; 43 44 const float completedLevelCost = 45 level * (level + 1) / 2.0f; 46 47 const float currentProgressCost = 48 (skill.Experience / experienceLimit) * (level + 1); 49 50 const float cost = 51 completedLevelCost + currentProgressCost; 52 53 // 直接用客户端提交的分组选择预算槽位。 54 budget[skill.Attribute] -= cost; 55 totalBudget -= cost; 56 } 57 58 // 保留原始反编译中的比较方向，不能随意改成 \u0026lt;。 59 for (float remaining : budget) 60 { 61 if (!(remaining \u0026gt;= -0.1f)) 62 return false; 63 } 64 65 if (totalBudget \u0026lt; -0.1f) 66 return false; 67 68 return true; 69} 技能点消耗由“已取得等级的固定消耗”和“当前等级内经验进度的消耗”组成：\n技能等级 经验值上限，不含边界 技能点消耗 无技能 10,000 经验值 ÷ 10,000 初级 100,000 1 ＋ 2 × 经验值 ÷ 100,000 中级 1,000,000 3 ＋ 3 × 经验值 ÷ 1,000,000 高级 10,000,000 6 ＋ 4 × 经验值 ÷ 10,000,000 更高的技能等级会被创建阶段的等级检查拒绝。每个属性分组的剩余预算与总剩余预算在最后统一检查，并允许约 0.1 点的浮点计算误差。\n漏洞一：负经验值抵消技能消耗 # 负数可以通过上界比较 # 原有经验值检查等价于：\n1if (skill.Experience \u0026gt;= experienceLimit) 2 return false; 负数小于这里的正数上限，因此不会被这条比较拒绝。它随后进入技能消耗公式，降低当前等级进度对应的消耗。\n负经验值会降低消耗，但不一定让总消耗立即变成负数。 对已经包含固定等级消耗的技能，需要负值的绝对值足够大，才能把该技能的总消耗拉到零以下。\n一个高级技能如何增加 34 点预算 # 以等级为 3 的高级技能为例，报告中的有限负数示例可以按以下代码理解：\n1const int level = 3; 2const float experience = -100000000.0f; 3const float experienceLimit = 10000000.0f; 4 5const float completedLevelCost = 6 level * (level + 1) / 2.0f; // 6 7 8const float progressCost = 9 (experience / experienceLimit) * 10 (level + 1); // -40 11 12const float cost = 13 completedLevelCost + progressCost; // -34 14 15budget[attribute] -= cost; // 减去 -34，等于增加 34 这个技能在预算计算中产生了负消耗。服务端从预算里减去负数，反而增加了剩余点数。\n假设人物四项基础属性均为 3，那么体质分组的初始预算为 6。两项同属体质的技能可以形成下面的组合：\n技能 分组 等级 经验值 计算结果 跑步 0 3 9,999,999 消耗约 10 点，单独已经超出 6 点预算 耐力 0 3 −100,000,000 消耗为 −34 点，增加剩余预算 其余技能保持零等级、零经验时，体质分组剩余预算约为：\n16 − 10 − (−34) ＝ 30 最终检查发生在技能遍历结束后。中间出现的超额消耗，被后续负消耗抵消，最后的剩余预算又回到了允许范围。\n浮点数边界需要逐个区分 # 合法经验值应同时满足：\n1std::isfinite(experience) \u0026amp;\u0026amp; 2experience \u0026gt;= 0.0f \u0026amp;\u0026amp; 3experience \u0026lt; experienceLimit 不能把负数、正无穷大、负无穷大和非数值一概视为相同情况：\n有限负数： 可以通过现有上界比较；足够大的负值会产生负消耗，这是本文计算示例使用的路径。 正无穷大： 大于经验上限，会被现有上界比较拒绝，不能宣称它也能直接绕过。 负无穷大： 不会触发这条上界拒绝分支。按所见算术路径推导，它可能产生负无穷消耗和正无穷剩余预算；这属于静态推导，不是特殊浮点输入的完整运行复现。 非数值： 浮点比较会出现无序结果。即使通过了经验上界比较，仍须检查后续分支；在合法分组槽位内，它污染的分类预算会被原始末尾检查拒绝。 汇编与反编译证据 # 下面两段汇编取自当时保存的反汇编记录。指令和寄存器保留原样，地址列及地址生成的标签统一以 xxxxx 遮盖。30h、34h、38h 等结构体字段偏移和对象步长保留，它们用于说明数据如何被访问。\n等级检查与经验值上界比较 # 1; 技能预算校验函数：sub_xxxxx 2xxxxx movzx eax, byte ptr [rsi+30h] ; 读取技能等级 3xxxxx cmp al, 4 4xxxxx jnb loc_xxxxx ; 等级不小于 4，拒绝 5xxxxx movd xmm6, eax 6xxxxx movaps xmm0, xmm9 7xxxxx cvtdq2ps xmm6, xmm6 8xxxxx mov ebx, eax 9xxxxx movaps xmm1, xmm6 10xxxxx addss xmm1, xmm8 11xxxxx call powf ; 计算该等级的经验上限 12xxxxx movss xmm3, dword ptr [rsi+34h]; 读取客户端经验值 13xxxxx movaps xmm4, xmm0 ; 保存经验上限 14xxxxx comiss xmm3, xmm4 15xxxxx jnb loc_xxxxx ; 经验值不小于上限，拒绝 rsi 指向当前技能对象，等级位于对象内偏移 30h，经验值位于 34h。经验值被读入 xmm3，计算出的上限位于 xmm4；comiss 比较二者，后面的 jnb 在普通有序比较下对应“大于或等于”。\n因此，有限负经验值不会触发这里的拒绝分支。结合完整函数的反编译记录，在已分析路径中未观察到进入消耗计算前的零下界和有限性检查。\n客户端分组直接参与索引与扣减 # 1xxxxx mov rax, [rsp+0A8h+var_78] 2xxxxx addss xmm6, xmm10 3xxxxx divss xmm3, xmm4 ; 经验值除以经验上限 4xxxxx test rax, rax 5xxxxx lea rcx, [rsp+0A8h+var_88] 6xxxxx cmovnz rcx, rax ; 取得预算数组 7xxxxx movzx eax, byte ptr [rsi] ; 读取客户端声明的分组 8xxxxx add rsi, 38h ; 指向下一项技能 9xxxxx movss xmm0, dword ptr [rcx+rax*4] 10xxxxx mulss xmm3, xmm6 ; 经验进度对应的消耗 11xxxxx addss xmm3, xmm2 ; 加上等级基础消耗 12xxxxx subss xmm0, xmm3 ; 扣减当前分组预算 13xxxxx subss xmm7, xmm3 ; 扣减总预算 14xxxxx movss dword ptr [rcx+rax*4], xmm0 这段指令直接展示了两个关键事实：\nmovzx eax, byte ptr [rsi] 读取分组字段，[rcx+rax*4] 随即用它选择预算槽位；在这一过程中没有先按技能标识派生真实分组。 技能消耗位于 xmm3，两条 subss 分别从分类预算和总预算中扣除该值。消耗为负时，两处扣减都会增加剩余预算。 这证明了所分析预算路径中的行为。对其他未追踪的调用路径，不能仅凭这一段指令断言其校验情况。\n末尾预算比较不能随意改写 # 末尾分支有完整的反编译记录，现有材料没有保留相应的汇编摘录。下面按原始分支整理变量名，并省略临时存储清理：\n1int index = 0; 2bool accepted = false; 3 4while (budget[index] \u0026gt;= -0.1f) 5{ 6 if (++index \u0026gt;= 4) 7 { 8 if (totalBudget \u0026lt; -0.1f) 9 break; 10 11 accepted = true; 12 break; 13 } 14} 15 16return accepted; 分类预算要求 remaining \u0026gt;= -0.1f 成立才能继续。非数值与有序数比较时不满足这个条件，因此会被拒绝。把它改写成“仅当 remaining \u0026lt; -0.1f 时拒绝”，会改变非数值输入的语义：\n1// 两种写法对普通有限数一致，对非数值并不等价。 2if (!(remaining \u0026gt;= -0.1f)) return false; // 非数值被拒绝 3if (remaining \u0026lt; -0.1f) return false; // 非数值不触发这一分支 总预算单独使用“小于阈值则拒绝”的比较，但它之前已经有四项分类预算检查。不能只看总预算这一句，就认定非数值经验能够通过整套校验。\n这些特殊浮点分支的讨论，以相关运算能够继续执行为前提。原始记录没有运行时浮点异常控制状态，也没有特殊浮点输入的完整运行复现；本文确定的负消耗计算示例使用可正常表示的有限负数。\n漏洞二：客户端伪造技能分组 # 技能身份与预算归属没有绑定 # 技能真实所属的分组属于服务端规则。例如：\n技能标识 技能 正确分组 RunningSkill 跑步 体质，0 EnduranceSkill 耐力 体质，0 ThieverySkill 盗窃 敏捷，1 RiflesSkill 步枪 力量，3 但在已分析的预算校验路径中，扣减使用的是客户端提交值：\n1budget[skill.Attribute] -= cost; 该路径中未观察到根据技能标识查找服务端定义、再核对分组的过程：\n1const FServerSkillDefinition* definition = 2 FindServerSkillDefinition(skill.ClassName); 3 4if (definition == nullptr) 5 return false; 6 7if (skill.Attribute != definition-\u0026gt;Attribute) 8 return false; 这样一来，客户端声明的技能身份与客户端声明的预算分组之间，缺少了服务端规则的约束。\n不使用负数，也可以转移消耗 # 仍以四项基础属性均为 3 为例，每个分组各有 6 点预算。等级 3、经验 0 的高级技能，每项消耗正好为 6。\n正常情况下，跑步与耐力都应从体质预算扣除：\n1体质预算：6 2跑步消耗：6 3耐力消耗：6 4剩余预算：6 − 6 − 6 ＝ −6，应拒绝 如果客户端把耐力的分组改为敏捷，提交的字段组合就变成：\n1// 下列为字段赋值示意，其余人物数据保持合法。 2SerializedSkillTemplate running; 3running.ClassName = TEXT(\u0026#34;RunningSkill\u0026#34;); 4running.Attribute = 0; // 正确：体质 5running.Level = 3; 6running.Experience = 0.0f; 7 8SerializedSkillTemplate endurance; 9endurance.ClassName = TEXT(\u0026#34;EnduranceSkill\u0026#34;); 10endurance.Attribute = 1; // 伪造为敏捷，真实分组应为体质 11endurance.Level = 3; 12endurance.Experience = 0.0f; 两个技能各自扣减不同的预算槽位：\n1体质预算：6 − 6 ＝ 0 2敏捷预算：6 − 6 ＝ 0 3总预算：24 − 12 ＝ 12 所有预算都满足最终检查，但人物实际获得了两个本应共同消耗体质预算的高级技能。这条路径不需要负经验值，伪造分组本身就足以破坏分类预算限制。\n这里讨论的是把消耗转移到 0 至 3 的有效预算槽位。越界分组属于另外的输入安全问题，不能从这一示例直接推导出任意内存读写或代码执行能力。\n两项缺陷组合后的影响 # 两项问题可以配合：先通过伪造分组，让多个技能的消耗落到同一个预算槽，再利用该槽位中的负消耗抵消正常消耗。这会扩大能够通过校验的异常技能组合。\n但修复时必须分别处理两个根因：\n只拒绝负经验值，仍不能阻止技能被划入错误分组。 只校正技能分组，仍不能阻止正确分组内的负消耗抵扣。 触发条件是能够发起人物创建请求，并改变其中的技能数据，不需要服务端管理权限。主要影响是人物初始技能配置、角色成长和多人游戏公平性。若异常值被写入档案，影响还可能延续到后续加载过程。\n现有证据不支持把它描述为任意提高基础属性、设置任意技能等级、接管账户或执行服务端代码。\n修复建议与参考伪代码 # 修复需要同时约束经验值的取值范围，以及技能身份与所属分组之间的关系。以下代码是基于根因整理的建议方案，不代表官方实际采用的补丁实现。\n用服务端定义决定技能分组 # 服务端应先通过受信任的技能表查询技能定义。未知技能应被拒绝，预算扣减使用查到的真实分组。\n1const FServerSkillDefinition* definition = 2 FindServerSkillDefinition(skill.ClassName); 3 4if (definition == nullptr) 5 return false; 6 7const uint8_t attribute = definition-\u0026gt;Attribute; 8if (attribute \u0026gt;= 4) 9 return false; 10 11// 如果为了协议兼容保留客户端分组，也必须验证一致性。 12if (skill.Attribute != attribute) 13 return false; 在输入、单项消耗和剩余预算三个层面检查 # 下面的参考伪代码保留了完整的预算计算过程，同时加入技能白名单、重复项检查、经验值范围检查和中间结果检查。基础属性与技能集合的辅助校验需要按游戏规则实现，示例只保留调用位置；技能表查询假定返回稳定、唯一的规范技能定义。\n1bool ValidateSkillsWithServerRules(const FCharacterTemplate\u0026amp; character) 2{ 3 // 辅助校验的具体规则省略，不能只验证预算。 4 if (!ValidateCreationAttributesByServerRules(character)) 5 return false; 6 if (!ValidateRequiredSkillSetAndCount(character.Skills)) 7 return false; 8 9 double budget[4] = 10 { 11 character.Constitution * 2.0, 12 character.Dexterity * 2.0, 13 character.Intelligence * 2.0, 14 character.Strength * 2.0 15 }; 16 17 for (double remaining : budget) 18 { 19 if (!std::isfinite(remaining) || remaining \u0026lt; 0.0) 20 return false; 21 } 22 23 double totalBudget = 24 budget[0] + budget[1] + budget[2] + budget[3]; 25 if (!std::isfinite(totalBudget)) 26 return false; 27 28 TSet\u0026lt;const FServerSkillDefinition*\u0026gt; seenSkills; 29 30 for (const FSkillTemplate\u0026amp; skill : character.Skills) 31 { 32 const FServerSkillDefinition* definition = 33 FindServerSkillDefinition(skill.ClassName); 34 if (definition == nullptr) 35 return false; 36 37 // 按服务端规范技能定义去重，不按客户端原始名称去重。 38 if (seenSkills.Contains(definition)) 39 return false; 40 seenSkills.Add(definition); 41 42 const uint8_t attribute = definition-\u0026gt;Attribute; 43 if (attribute \u0026gt;= 4 || skill.Attribute != attribute) 44 return false; 45 46 const uint8_t level = skill.Level; 47 if (level \u0026gt; 3) 48 return false; 49 50 const double experience = 51 static_cast\u0026lt;double\u0026gt;(skill.Experience); 52 if (!std::isfinite(experience) || experience \u0026lt; 0.0) 53 return false; 54 55 const double limit = 56 std::pow(10.0, static_cast\u0026lt;double\u0026gt;(level) + 4.0); 57 if (!std::isfinite(limit) || experience \u0026gt;= limit) 58 return false; 59 60 const double cost = 61 level * (level + 1) / 2.0 + 62 (experience / limit) * (level + 1); 63 if (!std::isfinite(cost) || cost \u0026lt; 0.0) 64 return false; 65 66 // 扣减使用服务端定义中的分组。 67 budget[attribute] -= cost; 68 totalBudget -= cost; 69 70 if (!std::isfinite(budget[attribute]) || 71 !std::isfinite(totalBudget)) 72 { 73 return false; 74 } 75 } 76 77 for (double remaining : budget) 78 { 79 if (!std::isfinite(remaining) || remaining \u0026lt; -0.1) 80 return false; 81 } 82 83 return std::isfinite(totalBudget) \u0026amp;\u0026amp; totalBudget \u0026gt;= -0.1; 84} 使用更高精度计算不能替代输入校验。对于异常经验值和不一致的分组，应直接拒绝请求；静默把它们截断为某个“正常值”，容易掩盖异常来源，并导致客户端与服务端对人物状态的理解不一致。\n角色写入数据库之前还应验证完整模板。导入、恢复等写入入口也应复用数值合法性、技能身份与分组一致性校验，并按对应阶段采用预算与成长规则。\n回归测试与既有数据排查 # 测试需要分别覆盖两项缺陷，再检查它们的组合情况：\n测试场景 代表输入 修复后预期 正常创建 规则允许的技能组合 接受 负经验值 −1、−100,000,000、最小有限负数 拒绝 非有限经验值 非数值、正无穷大、负无穷大 全部拒绝 经验值等于上限 高级技能经验为 10,000,000 拒绝 上限前的有效值 高级技能经验为 9,999,999 仅在预算也允许时接受 分组不一致 耐力声明为敏捷分组 拒绝 分组越界 4、255 拒绝并保持服务端稳定 未知或重复技能 未登记标识、同一技能出现两次 拒绝 技能集合不完整 缺少规则要求的技能 拒绝 分类预算超额 两个同组高级技能超出分组预算 拒绝 组合输入 错误分组与负经验值同时出现 拒绝 既有异常档案 负经验、非有限值、分组不一致 按明确策略隔离、修复或拒绝加载 创建预算复核应针对人物创建记录或可靠的初始角色快照。已经正常成长的角色，应按当前技能状态与成长规则排查，不能因为其技能超过出生时的预算，就直接判定异常。排查项目包括重复技能、错误分组、越界经验和非有限数值；数据处置应保留记录，区分历史数据迁移、意外损坏和主动篡改。\n发现与处理经过 # 日期 事件 8 月 7 日至 8 月 10 日 发现漏洞。 8 月 17 日 向官方提交漏洞。 8 月 17 日 官方确认漏洞。 8 月 25 日 漏洞修复。 8 月 26 日 测试账号被封禁。 8 月 30 日 测试账号解封。 账号封禁与解封作为后续事件记录，现有材料没有说明封禁原因。\n这次分析涉及两项需要由服务端保证的约束：技能消耗必须来自合法数值，技能所属分组必须来自服务端规则。只有先建立这两个前提，分类预算与总预算的最终比较才有意义。\n","date":"2026-09-17","externalUrl":null,"permalink":"/scum/negative-experience-bypass/","section":"SCUM","summary":"负经验值能让技能消耗变成负数，伪造技能分组则能把消耗转移到其他属性预算。两项缺陷可以独立存在，也能组合影响人物创建校验。","title":"SCUM 人物创建技能校验漏洞：负经验值与分组伪造","type":"scum"},{"content":"","date":"2026-09-17","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"2026-09-17","externalUrl":null,"permalink":"/tags/%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%A0%A1%E9%AA%8C/","section":"Tags","summary":"","title":"服务端校验","type":"tags"},{"content":"","date":"2026-09-17","externalUrl":null,"permalink":"/tags/%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/","section":"Tags","summary":"","title":"漏洞分析","type":"tags"},{"content":"","date":"2026-09-17","externalUrl":null,"permalink":"/tags/%E9%80%86%E5%90%91%E5%88%86%E6%9E%90/","section":"Tags","summary":"","title":"逆向分析","type":"tags"},{"content":"","date":"2026-09-17","externalUrl":null,"permalink":"/","section":"猪的逆向vlog","summary":"","title":"猪的逆向vlog","type":"page"},{"content":"","date":"2026-09-17","externalUrl":null,"permalink":"/tags/%E9%80%86%E5%90%91%E5%9F%BA%E7%A1%80/","section":"Tags","summary":"","title":"逆向基础","type":"tags"},{"content":"把抽象概念变成可以操作、观察和复现的小实验。\n","date":"2026-09-17","externalUrl":null,"permalink":"/labs/","section":"实验室","summary":"把抽象概念变成可以操作、观察和复现的小实验。\n","title":"实验室","type":"labs"},{"content":"","date":"2026-09-17","externalUrl":null,"permalink":"/tags/%E5%AD%97%E8%8A%82%E5%BA%8F/","section":"Tags","summary":"","title":"字节序","type":"tags"},{"content":"同一段内存，在不同的字节序和整数类型下可能代表不同的数值。这个实验把四个输入字节按地址从低到高排列，并同时显示大小端序下的 uint32、int32 和十六进制数值。\n打开字节序观察器 →\n试一个例子 # 输入 78 56 34 12，也可以连续输入 78563412：\n读取方式 十六进制数值 无符号十进制数值 小端序：低地址存低位字节 0x12345678 305419896 大端序：低地址存高位字节 0x78563412 2018915346 再试试 FF FF FF FF：两种字节序的结果相同，但无符号解释得到 4294967295，有符号解释得到 -1。\n实验边界 # 固定读取 4 字节，采用 32 位整数；有符号数按二进制补码解释。 接受四组两位十六进制字节，或连续八位十六进制字符，不需要 0x 前缀。 所有计算均在浏览器本地完成，不上传输入，不依赖外部库。保存实验目录中的 HTML、CSS 和 JavaScript 文件后，可以离线打开。 本实验用于观察数据表示，不会识别真实文件格式、反汇编指令或推断目标程序的数据类型。实际分析仍需结合字段定义和读取指令。 ","date":"2026-09-17","externalUrl":null,"permalink":"/labs/byte-order/","section":"实验室","summary":"一个可离线运行的静态实验：保持内存字节不变，观察读取顺序与符号解释如何影响数值。","title":"字节序观察器","type":"labs"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"记录开发中的具体问题、解决过程和验证结果。\n","externalUrl":null,"permalink":"/posts/","section":"技术文章","summary":"记录开发中的具体问题、解决过程和验证结果。\n","title":"技术文章","type":"posts"},{"content":"从可复核的证据出发，记录分析过程、适用版本与尚待验证的问题。\n","externalUrl":null,"permalink":"/reverse/","section":"逆向研究","summary":"从可复核的证据出发，记录分析过程、适用版本与尚待验证的问题。\n","title":"逆向研究","type":"reverse"}]