CheatSheet之二进制漏洞利用

Published: 2021年12月10日

In Vuln.

很久没碰这个了,打算复习下,挖个新坑慢慢更新...

准备

工具

就辣么些工具,pwntools自不必多说,qira是之前常用的无时间调试器,也可以使用rr,其他工具会在相关小节处说明...

保护机制(mitigation)

应用可使用checksec可检查保护机制,常见的有如下机制:

表. 应用防护措施
防护名称说明绕过方式
PIE/PIC程序/对象本身是否支持地址无关,支持时加载段地址可随机依赖于ASLR,绕过和ASLR一致
Canary-fno-stack-protector/-fstack-protector-all1. 劫持__stack_chk_fail
2. 爆破canary:若重启不变,如fork则可逐位爆破
3. 覆写canary:canary存在于TLS中若能覆盖到tls则可爆破
4. 泄漏canary:在函数返回前泄漏它,之后溢出再返回
5. 进行信息泄漏(stack smash):若会输出,则覆盖到argv[0]可泄漏信息
6. 覆写栈上的其他信息,不覆盖那么多就没事了
NX-z exestack可开启栈执行权限1. 直接找读写执行区域
2. ROP
3. 通过mprotect修改权限位
RELROPARTIAL时GOT可写,FULL时初始化后GOT会修改为只读
Fortify位置参数必须连续使用,且格式化串位于可写的区域时不能包含%n此时没法写了,还能用于信息泄漏

而操作系统也有很多:

表. 系统防护措施
防护名称说明绕过方式
ASLR从/proc/sys/kernel/randomize_va_space可看,0关,1栈,加载基址(PIC/PIE),mmap,vdso随机化,2堆(brk)也随机1. 爆破:如32位下若通过fork服务,那么可爆破约512次即可获得正确地址
2. 信息泄漏:如pwntools的DynELF先定位到文件的头部,解析文件后获取实际的位置
3. 部分覆盖,由于对齐随机化只涉及高位
4. 使用Spray去提高命中率
KCANARY同CANARY信息泄露
SMEP/SMAP特权模式(Ring 0)下禁止执行用户页(User bit)代码/访问数据,由CR4 bit20/21激活,可通过cat /proc/cpuinfo |grep "smep|smap" (普通权限)查看,可以直接使用nosmep启动参数关闭它 1. ROP
2. 修改CR4关闭SMEP
KASLR内核空间的随机化,启动时可使用nokaslr参数关闭1. 信息泄漏
2. 利用dmesg/kallsyms获取地址
KPTI(KAISER/PTI)用户态和内核态使用不同的页表,用户态只保留进出内核必需的内核页,可通过nopti参数关闭
MMAP_MIN_ADDR限制mmap映射的最低地址,防止NULL指针解引用(mmap建立页表后null指针有效),可从/proc/sys/vm/mmap_min_addr查看当前值此时没法写了,还能用于信息泄漏
RANDSTACK为内核栈基址加入一些随机偏移
STACKLEAK进出内核态时擦除内核栈

除此之外,在内核层还有很多防护措施,如:

  1. kptr_restrict:之前所有权限可以直接看kallsyms,该功能限制了权限。该值为0且perf_event_paranoid<=1(需支持PERF_EVENTS)则普通权限可以查看,有CAP_SYSLOG权限的可在它为01时查看,为2表示所有用户都不可查看
  2. dmesg_restrict:dmesg中可能会出现内核符号的地址,若该值为1则只有CAP_SYSLOG权限的用户可见

其他可参考:https://www.anquanke.com/post/id/238363

训练

上次小小找我要题玩,收集了一些训练网站,先扔这里:

PWN:pwnable.twpwnable.kreonew

RE:reversing.krcrackmes

WEB:juice-shopskfPortSwigger all lab

CRYPTO:cryptohack

ALL:rootmepicoctfw3challsbill’s security site

Other:gamesmashthestackvulnhubpenlaboverthewiremicrocorruptionbackdoorhacktheboxpwnable.xyzhacker101iowargameexploit.education

过程

不多说,可参考下图

image-20211210202709854

漏洞类型

应用逻辑

giegie不如的但还是扔这里,比如system/popen的命令注入,以及自己实现的命令执行接口(可能不是直接调这两个函数而是调其他服务区执行)

缓冲区溢出

