一行代码蒸发了¥6,447,277,680 人民币!

(点击上方公众号,可快速关注)


作者:高金,原文写于4月23日

https://zhuanlan.zhihu.com/p/35989258


现在进入你还是先行者,最后观望者进场才是韭菜。

背景

今天(4月23日)有人在群里说,Beauty Chain 美蜜 代码里面有bug,已经有人利用该bug获得了 57,896,044,618,658,100,000,000,000,000,000,000,000,000,000,000,000,000,000,000.792003956564819968 个 BEC

那笔操作记录是 0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f

640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1

下面我来带大家看看,黑客是如何实现的!

我们可以看到执行的方法是 batchTransfer

那这个方法是干嘛的呢?(给指定的几个地址,发送相同数量的代币)

整体逻辑是

你传几个地址给我(receivers),然后再传给我你要给每个人多少代币(value)

然后你要发送的总金额 = 发送的人数* 发送的金额

然后 要求你当前的余额大于 发送的总金额

然后扣掉你发送的总金额

然后 给receivers 里面的每个人发送 指定的金额(value)

从逻辑上看,这边是没有任何问题的,你想给别人发送代币,那么你本身的余额一定要大于发送的总金额的!

但是这段代码却犯了一个很傻的错!

代码解释

640?wx_fmt=jpeg

这个方法会传入两个参数

_receivers

_value


_receivers 的值是个列表,里面有两个地址

  • 0x0e823ffe018727585eaf5bc769fa80472f76c3d7

  • 0xb4d30cac5124b46c2df0cf3e3e1be05f42119033

_value 的值是 8000000000000000000000000000000000000000000000000000000000000000

我们再查看代码(如下图)

640?wx_fmt=jpeg

我们一行一行的来解释

uint cnt = _receivers.length;

是获取 _receivers 里面有几个地址,我们从上面可以看到 参数里面只有两个地址,所以 cnt=2,也就是 给两个地址发送代币

uint256 amount = uint256(cnt) * _value;

uint256

首先 uint256(cnt) 是把cnt 转成了 uint256类型

那么,什么是uint256类型?或者说uint256类型的取值范围是多少...

uintx 类型的取值范围是 0 到 2的x次方 -1

也就是 假如是 uint8的话

则 uint8的取值范围是 0 到 2的8次方 -1

也就是 0 到255

那么uint256 的取值范围是

0 – 2的256次方-1 也就是 0 到115792089237316195423570985008687907853269984665640564039457584007913129639935

python 算 2的256次方是多少

640?wx_fmt=jpeg

那么假如说 设置的值超过了 取值范围怎么办?这种情况称为 溢出

举个例子来说明

因为uint256的取值太大了,所以用uint8来 举例。。。

从上面我们已经知道了 uint8 最小是0,最大是255

那么当我 255 + 1 的时候,结果是啥呢?结果会变成0

那么当我 255 + 2 的时候,结果是啥呢?结果会变成1

那么当我 0 - 1 的时候,结果是啥呢?结果会变成255

那么当我 0 - 2 的时候,结果是啥呢?结果会变成254

那么 我们回到上面的代码中,

amount = uint256(cnt) * _value

amount = 2* _value

但是此时 _value 是16进制的,我们把他转成 10进制

(python 16进制转10进制)

640?wx_fmt=jpeg

可以看到 _value = 57896044618658097711785492504343953926634992332820282019728792003956564819968

那么amount = _value*2 = 115792089237316195423570985008687907853269984665640564039457584007913129639936

可以在查看上面看到 uint256取值范围最大为 115792089237316195423570985008687907853269984665640564039457584007913129639935

此时,amout已经超过了最大值,溢出 则 amount = 0

下一行代码 require(cnt > 0 && cnt <= 20); require 语句是表示该语句一定要是正确的,也就是 cnt 必须大于0 且 小于等于20

我们的cnt等于2,通过!

require(_value > 0 && balances[msg.sender] >= amount);

这句要求 value 大于0,我们的value是大于0 的 且,当前用户拥有的代币余额大于等于 amount,因为amount等于0,所以 就算你一个代币没有,也是满足的!

balances[msg.sender] = balances[msg.sender].sub(amount);

这句是当前用户的余额 – amount

当前amount 是0,所以当前用户代币的余额没有变动

for (uint i = 0; i < cnt; i++) {

balances[_receivers[i]] = balances[_receivers[i]].add(_value);

Transfer(msg.sender, _receivers[i], _value);

}

这句是遍历 _receivers中的地址, 对每个地址做以下操作

balances[_receivers[i]] = balances[_receivers[i]].add(_value);


_receivers中的地址的余额 = 原本余额+value

所以 _receivers 中地址的余额 则加了57896044618658097711785492504343953926634992332820282019728792003956564819968 个代币!!!

Transfer(msg.sender, _receivers[i], _value); } 这句则只是把赠送代币的记录存下来!!!

总结

就一个简单的溢出漏洞,导致BEC代币的市值接近归0

那么,开发者有没有考虑到溢出问题呢?

其实他考虑了,

640?wx_fmt=jpeg

可以看如上截图

除了amount的计算外, 其他的给用户转钱 都用了safeMath 的方法(sub,add)

那么 为啥就偏偏这一句没有用safeMath的方法呢。。。

这就要问写代码的人了。。。

啥是safeMath

640?wx_fmt=jpeg

safeMath 是为了计算安全 而写的一个library

我们看看他干了啥?为啥能保证计算安全.

function mul(uint256 a, uint256 b) internal constant returns (uint256) {

uint256 c = a * b;

assert(a == 0 || c / a == b);

return c;

}


