pwn做题流程:通过漏洞拿到shell(/bin/sh),一般flag文件就在shell的默认目录下,直接cat flag
新手区通关,完结撒花!
0x01 get_shell
nc连接后直接进入shell,ls可以看到当前目录下有flag,cat输出即可
绿色箭头处是输入的shell命令
0x02 CGfsb
使用到了python2的一个模块pwn(pip时Ubuntu下是pwn、CentOS7中是pwntools,但是python中的模块就叫pwn),不能在windows下使用。
1 | #Ubuntu下安装pwn |
先来看一下pwn模块的使用:
然后分析本体程序
23行存在printf格式化漏洞。
先来看一下相关知识:
我浅显地总结一下:
常见的printf有两(及以上)个参数,如printf("%d", &a);
。但其实printf只需要一个参数,printf("%d%d");
,需要参数时从栈顶依次读入即可。前面的printf("%d", &a);
本质上就是先把a的地址压入栈内,然后printf读栈顶元素。
printf函数的第一个参数就是格式化字符串
1 | %d - 十进制 - 输出十进制整数 |
特别说一下:
%n
,如printf("abcdefg%n");
,%n前面写了7个字符,所以向ESP(栈顶指针)所指向的地址处所指向的地址处(ESP指向了栈顶,此处存储着一个地址)写入7。%10$n
,与%n
类似,不过是向[ESP + 10]处指向的内存处写入7。
结合本题,目的是将pwnme改为8,所以要构造长度为8的字符串(要包含pwnme的地址,否则你怎么把%n的结果传过去?),其后跟着%?$n
,这个?
是字符串中pwnme地址在栈中的位置(相对ESP的偏移)。
pwnme是在bss区,地址不会变动,恒为0x804A068
所以只需要找到偏移量即可。
message输入:AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
AAAA(十六进制0x41414141)的目的是标记,便于发现此字符串在栈中的位置。
0xffe269be是栈顶元素,后面的依次往后排。可以发现0x41414141是第十个,即偏移量为10.
可以构造payload:p32(0x804A068) + 'aaaa%10$n'
或'aaaa' + p32(0x804A068) + '%11$n'
,
aaaa只是为了占位,满足长度为8而已。
1 | from pwn import * |
1 | from pwn import * |
0x03 when_did_you_born
此题太过暴力,爱蛙人士强烈谴责。
在两次判断v6是否等于1926的中间将v6改为1926即可拿到flag。
可以通过get(&v5);
栈溢出。
v5在sp(栈顶),v6在sp+8,所以get的值只需要前8个字符随意,紧跟着一个1926,皆可改变[sp+8]([sp+8]表示sp+8处的值),即v6.
1 | from pwn import * |
0x04 hello_pwn
比前面0x03简单一些。
read时先写四个任意字符,到达0x60106c处,然后写入nuaa的倒序,即aaun。
因为dowrd_60106c是int类型,x86一般以Little-Endian存储(高地址存高位,低地址存低位)。但是我们的payload是字符串,一个字节就是一个字符,而数据存储整体上来看是由高地址往低地址延伸,所以对于字符串,高地址存靠前的字符,低地址存靠后的字符。正好和dowrd相反。
1 | from pwn import * |
0x05 level0
数组长为0x80,但是可以读入0x200个字节。
栈中结构:
+0008处的r是返回地址,程序return后从r所存储地址处开始运行。所以我们写满0x80个字符(buf),再写满0x08个字符(s),之后,我们就可以覆写r所存储的地址值了。
0x400596处是一个调用shell的函数,所以覆写的r为0x400596
1 | from pwn import * |
调出shell后要自己输入cat flag哦~
0x06 level2
缓冲区0x88,允许读入0x100。明显的缓冲区溢出。
发现/bin/sh
字符串在0x804A024,名为hint.
system
函数的地址是0x8048320。这里有个大坑要注意:
有_system和system两个函数,我们找的是_system的地址。
_system:
system:
system中的extrn system:near
声明一个外部近指针system,具体内容可在稍后定义。
1 | from pwn import * |
payload组成:0x88个缓冲区字符,0x04个覆盖ebp地址的字符,覆写返回地址为system函数(system的栈帧中的ebp),p32(0)填充ebp+4,p32(bin_sh)自然就是system的参数(ebp+8)喽~
参考阅读《加密与解密》P106.
对了,使用p32()是因为这是32位程序。
0x07 string
主函数。动态分配内存(malloc),地址赋给v3。然后v3赋给v4,相当于v3和v4都指向同一地址。然后给出v4指向的地址和下一个第一个地址。
输入name,没有漏洞。
选择east或up。但是选up会被dragon干掉,所以必须选east.
嗯,下图printf(&format, &format);
存在格式化字符串任意写的漏洞。可参考0x02 CGfsb
最关键的一步:
第17行是将v1转化为可执行函数。
本题没有出现system函数,所以要在此处写个shellcode。
当我们在获得程序的漏洞后,就可以在程序的漏洞处执行特定的代码,而这些代码也就是俗称的shellcode。
但是,要运行至此处,要先满足if ( *a1 == a1[1] )
a1是前面提到的v4传入函数的形参,就是个地址。a[0]=v4[0]=v3[0]=68
, a[1]=v4[1]=v3[1]=85
。要将a[0]和a[1]修改为相同的值。
可以通过前面提到的格式化字符串漏洞来修改。
函数sub_400BB9()内的v2是我们输入的v4的地址,我们需要知道v2在栈内的位置,这样才能通过%?$n
向v2指向的地址处写入字符串长度。
1 | #查看sub_400BB9()栈内情况 |
上面程序为什么这么写,待会在后面的正式的交互代码中解释。这里只说一下最后一句。p.recvuntil('I hear it')
必须要写上,否则程序的debug末尾只能看到发送了数据,看不到之后print的format字符串。如下图:
上上图选中处,0xc23010是v2的内容,因为v2在format(就是许下的愿望wish)的前面一位,而通过0x41414141(图中是0x2d70252d41414141,是因为这是64位程序)可以找到format的起始位置。v2是栈内第7个参数。
所以wish就写成%85c%7$n
,作用是将85写入栈内第7个参数所指向的地址。
1 | from pwn import * |
获得执行system(“/bin/sh”)汇编代码所对应的机器码:asm(shellcraft.sh())
。注意要指明arch和os。arch有i386(x86)和amd64(x64)。攻防世界的题解区有人说这个函数失效,其实是因为他没指明环境。不同环境下的汇编代码是不同的。
代码的第二段从printf("secret[0] is %x\n", v4, a2);
输出的字符串中,提取v4的地址,注意把末尾的\n
剔除。
然后代码的第四段Give me an address,注意源代码中_isoc99_scanf("%ld", &v2);
,读入的不是字符串,是int64,是个数字,不要输入0x开头的字符串,也不要类似于1003fd2c的十六进制字符串,就输入一个十进制数字就行。不要使用p64()转换!!!int转换即可,但是send发送的是一个字符串,所以再str一下。
总结一下POP攻击链:通过格式化字符串漏洞修改v4[0]的值,使之与v4[1]相等。然后读入shellcode并运行。(当然你也可以改v4[1]的值)
嗯,最后我想说一下shellcode的撰写。正好前两天,学长PwnHt讲了讲这方面的内容。
参考链接:
LINUX 系统调用表
- 32位: http://shell-storm.org/shellcode/files/syscalls.html
- 64位: https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/
算了,具体内容我掌握的还不行,所以先放一放吧。过段时间再回来补。
TODO
0x08 guess_num
猜10次数字,范围1~6。猜对直接拿到flag.
本题考查随机数函数rand().
漏洞点在于:
相同的环境下,如果不设定(srand)一个种子(seed)的话,每次随机到的随机数序列是相同的。比如种子为0时,在linux下的随机数序列为{2,5,4,2,6,2,5,1,4,2,…}
(想要每次运行时得到的随机数序列不同,可以每次运行都设定一个不同的种子,比如时间)
所以我们可以通过输入v7时覆盖掉seed[0],使得种子恒定。然后本地用C语言,针对某一种子,求出其随机数序列中的前十个。
1 |
|
注意windows和linux求出的随机数序列不同,这是个坑。
Ubuntu下gcc 8.cpp -o 8
,得到可执行文件8。运行可得:
1 | from pwn import * |
0x09 int_overflow
整数溢出。
读入长度为0x199的passwd。
若passwd长度介于[4,8),则将passwd写入栈内。
目标是覆写上图的返回地址r,改为调用system("cat flag");
的函数的地址。
要想写dest,首先要过长度介于[4,8)的判断。
这里有整数溢出的漏洞。
对于_int8(8bit),最大为255。当传入256时,高位截断,取其地位,故256和1相等。
所以我们可以构造passwd的长度介于[255+4,255+8)
1 | from pwn import * |
emmmm,开始时第5行的p.recv()
我没写,出现错误如下图:
明显看出先输入了aaaaaaaaa(passwd),再输出的Please input your passwd:
猜测原因是read(0, &s, 0x19u);
没读完0x19个字符。
添加上p.recv()
,收到输出信息时再输入passwd.(上面的程序)
下面的程序也可以:
1 | from pwn import * |
*0x18
是因为还要有个\n
0x0A cgpwn2
和0x06 level2原理相同,唯一的区别在于此题没有cat flag
或/bin/sh
的字符串,需要自己构造。
name位于bss区,可将字符串写入name变量。
1 | from pwn import * |
当然你也可以写入/bin/sh
,拿到shell后再cat flag
0x0B level3
给出了libc。存在write函数。
- 用write函数泄露出write的真实内存地址(通过泄露got表中对应的write条目实现)
- 然后利用write函数真实内存地址减去给的libc中write函数的偏移得到imageBase(libc加载到内存中的基址)
- 最后imageBase加上libc中system的偏移就是真实内存地址了,/bin/sh地址同理。
构造ROP执行system
思路:程序流程非常简单,可以突破的点只有read函数。通过覆盖返回地址,执行两次main函数。第一次泄漏write函数的地址,第二次执行system函数。
参考:
JarvisOJ-PWN-Writeup专题-level3 (非原题)
1 | from pwn import * |
payload1组成:缓冲区0x88个字符,长为4的ebp,返回地址覆写为PLT表中的write地址,接着是运行完write()后的返回地址(main的地址,因为等下payload2还要再次利用vulnerable_function()这个函数,所以要运行完write()要再次运行main()),之后就是write()的三个参数,其中第一个随便写,第二个是write的内容的地址(GOT表中write的地址,其指向内存中的write的地址。个人理解,存疑,TODO),第三个是write()内容的长度。
然后发送payload1,接受输出的write在内存中的地址。求出libc基址(内存中write的地址减去libc中write相对libc基址的偏移),求出system和/bin/sh
在内存中的地址。
然后就是喜闻乐见的payload2,应该不用解释,因为前面的题中解释了好几次了。
关于GOT,PLT
动态链接时,因为不知道模块加载位置,将地址相关代码抽出,放在数据段中就是got表。
为了实现地址的延迟绑定,再加了一个中间层,是一小段精巧的指令,用于在运行中填充got表。这些指令组成plt表。
参考《程序员的自我修养》第7章动态链接
存放函数地址的数据表,称为重局偏移表(GOT, Global Offset Table),而那个额外代码段表,称为程序链接表(PLT,Procedure Link Table)。它们两姐妹各司其职,联合出手上演这一出运行时重定位好戏。
我个人理解GOT里面放的是函数在内存中的地址,不知道理解地对不对。