字符串:无界字符串复制(unbounded string copy)、差一错误(off-by-one error)、空结尾错误(null termination error)以及字符串截断(string truncation)

栈溢出

  1. 覆盖返回地址:直接劫持到PC
  2. 覆盖BP:返回前会从栈上恢复上一个BP,于是可以修改BP,在下次返回时会把BP给SP于是能转移栈
  3. 覆盖其他信息:随意咯
  4. 覆盖线程栈:当能在线程上进行大量的向上溢出或向下溢出时,绕过保护页即可操作另外的线程栈,可能会迷惑于保护页是4096字节,而栈缺页只能处理约32字节的异常,但其实它们没什么关系...

其他位置溢出

  1. 堆溢出:堆溢出在利用时有两种方式,一种是覆盖其他区域的数据(如覆盖后续堆块里的指针),另一种是覆盖堆的元数据,并利用堆管理机制进行更进一步利用。
  2. BSS/DATA段溢出:这里的溢出就是修改数据段的数据,这里面可能包含指针,或者其他可导致进一步利用的数据。

堆漏洞

堆漏洞是一种相对复杂的漏洞,复杂也代表着容易出现/利用难度大/不易被发现/不易防御等,堆漏洞可能是由于UAF或缓冲区溢出,前者利用相对简单,而后者花样就太多了...先描述下堆管理器,本文只涉及ptmalloc2,其数据处理流程如下,详细可看图。:

1.free:符合fastbin大小的直接扔进去,否则先尝试合并前面的,再尝试合并后面的,之后若是topchunk就酱,否则扔unsortedbin

2.malloc:满足fastbin先从这里取,没有且满足smallbin就从它里面取,还没有就去unsortedbin找(边找边往smallbinlargebin里扔,找到正好一致的停止,否则空了或到达阈值后,从small/large里找,切分的也扔unsortedbin,没有再去unsortedbin直到为空),没有尝试topchunk,若大小不够先malloc_consolidate清空fastbin再试(large中的会先合并fastbin),还不行就申请空间。

3.remalloc:缩小时若多的大于最小chunk则分割并释放,扩大时若后面为topchunk尝试直接扩,为满足大小的空闲chunk也将其解链并扩张,否则新分配一块区域。

注:(1) 在2.26后引入了每线程缓存tcache,它和fastbin类似但优先级最高,它的检查更少所以超好用,详见之前文章

(2) 只有tcache和fastbin是FILO,其他都是FIFO。

这里面涉及的块如下:

image.png

块使用mchunk表示,空闲块由area索引,其结构如下:

#define PREV_INUSE 0x1
#define IS_MMAPPED 0x2 
#define NON_MAIN_ARENA 0x4

struct malloc_state
{
  __libc_lock_define (, mutex);   /* 并发锁用于多线程顺序访问..  */
  int flags; /* Flags (formerly in max_fast).  */

  /* Set if the fastbin chunks contain recently inserted free blocks.  */
  /* Note this is a bool but not all targets support atomics on booleans.  */
  int have_fastchunks;

  mfastbinptr fastbinsY[NFASTBINS];  /* Fastbins */
  mchunkptr top; /* Base of the topmost chunk -- not otherwise kept in a bin */
  mchunkptr last_remainder;  /* The remainder from the most recent split of a small request */
  mchunkptr bins[NBINS * 2 - 2];  /* Normal bins packed as described above */
  unsigned int binmap[BINMAPSIZE];  /* Bitmap of bins */
  struct malloc_state *next;  /* Linked list */

  /* Linked list for free arenas.  Access to this field is serialized
     by free_list_lock in arena.c.  */
  struct malloc_state *next_free;

  /* Number of threads attached to this arena.  0 if the arena is on
     the free list.  Access to this field is serialized by
     free_list_lock in arena.c.  */
  INTERNAL_SIZE_T attached_threads;

  /* Memory allocated from the system in this arena.  */
  INTERNAL_SIZE_T system_mem;
  INTERNAL_SIZE_T max_system_mem;
};

接下来介绍堆上的两种漏洞与相关的利用技巧:

UAF

释放后使用,有两种情况,chunk被释放后再分配前被使用,此时可以修改链表指针搞事情,另一种是被再分配后之前的指针再使用,就会有类型混淆的问题,除了写,它也能泄漏指针。

除了直接的UAF还有DoubleFree,它其实是特殊的UAF,释放第一次后再次的释放操作就是使用,但是可以将其转换为普通的UAF,即再分配一次,此时得到块指针但块仍然在堆分配器结构里。

