首页
社区
课程
招聘
[分享]Linux pwn从入门到熟练(三)
发表于: 2019-10-29 14:22 18073

[分享]Linux pwn从入门到熟练(三)

2019-10-29 14:22
18073

本文首发于安全客文章

Linux pwn系列继续更新。近期终于花了一点时间把自己的坑填上。今天将首先为大家带来上篇文章遗留题目的解答。再次,将介绍两种pwn的方式。这两种pwn都是针对开启了NX保护的程序。其间,还给大家分享了我更新的工具getOverFlowOffset
该工具经过升级,能够同时应对开启和没有开启PIE的程序。支持分析32位和64位程序。欢迎大家提issue :)。

“纸上得来终觉浅,绝知此事要躬行”
——《冬夜读书示子聿》

时间久远,怕大家找不到从前的文章,特此给出传送门:
Linux pwn从入门到熟练(二)
Linux pwn从入门到熟练

前述Linux pwn从入门到熟练(二)这篇文章留了一道习题pwn7给大家做。下面给出一种参考解答。

可以发现。栈是不可以执行的。但是没有开启PIE/ALSR,即地址随机化。因此IDA查看的函数地址是可以直接使用的。

可以发现,函数gets存在栈溢出漏洞。

这里,我推荐一个自己写的工具getOverFlowOffset
该工具经过我的升级,能够同时应对开启和没有开启PIE的程序。
它会自己检测程序是否开启了PIE,对于开启了PIE的程序,它会通过程序里面调用的其他库函数泄露正确的地址,并将存在漏洞的返回地址修正。比如:

在本程序中,没有开启PIE,因此有如下的结果:

可以发现,溢出点距离EBP的距离是108字节。该程序是32位程序,因此距离存储了返回地址的距离是112字节。

从该程序的提示和查看导入函数表我们可以发现,并没有可以直接用于获取shell的系统函数了(如:system, execve)。我们会马上想到上一篇文章提到的写shellcodes, 构造syscall的方法。但是,我们前面查保护的时候又发现,该程序开启了栈不可执行保护(NX)。因此也是不可能构造shellcode 了。我们需要自己主动的从系统库libc中提取用于获取shell的库函数。

那么我们怎么提取用于获取shell的库函数呢?
libc动态库载入时,其内库函数地址的构成:

包括两个主要步骤,

那么如何获取libc的基地址呢?
我们从上述库函数f载入地址的构成就能够窥探出一丝技巧:如果我们泄露任意一个pwn7程序已经载入的属于libc动态库的函数地址f@load(比如__libc_start_main),然后在函数f在libc中的偏移f_offset@libc已知的情况下,就能够反推出libc载入的基地址libc@load了,即:

其中f_offset@libc对于一个确定的动态库libc是固定的,且可以静态的获得。
因此,pwn7漏洞利用的大致步骤为:

这里,为了通过泄露的库函数地址,来获得libc的基地址,我们借助了一个工具:
需要借助的工具。LibcSearch

该工具的安装方法为

一般的使用方法为

为了泄露__libc_start_main地址的栈空间分布变化

上述图中的右侧图展示了对应栈空间里面数值表达的含义。

为了获取shell时栈空间分布变化

注意,选择libc的版本时,选择32位的,即第1个选项。

对于64位程序,有一个可以获取通用ROP的方案,该方案来自于论文:
[black asia 2018]return-to-csu: A New Method to Bypass 64-bit Linux ASLR
Paper,Slides

在某些程序中,我们会发现可以用来构造ROP的 gadgets较少。因此可以利用上述通用ROP方案。由于,该方法的核心是利用函数__libc_csu_init中的代码,因此成为ret2csu。
构造ROP的核心步骤包括三点:
其一是获得用于获取shell的库函数地址,
其二是安排该库函数在合适的位置被调用,
其三是如何巧妙的向函数传参数。

主要思想是:在每个64位的linux程序中都有一段初始化的代码,该代码中含有一段可以被用来间接给函数输入参数赋值的代码。

该段通用代码位于__libc_csu_init函数中:

借用论文中的gadgets图来说明调用方式:

