0%

软件安全——第10章

Chapter 10 漏洞利用

10.0 概念

安全事件与软件漏洞

  • 安全事件:攻击者在给定时间段内,利用漏洞或其他攻击手段在攻击对象中注入并触发恶意代码,产生拒绝服务、信息泄露、信息窃取、目标控制等后果的过程。
  • 漏洞:系统设计、实现或操作管理中存在的缺陷或弱点,能被利用而违背系统的安全策略。

漏洞分类与标准

按照漏洞威胁可分为:

  • 提权漏洞
  • 获权漏洞
  • 拒绝服务漏洞
  • 恶意软件植入漏洞
  • 数据丢失或泄露漏洞

按照漏洞成因可分为:

  • 输入验证漏洞
  • 访问验证漏洞
  • 竞态漏洞(竞争条件漏洞)
  • 意外情况处理漏洞
  • 设计漏洞
  • 配置漏洞
  • 环境漏洞

按照漏洞的严重性可分为:

  • A 类漏洞(高):威胁性最大的漏洞,往往由较差的系统管理或错误设置造成。
  • B 类漏洞(中):较为严重的漏洞,例如允许本地用户获得增加的和未授权的访问。
  • C 类漏洞(低):严重性不是很大的漏洞,例如允许拒绝服务的漏洞。

按照攻击被利用的方式可分为:

  • 本地攻击漏洞
  • 远程主动攻击漏洞
  • 远程被动攻击漏洞

重要网站:

  • CVE:漏洞暴露平台
  • CNVD:国家互联网应急中心
  • CNNVD:国家信息安全测评中心

漏洞利用对系统的威胁

  • 非法获取访问权限(获权)
    • 未经授权使用资源,如打印、读取、写、执行文件等
  • 权限提升(提权)
    • 用户账号由低权限提升到高权限
  • 拒绝服务
    • 使计算机软件或系统无法正常工作、无法提供正常服务
    • 本地拒绝服务:运行在本地的程序无法正常工作;远程拒绝服务:发送特定网络数据报文使提供服务的程序异常或退出
  • 恶意软件植入
    • 主动植入:利用正常功能或漏洞将恶意代码植入到目标之中,无需用户干预
    • 被动植入:将恶意代码植入时需要用户配合操作(木马、病毒等)
  • 数据丢失或泄露
    • 由于对文件访问权限设置错误导致数据被非法读取。如读取密码
    • 没有充分验证用户输入导致数据被非法读取。如Web相关漏洞
    • 系统漏洞,如DNS域传送漏洞

漏洞产生原因

逻辑错误、缺陷、社工、策略失误等,可以分为技术因素和非技术因素(大多为人为因素)

  • 技术错误:即上文提到的按漏洞成因分类的7种
  • 非技术错误:缺乏规范导致维护困难、缺乏测试与维护、开发团队不稳定、缺乏进度控制等

漏洞利用方式

  • 本地攻击模式:攻击者进入目标系统具有一定权限后进行的攻击。如内网渗透等
  • 远程主动攻击模式:攻击者以网络连接发现目标漏洞后进行攻击
  • 远程被动攻击模式:攻击者向目标发送网页、文档等点击后触发漏洞

软件漏洞生命周期

  • 漏洞挖掘
  • 漏洞重现
  • 漏洞诊断
  • 漏洞修复
  • 补丁测试
  • 补丁推送

(攻击者在其中任何一个时候都能够对系统造成损失)

典型软件漏洞

  • 缓冲区溢出:在下面会具体说明
  • 注入攻击:如SQL注入、系统命令注入、脚本注入等,多为对用户的输入检查不严格导致。
  • 跨站漏洞:跨站脚本攻击(XSS),攻击者将恶意脚本代码嵌入到Web页面中,用户打开后执行其中的代码触发漏洞或获取网站权限。(被动攻击)
  • 权限漏洞:访问控制机制上存在的漏洞。如Linux Kernel Pwn即利用Linux内核模块实现用户权限到root权限的提升。

10.1 栈缓冲区溢出