OverFlow

上面提到堆上溢出的利用分两种,堆上数据由于依赖于环境需要具体情况具体分析因此不多说,这里主要关注溢出元数据,堆管理器利用这些元数据管理堆,破坏后将导致堆管理器错误的处理堆,以更进一步制造读写原语,溢出可能溢出一字节,也可能溢出多字节,溢出的块可能是小块也可能是大块,可能处于使用状态也可能处于空闲状态,由于情况多变因此利用技巧特别多,这里列出常见的利用方式

再来康康堆上常见的漏洞/攻击手法,它们大多都有🏠:

名称 条件 检查 说明
unlink 找到一个指向P的指针X P->fd->bk==P&&P->bk->fd==P 最原始的,在将一个chunk从双链中取出时,会由unlink宏修改链表中前后chunk的指针,只要让这个宏作用于某可控区域即可对任意地址写一个指针大小(此处有个副作用需要处理),如溢出到size的最低位即可认为相邻chunk未使用,再控制前一个的大小与fd/bk即可在下一个chunk被释放并触发合并操作时修改数据。另P->fd和P->bk指向X前,通过验证则会使X指向X前的区域(P->bk->fd=X=P->fd),由于X是指针因此通过修改X的内容则能再覆盖到X令它指向任意内容,以进一步利用
Off-By-One 只溢出一字节 一字节将能覆盖到下一个chunk的size位,可令其增大覆盖其下一个chunk,还有种特殊的off-by-null可修改上一chunk为释放状态,且能修改大小...
House of Prime 修改chunk的size为8就会在处理fastbins时下标为-1,操作到main_area的max_fast,它存储fastbin存放的最大chunk大小
House of Mind 修改三个标志位使计算出错误的area,再从那里面获取错误的块
House of Force 能通过某种方式修改top chunk的size(如溢出)为极大值 malloc一个小于top chunk size的大数,造成整数溢出,让top chunk环绕到想要控制的位置,下次再从top chunk里分配内存则会分配到想要控制的区域。
House of Lore
House of Spirit 改写free的参数 令其指向想要控制的符合fastbin结构的区域,再次分配时则可以分配到该区域,如栈溢出改写free的参数,并在栈上构造fastbin chunk,则可以在下次分配时分配到栈上的空间。
House of Roman
House of Einherjar 类似于House of Force,但是它作用于临近top chunk的chunk,只需要将pre_inuse位去掉,并伪造合适的大小,就会在该chunk被释放时,三个chunk合并成一个topchunk,从而将top chunk前移到想要控制的区域。
House of Orange

这里把"The"去掉了...更多可见how2pwn...

不同的堆实现和防护方式不尽相同,这里只记录ptmalloc2默认设置下的防护与绕过方式:

表. 堆防护措施
利用点防护方式绕过方式
Fast Bin分配时只会检查分配的chunk size位是否正确fastbin是单链表很好利用,找或构造含size值的区域
在释放时只会检查当前释放的和上次释放的是否一致间隔释放
.........

可安装对应的源码方便调试,如ubuntu下用 apt install glibc-source 再调试时指定目录

格式化串

格式化串函数是变参函数,由函数内部根据格式化串确定参数个数,详见维基百科,它支持三类格式指示符,如%d这种能直接输出栈上的值,%s这种将栈上的值作为指针输出指向的数据,%n这种把输出的个数写入栈上对应处所指示的位置(注:x64需要多参数)。另外它的还支持位置参数%X$d与宽度%[width]d可用于指定哪个位置的数据,输出数据的长度。 ​

在利用时,分两种情况:

  1. 格式化串在栈上:这时一般可以直接构造任意读写的payload
  2. 格式化串不在栈上:这时需要用间接的方式,格式化串操作的目标在栈上,则可在栈上寻找指向栈地址B的指针A,通过修改A来构造B,通过B来实现任意地址读写。实际的利用一般是找A->B->C的三个位置,这样A只需要改B的一字节即可控制C的所有位

利用时可使用libformatstr,也可以用pwntools...

整型溢出

这里包括宽度溢出与符号转换出错,这本身只是BUG,但是一般会造成其他漏洞,如转换为缓冲区溢出或数组越界(特别容易出现个数*单个长度这种溢出)。 ​

未初始化漏洞