在64位的程序中,当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。
因此,对应于上述提到的三点核心的后面两点:
其二是安排该库函数在合适的位置被调用:可以发现,在gadget 2中,可以利用callq来调用地址%r12+%rbx*8指向的函数。我们可以设置rbx=0,那么就变成%r12寄存器指向的函数。而%r12寄存器的值可以利用gadget 1中的代码从栈中指定位置获取。

其三是如何巧妙的向函数传参数:从gadget 2中可以发现64位程序前三个输入参数存入的寄存器rdi, rsi, rdx分别可以从寄存器r15d, r14, r13中获取值。而结合gadget 1,可以发现r15d, r14, r13的值可以从栈中获取。那么通过合理的分配栈中的数据,我们就可以顺利的控制参数数值了。三个参数对于大部分的漏洞利用而言,基本够用了。

下面以一道zhengmin大神的level 5 , 64位程序来讲解。
Pwn8
那么我们回到本题中,迅速的三连。

开启了栈不可执行保护(NX)。没有开启PIE和canary。

溢出的原因是对于char类型变量,可以输入超长的长度。

距离EBP的偏移是128,距离返回地址的覆盖是136字节。

值得注意的是,本题中的__libc_csu_init汇编结果不同,寄存器赋值的顺序也变了。但是只要利用的思路理解了,稍微调整一下即可。

调用write_got泄露write_got地址的栈

序号后面的寄存器内容表示,执行完对应指令后,寄存器的变化。
标记红色的为关键的模块。
包括如何将栈中的地址映射到不同的寄存器中;再到寄存器赋值到64位程序输入参数中;最后到利用callq调用程序;最后修正rsp的指针,来跳转到主函数位置。

调用read_got将字符串/bin/sh加载到bss段中

此处部分的栈布置和前述利用write@got泄露write@got差不多。只是callq调用的函数变成了read@got。输入的参数变成了0, bss_base, 16.表示向地址bss_base输入16个字节。

调用execve执行获得shell

此处的bss_base地址中已经存储了execve的地址。注意,由于callq 调用时,是取目标地址指向的地址来调用函数,因此需要借助bss_base来转储一下内容。即callq [bss_base]=callq execve_address。否则是不会成功的。

运行时注意选择64位的libc库。即第0个选项。

这里解释一下,为什么在放完gadget 2地址之后,要padding 0x38个数据。才能够放入返回地址。

这是因为在执行完callq之后,我们会使得程序往后执行,且不进行跳转。从而可以最终执行到0x400628位置的retn函数,调用到我们布置的main函数,重新开始执行漏洞。我们在csu中设置了rbx=0, rbp=1.从而在执行到0x4005fd的时候,rbx加1,和rbp相等,从而不会执行跳转。继续往后执行,在到达retn之前,0x400624执行了add rsp, 38h的操作,将栈接着抬高了0x38,所以我们需要padding 0x38的数据,才能够让pwn8程序成功获取我们布置的返回地址。

同时,也由上图也可以看出为什么在放置了csu_end_addr之后,不是直接放置rbx参数的地址。因为[rsp+38h_var_30],可以发现该指令取参数是在当前的rsp基础上增加了8的。因此需要padding 8个‘a’。

上述64位的ROP是不是看起来已经很完美了?大家是不是跃跃欲试的想要带着上面这把“屠龙霸刀”到处找64位程序来练练手?恩,怕是要“欲试未半而中道崩殂”了。

看官且瞅瞅我这道菜。
pwn9

让我们继续快速三连

仅仅开启了NX。

存在漏洞的是read函数。Buf仅仅申请了0x50个字节长度,然而read允许读取0x60个字节长度。

距离EBP的偏移是80个字节,返回地址是88个字节。

发现:有没有发现奇怪的点。对!能够允许溢出的长度非常有限,仅仅16个字节,刚好两个寄存器的长度。那么也就仅仅够覆盖EBP和返回地址了。我们看看前面ret2csu的构造,在溢出之后,需要很多字节来部署寄存器rdi, rsi, rdx的值,还要处理调用完函数之后0x38个字节的padding。因此,ret2csu无法直接使用了。我们也可以就此总结,ret2csu虽然通用,但是需要有较大的溢出空间。

怎么办呢?