如上面的乘法. 他在计算后,用assert 验证了下结果是否正确!

如果在上面计算 amount的时候,用了 mul的话, 则 c / a == b 也就是 验证 amount / cnt == _value

这句会执行报错的,因为 0 / cnt 不等于 _value

所以程序会报错!

也就不会发生溢出了...

那么 还有一个小问题,这里的 assert  require 好像是干的同一件事

都是为了验证 某条语句是否正确!

那么他俩有啥区别呢?

用了assert的话,则程序的gas limit 会消耗完毕

而require的话,则只是消耗掉当前执行的gas

总结

那么 我们如何避免这种问题呢?

我个人看法是

  1. 只要涉及到计算,一定要用safeMath

  2. 代码一定要测试!

  3. 代码一定要review!

  4. 必要时,要请专门做代码审计的公司来 测试代码

这件事后需要如何处理呢?

目前,该方法已经暂停了(还好可以暂停)所以看过文章的朋友 不要去测试了...

640?wx_fmt=jpeg

不过已经发生了的事情咋办呢?

我能想到的是,快照在漏洞之前,所有用户的余额情况

然后发行新的token,给之前的用户 发送等额的代币...



觉得本文有帮助?请分享给更多人

关注「程序员的那些事」,提高编程技能

640?wx_fmt=png

资源下载链接为: https://pan.quark.cn/s/67c535f75d4c 在机器人技术中,轨迹规划是实现机器人从一个位置平稳高效移动到另一个位置的核心环节。本资源提供了一套基于 MATLAB 的机器人轨迹规划程序,涵盖了关节空间和笛卡尔空间两种规划方式。MATLAB 是一种强大的数值计算与可视化工具,凭借其灵活易用的特点,常被用于机器人控制算法的开发与仿真。 关节空间轨迹规划主要关注机器人各关节角度的变化,生成从初始配置到目标配置的连续路径。其关键知识点包括: 关节变量:指机器人各关节的旋转角度或伸缩长度。 运动学逆解:通过数学方法从末端执行器的目标位置反推关节变量。 路径平滑:确保关节变量轨迹连续且无抖动,常用方法有 S 型曲线拟合、多项式插值等。 速度和加速度限制:考虑关节的实际物理限制,确保轨迹在允许的动态范围内。 碰撞避免:在规划过程中避免关节与其他物体发生碰撞。 笛卡尔空间轨迹规划直接处理机器人末端执行器在工作空间中的位置和姿态变化,涉及以下内容: 工作空间:机器人可到达的所有三维空间点的集合。 路径规划:在工作空间中找到一条从起点到终点的无碰撞路径。 障碍物表示:采用二维或三维网格、Voronoi 图、Octree 等数据结构表示工作空间中的障碍物。 轨迹生成:通过样条曲线、直线插值等方法生成平滑路径。 实时更新:在规划过程中实时检测并避开新出现的障碍物。 在 MATLAB 中实现上述规划方法,可以借助其内置函数和工具箱: 优化工具箱:用于解决运动学逆解和路径规划中的优化问题。 Simulink:可视化建模环境,适合构建和仿真复杂的控制系统。 ODE 求解器:如 ode45,用于求解机器人动力学方程和轨迹执行过程中的运动学问题。 在实际应用中,通常会结合关节空间和笛卡尔空间的规划方法。先在关节空间生成平滑轨迹,再通过运动学正解将关节轨迹转换为笛卡
以下是一个简单的 MATLAB 代码,用于可视化一滴水滴蒸发模型: ```matlab % 模拟一滴水滴蒸发的过程 % 设置初始参数 r = 10; % 水滴半径,单位为毫米 h = 5; % 水滴高度,单位为毫米 T = 25; % 初始温度,单位为摄氏度 RH = 50; % 初始相对湿度,单位为百分比 t = 0; % 初始时间,单位为秒 % 设置模拟时间 t_end = 3600; % 模拟时间为1小时 % 计算初始表面积和体积 A = pi * r^2; % 表面积 V = (4/3) * pi * r^3; % 体积 % 开始模拟 while t < t_end % 计算当前温度和饱和水汽压 T_sat = 0.6108 * exp(17.27 * T / (T + 237.3)); % 饱和水汽压 P_sat = RH * T_sat / 100; % 当前水汽压 % 计算当前表面积和体积 A = pi * r^2; % 表面积 V = (4/3) * pi * r^3; % 体积 % 计算当前蒸发速率 k = 0.07; % 蒸发系数 m = k * A * (P_sat - 101.3) / V; % 蒸发速率 % 更新水滴半径和高度 dr = -m * t; % 水滴半径变化量 dh = -2 * dr; % 水滴高度变化量 r = r + dr; % 更新水滴半径 h = h + dh; % 更新水滴高度 % 更新时间 t = t + 1; % 时间增加1秒 % 可视化水滴 clf; % 清空画布 hold on; [X,Y,Z] = sphere(50); % 生成球体网格 surf(X*r,Y*r,Z*r + h); % 画出水滴 axis equal; % 设置坐标轴比例相等 axis([-r r -r r 0 2*r]); % 设置坐标轴范围 view(-30,60); % 设置视角 title(sprintf('Water Droplet Evaporation Simulation (t=%.0f s)', t)); % 设置标题 xlabel('x (mm)'); % 设置x轴标签 ylabel('y (mm)'); % 设置y轴标签 zlabel('z (mm)'); % 设置z轴标签 drawnow; % 刷新画面 end ``` 运行代码后,将会生成一张动态的水滴蒸发模拟图像。可以通过改变初始参数以及模拟时间,来探索不同条件下水滴的蒸发情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值