在某条件下若一个变量未被初始化,则可以想办法控制它的值,如它在栈上则在其他条件或在其他函数下为其赋值,若在堆上可使用堆风水控制值,之后再进入漏洞函数触发未初始化逻辑,就可能造成其他漏洞...

另外,未初始化区域可读时存在信息泄漏,可利用它泄漏敏感信息(如密码)或指针从而绕过地址随机化...

访问越界

能控制下标时,访问到其他位置去啦.... ​

空指针解引用

在用户空间一般是直接出错,正常情况虚拟地址空间的0地址不会也不能被映射,因此很多代码会把0认为不存在/NULL,解引用也是抛异常崩溃,若修改了mmap_min_addr的值则可映射0地址,此时的解引用可能就会造成其他问题了,这一版用于内核漏洞的提权,一些函数指针未赋值时会是0,若能映射0并修改就能劫持PC。

条件竞争

存在多种类型,如double fetch即对某资源校验与使用时分开读区,资源被共享访问时就能在通过校验后更改数据从而绕过校验...

利用技术

OffByOne

只修改部分位,来绕过某些限制,如修改地址时可能由于ASLR无法知道具体地址,但由于它只有高位可变,因此只写低位可绕过该限制... ​

Shellcode

写shellcode一般会有两个限制:

  1. shellcode长度
  2. 坏字符

Spray

就是在无法确定具体位置/偏移时,扔大量重复数据,增大命中正确位置的概率,如:

  1. 栈喷射(stack spray):一般是布置很多RET的Gadget
  2. 堆喷射(heap spray):一般是布置很多NOP让其滑行到最终的位置

具体布置什么需要根据实际情况来,这是一种思维,另外它还有种意思是大量分配,这可以确保像tcache被清空,不会出现chunk合并等,用于堆风水,所谓堆风水其实就是堆排布,由于堆分配策略是确定的,因此在可以控制分配释放时可以通过精心编排分配释放步骤来定义堆的布局。

返回导向编程(ROP)

利用技巧 说明
Ret2text 已有代码就能利用时,直接rop到此处
ret2shellcode 若有些区域有WX权限,则把shellcode写到那里面在rop过去
ret2syscall 若目标代码中有syscall/int80则可通过rop构造syscall,这一般在静态链接的程序中才会出现,此时可尝试onegadget一键生成
ret2libc libc中一定有syscall/int80,不过也有封装好的syscall,若PLT中有就可以直接用,若没有则由于libc是动态库位置可能不固定需要想办法获取,如通过已解析过的GOT泄露某地址(如__libc_start_main )再由相对偏移求得
ret2csu x64一般由寄存器传参,需要自己去找Gadget,不过__libc_csu_init是个大宝藏,它含能控制多个寄存器的Gadget
ret2dlresolve 该方式在非PIE可不用信息泄漏直接调用syytem等函数,它通过伪造动态解析所需的数据结构,实现任意外部函数调用
ret2usr 内核态与用户态内存只存在单向隔离,由于用户态内存完全可控(任意地址读写),因此在用户态部署任意代码,再在内核态找到合适的Gadget返回到该处
ret2dir 由于SMAP/SMEP没法在特权模式下直接读/执行用户态数据/代码了,于是就想找片用户态与内核态都能映射了的区域,比如说直接映射区,在64位下物理内存基本都会映射进去,于是用户态分配大量(堆喷)的内存后,内核态用直接内存映射就能相应的读/执行啦。注,在v3.8.13之前有RWX,之后只有RW啦...另外还有个就是找别名,除了这里的堆喷,还可以利用proc计算,或有任意读时去根据关键模式串进行搜索。
ret2vdso 因为里面有系统调用指令int80/sysenter/syscall,因此知道它的地址时可从那进系统调用,这种方法的关键是定位vdso的地址
srop 内核在dispatch信号时,会把当前上下文存储在用户栈,并在栈顶存放sigreturn调用的地址,于是信号处理handler执行结束后返回到sigreturn,该处的系统调用恢复栈上的上下文,因此可通过它控制所有通用寄存器,这种利用的关键是有足够的空间布局上下文以及找到sigreturn gadget

ROP找Gadget的工具有ROPgadgetropperRopper等,可以多试几个。有几个利用比较复杂,下面详细说明下:

ret2dlresolve