这里介绍一种fake frame的方式,可以在溢出空间有限的时候,实现ROP。
在介绍这个操作之前,先给大家介绍两个汇编指令:leave和ret。

Leave指令相当于

Ret指令相当于:

一般程序的结束都是leave;retn。如果我们溢出的返回地址同样还是leave;retn,会发生什么呢?我们把两个leave; retn分别转换成上述解释的操作,来一一解释流程。

序号表示,执行完对应指令的操作之后,寄存器的变化情况。
可以发现,在初始栈中原来放置ebp的位置布置成未来要跳转的新的函数块的起始地址,可以将当前的rsp引导过去。而在目标地址的起始位置开始安装如下规律布置内容,就可以连续的调用自己想要的函数,且输入的参数长度可以自定义。
即: fake_frame_i | 要执行的函数地址 | leave ret 地址 | 参数1 | 参数2 | …
其中步骤1~3是原始程序中的leave; ret;后续的4~6是新增加的gadget里面的leave; ret。

基于上述总结的思路,我们就可以构造下面完整的EXP了。

首次的溢出是为了让puts函数输出栈中存储的rsp的值。

为了输出puts@got的地址,栈分布情况

其中0x400793,用于pop第一个输入参数rdi。借助ROPgadget找到:

其中0x400676是用于重新载入有漏洞的read函数的。
其后填充40个字节,是由于前面已经有5*8的位置占用了。
0x4006be是leaver ret的地址。

为了执行execve("/bin/sh",0 ,0)的栈分布情况:

其中:
pop_rdx_pop_rsi_ret=libc_base+0x00000000001306d9
这个部分的地址需要自己借助ROPgadget等工具来找到并且更新,不同机器会不一样。

这里需要解释一下为什么在执行execve的时候,需要stack-48,降低栈的高度来引rsp。

这是因为,在第一次泄露puts@got函数地址,返回到带有漏洞的函数(即0x4000676)继续执行时,存在会改变rsp数值的操作。Rsp改变了,也就导致了溢出数据时的起始地址发生了改变,如果不进行调整,将无法跳转到正确的位置。我们发现在0x4000676有两处操作改变了rsp的数值。

后期跟进栈平衡原则,rsp的内容不会再有变化了。所以,我们这个时候输入payload数据会载入到rsp-48的位置,那么我们代码跳转的位置也需要相应的调整。

执行结果:

最后,照旧给大家留一道练习题来巩固一下。 我们下期见。
Pwn10

参考资料:


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 4
支持
分享
最新回复 (14)
雪    币: 786
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
学习了,赞
2019-10-31 21:06
0
雪    币: 965
活跃值: (89)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
学习了
2019-11-6 08:54
0
雪    币: 428
活跃值: (14095)
能力值: ( LV13,RANK:606 )
在线值:
发帖
回帖
粉丝
4
师傅,蒸米的 level5,你的 exp 的第 64 行直接 sh.send(p64(execve_addr) + '/bin/sh\x00') 就可以写到 .bss 段了嘛?不太理解,具体 send 到哪里了?
2019-12-2 18:53
0
雪    币: 6339
活跃值: (2091)
能力值: ( LV7,RANK:150 )
在线值:
发帖
回帖
粉丝
5
yichen115 师傅,蒸米的 level5,你的 exp 的第 64 行直接 sh.send(p64(execve_addr) + '/bin/sh\x00') 就可以写到 .bss 段了嘛?不太理解[em_85], ...
前面的csu执行了read函数,将read的内容存储到了.bss段
2019-12-2 20:57
0
雪    币: 428
活跃值: (14095)
能力值: ( LV13,RANK:606 )
在线值:
发帖
回帖
粉丝
6
啊,,师傅说的 read 的内容就是后面 send 的 (p64 (execve_addr) + '/bin/sh\x00') 嘛!?
2019-12-2 22:02
0
雪    币: 6339
活跃值: (2091)
能力值: ( LV7,RANK:150 )
在线值:
发帖
回帖
粉丝
7
yichen115 啊,,师傅说的 read 的内容就是后面 send 的 (p64 (execve_addr) + '/bin/sh\x00') 嘛!?
对的。
2019-12-3 20:27
0
雪    币: 159
活跃值: (695)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
谢谢师傅分享。
最后于 2019-12-7 19:54 被xiaozhu头编辑 ,原因:
2019-12-7 19:54
0
雪    币: 230
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
大佬,问一下,我每次python   ***.py 之后都会把我的payload打印出来,然后报错TypeError: Can't convert 'bytes' object to str implicitly,请问哈,这是为啥。
2020-3-18 11:10
0
雪    币: 14569
活跃值: (17634)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
10
海天一空 大佬,问一下,我每次python ***.py 之后都会把我的payload打印出来,然后报错TypeError: Can't convert 'bytes' object to str impl ...
payload贴出来才能看到什么问题
2020-3-18 14:11
0
雪    币: 230
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11

