PWN - 栈溢出攻击入门

前言

经过一场CTF比赛,感觉以前学的知识都是没有得到升华。上网找了很多关于栈溢出攻击的实现方法,大家都说得很专业,我基本连入门都没看明白。经过一番尝试,这里做个小结,作为一个简单的入门。

工具就不多说了,大家都可以网上很容易找到各种安装教程,毕竟都只是一些常用的工具:

  • checksec
  • IDA Pro
  • readelf
  • gdb
  • ROPgadget
  • python with pwntools

第一步,用checksec进行文件安全信息分析。

checksec pwn

可以看到文件的一些基本信息:

参数参数含义当前值说明
Arch程序位数amd64-64-little这是一个64-bit的程序,子函数调用要用64-bit的方法。
RELRO

”Partial RELRO”,说明我们可以对GOT表具有写权限。

”Full RELRO”,简单理解为GOT表不可写。

“Canary RELRO”,不但保护了,而且还随机化了。

Full RELRO不能通过改写GOT表破解。
Stack是否有栈溢出保护Canary found有栈溢出保护(下面我们会先破解这个保护
NX

non-executable, 堆栈是否禁止执行

enable

堆栈禁止执行权限,

不考虑写shellcode到堆栈然后执行。

PIEELF的地址是否进行随机化加载No PIE (0x400000)ELF没有随机化加载(松了一口气)

第二步,用IDA Pro打开文件进行反编译分析

打开pwn文件,找到main函数,按键盘F5进行反编译成C++语言:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  ...
  char buf[40]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 v6; // [rsp+38h] [rbp-8h]
  ...
  while...{
  ...
    v4 = read(0, buf, 0x200uLL);
  ...
    write(1, buf, (unsigned int)(v4 + 8));
  ...
  }
  return 0;
}

可以看出read函数读buf的时候是0x200个字节,但是buf定义只有40(0x28)个字节。这里很明显可以通过read读入实现栈溢出攻击,然后write把需要的内容打印出来。

从上面图中可以看出,栈溢出保护是检查var_8的值和之前的值有没有变化而判断有没有栈溢出。从下面的图中可以看到var_8其实就是我们buf后面的unsigned __int64 v6。

所以我们可以先尝试让buf的40个字节填满,然后当write去输出buf的时候就会把var_8/v6,也就是我们常说的canary打印出来,从而我们获取当前的canary,然后在第二次输入(read)的时候,我们就可以通过输入<buf[40]> + <canary>去到leave;retn;这个分支了。

第三步,用ROPgadget尝试找到ROP链

运行下面的命令:

ROPgadget --binary ./pwn --only "pop|ret"

我们可以找到两个挺有用的片段的地址:

  •         片段一:pop rdi; ret;
  •         片段二:pop rsi; pop r15; ret

这就涉及了调用子函数的方法:

位数调用子函数方法
32位

函数参数在函数返回地址的上方

32位程序函数调用时,依次将子函数的参数从右到左入栈,然后再压栈eip和ebp。

64位

64位程序如果子函数的参数数量<=6个,则会将参数从左到右依次存入rdi,rsi,rdx,rcx,r8,r9这6个寄存器中,如果还有参数,则像32位一样压栈。

ssize_t write(int fd, const void *buf, size_t n)
{
  ...
}

__int64 __fastcall system(__int64 a1, __int64 a2, __int64 a3, __int64 a4, u32 *a5, u32 a6)
{
  ...
}

可以看到,write和system的函数如上所示。

如果我们要执行system("/bin/sh"),就要先将"/bin/sh"的地址放到rdi(第一个参数)然后再call system。这就可以用到片段一。构建让程序read的ROP链如下:

<buf[40]> + <canary> + <rbp> + <片段一地址> + <binsh字符串的真实地址,会被pop去rdi> + <system函数的真实地址>

如果我们要write(xx, buf, xx),就要先将要输出的buf的地址放到rsi(第二个参数)然后再call write。这就要用到片段二。片段二其中,pop r15我们不需要用到,所以在构建ROP链的时候,要多加一个无用数让它pop出来。构建让程序read的ROP链如下:

<buf[40]> + <canary> + <rbp> + <片段二地址> + <buf地址,会被pop去rsi> + <没用的数据,会被pop去r15> + <write函数的真实地址>

<rbp> 我这里一般取0,因为leave的时候会pop rbp;所以如果是leave;retn;的程序段,要放多一个栈数据。

  • 汇编命令leave等价于mov rsp rbp;pop rbp;
  • 汇编命令retn等价于 pop rip;

第四步,尝试找到函数的真实地址

在第二步可以看得出,system函数是在动态链接库里面的函数,而且"/bin/sh"字符串,也只有在动态库里面有。为了构建我们的ROP链去运行system("/bin/sh"),我们需要找到这个函数的真实地址。因为每次加载动态链接库后的地址都会不一样,我们需要在运行的过程中找到动态链接库的基址。

PLT地址 -> GOT地址 -> 真实内存地址

选择一个函数,在pwn里面有plt地址链接到动态链接库。我们这里可以选择puts函数,然后把puts函数的got地址所指向的数据(真实内存地址)打印出来,就可以得到puts函数的真实地址。

<buf[40]> + <canary> + <rbp> + <片段二地址> + <puts函数的got地址> + <pop去r15的没用的数据> + <write函数的plt地址>

最后把puts函数的真实地址减去puts函数在动态链接库里面的相对地址,就可以计算出动态链接库的基址。

libc_base = puts_real_address - puts_address_in_libc

有了动态链接库的基址,那么我们就可以求出动态链接库里面system函数和"/bin/sh"字符串的真实地址:

system_real_address = libc_base + system_address_in_libc

第五步,用python写出对应的程序

如果上面都理解了,这里就不多做阐述了。

附件:一些名字的解释

缩写含义
RELRORelocation Read-Only, 重定向只读
PLTProcedure Link Table,程序链接表
GOTGlobal Offset Table,全局偏移表

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值