文章目录
  1. 1. 知识点一
    1. 1.1. Linux管道
    2. 1.2. 命令行参数
    3. 1.3. 字节序
    4. 1.4. 环境变量参数
    5. 1.5. objdump使用
    6. 1.6. 函数指针
  2. 2. 知识点二
    1. 2.1. 函数调用约定
    2. 2.2. 基本的缓冲区溢出攻击模型
    3. 2.3. Shellcode
    4. 2.4. __builtin_return_address函数
    5. 2.5. 理解多层跳转
    6. 2.6. strdup函数
    7. 2.7. grep命令
  3. 3. 知识点三
    1. 3.1. checksec脚本
    2. 3.2. 栈帧
    3. 3.3. NX选项
    4. 3.4. GOT和PLT
    5. 3.5. 信息泄露的实现
    6. 3.6. libc.so.6文件的作用

知识点一

Linux管道

Linux管道可以将一个进程的标准输出作为另一个进程的标准输入,管道的操作符号为“|”,比如ls命令可用于查看当前目录下的文件列表,而grep命 令可用于匹配特定的字符,因此ls | grep test命令可用于列出当前目录下文件名包含test的文件。

命令行参数

C语言的main函数拥有两个参数,为int类型的argc参数,以及char**类型argv参数。其中argc参数的值表示命令行参数的个数,而argv则指向一个字符串数组,该数组存储了具体的命令行参数的内容。注意程序本身的名字为命令行的第一个参数。
打印命令行参数信息的示例代码

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main(int argc, char** argv)
{
int i;
for (i = 0; i < argc; ++i)
{
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}

Linux的xargs命令可以将输入数据当做命令行参数传给指定的程序。比如执行命令python -c “print ‘AAA BBB CCC’” | xargs ./test

字节序

字节顺序,又称端序或尾序(英语:Endianness)。对于内存中存储的0x11223344这样一个值,从低地址往高地址方向的每一个字节来看,其内容在内存里的分布可能为0x11,0x22,0x33,0x44,也可能为0x44,0x33,0x22,0x11.即大端格式和小端格式。

环境变量参数

在Linux/Windows操作系统中, 每个进程都有其各自的环境变量设置。 缺省情况下, 当一个进程被创建时,除了创建过程中的明确更改外,它继承了其父进程的绝大部分环境变量信息。
扩展的C语言main函数可以传递三个参数,除了argc和argv参数外,还能接受一个char**类型的envp参数。envp指向一个字符串数组,该数组存储了当前进程具体的环境变量的内容,envp的最后一个元素指向NULL,此为envp结束的标识符。
打印环境变量参数信息的示例代码

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main(int argc,char** argv,char** envp)
{
int i =0;
while(envp[i])
{
printf("envp[%2d] = %s\n", i, envp[i]);
i +=1;
}
return0;
}

环境变量的格式为:环境变量名=环境变量值。
当父进程启动一个子进程时,子进程会继承父进程的换了变量信息。在Linux Shell下,通过export可以给Shell添加一个环境变量,此后通过Shell启动的子进程都会拥有这个环境变量。例如在Shell中执行export testenv=”Hello_World”之后,再执行./test,可以看到新的环境变量已经被子进程继承了。

objdump使用

使用objdump工具可以查看一个目标文件的许多内部信息,objdump有许多可选的参数选项,通过控制这些参数选项可以输出不同的文件信息。最常用的就是-d,用来获取二进制程序中代码段的反汇编指令列表,从而获取某一个函数的具体地址信息。

函数指针

函数指针(Function Pointer)是指向函数的指针,函数指针可以像一般函数一样,用于调用函数、传递参数。在C /C++这样的语言中,通过提供一个简单的选取、执行函数的方法,函数指针可以简化代码。
函数指针只能指向具有特定特征的函数,因而所有被同一指针运用的函数必须具有相同的参数和返回类型。
通常使用typedef来定义一个函数指针类型,如:
typedefvoid(*func)();
定义了func这样的函数指针类型,其可以指向返回值类型为void且没有函数参数的函数,比如void test()这样的函数,可以使用func myfp = test;来定义一个myfp变量,该变量指向test函数,通过执行myfp()可以达到执行test()函数同样的效果。

知识点二

函数调用约定

函数调用约定描述了函数传递参数的方式和栈协同工作的技术细节,不同的函数调用约定原理基本相同,但在细节上是有差别的,包括函数参数的传递方式、参数的入栈顺序、函数返回时由谁来平衡堆栈扥。这里重点讲解C语言函数调用约定。
在gdb中通过disas指令对main函数进行反汇编时,函数的开头和结尾的反汇编指令都是一样的:

1
2
3
4
5
push %ebp
mov %esp,%ebp
......
leave
ret

  1. 在函数的开头,首先是一条push %ebp指令,将ebp寄存器压入栈中,用于保存ebp寄存器的值,接着是mov %esp,%ebp将esp寄存器的值传递给ebp寄存器;在函数的末尾,leave指令相当于mov %ebp,%esp和pop %ebp两条指令,其作用刚好与开头的两条指令相反,即恢复esp和ebp寄存器的内容。
  2. 如果在函数A中调用了函数B,我们称函数A为主调函数,函数B为被调函数,如果函数B的声明为int B(int arg1, int arg2, int arg3),那么函数A中的调用函数B时的汇编指令的形式如下:

    1
    2
    3
    4
    push arg3
    push arg2
    push arg1
    call B
  3. 连续三个push将函数的参数按照从右往左的顺序进行压栈,然后执行call B来调用函数B。注意在gdb中看到的效果可能不是三个push,而是三个mov来对栈进行操作,这是因为Linux采用AT&T风格的汇编,而上面的指令使用的是Intel风格的汇编,比较容易理解。
    call指令的内部细节为:将下一条指令的地址压入栈中,然后跳转到函数B去执行代码。这里说的call下一条指令的地址也就是通常所说的返回地址。函数B最后一条retn指令会从栈上弹出返回地址,并赋值给EIP寄存器,达到返回函数A继续执行的目的。

基本的缓冲区溢出攻击模型

基本的缓冲区溢出攻击通常是通过改写函数返回地址的形式来发起攻击的。如A调用B函数,正常情况下B函数返回时执行retn指令,从栈上取出返回地址跳转回A函数继续执行代码。而一旦返回地址被缓冲区溢出数据改写,那么我们就可以控制函数B跳转到指定的地方去执行代码了。

Shellcode

Shellcode指缓冲区溢出攻击中植入进程的恶意代码,这段代码可以弹出一个消息框,也可以在目标机器上打开一个监听端口,甚至是删除目标机器上的重要文件等。
Shellcode通常需要使用汇编语言进行开发,并转换成二进制机器码,其内容和长度经常还会受到很多实际条件的限制,因此开发Shellcode通常都是非常困难的。在实际场景中,我们通常使用Metasploit这个工具来定制各种功能的Shellcode,当然也可以去网上查找一些现有的Shellcode进行测试,通常在shell-storm以及exploit-db等网站上都能找到一些比较成熟和稳定的shellcode,网址为:
http://shell-storm.org/shellcode

http://www.exploit-db.com/shellcode

__builtin_return_address函数

builtin_return_address函数接收一个参数,可以是0,1,2等。builtin_return_address(0)返回当前函数的返回地址,如果参数增大1,那么就往上走一层获取主调函数的返回地址。

理解多层跳转

retn指令从栈顶弹出一个数据并赋值给EIP寄存器,程序继续执行时就相当于跳转到这个地址去执行代码了。
如果我们将返回地址覆盖为一条retn指令的地址,那么就又可以执行一条retn指令了,相当于再在栈顶弹出一个数据赋值给EIP寄存器。

strdup函数

strdup可以用于复制一个字符串,我们通常使用字符串时会使用strcpy,而strdup只接受一个参数,也就是要复制的字符串的地址,strdup()会先用maolloc()配置与参数字符串相同大小的的空间,然后将参数字符串的内容复制到该内存地址,然后把该地址返回。strdup返回的地址最后可以利用free()来释放。

grep命令

当输出信息非常多的时候,我们很难快速找到我们感兴趣的信息。使用grep命令可以对匹配特定正则表达式的文本进行搜索,并只输出匹配的行或文本。
我们可以使用管道将一个程序的输出当做grep的输入数据,grep会根据给定的正则表达式参数对输入数据进行过滤。
对于grep的参数需要注意这样一个问题:当参数中存在空格时需要用双引号将参数包裹起来,此外,是正则表达式里面的通配符,如果要查找,需要使用反斜杠进行转移,即*。例如,如果我们要查找call %eax,在shell中执行objdump -d pwn| grep “call *%eax”即可。

知识点三

checksec脚本

操作系统提供了许多安全机制来尝试降低或阻止缓冲区溢出攻击带来的安全风险,包括DEP、ASLR等。在编写漏洞利用代码的时候,需要特别注意目标进程是否开启了DEP(Linux下对应NX)、ASLR(Linux下对应PIE)等机制,例如存在DEP(NX)的话就不能直接执行栈上的数据,存在ASLR的话各个系统调用的地址就是随机化的。
使用checksec.sh脚本可以方便的查看可执行程序是否启用了这些安全机制。例如:在shell中执行./checksec.h —file test
checksec脚本的下载地址为:
http://www.trapkit.de/tools/checksec.html

栈帧

在高级语言中,当函数被调用时,系统栈会为这个函数开辟一个栈帧,并把它压入到栈里面。新开辟的栈帧中的空间被它所属的函数所独占,当函数返回的时候,系统栈会清理该函数所对应的栈帧以回收栈上的内存空间。
每个函数都拥有自己独占的栈帧空间,有两个特殊的寄存器用于标识栈帧的相关参数:

  1. ESP寄存器,永远指向栈帧的顶端;
  2. EBP寄存器,永远指向栈帧的底部;

在调用一个函数的时候,函数所需要的参数首先会依次被压入的栈上,其次压入对应的返回地址,最后跳转到被调用的函数执行代码并开辟新的栈帧。新的栈帧的底部保存有EBP寄存器的值,基于EBP寄存器可以获取到被调用函数所需要的参数信息。

NX选项

NX即No-eXecute(不可执行)的意思,NX选项会将进程特殊区域的内存标记为不可执行,当CPU跳转到这些区域执行代码的时候便会产生异常,以阻止缓冲区溢出时直接在栈上执行恶意代码。
gcc编译器默认开启了NX选项,如果需要关闭NX选项,可以给gcc编译器添加-z execstack参数。在Windows下,类似的概念为DEP(Data Execution Prevention,数据执行保护),在最新版的Visual Studio中默认开启了DEP编译选项。

GOT和PLT

GOT(Global Offset Table,全局偏移表)是Linux ELF文件中用于定位全局变量和函数的一个表。PLT(Procedure Linkage Table,过程链接表)是Linux ELF文件中用于延迟绑定的表,即函数第一次被调用的时候才进行绑定。
所谓延迟绑定,就是当函数第一次被调用的时候才进行绑定(包括符号查找、重定位等),如果函数从来没有用到过就不进行绑定。基于延迟绑定可以大大加快程序的启动速度,特别有利于一些引用了大量函数的程序。打个比方,你一次性去超市买了一大堆物品,但是其中有些物品可能你永远也不会使用,这样就浪费了钱财;而延迟绑定就相当于需要的时候才去超市买东西,这样就节省了开支。
下面简单介绍一下延迟绑定的基本原理。假如存在一个bar函数,这个函数在PLT中的条目为bar@plt,在GOT中的条目为bar@got,那么在第一次调用bar函数的时候,首先会跳转到PLT,伪代码如下:

1
2
3
4
5
bar@plt:
jmp bar@got
patch bar@got

这里会从PLT跳转到GOT,如果函数从来没有调用过,那么这时候GOT会跳转回PLT并调用patch bar@got,这一行代码的作用是将bar函数真正的地址填充到bar@got,然后跳转到bar函数真正的地址执行代码。当我们下次再调用bar函数的时候,执行路径就是先后跳转到bar@plt、bar@got、bar真正的地址。

信息泄露的实现

在进行缓冲区溢出攻击的时候,如果我们将EIP跳转到write函数执行,并且在栈上安排和write相关的参数,就可以泄漏指定内存地址上的内容。比如我们可以将某一个函数的GOT条目的地址传给write函数,就可以泄漏这个函数在进程空间中的真实地址。
如果泄漏一个系统调用的内存地址,结合libc.so.6文件,我们就可以推算出其他系统调用(比如system)的地址。

libc.so.6文件的作用

在一些CTF的PWN题目中,经常可以看到题目除了提供ELF文件之外还提供了一个libc.so.6文件,那么这个额外提供的文件到底有什么用呢?
如果我们可以利用目标程序的漏洞来泄漏某一个函数的地址,那么我们就可以计算出system函数的地址了,当然,被泄露地址的函数必须也定义在libc.so.6中(libc.so.6中通常也存在有/bin/bash或者/bin/sh这个字符串)。
计算system函数地址的基本原理是,在libc.so.6中,各个函数的相对地址是固定的,比如函数A相对于libc.so.6的起始地址为addr_A,函数B相对于libc.so.6的起始地址为addr_B,那么,如果我们能够泄漏进程内存空间中函数A的地址address_A,那么函数B在进程空间中的地址就可以计算出来了,为address_A + addr_B - addr_A。

文章目录
  1. 1. 知识点一
    1. 1.1. Linux管道
    2. 1.2. 命令行参数
    3. 1.3. 字节序
    4. 1.4. 环境变量参数
    5. 1.5. objdump使用
    6. 1.6. 函数指针
  2. 2. 知识点二
    1. 2.1. 函数调用约定
    2. 2.2. 基本的缓冲区溢出攻击模型
    3. 2.3. Shellcode
    4. 2.4. __builtin_return_address函数
    5. 2.5. 理解多层跳转
    6. 2.6. strdup函数
    7. 2.7. grep命令
  3. 3. 知识点三
    1. 3.1. checksec脚本
    2. 3.2. 栈帧
    3. 3.3. NX选项
    4. 3.4. GOT和PLT
    5. 3.5. 信息泄露的实现
    6. 3.6. libc.so.6文件的作用