初探Pwn之栈溢出入门

参考文章
[PWN入门简介][https://blog.csdn.net/m0_51466347/article/details/121742858]
[PWN入门之栈溢出][https://blog.csdn.net/baidu_33751906/article/details/121687896]
[栈溢出原理和利用学习][https://blog.csdn.net/sinat_31054897/article/details/82223889]

PWN题目模式

一般pwn的题目都是由出题人给出自己的服务器IP以及端口(一般是Linux系统),这个端口在运行着一个进程。同时出题人再给出一个二进制文件,而这个文件正好就是那个进程正在运行的文件。

而解题人要做的就是通过找出出题人提供的这个二进制文件漏洞,并且利用这个漏洞获取出题人服务器的最高权限。

PWN题目需要用到的工具

IDA

这个不多说了,经典工具,静态分析必备。

checkesc

执行命令checkesc 文件可以查看文件的保护机制

各个项目的意义:

Relro:Full Relro(重定位表只读)
Relocation Read Only, 重定位表只读。重定位表即.got 和 .plt 两个表。
Stack:No Canary found(能栈溢出)
NX: NX enable(不可执行内存)
Non-Executable Memory,不可执行内存。
PIE: PIE enable(开启ASLR 地址随机化)
Address space layout randomization,地址空间布局随机化。通过将数据随机放置来防止攻击。

Python之pwntools

python的一个模块,执行pip install pwntools即可安装。

基本用法:

send(data): 发送数据
sendline(data) : 发送一行数据,相当于在末尾加\n      
recv(numb=4096, timeout=default) : 给出接收字节数,timeout指定超时
recvuntil(delims, drop=False) : 接收到delims的pattern
recvline(keepends=True) : 接收到\n,keepends指定保留\n
recvall() : 接收到EOF
recvrepeat(timeout=default) : 接收到EOF或timeout
interactive() : 与shell交互

ELF文件

e = ELF('/bin/cat')
print hex(e.address)  # 文件装载的基地址
0x400000
print hex(e.symbols['write']) # 函数地址
0x401680
print hex(e.got['write']) # GOT表的地址
0x60b070
print hex(e.plt['write']) # PLT的地址
0x401680

解题常用

context.arch = 'amd64'   //设置架构
context.log_level = 'debug' //显示log详细信息
libc = ELF('./libc-2.24.so')  //加载库文件

利用pwntools做出的基本EXP如下

##coding=utf8
from pwn import *
## 构造与程序交互的对象
sh = process('./stack-example')
success_addr = 0x0804843b
## 构造payload
payload = 'a' * 0x14 + 'bbbb' + p32(success_addr)
print p32(success_addr)
## 向程序发送字符串
sh.sendline(payload)
## 将代码交互转换为手工交互
sh.interactive()

PWN题目知识点

栈溢出漏洞

基础知识-栈(Stack)

关于栈的知识最好是去学习一下数据结构这门课,简单来说的话,我自己能总结出以下几点:

  1. 栈是先进后出的,也就是说,先进入栈的数据被压在底部,后进入栈的在顶部。取用数据时是先取顶部的数据。
  2. C语言中使用PUSH来将数据压入栈中,使用POP来将数据弹出。
  3. 栈一般用于存放局部变量,如函数的参数、返回地址、局部变量等,由系统自动分配和释放
  4. 递归调用时也是使用栈作为一个缓冲区

概述

关于栈溢出的定义或者说解释,我找到两个我认为比较清楚的:

栈溢出是缓冲区溢出中的一种。函数的局部变量通常保存在栈上。如果这些缓冲区发生溢出,就是栈溢出。最经典的栈溢出利用方式是覆盖函数的返回地址,以达到劫持程序控制流的目的。

栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变(覆盖)。是一种特定的缓冲区溢出漏洞,类似的还有heap、bss溢出等。

栈溢出的前提是:

  • 程序必须向栈上写入数据
  • 程序对某个函数或者某个模块的输入数据的大小没有控制得当。

常发生栈溢出的危险函数

输入:
gets():直接读取一行,到换行符’\n’为止,同时’\n’被转换为’\x00’;scanf(),格式化字符串中的%s不会检查长度;
vscanf():同上。
输出:
sprintf():将格式化后的内容写入缓冲区中,但是不检查缓冲区长度
字符串:
strcpy():遇到’\x00’停止,不会检查长度,经常容易出现单字节写0(off by one)溢出;
strcat():同上。

可利用的栈溢出覆盖位置

可利用的栈溢出覆盖位置通常有3种:

  1. 覆盖函数返回地址,之前的例子都是通过覆盖返回地址控制程序。

  2. 覆盖栈上所保存的BP寄存器的值。函数被调用时会先保存栈现场,返回时再恢复,具体操作如下(以x64程序为例)。

    调用时:

​ 返回时:如果栈上的BP值被覆盖,那么函数返回后,主调函数的BP值会被改变,主调函数返回指行ret时,SP不会指向原来的返回地址位置,而是被修改后的BP位置。
3. 根据现实执行情况,覆盖特定的变量或地址的内容,可能导致一些逻辑漏洞的出现。

利用栈溢出的思路

  1. 寻找危险函数(也就是寻找可能存在栈溢出的位置)

  2. 然后就是找到所操作地址距离我们需要覆盖地址的距离,通常方法是采用IDA进行查看并自己计算出地址的偏移。

    一般变量会有以下几种索引模式

    相对于栈基地址的的索引,可以直接通过查看 EBP 相对偏移获得。

    相对应栈顶指针的索引,一般需要进行调试,之后还是会转换到第一种类型。

    直接地址索引,就相当于直接给定了地址。

    我们会有如下覆盖需求:

    覆盖函数返回地址,查看EBP即可,例如BUUCTF的rip。

    覆盖栈上某个变量的内容,例如攻防世界新手区的int-overflow。

    根据现实执行情况,覆盖特定的变量或地址的内容。

栈保护技术

栈溢出利用难度很低,危害巨大。为了缓解栈溢出带来的日益严重的安全问题,编译器开发者们引入Canary机制来检测栈溢出攻击。

Canary中文译为金丝雀。以前矿工进入矿井时都会随身带一只金丝雀,通过观察金丝雀的状态来判断氧气浓度等情况。Canary保护的机制与此类似,通过在栈保存rbp的位置前插入一段随机数,这样如果攻击者利用栈溢出漏洞覆盖返回地址,也会把Canary一起覆盖。编译器会在函数ret指令前添加一段会检查Canary的值是否被改写的代码。如果被改写,则直接抛出异常,中断程序,从而阻止攻击发生

这也是我们在判断是否可执行栈溢出时需要使用checkesc来查看是否存在canary的原因。

CTF例题

test_your_nc(基本流程,签到题)

这道题我是参考wp去做的,主要是为了了解一下pwn题目的做题流程

首先我们得到一个靶机端口并拿到一个test文件

那么我们现在需要去分析这个文件

首先我们checkesc,检测文件的保护机制

checksec工具通过pip install pwntools可以得到

可以看出来是一个64位的文件,这里的NX enabled表示启用了NX保护,堆栈不可执行

这里介绍一下各个项目:

Relro:Full Relro(重定位表只读)
Relocation Read Only, 重定位表只读。重定位表即.got 和 .plt 两个表。
Stack:No Canary found(能栈溢出)
NX: NX enable(不可执行内存)
Non-Executable Memory,不可执行内存。
PIE: PIE enable(开启ASLR 地址随机化)
Address space layout randomization,地址空间布局随机化。通过将数据随机放置来防止攻击。

下面可以用IDA对这个文件进行静态分析,IDA64位打开,shift+F12查看字符串

发现/bin/sh,跟进看一下

发现在main函数里,进入main函数,F5查看伪代码

直接就是用system函数调用了shell,白给的后门。

所以我们直接nc这个端口即可

nc node4.buuoj.cn 27127

拿到flag

BUUCTF-PWN-rip(栈溢出覆盖返回地址)

依旧是一个开放的端口和一个文件,checkesc查询文件保护措施获得如下信息

根据得到的Stack:No canary found和NX未启用的信息,存在栈溢出的可能

IDA64位打开pwn1文件,进入main函数F5查看伪代码

main的下面还有个fun()函数

很明显是调用shell的一个后门函数。

接下来我们就需要用到栈溢出漏洞

栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变(覆盖)。是一种特定的缓冲区溢出漏洞

栈溢出的前提是:

  • 程序必须向栈上写入数据
  • 程序对某个函数或某个模块的输入数据的大小没有控制得当

前面的main函数的gets()没有进行输入的限制,存在栈溢出

gets()是C语言中的危险函数之一,它不进行边界检查。

gets ( ) 函数介绍:

get( )函数简单易用,它读取从输入流中读取一行,赋给字符串变量s,直到遇到换行符,然后丢弃换行符,存储其余的字符,并在结尾添加一个空字符使其成为一个 C 字符串,但是它无法检查数组是否装的下,因此它只知道开始处,并不知道数组有多少个元素,一旦过长就会覆盖栈中的其他数据。

在伪代码界面双击s变量进入Stack of main

发现只需要存入15个字节即可劫持函数返回地址(前面的16进制值为15,这个是栈的偏移地址,用于统计栈的大小,只要存入的数据大于15字节,就会产生栈溢出,使其覆盖到相邻的栈)

现在来构造payload:

那个system()函数的地址是0x401186,所以payload为

'a'*15+p64(0x401186)		#15个字节,p64是64位的,后面是目标的地址

那么我们为什么要这么构造payload呢

函数调用的时候,会开辟一个栈帧结构。首先会将调用的函数的参数入栈,然后依次压入调用函数的返回地址和当前栈底指针寄存器(BP)的值入栈。其中压入返回地址是通过call指令来实现的。
在函数调用结束的时候,将栈指针SP重新指向帧指针BP的位置,并弹出BP和返回地址IP。这样函数状态将恢复成进入子函数的状态,实现了函数栈的切换。

所以EXP:

from pwn import *

p = remote('node4.buuoj.cn',28153)
payload = 'a' * 15 + p64(0x401186) 

p.sendline(payload) # 向程序发送字符串
p.interactive() # 代码交互转为手工交互

拿到flag。
ps:这道题看师傅们的wp发现很多细节需要深挖一下,目前汇编水平还是有限,等再学学汇编再来分析一下。

BUUCTF-PWN-warmup_csaw_2016(栈溢出,覆盖返回地址)

拿到文件扔去kali用checksec看看

没有堆栈保护和空间地址随机化,可能存在栈溢出

IDA64对其进行静态分析。找到main函数,F5查看伪代码

看到get函数,这里没有对输入进行限制,存在栈溢出漏洞。shift+f12看一下字符串,找找利用的地方

可以看出这里可以直接看flag,点击查看

后面有函数的位置

按X找到地址

函数的地址是0x40060D

回去看一下v5的地方,v5的大小是0x40

只要我们输入的字符串长度=0x40+8(64位ebp的长度)即可溢出到返回地址,然后将返回地址覆盖成读出flag的那个函数地址即可完成利用

from pwn import *
p = remote('node4.buuoj.cn',29949)
payload = b'a'*(0x40+8)+p64(0x40060D)
p.sendline(payload)
p.interactive()

注意这里用python3的话,'a'之前要加b转成bytes类型,要不会报错提示前后类型不匹配(就这点问题折腾了一个多小时)

[这是当时出现问题时查的博客][https://blog.csdn.net/Zhangyannn/article/details/116086333]

再次运行拿到flag

攻防世界-PWN-hello_pwn(栈溢出,变量覆盖)

拿到一个文件,扔kali里checksec

64位没有开启堆栈保护和空间地址随机化。IDA分析,先看看main函数,f5看伪代码

下面有一个判断,如果dword_60106C == 1853186401就执行sub_400686()函数。

我们看一下这个函数

cat flag的函数,也就是说,满足条件即可拿到flag

只要让地址位于0x400686的双字节的值为1853186401即可

main函数上面有个read函数,取地址unk_601068,地址0x601068,而需要去进行判断的变量地址就在0x60106C,只相差4个字节。而且接受的长度是10,正好可以用1853186401覆盖掉。

写个脚本,用四个字节填充相差的字节,然后用p64打包这一串数字即可实现覆盖,完成判断

from pwn import *

r = remote("111.200.241.244",62215)
payload = b'a' * 4 + p64(1853186401)
r.sendline(payload)
r.interactive()

拿到flag。

posted @ 2023-10-13 12:56  M0urn  阅读(788)  评论(0编辑  收藏  举报