一、栈

  • 栈是一段连续的地址空间,是一个后进先出的数据结构,由高地址向低地址增长。
  • 每一个线程都有一个自己的栈,提供一个暂时存放数据的地方。
  • push将esp下压,pop将ebp上抬
  • esp指向栈顶,ebp指向栈帧底部。

注意:栈地址空间中保存的是程序执行时使用的局部变量,在main函数或其他函数中定义的变量均保存在栈中。代码中的字符串常量(如printf(“Hello world”))定义于rdata段,初始化的全局变量定义于data段,未初始化的全局变量定义于bss段。

栈中包含:

  • 函数参数
  • 函数返回地址
  • ebp的值
  • 一些通用寄存器的值
  • 当前执行的函数的局部变量

函数调用过程:

  1. call指令执行之前,程序首先将函数需要的参数逐一push进栈中。(有几种不同的参数压入顺序)
  2. 执行call指令跳转eip到函数开头。call指令将eip压入栈中。
  3. 函数开头一般会执行指令push ebp; mov ebp, esp,保存前一个栈帧的地址。
  4. 之后esp可能会根据该函数中定义的所有局部变量需要占用的空间进行自减,以保证esp和ebp之间能够有充足的空间保存局部变量。
  5. 开始执行函数功能,函数调用过程结束。

函数调用类型:(32位)

  • __cdecl:C调用规则,在后面的参数首先进入堆栈,参数返回后调用者负责清除堆栈,因此这种调用常会产生较大的可执行程序
  • __stdcall:标准调用,在后面的参数首先进入堆栈,被调用的函数返回前自行清理堆栈,生成的代码比cdecl小
  • __fastcall:将函数列表前两个参数放入寄存器,其他函数压栈,后面的参数首先进入堆栈,被调用者清理堆栈
  • Pascal:基本不用,前面的参数首先进入堆栈,不支持可变参数的函数调用

函数返回过程:

  1. esp上抬到达ebp的位置,局部变量不作处理全部放弃,也不会将这段内存清零。
  2. ebp找到父函数栈帧的底部(函数调用时已经保存在ebp指向的内存空间)并上抬(pop ebp)。
  3. retn返回,返回到父函数call指令之后一条指令。

二、栈溢出

特点:

  • 缓冲区在栈中分配
  • 拷贝的数据过长且没有进行检查
  • 覆盖了栈中的一些重要数据,如ebp、返回地址、某些关键局部变量等

产生原因:

  • C语言中有大量没有溢出保护的函数,如*scanf系列、*printf系列、gets函数、strcpy函数、strcat函数等(注:read函数理论上可以读取零字节,因此较strcpy等函数具有更高的危险性)
  • 程序员缺乏安全意识

shellcode和payload

  • shellcode是能完成一种特殊任务的自包含的二进制代码,根据不同的任务可能是发出一条系统调用或是进行高权限操作。
  • payload:shellcode以及实现跳转到shellcode的那部分填充代码的统称。

栈溢出shellcode的NSR模式:

  • S——shellcode,首先写入栈的低地址处
  • R——return address,返回地址,通常写入jmp esp的地址,在S之上
  • N——nops,无效指令,在R之上

注意:NSR模式构造的shellcode很可能会失败,原因多种多样。有时jmp esp后栈中的内容会被修改,导致shellcode执行失败。此时可以利用SEH进行攻击。

什么是SEH?
结构化异常处理(StructuredExceptionHandling,SEH)是Windows操作系统处理程序错误或异常的技术。SEH是Windows操作系统的一种系统机制,与特定的程序设计语言无关。SEH的注册结构体只能作为局部变量存在于当前线程的调用栈,如果一旦结构体的地址不在当前调用栈的范围中,则在进行异常分发时,将不会进入该函数。即我们可以通过栈溢出修改SEH中函数指针的值。SEH是基于线程的一种处理机制,且依赖于栈进行存储和查找,所以被称作是基于栈帧的异常处理机制。有关于其具体实现与功能需要对windows异常处理机制有更加深入的了解,考试不做要求。