这个步骤有点多,需要先简单看看ELF动态重定位的过程,详细可见,从.dynamic节开始,每个可动态重定位的文件都包含该节,它含所有动态重定位相关的信息,其实就是相关节的偏移/大小等信息:

typedef struct dynamic{
  Elf32_Sword d_tag;  // 类型,如.rel.plt .got .got.plt等
  union{
    Elf32_Sword d_val;  // 根据类型,有不同含义,如STRSZ型此处为.dynstr表的大小
    Elf32_Addr  d_ptr;  // 而STRTAB型为.dynstr的位置
  } d_un;
} Elf32_Dyn;

这里先看它里面的.got.plt.plt两个节,前者保存函数地址,后者是一个蹦床,在懒解析时第一次先压入重定位索引再蹦到_dl_runtime_resolve

root@kali:~# objdump -j .plt -d test

Disassembly of section .plt:

000003e0 <.plt>:
 3e0:   ff b3 04 00 00 00       pushl  0x4(%ebx)  # link_map
 3e6:   ff a3 08 00 00 00       jmp    *0x8(%ebx) # _dl_runtime_resolve
 3ec:   00 00                   add    %al,(%eax)
  ...

000003f0 <printf@plt>:
 3f0:   ff a3 0c 00 00 00       jmp    *0xc(%ebx) # got位置
 3f6:   68 00 00 00 00          push   $0x0             # 重定位索引
 3fb:   e9 e0 ff ff ff          jmp    3e0 <.plt> 

00000400 <__libc_start_main@plt>:
 400:   ff a3 10 00 00 00       jmp    *0x10(%ebx)
 406:   68 08 00 00 00          push   $0x8
 40b:   e9 d0 ff ff ff          jmp    3e0 <.plt>

其中ebx.got.plt地址,该节里面每4/8字节是一个地址:

got[0] : .dynamic 
got[1] : link_map , 它是动态链接器加载每个对象会生成一个并链接在一起,含基址等信息
got[2] : _dl_runtime_resolve , 动态链接器的函数,用于查找真实地址
plt[1]->got[3] : 函数1地址
plt[2]->got[4] : 函数2地址
...

解析的过程是由重定位索引查重定位项,此处名字叫.rel.plt

typedef struct elf32_rel {
  Elf32_Addr    r_offset;         //地址,其实是在.got.plt表中,解析后会修改它,之后就不必重新解析了
  Elf32_Word    r_info;           //type与index 
} Elf32_Rel;

#define ELF32_R_SYM(i)    ((i)>>8)  // 符号表中的下标
#define ELF32_R_TYPE(i)   ((unsigned char)(i))  // 类型

通过重定位项获取到符号表.dynsym中对应项,从而根据它与字符串表.dynstr取到符号名称:

typedef struct elf32_sym{
  Elf32_Word    st_name;    //在.dynstr中的偏移
  Elf32_Addr    st_value;   //符号值,根据类型不同可能是绝对值,地址什么的
  Elf32_Word    st_size;    //符号大小
  unsigned char st_info;  //28Binding:4Type 绑定指示符号是局部全局弱符号,类型指示符号为对象函数节区文件等
  unsigned char st_other; //符号可见性
  Elf32_Half    st_shndx;   //符号所在段下标,或者是特殊常量,如未定义的引用或者未初始化的全局符号等
} Elf32_Sym;

之后就是根据名称与link_map去查找真正的位置:

struct link_map{
    ElfW(Addr) l_addr;    //加载基址
    char *l_name;                   // 文件绝对地址
    ElfW(Dyn) *l_ld;          // .dynamic节,它含上面提到的所有和动态链接相关的节信息
    struct link_map *l_next, *l_prev;   // 链表指针
}
image.png

计算过程如下:

//获取符号表的首地址  
const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);    
//获取字符串表的首地址
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);               
//该函数在.rel.plt中的地址
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); 
//该函数在符号表中的地址
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];          
//该函数在.got.plt表中的绝对地址,等下解析出真实地址可修复它
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);             
//关键部分,此函数是遍历link_map,根据符号名查找符号地址
_dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);

从上可见_dl_runtime_resolve有两个参数,分别是重定向表项索引与link_map地址,在控制索引让其计算出的重定向项在我们伪造的重定向项,从而指向伪造的符号表项,从而指向伪造的符号名称,并让PC指向plt[0]即可可执行任意存在的函数,同理也可以伪造link_map等实现同样的目的。

