当前位置:网站首页 > 技术博客 > 正文

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/1vRCd4bMkqnY1nT2uhSYw 提取码: 5rx6
后文的许多题解用的都是这个课中的例题

1、linux 下的命令行中自带了文本解码的工具,解码 base64:
2、python3 中可以用 用垃圾字符 a 从左向右填充这个字符串到长度为num
3、python 是一个很好的计算器
4、 在程序中寻找含有sh的字符串

1、一切从 开始
2、打开连接
本地:
远程: (后文以此为例)
3、此时 ”io“ 这个对象已经和本地或远程的一个进程连接上了,我们可以对io进行一系列操作
4、从服务器接收数据
接收一行
全接收完
通过这种方法接收数据可以得到最本真的数据,包括转义字符等东西也会全部显示出来
在读到 0x 之前一直读入数据
读入 14 位数据
5、向服务器发送数据
例如: 或 (意思是讲 这个整数打包成 64 比特宽度的字节流的形式发送出去,如果是 32 位的程序就用 p32() )
需要注意的是,send() 中填的数据类型必须为字节流,而不是对象,因为对象并不是用二进制表示的编码,如果要发送字符串,也不能直接发送 "",因为 py 中用引号括起的字符串是一个对象,我们需要用 b"" 将它转化为一个bite类型的数据发送出去
6、 进入交互模式
7、 函数可以得到调用shell的汇编代码,我们可以用 将它转换为机器码,并发送给待攻击的服务器
8、关于 64 位程序
(1)必须用 把环境转成 64 位
(2)获取 shellcode 必须用
(3)16 进制转字节流必须用 elf = ELF('https://www.cnblogs.com/X0H3M1/articles/程序名')elf.plt['system']next(elf.search(b'/bin/sh'))context.log_level = 'debug'` 打开很有用的调式模式

1、 进入 pwndbg 动态调试( gdb 没写反)
2、 或 或 在某处设置断点
3、 运行程序 步过 步进
4、 查看多少栈
5、 显示虚拟内存空间的分布
6、 查看当前的断点 删除某一个断点
7、 (也就是 continue 的缩写)让程序继续执行到下一个断点或结束
8、 查看 got 表
9、 查看 printf 函数的真实地址
10、 查看该地址后 10 个内存单元的内容
11、 查看该地址信息,包括偏移等
12、 查看堆块内存分布
13、 查看堆信息
14、 查看所有的变量信息
15、 查看 bss 段起始位置

1、the NX bits:栈不可执行
2、ASLR:内存随机化
3、PIE
4、Canary(金丝雀):
5、RELRO

函数调用栈的过程是十分复杂的,这里简单记一下笔记 多了我也写不明白

1、基础的寄存器

函数调用栈主要涉及到三个寄存器:
esp(栈指针寄存器):存储当前栈顶的位置,也就是始终指向栈顶
ebp(基址指针寄存器):存储当前函数状态的基地址,指向当前系统栈中最顶部的栈帧的底部
eip (指令指针寄存器):存储 CPU 读入指令的地址,CPU 通过 eip 读取即将执行的指令

2、汇编基础

需要记住的汇编指令有:
mov A, B:将 B 赋值给 A ,也就是
pop A:将当前栈顶的值赋给 A ,然后弹出这个值
push A:将 A 入栈
ret:等效于 ,将栈顶的值(也就是 return address)赋给eip,让cpu执行那里的指令
call addr:调用函数

3、调用函数

调用一个函数时,先将堆栈原先的基址(EBP)入栈,以保存之前任务的信息。然后将栈顶指针的值赋给EBP,将之前的栈顶作为新的基址(栈底),然后再这个基址上开辟相应的空间用作被调用函数的堆栈。函数返回后,从EBP中可取出之前的ESP值,使栈顶恢复函数调用前的位置;再从恢复后的栈顶可弹出之前的EBP值,因为这个值在函数调用前一步被压入堆栈。这样,EBP和ESP就都恢复了调用前的位置,堆栈恢复函数调用前的状态。(来源于EBP 和 ESP 详解_测试开发小白变怪兽的博客-CSDN博客_ebp)

1、开始做题,拿到程序之后,用 查看文件信息,用 查看保护措施
2、IDA 静态分析
3、 进入 pwndbg 动态调试(gdb没写反)
(1) 或 在某函数开头设置断点
(2) 运行程序 步过 步进
(3) 查看多少栈
4、基本流程:填充垃圾字符到 ebp,ebp 下一个地址就是函数返回地址,将返回地址修改为后门地址
一个最基本的exp:

 

基本原理:利用程序中可读可写可执行的巨大漏洞段注入调用 shell 的 shellcode 并利用栈溢
出跳转函数返回地址为 shellcode 的段并执行
这里以一道很简单的 ret2shellcode 为例:(绝对不是懒得重新写了)
XMCVE2020--ret2shellcode:
拿到程序后,还是一套流程,file 知道这是 32 位程序,checksec 发现程序没开保护并且有可读可写可执行(RWX)区域
拖 IDA 分析,main 函数如下

 

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

可以看出,buf2 可读可写可执行,并且地址是固定的
那就好办了,我们可以通过栈溢出把 shellcode 赋给 ,并且让 main 函数返回到 的地址执行它
exp:

 
 

这等效于
我们可以溢出一大段数据,篡改栈帧上自返回地址开始的一段区域为 gadget 的地址,达到把上面的代码连贯起来执行的效果
以一道最简单的 ret2syscall 为例:
XMCVE 2020 ret2syscall
开始先 file 和 checksec 看到它是32位程序,没有保护
用gdb调试,计算出偏移量为108 + 4 = 112
之后我们就可以开始拼凑 gadget 了
使用插件:ROPgadget
先安装(bing)
基本命令: 在二进制文件中查找有pop或ret的汇编语句
在后面加上 ,就可以找到有 eax 的 gadget,以此类推,可以找到 ebx, ecx, edx
以这道题为例,我们先输入 ,终端找了一会,给我们返回了如下结果:
image
我们发现地址为 0x080bb196 的gadget十分的好看,就可以利用它构造一小段payload,先用vim打开exp,记录它的地址,接着同理,找到合适的 ebx, ecx, edx 的地址
然后,用 查找 int 0x80
我们还可以用这个命令找一些字符串的地址,比如 ,找到 "/bin/sh" 的地址,记录下来(也可以在 IDA 的 shift+F12 字符串总览里按 ctrl + F 搜索)
以上这些操作进行完之后,我们就可以开始拼凑了
exp:

 

ps:flat() 函数接收一个列表类型的数据,并将列表中的每个元素转化成字节型数据,不足一字节的补足到一字节

上道 ret2syscall 的题,我们拿到的程序是静态链接的,程序中包含了所有将要用到的库函数,所以我们可以很方便地找到 gadget ,但是在很多时候,题目中的程序都是动态链接的,程序主体往往很小,我们不能在其中找到完整的 gadget,此时我们就可以从它用到的库中寻找出路,也就是 libc

知识点:为什么system调用的参数要向上找两个字节?

要调用 shell ,我们需要让系统执行这样的指令:
image
而函数调用栈的结构长这样:
image
可以看到,父函数压入的子函数的参数 arg1, arg2 越过了 Return Address 和 Caller's ebp 两个字长后,才是子函数的局部变量,根据函数调用约定,子函数会自动越过 Return Address 和 Caller's ebp ,网上找他所需要的参数,子函数自己是知道这一点的
而 system 的汇编中,在 pop 掉它自己之后,第一行便是 所以此时栈的结构会变成这样:
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 ,但我们可以构造出形如 的代码段并让程序执行,根据基础知识,我们需要整出这样的结构
image
汇编代码中有 system 但没有 /bin/sh ,所以我们需要自己写入一个 "/bin/sh" 并填入它的地址
翻一翻 bss 段(用来存储一些全局变量的),发现在 0x0804A080 的地方藏了个 char buf2,那么我们可以先调用一个 gets,然后把 /bin/sh 输入到 buf2 里,再在调用 system 的时候返回到 buf2,也就是这样一个结构:
image
exp 如下:

 

XMCVE 2020 ret2libc3
32位程序无保护,偏移量 56 + 4
拖进 IDA 一看,欸,没有 system 也没有 /bin/sh
先找漏洞点,乍一瞅看不出来

 
 

看似只是将 复制到了 中,但是 只开了 56 长度, 的长度有 256 很明显会发生栈溢出
好了回到刚才的问题,没有 system 和 /bin/sh 怎么溢出?
这是一个动态链接的程序,所以 ret2syscall 不可行,但是动态链接也有它的漏洞
动态链接的程序运行时,会把需要的动态链接库整个载入到内存中,就像这样:
image
就算开启了内存随机化保护,动态链接库也是一个不会被拆开的整块,也就是说,各个函数间的相对距离是永远不变的,而我们肯定有 libc 文件,所以只需要知道其中任意一个函数载入内存后的地址,就可以推出所有函数的地址
而这道题比较的简单,它给我们的程序就是一个内存查询工具,所以我们只需要让他查询一个已执行函数的 got 表里存了什么,就能知道这个函数在内存中的地址,也就能得到 system 的地址
exp:

 

XMCVE 2020 练习题 pwn2_x64
这道题比较简单,给了 system 和 /bin/sh 主要特殊的点在于:它是个64位程序
特殊在什么地方呢?一般的32位程序中,调用函数时传的参数都被压在栈里了,但是 x64 不太一样,在调用函数时,前 6 个参数会挨个依次存在 rdi, rsi, rdx, rcx, r8, r9 这几个寄存器中,之后的参数才会压到栈里,system 只有一个参数,我们在构造 payload 的时候要整出这样一个结构:
image
在脑海中把这 3 行栈模拟一遍就能想明白了,exp 如下:

 

XMCVE 2020 练习题 pwn3
32 位,无保护,偏移量 136 + 4
但是这道题有一个跟上面的 ret2libc3 不一样的地方,上面那道题给了我们内存查找的实现,我们可以直接很方便的得到 libc 的基地址,但是这道题什么都没有,需要我们自己泄露

 

依旧是一个简单的栈溢出漏洞,现在的主要问题是如何找到 libc 的基地址
我们知道,利用 ROP,我们可以控制程序的执行流,让我们想执行什么就执行什么,我们通过让它执行 拿到了shell,那我们可不可以让他执行 让它把地址自己告诉我们呢?显然是可以的,又因为这个时候,write 函数肯定执行过了,所以我们可以让它输出 write 的 got 表内容,得出 libc 基地址
只要构造这样一个结构就好了:
image
但是如果这样整的话,write 完之后程序就结束运行了,这显然不是我们想要的,那么既然我们可以用 ROP 做到任何事,为什么不能再让它执行一次 vulnerable_function 呢?
所以第一个 payload 如下:
接下来,只需要接收到它给你发送的地址,然后再利用这个地址搞到 system 和 /bin/sh 就好了
exp:(应该是目前为止最长的了)

 

NSSCTF-889-Where_is_shell
很久没写题解了,这道题本来只是一个简单的ret2text,但是其中涉及了一个没有接触过的知识点
调用shell的方法,除了 和 之外,linux 中的 shell 自带了一些变量,其中 $0 是指 shell 本身的文件名,这道题代码段的 0x 中有 x24%x30 即 $0,我们可以给 system 传 $0 的参拿到 shell
小坑:注意堆栈平衡
exp:

 

利用格式化字符串漏洞,就是利用 printf 函数的设计缺陷来达到内存泄漏或篡改内存的目的

基本格式:
重点有以下两个:
parameter:获取格式化字符串中的指定参数
例如: 执行后只会输出
type:输出的类型
%d:有符号整数
�整数
%x:16进制无符号整数,但是不会输出 0x
%c:输出一个字符
%s:输出一个指针所指地址内存放的字符串
%p:输出一个地址,有 0x
%n:不输出字符,但是把已经成功输出的字符个数写入对应的指针参数所指的变量中
其中 %s 和 %n 要重点理解,类比于 got 表的地址和 got 表中存放的地址

我们知道,printf 函数的一般格式是这样的:

 
 

输出了一些奇怪的值

 

它到底输出了什么?
我们联想一下,在32位程序中,调用函数的参数传递是依靠栈来进行的,在正常情况下,程序老老实实地取用了 "hello world" 字符串
image
但是别忘了,栈上还有其他的数据,所以如果参数一旦填多,就会强行把数据输出出来
image
所以我们就可以泄露栈上的数据了

XMCVE 练习题 fmtstr1
32位,有 Canary,不能栈溢出,IDA 静态分析如下:

 

程序逻辑为把你输入的东西输出出来,如果变量 的值为 4 就给你 shell
可以很容易地看出,漏洞出在 这里,我们可以利用格式化字符串漏洞
双击变量 跟进,得到 的地址为 0x804A02C,我们可以利用 %n 将 4 写入到 中
先上 payload:p32(0x804A02C) + b"%11$n"
程序先 read 再 printf,也就是会把我们输入的内容在调用 printf 时再压入栈中一次,用作 printf 的参数,上 gdb 动态调试一下,可以看到在刚刚调用 printf 时栈是这样的
image
可以看到,在地址 0xFFFFCE80 和 0xFFFFCE84 中的,就是刚刚压进来的给 printf 的参数,其中CE80 为格式化字符串,CE84 为格式化字符串的参数,printf 会从格式化字符串,也就是 CE80 开始向高地址找参数,我们从 CE80 往高地址数,数到 read 进去的 'aaaa ' 刚好是11个字节,所以偏移量为11

记录目前用时最长的一道题(2天)—— BUUCTF wdb_2018_2nd_easyfmt
2018年网鼎杯的比赛原题,32位无保护,无栈溢出,有格式化字符串漏洞,无system无 /bin/sh
IDA 静态分析如下:

 

我们知道,利用格式化字符串漏洞,我们可以篡改内存中的值,在这道题什么都没有的情况下,我们可以从 libc 里找突破口
众所周知,got 表中存放了某函数在内存中的真实地址,以供动态链接使用,那我们如果能篡改 got 表,就可以执行我们想要的函数了,所以我们可以挑一个函数,将它的 got 表内容修改为 system 函数的真实地址,puts 函数明显符合我们的需求
要实现把 puts 的 got 表内容修改位 system 的真实地址,需要以下几步:
1、泄露 libc 地址
2、计算 system 地址
3、将 system_addr 写入puts@got 中
因为到 的时候 puts 函数肯定执行过了,所以我们可以利用格式化字符串漏洞泄露 puts@got 的地址,得出 libc 的基地址,这部分用 gdb 计算偏移,用 泄露
payload1:
(坑点:偏移量的计算很搞心态,在底下会详细说,只能用 %s 不能用 %p,因为 %p 不解引用,用 %p 只能得到 got 表在哪,不能知道 got 表里放了什么东西)
计算 system 地址:略
篡改 got 表: 这是这道题最难也是最搞心态的一点,能写多详细就写多详细
程序的逻辑为先读入 ,再将 作为 printf 的参数,而 是个局部变量,读入的数据会存放在栈上,所以如果我们在栈上通过 写入 got 表的地址,再通过 ,就可以覆写 got 表
image
这里在构造 payload 计算偏移时,可以先把关键值空出来,在 gdb 里动态调试
我们在 gdb 里调试,看到 puts 的真实地址是 0xf7e0cd90,system 的真实地址是0xf7de23d0,如果一次全部覆盖完,要输出的空格数太多了,所以我们可以先修改后两个字节,再修改前面两个字节,修改前两个字节时,要指向的地址自然就是 printf@got + 2
payload2:
完整 exp 如下:

 

总结一下这道题的坑点:
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')
这里介绍一个十分好用的指令:
显示这个地址的信息,我们主要能用他得出打开 PIE 保护的情况下当前地址相对于基地址的偏移
用格式化字符串泄露地址并算出基地址之后,我们要做的就是把 bss 段里的 chunk_addr 覆盖成 atoi 的 got 表的地址,对于偏移量的计算,这里有两种方法:
1、在 IDA 中查看

偏移量为 0xF8 - 0xE0 = 0x18 = 24,我们要利用 name 溢出,垃圾字符的长度就是 24 - 电话号码的长度 11 = 13
2、gdb 调试
我们输入 可以查看 bss 段的130个字节

如图,可以自己数出来
exp:

 

待更新……

版权声明


相关文章:

  • 调用swap函数实现交换a和b的值2025-05-16 17:30:01
  • 安卓seekbar样式2025-05-16 17:30:01
  • seo案例分析100例2025-05-16 17:30:01
  • java中匿名内部类怎么用2025-05-16 17:30:01
  • tiny xml2025-05-16 17:30:01
  • 表单 html2025-05-16 17:30:01
  • combox-p2025-05-16 17:30:01
  • 键盘的快捷键大全2025-05-16 17:30:01
  • java中内部类包括2025-05-16 17:30:01
  • t-sne算法2025-05-16 17:30:01