fs:[0]指向SEH的初始地址,通过获取该地址以确定溢出的长度,这个长度必须确定以保证能够正确覆盖关键函数指针。将指针覆盖为类似于jmp esp指令的地址,即可执行shellcode。

shellcode的特点

  • 长度受限(不是所有),具体问题具体分析。
  • 不能使用特定字符,如’\x00’(read函数是唯一能够接收’\x00’的函数)
  • API函数自搜索和重定位能力
  • 一定的兼容性(很难,windows和linux的内存空间和内核排布很不一样)

练习题

1. 分析如下代码片段,回答下列问题。(25分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool guess(){
FILE* fd = open('/etc/urandom', 'r');
int answer = 0;
read(fd, (char*)(&answer), 4);
char buffer[0x10] = {0};
// 此时answer是一个随机数,不可预测
printf("Guess a number that I'm thinking...");
printf("You can input something first...");
scanf("%s", buffer);
printf("Now guess...");
scanf("%d", &guess);
if(guess == answer){
printf("Congratulations!");
system("calc.exe");
}else{
printf("Guess incorrect, no calc!!!");
exit(0);
}
return True;
}

注:①本函数中先定义的随机变量在栈中的地址在后定义的随机变量之上。②数字a的ASCII码为0x30+a,大写字母的ASCII码为0x40+字母序号(A为0,B为1,…,Z为25),小写字母的ASCII码为0x60+字母序号

(1) 简述该函数的执行流程,说明弹出计算器所需的条件。(4分)
(2) 本函数中存在什么漏洞?(3分)
(3) 当第一个输入为’12345678901234567890abcdefABCDEF\n’时,answer的值为_________(填16进制,2分),返回地址的值为_________(填16进制,2分),那么第二个输入为_________(3分)时可以成功弹出计算器。弹出计算器之后,这个程序很有可能会______________________(2分),原因是_______________________(2分)。
(4) 请写出改进方案,修复这个漏洞使计算器不那么容易被弹出。(7分)

答案:
(1) 本程序产生了一个随机数,之后可以在buffer中写入字符串,然后输入一个数字,如果这个数字与随机数相等,则弹出计算器。弹出计算器的条件是猜的数字与随机数相等。
(2) 栈溢出漏洞
(3) 0x30393837;0x42416665;809056311;崩溃;返回地址可能无效
(4) 将scanf("%s", buffer);修改为scanf("%16s", buffer);

2. 分析如下代码片段,回答下列问题:(30分)

1
2
3
4
5
6
7
8
9
void vuln(){
void (*exec)(const char*) = NULL;
void (*func)(const char*) = system;
char buffer[16];
gets(buffer);
printf("%s", buffer);
if(exec != NULL)
exec(buffer);
}

注:①通过反汇编找到vuln函数的起始地址为0x405678。②本函数中先定义的随机变量在栈中的地址在后定义的随机变量之上。

(1) 简述该函数的执行流程。(4分)
(2) 通过反汇编可以得到该函数的汇编代码,已知前几条语句如下,请填写前两空,第三空填写的数值x一定有______________。(3分)

1
2
3
4
push ebp
mov ___, ___
sub esp, ___
......

(3) 当输入长度为________时,程序会______________________________,从而获得system函数在内存中的地址。(4分)
(4) 如果这个函数只被调用了一次,能否仅通过这个函数弹出计算器?_____ 。为什么?__________________________________ 。(4分)
(5) 第一次函数调用时,你输入了第(3)题长度的payload,printf函数只显示了你输入的内容,可能的原因是_____________________________。(4分)在函数下一次调用时,你应该如何获取system函数的地址?__________________________________。(4分)
(6) 通过第一次函数调用的漏洞利用,获取到system函数的地址为0x7FAD8341,在该函数第二次被调用时,能否利用漏洞弹出计算器?若能,写出一个有效的输入并简述函数执行流程;若不能,说明理由。如果system函数的地址为0x7FAD8300呢?(7分)

答案:
(1) 函数首先接收任意长度的输入,然后检查exec函数指针是否为空,若不为空则执行到exec处。
(2) ebp;esp;x>=24
(3) 16;在printf输出时将system函数的地址接在输入后面输出
(4) 不能;无法控制exec的值,system的地址也不知道因此无法修改返回地址
(5) system函数地址最低位为空字节;尝试输入17、18、19字节数据,直至能够输出system函数的高位地址
(6) 能。输入为calc.exe(后接12字节空格)\x41\x83\xAD\x7F;不能,会截断。

10.2 堆缓冲区溢出与格式化字符串漏洞

一、堆

  • 堆是存放程序中请求操作系统分配给自己的内存段,其大小不固定,可以进行动态扩大与缩小。操作系统中采用动态链表管理,其内存不一定连续。
  • 每一个进程都有一个自己的堆(区别栈是每一个线程有一个)
  • 使用malloc系列函数(malloc、calloc、realloc等)/new/mmap等函数分配堆空间;使用free/delete/unmap等函数释放堆空间。(调用的系统函数基本上相同)

注意:两个程序动态申请的堆空间地址完全有可能相同。要搞清楚虚拟地址和物理地址的区别,程序中直接操作的是虚拟内存地址,由操作系统从物理地址中映射而来,并非物理内存地址。但两个程序分配的堆地址的物理地址也有可能相同。两个程序可以交错使用这块内存。

结构:堆表和堆块。

  • 堆表:位于堆空间起始位置,用于索引堆区中所有堆块的重要信息。在windows堆管理机制中,将堆表分为两种:快表(Lookaside)和空表(FreeList)。
    • 空表:共128项的数组,每个数组是一个双向链表头,除索引0的链表外每个链表中存放相同大小的堆块。其中索引为x的链表存放大小为8x的空闲堆块,因此1~127共可以存放8~1016字节的堆块。(32位)索引为0的链表存放大小大于等于1024的堆块,按照从小到大的顺序排列。在实际分配内存流程中,为防止堆空间的碎片化,会对相邻的均处于空表中的堆块进行合并与重新排列。
    • 快表:共128项的数组,采用单向链表存储,其中的堆块不合并且每一个链表最多存放4个堆块。其中索引为1~127的链表存放的堆块大小与空表相同。
  • 堆块:一块块分离的,碎片化的地址空间,由块首和块身构成。
    • 块首:头部几个字节,用于标识自身信息(大小,使用情况等)
    • 块身:数据存储区域,紧跟块首,内存分配得到的地址指向块身的头部。在堆块被释放时,块身的前8个字节用于存放堆表中双向链表的向前指针Flink和向后指针Blink。
  • HEAP区总体结构:
    • hHeap:HEAP的总体管理结构,句柄
    • 双指针区:定位分配内存、释放内存的位置,存放一些成对出现的指针
    • 用户分配内存区

堆管理相关寄存器:

  • eax:一般代码中用作返回值
  • ecx:一般代码中用作计数器
  • mov [ecx], eax; mov [eax+4], ecx

堆块关键操作:

  • 申请堆块时的脱链操作:即双向链表的脱链操作,汇编指令为mov [ecx], eax; mov [eax+4], ecx
  • 释放堆块时的入链操作:根据设计思想,应首先判断前后是否能进行堆块合并,若进行合并则需要进行脱链操作,再将合并后的堆块放入指定的链表中。

二、堆溢出

利用思路1:

  • 使用mov [ecx], eax; mov [eax+4], ecx完成对任意地址的控制。
  • 使用[esi+0x4C]指向下一个空闲堆块头部结构
  • 当有不能处理的异常发生时,系统调用UnhandledExceptionFilter函数,其实就是call [0x77EC044c],即执行0x77EC044c指向的异常处理程序。因此修改此处地址即可。
  • 随着防护措施的改进,上述思路在当前windows系统中已基本不可用。

利用思路2:堆喷射

  • 向堆中注入大量数据,使数据填满特定内存地址空间,当栈溢出时可以引导EIP到堆的空间。其中填充的数据是大量重复的nop指令,如果之后eip能够指向这段nop指令,就能够一直执行到后面的shellcode。这段nop充当类似“滑梯/滑板”的作用。
  • 目前依然流行,常见于解释JavaScript的浏览器和PDF解释器
  • 申请大量内存时。堆很有可能覆盖到0x0A0A0A0A(162M)、0x0C0C0C0C(192M)、0x0D0D0D0D(208M)
    传统的堆喷射使用JavaScript分配内存,根据堆喷射的思想,都是用同样一个指令覆盖一大片内存地址。在每块分配到的内存最后都附加shellcode。这个指令应相当于Nops的作用,且该指令指向的地址正好应落在覆盖的这片大的内存地址中。
    指令0C0C对应汇编代码为or al, 0C,对寄存器影响最小,可以起到Nops的作用。如果将eip指向0x0C0C0C0C这个地址(产生异常时可能可以实现),就会在这片内存中不断执行下去直到shellcode为止。
  • 优点:
    • 增加缓冲区溢出攻击的成功率
    • 覆盖地址可以简单使用类NOP指令覆盖
    • 可用于堆栈溢出攻击,用slidecode覆盖堆栈返回地址
  • 缺点:
    • 会导致被攻击进程内存占用暴增,容易被发现
    • 不能用于主动攻击,一般通过栈溢出利用或其他漏洞进行协同攻击
    • 如果目的地址被shellcode覆盖,则shellcode执行会失败,因此不能保证一定成功
  • 检测与防范:
    • 发现应用程序的内存大量增加,检测堆上的数据,看是否包含大量slidecode
    • 浏览器的脚本解释器开始重复申请堆的时候,监控模块记录堆的大小、内容和数量,如果重复堆请求到达阈值或者覆盖指定地址则立即阻止脚本执行。
    • 对于一些利用脚本进行攻击的情况,可以通过hook脚本引擎,分析脚本代码,根据一些堆喷射常见特征检测是否受到攻击
    • 开启DEP

利用思路3:Use After Free

  • 当一块堆空间被释放,对这段内存空间进行操作即称为UAF。
  • 由于windows内存空间中被释放的堆块的块身中存放有两个指针,通过UAF可以对这两个指针进行修改以破坏堆表的双向链表结构。

三、格式化字符串漏洞

  • 适用于printf函数系列。
  • 产生原因:
    • printf的不定长参数个数,且没有进行检查。
    • printf的%n能够对一段地址空间的值进行修改。(其功能是将前面已经打印出的字符个数赋给对应的地址
  • 通过静态扫描容易发现。

模拟题

3. 分析如下代码片段,回答下列问题:(24分)

1
2
3
4
5
6
7
8
9
10
void vuln(){
char of[0x10] = {0};
char buf1[0x100] = (char*)malloc(0x100);
printf("%p", buf1);
char buf2[0x100] = (char*)malloc(0x100);
scanf("%s", buf1);
printf("%s", buf1);
gets(of);
free(buf2);
}

注:①本函数中先定义的随机变量在栈中的地址在后定义的随机变量之上。②执行函数发现,函数的第一个输出为0x1030000,第二个堆块紧邻第一个堆块且在第一个堆块之上。

(1) 本函数存在2个漏洞,分别是____________________________(2分)。
(2) 本题能否直接在栈上写入shellcode?____ ,原因是__________________________。(4分)
(3) 本题能否直接在buf1中写入shellcode?____ ,原因是__________________________。(4分)
(4) 本题能够直接在buf2中写入shellcode,前面的地址填充无效地址?____ ,原因是___________________________。(4分)
(5) 由第(2)题和第(3)题,你认为第一个输入应该输入__________________,这样可以__________________________。之后第二个输入应为____________________________________,这样可以防止___________________,由此可以_______________________。(10分)

答案:
(1) 栈溢出漏洞、堆溢出漏洞
(2) 不能,不知道jmp esp或栈的地址
(3) 不能,buf1的地址后两字节均为空字节且buf的大小只有0x100。无法将返回地址覆盖为buf1中的任何地址(地址的第二个字节一定为\x00),会导致截断
(4) 不能,这样会破坏buf2堆块的头部结构,在free时会产生错误
(5) 0x100字节的垃圾数据,通过printf获取buf2堆块的块首信息;vuln函数的起始地址;函数异常退出;再次输入并在第二次调用时在buf2中溢出shellcode【出题漏洞:第一次申请的buf2和第二次不一样,块首信息可能不同】

10.3 整数溢出与其他漏洞

一、整数溢出

在程序计算过程中,有时会因为计算数值问题导致计算结果不正确。如计算int整型的0x12345678 × 0x87654321,这会导致溢出。有时这种溢出会导致严重的后果,如输入字符串长度时若不加检查输入负数,会导致过量读或过量写漏洞等。

防范方式

  • 整数安全意识:形成关于特殊数据输入的意识,比如之前先确定最大和最小输入,使用合适的数据类型。
  • 避免隐患运算直接操作:尽量避免对两个正数相加或相乘之后,再取结果比较。
  • 越界判断:在使用变量申请内存,或者作为数组下标时,注意对越界的监测。
  • 代码审计、安全检测

模拟题

4. 分析如下代码片段,回答下列问题:(34分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void vuln(){
char buf[0x50] = {0};
int len = 0x50;
char alphabet[0x20];
for(int i=0; i<26; i++)
alphabet[i] = 'A' + i;
read(stdin, buf, len); // 最多只能读取0x50个字节
for(int i=0; i<0x50; i++)
buf[i] &= 0xDF;
for(int i=0; buf[i]!='\n'&&buf[i]!='\0'; i++){
int temp = buf[i] - 0x40;
temp += 3;
if(temp >= 26)
temp -= 26;
buf[i] = alphabet[temp];
}
system(buf);
}

(1) buf[i] &= 0xDF的作用是____________________,这段代码实现的功能是______________________________________________________。(6分)
(2) 本题的漏洞产生的原因是_____________________________,要想执行calc.exe,首先需要解决的问题是____________________________。由于___________(填功能)的执行在___________(填功能)之前,因此可以事先______________________,这样缓冲区在读取到变量alphabet的_________(填’高地址一端’或’低地址一端’)时就能够____________________。从而将______(填一个可打印字符)成功写入system命令字符串的缓冲区buf中(参考ASCII码值:0x2B=‘+’; 0x2E=‘.’; 0x31=‘1’; 0x41=‘A’)(16分)
(3) 除了(2)的漏洞外,如果输入字符的ASCII码大于______的ASCII码,就会造成溢出,读取到alphabet高地址位置的值。如果alphabet的首地址为0x6000,那么想要让buf中存放该函数的返回地址,应该输入ASCII码为_____________________(填4个16进制数)的字符。(6分)
(4) 简述本题代码的漏洞修补方案,写出修改后的代码。(6分)

10.4 漏洞利用与发现

1. 漏洞利用

漏洞研究包含漏洞挖掘、漏洞分析、漏洞利用与漏洞防御四个方面。

漏洞的来源:黑客自行挖掘出来的漏洞,还未修复(被称为零日漏洞,即0-day漏洞)、从公开发布的POC或黑客交换漏洞得到(此类漏洞为0-day漏洞或1-day漏洞)、从已经发布的漏洞公告和漏洞补丁中获取的漏洞(又被称为n-day漏洞)。

漏洞利用的条件:用户没有打补丁或更新安全工具、管理员没有打补丁或更新安全工具、存在脏数据渗透路径

Exploit的结构

  • Exploit:利用漏洞实现shellcode植入和触发的过程
  • Exploit ≈ Payload + Shellcode
  • payload是针对于特定漏洞设计的用于触发漏洞的部分,与漏洞本身紧密相关,而shellcode与漏洞本身无关,只是一段可任意执行的代码。

漏洞利用的目标:

  • 修改内存变量 (邻接变量)
  • 修改代码逻辑(代码的任意跳转)
  • 修改函数的返回地址
  • 修改函数指针(C++)[虚函数](虚函数用于C++函数重载)
  • 修改异常处理函数指针(SEH,VEH,UEF,TEH)
  • 修改线程同步的函数指针

漏洞利用的过程:

  • 定位漏洞点:利用静态分析和动态调试确定漏洞机理,如堆溢出、栈溢出、整数溢出的数据结构,影响的范围
  • 按照利用要求,编写Shellcode
  • 溢出,覆盖代码指针,使得Shellcode获得可执行权

2. Ret2Libc + ROP

这是一种将返回地址修改到dll文件代码段的漏洞利用方式,可以不使用shellcode就能够执行指定功能。特点是向dll借代码而不是自己写shellcode,也可以通过这种利用方式完善payload。其中ROP返回到的地址可能不是函数的起始地址,而是函数中某一个位置的地址。

Ret2Libc -> VirtualProtect

在dll文件中有一个VirtualProtect函数可以用于关闭栈不可执行保护(NX),从而可以在栈上写入shellcode并执行。首先使用Ret2Libc将执行VirtualProtect函数,传入合适的参数。返回后转到jmp esp的地址,即可执行shellcode。

Ret2Libc的防护措施:ASCII armoring

将所有库函数的起始地址都包含一个零字节,使得无法完整溢出。
恶意代码对抗措施:Ret2plt

  • Linux ELF文件中大多数有一个.plt节,其中存放一系列jmp指令跳转到所有需要引用的库函数。因此可以将返回地址写到.plt节中跳转到需要的库函数中。.plt节的深入原理与Linux的间接跳转原理有关,不要求掌握。

练习题

5. 分析如下代码片段,回答下列问题:(29分)

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
char key[10] = {0};
(void)(*func(void)) = NULL;
void vuln(){
printf("Please input length: ");
char buf[0x40];
int len = 0;
scanf("%d", &len);
if(len > 8){
printf("You greedy man!");
exit(0);
}
read(stdin, buf, (unsigned int)len); // 读取len长度输入
}
void foo1(unsigned int a, unsigned int b, unsigned int c){
if(a != 0x12345678 || b != 0x87654321 || c != 0xdeadbeef){
printf("Argument error!");
exit(0);
}
strcpy(key, "calc.exe");
}
void foo2(){
// 下面这一部分函数的功能是:检查eax=0x12345678且ebx=0x87654321
// 如果不是则调用exit函数退出程序。
__asm__("cmp eax, 12345678h"
"jnz ex"
"cmp ebx, 87654321h"
"jnz ex"
"jmp foo2proc");
ex:
exit(0);
/////////////////////////////////////
foo2proc:
func = system;
}
1
2
3
4
5
6
7
8
9
10
11
12
(0x1000)
pop eax
ret
(0x1010)
xor ebx, eax
ret
(0x1020)
mov ebx, 0cafebabeh
ret
(0x1030)
add esp, 0ch
ret

(1) 要想在全局变量key中保存字符串’calc.exe’,需要满足的条件是________________________________________;要想在全局变量func中保存函数指针system,需要满足的条件是___________________________;(4分)
(2) 本题的栈溢出需要通过_______漏洞触发,在vuln函数输入len为_________,可以绕过检查,从而能够写入很长的字符串。输入字符串的第________(填16进制数)位能够覆盖vuln函数返回地址。(6分)
(3) 设vuln函数的返回地址在栈中保存的位置为0x100,那么如果需要在全局变量key中保存字符串’calc.exe’,需要将这个地方覆盖为_______________,这个函数的第一个参数应该写在__________________(填’0x104’或’0x108’)的位置,原因是____________________________。(6分)
(4) 如果想在全局变量func中保存函数指针system,在调用foo2函数之前需要首先使用0x1000和0x1010的两段代码碎片,这种漏洞利用方式被称为__________。(2分)
(5) 写出read函数执行时你的完整输入,格式:每行按"地址:值"格式书写,如"0x100:0x12345678"。已知函数的返回地址为0x100,foo1函数的起始地址为0x4013AB,foo2函数的起始地址为0x402880,全局变量key的地址为0x602040,func的地址为0x602050,可以写入任意字节(含零字节),提示:你需要首先计算输入的起始地址然后再开始书写。(8分)
(6) 修复这个漏洞最简单的修改方法是______________________。(3分)

答案:
(1) 调用foo1函数且参数从左到右的值依次应为0x12345678、0x87654321、0xdeadbeef;调用foo2函数且调用时eax的值应为0x12345678,ebx的值应为0x87654321
(2) 整型溢出漏洞;负数;0x48~0x4b
(3) foo1函数起始地址(或foo1函数中strcpy语句的起始地址);0x108;0x104应为foo1函数的返回地址
(4) ROP
(5)

  • 0xbc~0xff:垃圾数据
  • 0x100~0x103:0x1020
  • 0x104~0x107:0x1000
  • 0x108~0x10b:0x4D9BF99F(0x4D9BF99F XOR 0xcafebabe = 0x87654321)
  • 0x10b~0x10f:0x1010
  • 0x110~0x113:0x1000
  • 0x114~0x117:0x12345678
  • 0x118~0x11b:0x402880(foo2函数起始地址)
  • 0x11c~0x11f:0x4013AB(foo1函数起始地址)
  • 0x120~0x123:0x1030(思考这里将返回地址设为0x1030的作用是什么)
  • 0x124~0x127:0x12345678
  • 0x128~0x12b:0x87654321
  • 0x12c~0x12f:0xdeadbeef
  • 0x130~0x133:0x602050
  • 0x134~0x137:0xdeadbeef
  • 0x138~0x13b:0x602040

(6) if判断条件加上对负数的判断

6. Ret2csu是Linux平台下一种经典的ROP利用方法,它利用了每一个linux系统中可执行文件都存在的函数__libc_csu_init函数。下面是该函数的一个片段,分析如下代码片段,回答下列问题:(20分)(注:考试中不会出64位机器的题目,这里仅做练习,必要信息都会给出)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
loc_401A50:					; 下面是每一条指令的地址
mov rdx, r13 ; 0x401A50
mov rsi, r15 ; 0x401A53
mov rdi, rbp ; 0x401A56
call ds:[r12+rbx*8] ; 0x401A58
add rbx, 1 ; 0x401A5C
cmp r14, rbx ; 0x401A60
jnz short loc_401A50 ; 0x401A63
add rsp, 8 ; 0x401A65
pop rbx ; 0x401A69
pop rbp ; 0x401A6A
pop r12 ; 0x401A6B
pop r13 ; 0x401A6D
pop r14 ; 0x401A6F
pop r15 ; 0x401A71
retn ; 0x401A73

(1) 经过检查机器码发现,pop r15指令占2字节,其高1字节可表示另一条指令:pop rdi。同样pop r14指令的高1字节可表示pop rsi指令。在上述汇编代码中,将rip改为___________可以执行pop rsi,改为__________可以执行pop rdi。(4分)
(2) 这段代码中有一个比较判断,当使用call指令执行代码之后,如果不再需要call调用,应该跳过这个判断,需要满足_________________。(3分)
(3) 在64位Linux系统中,函数调用的前6个参数分别存放在rdi,rsi,rdx,rcx,r8,r9寄存器中。若要调用某函数,其前三个参数需要是0x12345678,0x87654321,0xdeadbeef,在ROP覆盖返回地址时,可以将返回地址修改为________________,可连续设置多个寄存器的值为自己设定的任意值。(3分)
(4) 设此时某关键函数foo的地址保存在0x602020处,请写出ROP的内容以执行"foo(0x12345678,0x87654321,0xdeadbeef)"(从返回地址开始写,返回地址保存在栈中的地址为0x100000)(10分)
格式示例:

1
2
3
0x100000: 0x1234567887654321
0x100008: 0xdeadbeefdeadbeef
...

答案:
(1) 0x401A70;0x401A72
(2) r14==rbx+1
(3) 0x401A69
(4)

  • 0x100000~0x100007:0x401A69
  • 0x100008~0x10000f:0(rbx)
  • 0x100010~0x100017:0x12345678(rbp)
  • 0x100018~0x10001f:0x602020(r12)
  • 0x100020~0x100027:0xdeadbeef(r13)
  • 0x100028~0x10002f:1(r14)
  • 0x100030~0x100037:0x87654321(r15)
  • 0x100038~0x10003f:0x401A50