ret2vdso

为了将某些频繁使用的查询类系统调用直接转换为函数调用,有了vsyscall机制,它地址固定且有些危险的指令因此可被用于rop,且由于ABI它一直保留着,不过由于安全问题可选择只读后陷入(Emu):

(gdb) x/5i 0xffffffffff600000
   0xffffffffff600000:  mov    $0x60,%rax
   0xffffffffff600007:  syscall
   0xffffffffff600009:  retq
   0xffffffffff60000a:  int3
   0xffffffffff60000b:  int3
(gdb) x/5i 0xffffffffff600400
   0xffffffffff600400:  mov    $0xc9,%rax
   0xffffffffff600407:  syscall
   0xffffffffff600409:  retq
   0xffffffffff60040a:  int3
   0xffffffffff60040b:  int3
(gdb) x/5i 0xffffffffff600800
   0xffffffffff600800:  mov    $0x135,%rax
   0xffffffffff600807:  syscall
   0xffffffffff600809:  retq
   0xffffffffff60080a:  int3
   0xffffffffff60080b:  int3
(gdb)

更新的就是vdso了,它在x86下只有8bit随机,在x64下v3.18.2前为11bit,之后为18bit,所以可以爆破,32位上它里面有int 0x80/sysenter等指令,也有sigreturn等gadget,64位没找到危险指令。信息泄漏的话,可泄漏l d.so中的_libc_stack_end_rtld_global_ro的成员,前者可计算栈地址,后者可以直接计算出vdso的地址,这里看看栈地址到vdso地址的计算,栈上可以获取很多指针,通过它可知libc/栈等地址,如下是栈布局信息:

...
local variables of main
saved registers of main
return address of main          ;; main函数返回地址,为libc地址 
argc                                                ;; 参数个数
argv                                                ;; **argv,指向argv数组,见下面,可知栈地址
envp                                                ;; **envp,指向envp数组,见下面
stack from startup code
argc
argv pointers                               ;; *argv数组,直到空指针为结束
NULL that ends argv[]
environment pointers                ;; *envp数组,直到空指针为结束
NULL that ends envp[]
ELF Auxiliary Table         ;; 辅助信息表,它是{key,value}数组,直到空项为结束
argv strings                                ;; 原始的参数字符串
environment strings                 ;; 原始的环境变量字符串
program name                                ;; 进程名称
NULL

辅助信息表还能获取更多信息:

➜  ~ LD_SHOW_AUXV=1 ls
AT_SYSINFO:      0x7fffb7dcfc80                     #__kernel_vsyscall  32位
AT_SYSINFO_EHDR: 0x7fffb7dcf000                     #vdso
AT_HWCAP:        1fabfbff                     #arch dependent hints at CPU capabilities
AT_PAGESZ:       4096                         #system page size
AT_CLKTCK:       100                          #frequency at which times() increments
AT_PHDR:         0x55a7f773b040               #program headers for program
AT_PHENT:        56                           #size of program header entry
AT_PHNUM:        9                            #number of program headers
AT_BASE:         0x7f3140848000                     #libcbase
AT_FLAGS:        0x0                          #flags
AT_ENTRY:        0x55a7f7740430               #entry point of program
AT_UID:          0                            #real uid
AT_EUID:         0                            #effective uid
AT_GID:          0                            #real gid
AT_EGID:         0                            #effective gid
AT_SECURE:       0                            #secure mode boolean
AT_RANDOM:       0x7fffb7ca7ee9                     #stackcannry
AT_EXECFN:       /bin/ls                      #filename of program 
AT_PLATFORM:     x86_64

因此可通过信息泄漏获取vdso的地址。

srop

这种方法的优点是能控很多寄存器,先要想办法把fake sigcontext放到栈上,由于它比较长,因此可能需要先用read的gadget再次溢出。接下来是找sigreturn的gagdet,或者找控rax+syscall的gadget,这里控rax可通过返回值控(如read),因此关键是找syscall,如下是[see0]总结的方式:

之后就是伪造上下文,像system这种调用的参数含指针,因此可能需要先布局信息泄漏的sigcontext,对于链式SROP需要知道栈的位置,否则也可以直接把栈迁移到可写区域并用read再布局。

Stack Pivot