最后于 2020-3-18 14:28 被海天一空编辑 ,原因:
2020-3-18 14:26
0
雪    币: 230
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
pureGavin payload贴出来才能看到什么问题
[DEBUG] PLT 0x8048430 printf
[DEBUG] PLT 0x8048440 gets
[DEBUG] PLT 0x8048450 time
[DEBUG] PLT 0x8048460 puts
[DEBUG] PLT 0x8048470 __gmon_start__
[DEBUG] PLT 0x8048480 srand
[DEBUG] PLT 0x8048490 __libc_start_main
[DEBUG] PLT 0x80484a0 setvbuf
[DEBUG] PLT 0x80484b0 rand
[DEBUG] PLT 0x80484c0 __isoc99_scanf
[*] '/home/fan/ret2libc3/ret2libc3'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[DEBUG] PLT 0x176b0 _Unwind_Find_FDE
[DEBUG] PLT 0x176c0 realloc
[DEBUG] PLT 0x176e0 memalign
[DEBUG] PLT 0x17710 _dl_find_dso_for_object
[DEBUG] PLT 0x17720 calloc
[DEBUG] PLT 0x17730 ___tls_get_addr
[DEBUG] PLT 0x17740 malloc
[DEBUG] PLT 0x17748 free
[*] '/lib/i386-linux-gnu/libc-2.23.so'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
Traceback (most recent call last):
  File "3.py", line 31, in <module>
    payload = 'a'*0x6c + "junk" +p32(puts_addr) + p32(pop_ebp_ret) + p32(gets_got) + p32(main_addr)    #用junk覆盖ebp  puts_addr继续覆盖返回值地址, 首先调用puts把got的地址打印出来
TypeError: Can't convert 'bytes' object to str implicitly
2020-3-18 14:33
0
雪    币: 14569
活跃值: (17634)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
13
海天一空 [DEBUG] PLT 0x8048430 printf [DEBUG] PLT 0x8048440 gets [DEBUG] PLT 0x8048450 time [DEBUG] PLT 0x ...
root@gavin:/home/gavin# python3
Python 3.7.5 (default, Nov 20 2019, 09:21:52) 
[GCC 9.2.1 20191008] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> buf="abc"
>>> buf+=p64(0xdeadbeef)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "bytes") to str
>>> 
我用一种简单的方法复现了你的payload的报错,你自己看下报错的内容和错误原因
2020-3-18 18:24
0
雪    币: 230
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
14
pureGavin root@gavin:/home/gavin#&nbsp;python3 Python&nbsp;3.7.5&nbsp;(default,&nbsp;Nov& ...
#不太行。
pop_ebp_ret = 0x080486ff
main_addr = elf.symbols["main"]                
payload = 'a'*0x6c + "junk" +p32(puts_addr) + p32(pop_ebp_ret) + p32(gets_got) + p32(main_addr)        
p.sendlineafter("Can you find it !?",payload)
2020-3-18 21:18
0
雪    币: 14569
活跃值: (17634)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
15
海天一空 #不太行。 pop_ebp_ret = 0x080486ff main_addr = elf.symbols["main"] payload = 'a'*0x6c + & ...
什么叫不太行??你看看你的payload有几种数据类型,或者也可以找个翻译软件直接翻译一下“TypeError: Can't convert 'bytes' object to str implicitly”这句话是什么意思
2020-3-18 22:17
0
游客
登录 | 注册 方可回帖
返回
//