栈迁移,一般用在栈溢出上用于不使用信息泄露获取栈位置,或溢出不够时也可以先迁移,在部分位置已知时(可写数据区域+迁移Gadget)可将栈转换到已知区域,此时可以知道栈的位置即为指定的位置,迁移后再多次ROP进行漏洞利用,这里的Gadget一般是修改BP,再由BP改SP,例如非PIE程序可迁移到BSS段:

...
leave ret       // leave=mov esp,ebp&&pop ebp 即完成SP=BSS
bss addr        // 新栈的地址
pop ebp ret     // 修改BP为新栈地址
0x1000              
bss addr        // 已知的BSS的地址
0               
pop3ret         // 弹出参数 
read            // read(0,bss,0x10000) 这里先部署新栈,便于迁移后继续ROP

类型混淆(Type Confusion)

通过某种方式,让一个结构被当作另一种结构,比如UAF的利用,或有时某对象根据某位定义类型,复写该位后转换为其他类型...

指针覆盖

  1. 最常见的的返回地址覆盖
  2. 函数指针覆盖:GOT,虚函数表(文件IO),__free_hook__malloc_hook__realloc_hook, __libc_atexit(需要有rw),_rtld_global里的函数指针,prepare_handler_fini_array(一般不可写 -z norelro)

内核利用

内核漏洞一般用于提权,这时和用户态的不同是它位于本地,此时已经有部分权限,因此可获取部分信息:

image.png

具体的,可以通过

修改cred

该结构用于访问控制,含主体权限(cred)与客体权限(real_cred),这里只关注前者,将里面的uid,主要是euid修改为0,若存在任意写则可直接为其赋值,若使用ROP则一般是用如下两个函数完成:

commit_creds(prepare_kernel_cred (0));

劫持vdso

能够任意读写时,可以找一片有写权限,root进程有执行权限,root进程会自动触发的区域,劫持它去反弹一个shell。嗯没错就是vdso(vsyscall也可),这片区域在之前是内核rw,用户态rx的,且它存在有两个目的,自适应系统调用指令与避免某些不敏感的系统调用,故它会被频繁访问,因此可劫持它,如把它里面的代码修改为shellcode,shellcode判断若是调用进程uid为0则弹reverse shell,则在root进程访问时会以它的身份执行这段shellcode。昂,现在这片区域内核不可写啦...

call_usermodehelper

该函数可以以root权限创建用户进程,故可让它执行编译的反弹shell,它的定义如下:

static inline int
call_usermodehelper(char *path, char **argv, char **envp, enum umh_wait wait)
{
    struct subprocess_info *info;
    gfp_t gfp_mask = (wait == UMH_NO_WAIT) ? GFP_ATOMIC : GFP_KERNEL;

    info = call_usermodehelper_setup(path, argv, envp, gfp_mask);
    if (info == NULL)
        return -ENOMEM;
    return call_usermodehelper_exec(info, wait);
}

它会被内联,不过不重要,跟进一步还可以去找到run_cmd,它只有一个参数即可执行文件路径,若有任意读写漏洞且调用这些函数的路径位于RW区域时,也可直接修改路径字符串,修改后调用函数即可。这里隆重介绍下prctl函数,它的参数很多,且调用了函数指针:

SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3, unsigned long, arg4, unsigned long arg5)
{
    error = security_task_prctl(option, arg2, arg3, arg4, arg5);
    ...
}
int security_task_prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5)
{
    struct security_hook_list *hp;
    list_for_each_entry(hp, &security_hook_heads.task_prctl, list) {
        thisrc = hp->hook.task_prctl(option, arg2, arg3, arg4, arg5);
        ...
        }
    }
}

struct security_hook_list {
    struct list_head        list;
    struct list_head        *head;
    union security_list_options hook;
    char                *lsm;
} __randomize_layout;

因此可通过覆写它劫持控制流。

小技巧

一个小总结吧...

1.libc里的__environ指向栈上的环境变量数组

2.栈上会有__libc_start_main,它是libc里的地址

3.__libc_csu_init_dl_runtime_resolve等含万能gadget,不过前者是binary本身的,后者属于ld,因此后者需要泄漏出地址

4.无libc时,且需要libc时,要么完全靠猜加爆破(如猜为ubuntu系统的,根据流行度尝试),通过泄漏的库地址猜测(libc-databaselibcSearcher),要么直接用Dynelf全部泄漏,若不需要libc,如只需使用它的某个导出函数,可用ret2

5.libc可尝试用one gadget,挑出满足约束的一个即可,在静态链接时,会存在syscall/int80指令,因此可使用工具自动查找gadget链,不过虽然它比较简单但是生成的payload比较大

6.双链的首尾chunk会指向bins,属于main_area的chunk可泄漏main_area的地址,它位于libc的.data段,因此可泄漏libc地址,可从__malloc_trim定位到main_area的偏移,如small chunk的doublefree/uaf泄漏...

7.mmap/mprotect获取RWX的页,可布局shellcode

8.在需要物理地址时(如虚拟机攻击)可使用/proc/self/pagemap,它存储了每个虚拟页到物理桢见的映射,使用时用虚拟页作为偏移读到的就是物理桢的信息。

挖掘方式

自动化

FUZZ

手动

危险函数逆向追踪

类型 函数 说明
输入 scanf, sscanf, fscanf, vfscanf 使用 %s, %[] 等无边界限制的格式符会导致缓冲区溢出。
gets 极度危险,不对输入长度进行检查,必然导致缓冲区溢出,已被废弃。
fgets 相对安全,但若未正确处理读取的长度和内容,仍可能存在问题。
read 若读取的长度可控,可能导致缓冲区溢出。
getchar, getc, fgetc 单字符读取,在循环中使用时,若循环终止条件不当,可能导致溢出。
getline 动态分配内存,需手动释放,否则导致内存泄漏。若长度指针溢出也可能导致问题。
字符串 strcpy, wcscpy 不检查边界,极易导致缓冲区溢出。
strcat, wcscat 不检查边界,极易导致缓冲区溢出。
strncpy, wcsncpy 当源字符串长度大于等于n时,不会在目标字符串后添加 \0,可能导致后续字符串操作越界。
strncat, wcsncat 需要精确计算剩余空间大小,否则仍可能溢出。
格式化输出 printf 系列 当第一个参数(格式化字符串)可控时,存在格式化字符串漏洞。
sprintf, vsprintf 同时具有格式化字符串漏洞和缓冲区溢出风险。
snprintf, vsnprintf 相对安全,但需检查返回值。若返回值大于等于size,表示输出被截断。错误处理该返回值可能导致问题。
syslog, err, warn... 当格式化字符串可控时,存在格式化字符串漏洞。
内存 memcpy, memmove 不检查边界,当拷贝长度过大时导致缓冲区溢出。
bcopy 已废弃,功能同 memmove 但参数相反,同样存在溢出风险。
memset 当写入长度过大时导致越界写。
alloca 在栈上分配内存,分配过大会导致栈溢出。
realloc 重新分配内存可能失败返回NULL,若未检查返回值并继续使用原指针,在原指针被隐式释放的场景下会导致UAF。
执行 system, popen 若命令由外部输入拼接而成,极易导致命令注入。
exec* (e.g., execlp, execvp) 若参数部分可控,且使用了会通过shell解析的函数(如 execlp),可能导致命令注入。
文件 realpath 早期实现存在缓冲区溢出。
getwd 已废弃,存在缓冲区溢出风险,应使用 getcwd

分析高危位置

  1. 结构化解析
  2. 编解码转换,当出现少变多时容易出现溢出
  3. 异常处理,容易出现未初始化,UAF等

在逆向时可以多下几个版本,如windows/linux 它们可能保护不一样,有的没去符号甚至保留调试新消息

参考

[1] C和C++安全编码(原书第2版) -- Robert C.Seacord[著]; 卢涛[译]

[2] 程序员的自我修养: 链接、装载与库-- 俞甲子, 石凡, 潘爱民[著]

[3] CTF-Wiki

[4] 容器环境相关的内核漏洞缓解技术

[5] https://github.com/shellphish/how2heap

[6] New Reliable Android Kernel Root Exploitation Techniques -- INetCop Security dong-hoon you (x82)

[7] ret2dir: Rethinking Kernel Isolation -- Vasileios P. Kemerlis, Michalis Polychronakis, Angelos D. Keromytis

[8] Glibc 堆利用的若干方法 -- 裴中煜,张超,段海新

[9] House of IO - Heap Reuse -- Maxwell Dulin (ꓘ)

[10] Sigreturn Oriented Programming -- angelboy

[11] Modern Binary Exploitation - CSCI 4968 -- RPISEC

[12] Heap Feng Shui in JavaScript -- Alexander Sotirov

social