安全面经。。。

对于在网上找到的面经中的问题以及秋招过程中遇到的问题的整理和回答。

原帖链接:信息安全工程师面经– 腾讯阿里网易美团360京东华为

网易

A.关于x86汇编

(1)常用的跳转指令

无条件跳转: JMP;
根据 CX、ECX 寄存器的值跳转: JCXZ(CX 为 0 则跳转)、JECXZ(ECX 为 0 则跳转);
根据 EFLAGS 寄存器的标志位跳转, 相关指令列出如下。

首先介绍EFLAGS 寄存器的运算结果标志位——

ZF标志(ZeroFlag):零位标志位,它记录相关指令执行后的结果是否为0,如果是0,那么ZF=1,如果结果不为0,那么ZF=0。

PF标志(ParityFlag):奇偶标志位,它记录相关指令执行后,其结果的所有二进制位中1个个数是否为偶数,如果是偶数,PF=1,反之为0。

SF标志(SignFlag):符号标志位,它记录相关指令执行后,其结果是否为负,如果结果为负,SF=1,如果非负,SF=0。

CF标志(Carry进位,Flag标志):进位标志位,一般情况,进行无符号运算时,它记录运算结果的最高位向更高位的进位值,或从更高位的借位值,如果运算结果的最高位产生了一个进位或借位,那么其值为1,否则其值为0。

OF标志(Overflow溢出,Flag标志):溢出标志位,在进行有符号数运算的时候,如果结果超出了机器所能表示的范围称为溢出,OF的值被置为1,否则OF的值为0。注意:这里所说的溢出,只是对有符号运算而言。

JE   ;等于则跳转
JNE  ;不等于则跳转

JZ   ;为 0 则跳转
JNZ  ;不为 0 则跳转
//观察零标志位ZF, ZF为1,JE/JZ跳转执行,否则JNE/JNZ跳转

JS   ;为负则跳转
JNS  ;不为负则跳转
//符号标志位SF为1则JS跳转;否则JNS跳转

JC   ;进位则跳转
JNC  ;不进位则跳转
//进位标志位CF为1则JC跳转;否则JNC跳转

JO   ;溢出则跳转
JNO  ;不溢出则跳转
//溢出标志位OF为1时JO跳转;否则JNO跳转

JP   ;奇偶位置位则跳转(比较结果中的1的个数为偶数时跳转)
//奇偶位标识PF置1时跳转
JNP  ;奇偶位清除则跳转
//奇偶位标识PF为0时跳转
JPE  ;奇偶位相等则跳转
//奇偶位标识PF置1时跳转
JPO  ;奇偶位不等则跳转
//奇偶位标识PF为0时跳转

使用无符号数比较的JCC指令

使用有符号数比较的JCC指令

根据标志寄存器中的相关标志位的值来进行跳转的条件跳转指令只能与那些能够影响标志寄存器的相关标志位的指令配合使用;能够直接影响标志寄存器的相关标志位的指令有:

  1. 算术运算指令 : add、sub、adc、sbb、inc、dec、neg、mul、div、imul、idiv,等等;
  2. 按位逻辑运算 : and、or、xor、not,等等;
  3. 比较运算指令 : cmp、test;
  4. 移位操作指令 : shr、shl、sar、sal、ror、rol、rcr、rcl;
  5. BCD数调整指令: aaa、aas、daa、das、aam、aad;
  6. 标志处理指令 : clc、stc、cmc、cld、std、cli、sti;

条件跳转指令总结
汇编:jmp系列跳转指令总结
标志寄存器及其标志位
汇编跳转指令总结
影响标志位的汇编指令

(2)堆和栈的区别

栈(快捷,但是自由度小):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。

堆(比较麻烦,但是自由度大): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

二者的区别总结如下——

  • 申请方式和回收方式不同:操作系统 VS 程序员(栈上的数据的生存周期只是在函数的运行过程中,运行后就释放掉,不可以再访问。而堆上的数据只要程序员不释放空间,就一直可以访问到,不过缺点是一旦忘记释放会造成内存泄露。)
  • 申请后系统的响应:堆会在申请后还要做一些后续的工作这就会引出申请效率的问题。
  • 申请大小的限制:能从栈获得的空间较小,堆获得的空间比较灵活,也比较大。
  • 堆和栈中的存储内容:栈中存储的一般都是函数生存周期中的临时变量(如函数的参数值,局部变量的值等),而堆中的内容由程序员具体安排。
  • 存取效率的比较:栈较快。

内存中的堆和栈到底是什么
堆和栈的区别 之 数据结构和内存

(3)函数调用过程

函数的调用离不开,最直接相关的是栈帧

栈帧,也就是stack frame,其本质就是一种栈,只是这种栈专门用于保存函数调用过程中的各种信息(参数,返回地址,本地变量等)。栈帧有栈顶和栈底之分,其中栈顶的地址最低,栈底的地址最高,SP(栈指针)就是一直指向栈顶的。在x86-32bit中,我们用 %ebp 指向栈底,也就是基址指针;用 %esp 指向栈顶,也就是栈指针。下面是一个栈帧的示意图——

理解函数调用过程,最关键的理解入栈内容(依次为函数参数、返回地址、调用者的ebp、被调用者的局部变量等)和出栈内容(和入栈内容相反)。

C函数调用过程原理及函数栈帧分析

B.软件断点和硬件断点

(1)硬件断点

  • 硬断点需要硬件寄存器提供支持,断点的数目受Embedded ICE中的Watchpoint数目的限制,但是可以在任何地方设置断点。
  • 硬件断点需要目标CPU的硬件支持,当前流行的ARM7/9内部硬件设计提供两组寄存器用来存贮断点信息,所以ARM7/9内核最多支持两个硬件断点,而ARM11则可以支持到8个硬件断点.这与调试器无关.

(2)软件断点

  • 通过在运行起来的程序中设置特征值实现,其数目不受限制,但是一般情况下软件断点只能在可写的存储器的地址中设置(比如:RAM),而不能在ROM(只读内存比如:Flash)中设置。
  • 通过在代码中设置特征值的方式来实现的.当需要在某地址代码处设置软件断点的时候,仿真器会先将此处代码进行备份保护,然后将预先设定好的断点特征值(一般为0x0000等不易与代码混淆的值)写入此地址,覆盖原来的代码数据.当程序运行到此特征值所在的地址时,仿真器识别出此处是一个软断点,便会产生中断.当取消断点时,之前受保护的代码信息会被自动恢复.

硬件断点和软件断点的区别

C.webview组件远程代码执行漏洞原理

Android API level 16以及之前的版本存在远程代码执行安全漏洞,该漏洞源于程序没有正确限制使用WebView.addJavascriptInterface方法,远程攻击者可通过使用Java Reflection API利用该漏洞执行任意Java对象的方法,简单的说就是通过addJavascriptInterface给WebView加入一个JavaScript桥接接口,JavaScript通过调用这个接口可以直接操作本地的JAVA接口。

简化版原理:在Android系统版本低于4.2(Android API level 小于17)时,使用WebView加载外部网页或者本地网页并访问执行网页中的JS代码,网页中含有恶意JS代码,攻击者可以找到存在“getClass”方法的对象,然后通过反射的机制,得到Java Runtime对象,然后调用静态方法来执行系统命令。具体代码示例如下——

//存在漏洞的代码——
WebView webView = new WebView (R.id.webView1);
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new TEST(), “demo”);
webView.loadUrl(“http://127.0.0.1/check.html”);


//攻击代码——
Check.html 代码:

<html>
<script>
function execute(cmd){
return demo.getClass().forName(‘java.lang.Runtime’).getMethod(‘getRuntime’,null).invoke(null,null).exec(cmd);
}
execute([‘/system/bin/sh’,’-c’,’echo  “hello” > /sdcard/check.txt’]);
</script>
</html>

调用demo对象的getClass方法得到java.lang.Runtime对象,然后通过java反射机制调用getRuntime方法获得runtime实例,最终通过exec方法执行命令。代码执行成功会在SD卡根目录下生成check.txt文件。

WebView远程代码执行漏洞学习并复现
Android WebView 远程代码执行漏洞简析

D.root判断

Root一般可以分为2种——

  • 不完全root:通过各种系统漏洞,替换或添加su程序到设备,获取Root权限,而在获取root权限以后,会装一个程序用以提醒用户是否给予程序最高权限,可以一定程度上防止恶意软件,通常会使用Superuser或者 SuperSU。
  • 完全root:替换设备原有的ROM(安卓ROM是手机重新刷入系统的一个程序包),以实现取消secure设置。

判断方法——

  • 查看系统是否是测试版:

root@android:/ # cat /system/build.prop | grep ro.build.tags
ro.build.tags=release-keys(判断是是test-keys(测试版),还是release-keys(发布版))

  • 检测系统目录中是否包含su等标志性文件(判断存在之后还需判断其是否能执行)。

Android root检测方法小结
Android 判断手机是否有root权限
Android检查手机是否被root

E.dex文件结构

(1)Android应用编译

(2)dex文件

dex文件作用——记录整个工程中所有类的信息。

dex文件结构——

在010Editor中查看dex文件头——

一张图搞懂dex——

dex文件解析(第三篇)

F.APK防护

(1)概述

纯干货:Android APP 防破解进化史

  • 原始社会时期——代码混淆
  • 奴隶社会时期——自我校验(签名、或计算自己应用dex的md5值等)
  • 封建社会时期——dex文件变形
  • 资本主义社会时期——Dex保护(隐藏dex文件;对dex文件结构进行变形)、So保护(修改Elf头、节表;选择开源加壳工具)
  • 社会主义时期——这个时期,技术的发展与普及让人人都是开发者成为可能。而破解者的破解技术和手段也在随之变化。单一的应用保护措施已经无法有效的应对破解者的攻击,所以还需要从多重维度和深度对应用进行加固保护(比如解决资本主义时期提到的几种保护措施中遗留的很多问题如下图)。

(2)DEX保护

DEX文件隐藏

基本流程如下图所示——

DEX字节码变形

该方法主要是对classes.dex文件的所有method部分的数据进行加密,以随机的名称存储在隐蔽的文件夹中,应用运行时将这部分数据解密出来,映射到内存中,通过修改codeoff指针重新指向这些method,同时还采用各种复杂的Anti-debugging技术来阻碍人工分析和调试,通过只解密恢复当前执行代码的策略来防止内存dump。

VMProtect

与其它大部分的保护程序不同,VMProtect可修改程序的源代码[2]。VMProtect可将被保护文件中的部分代码转化到在虚拟机(以下称作VM)上运行的程序(以下称作bytecode)中。

Vmprotect使用虚拟一个不同于x86的CPU来执行转化后的程序,这个CPU只支持简单运算以及最简单的无条件跳转指令,因此为了实现x86一条指令同样的功能,Vmp的CPU需要执行多条指令。这样令代码的阅读者需要阅读大量的代码才能知道其中的程序逻辑。

转化成Vmp Cpu 指令数据(bytecode)使用了简单的数据加密,阅读者不解密是无法了解bytecode的指令意义

VMP的 CPU 解释模拟程序,有用代码只有大概500条x86指令,但是使用了大量的垃圾指令,将其解释程序塞满了2000多条x86指令,同时将程序的正常逻辑打乱,不借助代码优化工具,人类的眼睛看起来会非常的费力。

将其主要特征整理如下——

  • 将由编译器生成的本机代码(native code)转换成字节码(ByteCode);
  • 将控制权交由虚拟机,由虚拟机来控制执行;
  • 转换后的字节码非常难以阅读,增加了破解的复杂度。

LBE安全大师张勇:软件加固技术浅析
Android APK加固技术方案调研

(3)资源加密

资源文件加密保护,从字面来看,无非是对APP中的资源文件进行加密(文件名/文件路径加密),在APP运行时对资源文件进行解密恢复,从而使应用正常访问资源文件。由于资源文件被加密,因此通过对APP进行反编译并不能看到真正的资源文件,从而保证资源文件的安全性。虽然资源文件加密保护大体思路如此,但是实现方案和效果则可能不同。

方案一

最简单的方法,按照Proguard的做法,直接在源码级别修改,将代码以及xml的R.string.name中替换到R.string.a,icon.png重命名为a.png 然后再交给Android编译。

方案二

直接修改resources.arsc的二进制数据,不改变打包流程,只要在生成resources.arsc之后修改它,同时重命名资源文件。

方案三

修改aapt来实现资源文件命名的混淆。让aapt在编译资源时生成混淆后的正确的resource.arsc文件,同时修改资源文件名称。

方案四

自己实现一个类似于资源打包器aapt的安装包解压、压缩打包器。

Android apk资源保护方案研究分析
资源混淆方案之微信vs美团
安装包立减1M–微信Android资源混淆打包工具

2019秋招

这里就不分公司了,把碰到的问题总结一下。

A.关于OD断点

(1)类型

  • 普通断点(F2-访问断点)
  • 内存断点(内存访问断点、内存写入断点)
  • 硬件断点
  • API断点(本质是CC断点)
  • 条件断点(本质是一个CC断点(CC是红色,条件是粉色),选到某行,快捷键 shift + F2。弹出的框中输入条件,如 EAX==400000,EAX==400000 || ECX == 6,EAX==400000 && ECX == 6,满足条件,就断。)

(2)原理

F2断点/CC断点

原理是将断下的指令地址处的第一个字节设置为0xCC,当然这是OD帮我们做的,而0xCC对应的汇编指令为int3,是专门用来调试的中断指令。当CPU执行到int3指令时,会触发异常代码为EXCEPTION_BREAKPOINT的异常,这样OD就能够接收到这个异常,然后进行相应的处理,这也是CC断点也叫int3断点的原因。

好处是可以设置任意个,缺点是容易被会检测出来。

内存断点

  • 将设置的内存断点的地址记录下来
  • 对这个地址的内存页面修改其属性
  • 如果是内存写断点,就修改为PAGE_READ(可读,可执行)
  • 如果是内存访问断点,就修改为PAGE_NOACCESS(不可访问)
  • 只要访问到这个页面就会产生相应的异常,然后由OD来判断是否与记录的断点一致,从而是否中断下来

硬件断点

这是计算机硬件提供给我们的功能.
硬件断点和DRx调试寄存器有关。从Inter CPU体系架构手册中,可以找到DRx调试寄存器的介绍。DRx调试寄存器总共有8个,从DRx0到DRx7。每个寄存器的特性如下:

  • DR0-DR3:调试 地址寄存器,保存需要监视的地址,如设置硬件断点;
  • DR4-DR5:保留,未公开具体作用;
  • DR6:调试 寄存器组 状态寄存器;
  • DR7:调试寄存器组 控制寄存器。

硬件断点原理是使用4个调试寄存器(DR0,DR1,DR2,DR3)来设定地址,以及DR7设定状态,比如:对这个401000是硬件读还是写,或者是执行;是对 字节还是对字,或者是双字。因此最多只能设置4个断点。

OllyDbg支持调试寄存器,其称为硬件断点。设置方法是在指定的代码行单击鼠标右键,执行设置断点、硬件执行命令。

硬件断点优点是速度快,在 INT3断点容易被发现的地方,使用硬件断点来代替会有很好的效果,缺点就是最多只能设置4个断点。   

B.IDA和OllyDbg的反汇编原理和差异

(1)反编译流程

反编译,是将可执行文件本身转换并还原成高级语言源程序的操作。

(2)IDA的反汇编流程

  1. 首先,确定需要进行反编译的代码区域(指令和数据混杂在一起)。以反编译可执行文件为例,可执行文件必须要符合可执行文件的通用格式,如window所使用的可移植可执行格式(ProtableExecutable,PE)或者Linux系统下常用的可执行和链接格式(Executableandlinkingformat,ELF)。这些格式通常含有特定的规则,IDA根据这些规则确定可执行文件中包含的代码和代码入口点(指令地址)的部分位置;
  1. 然后,获取到指令的起始地址后,IDA会进一步读取该地址所包含的值,并执行一次表查找,将二进制操作码的值和它的汇编语言的助记符对应起来;
  1. 接下来,IDA获取指令并解码任何所需要的操作数,同时对它们的汇编语言等价形式进行格式匹配(如X86汇编语言所使用的的两种主要格式Intel和AT&T格式),并将其在反汇编代码中输出;
  1. 最后,输出一条指令,继续反汇编下一条指令,并重复上述过程,直到反汇编完文件中的所有指令。

对于大多数的反汇编工具而言,主要的反汇编思想都是和上述差不多的,区别在于它们采取的用于选择下一条指令的算法的不同,线性扫描和递归下降是两种最主要的反汇编算法。采用了线性扫描的工具有GNU调试器(gdb)、WinDbg等,而IDA是一种典型的递归下降反汇编器(Ollydbg也是)。

(3)差异

线性扫描和递归下降

现线性扫描的一大特点就是简单方便,但是它存在一个问题:它无法知道整个程序的执行流。

递归下降反汇编算法的主要优点在于它具有区分代码与数据的强大能力,作为一种基于控制流的算法,它很少会在反汇编过程中错误地将数据值作为代码处理;其主要缺点在于它无法处理间接代码路径,如利用指针表来查找目标地址的跳转或调用。这个缺点是可以改进的,通过采用一些用于识别指向代码的指针的启发式方法,递归下降反汇编器就能够提供所有代码,并清楚地区分代码与数据。IDA就是在采用了递归下降算法的同时,增加了一些启发式方法来提高反汇编的准确性。

递归下降反汇编对于不同的指令的处理如下图所示——

IDA和Ollydbg

二者的差异主要体现在——

  • 目标不同:Ollydbg主要针对32位的windows平台下的可执行文件exe;而IDA不仅可以反汇编exe、还可以反汇编elf、dex等可执行文件。
  • 效果不同:Ollydbg的反汇编效果没有IDA好,估计是虽然采用了同样的递归下降算法,但其并没有像IDA一样增加一些启发式方法优化反汇编效果。

C.恶意软件分析流程

恶意代码分析是由浅入深,逐步深入的过程。初步分析目标是分析恶意代码的自启动特征以尽快的提供清除恶意代码的措施,然后是分析恶意代码的触发条件以便判断分析对象是否早本机进行了破坏活动,然后就是分析恶意代码的破坏功能,如果分析对象已经进行了破坏活动,需要提出恢复措施,然后再分析恶意代码的实现方式等其他特性。具体分析流程见下图。

D.函数运行相关

(1)C语言中的常量存储

局部变量、静态局部变量、全局变量、全局静态变量、字符串常量以及动态申请的内存区

  • 局部变量存储在栈中
  • 全局变量、静态变量(全局和局部静态变量)存储在静态存储区
  • new申请的内存是在堆中
  • 字符串常量也是存储在静态存储区

栈中的变量内存会随着定义所在区间的结束自动释放;
而对于堆,需要手动free,否则它就一直存在,直到程序结束;
对于静态存储区,其中的变量常量在程序运行期间会一直存在,不会释放,且变量常量在其中只有一份拷贝,不会出现相同的变量和常量的不同拷贝。

char *c="hello world";
//"hello world"这个字符串被当作常量而且被放置在此程序的内存静态区。
//c为一个字符型指针,若为局部变量,则存储在栈内,该指针变量里面存了个地址,该地址为字符串中第一个字母h的地址

(2)x86函数的调用和返回

x86-64 下函数调用及栈帧原理

函数的调用

子函数调用时,调用者与被调用者的栈帧结构如下图所示——

在子函数调用时,执行的操作有——

  • 父函数将调用参数从后向前压栈
  • 将返回地址压栈保存(call xxx)
  • 跳转到子函数起始地址执行(call xxx)
  • 子函数将父函数栈帧起始地址(%rpb) 压栈(子函数完成)
  • 将 %rbp 的值设置为当前 %rsp 的值,即将 %rbp 指向子函数栈帧的起始地址。

函数调用时在汇编层面的指令序列如下——

...   # 参数压栈
call FUNC  # 将返回地址压栈,并跳转到子函数 FUNC 处执行
...  # 函数调用的返回位置

FUNC:  # 子函数入口
pushq %rbp  # 保存旧的帧指针,相当于创建新的栈帧
movq  %rsp, %rbp  # 让 %rbp 指向新栈帧的起始位置
subq  $N, %rsp  # 在新栈帧中预留一些空位,供子程序使用,用 (%rsp+K) 或 (%rbp-K) 的形式引用空位

//注意:pushq和push的区别——GAS汇编指令通常以字母“b”,“s”,“w”,“l”,“q”或“t”为后缀,确定操作的操作数大小。

> b =字节(8位)
> s =短(16位整数)或单(32位浮点)
> w =字(16位)
> l = long(32位整数或64位浮点)
> q =四(64位)
> t =十个字节(80位浮点)

函数的返回

由于函数调用时已经保存了返回地址和父函数栈帧的起始地址,要恢复到子函数调用之前的父栈帧,我们只需要执行以下两条指令——

movq %rbp, %rsp# 使 %rsp 和 %rbp 指向同一位置,即子栈帧的起始处
popq %rbp # 将栈中保存的父栈帧的 %rbp 的值赋值给 %rbp,并且 %rsp 上移一个位置指向父栈帧的结尾处

//x86-64 架构中提供了 leave 指令来实现上述两条命令的功能。

执行 leave 后,前面图中函数调用的栈帧结构如下图——

调用 leave 后,%rsp 指向的正好是返回地址,x86-64 提供的 ret 指令,其作用就是从当前 %rsp 指向的位置(即栈顶)弹出数据,并跳转到此数据代表的地址处。

可以看出,leave 指令用于恢复父函数的栈帧,ret 用于跳转到返回地址处,leave 和ret 配合共同完成了子函数的返回。当执行完成 ret 后,%rsp 指向的是父栈帧的结尾处,父栈帧尾部存储的调用参数由编译器自动释放。

(3)函数调用约定

作用

函数调用约定是用于说明以下问题的——

  • 当参数个数多于一个时,按照什么顺序把参数压入堆栈
  • 函数调用后,由谁来把堆栈恢复原状
  • 编译器函数名的修饰规则

所以各种调用约定的区别就在于针对以上问题的约定规则不同。

分类

c语言中常用的调用约定有以下三种:_stdcall、cdecl和fastcall

1.常用场合

  • __stdcall:Windows API默认的函数调用协议。
  • __cdecl:C/C++默认的函数调用协议。
  • __fastcall:适用于对性能要求较高的场合。

2.函数参数入栈方式

  • __stdcall:函数参数由右向左入栈。
  • __cdecl:函数参数由右向左入栈。
  • fastcall:从左开始不大于4字节的参数放入CPU的ECX和EDX寄存器,其余参数从右向左入栈。*(fastcall在寄存器中放入不大于4字节的参数,故性能较高,适用于需要高性能的场合。)*

3.恢复栈原状

  • __stdcall:函数调用结束后由被调用函数恢复。
  • __cdecl:函数调用结束后由函数调用者恢复。
  • __fastcall:函数调用结束后由被调用函数恢复。
  • 不同编译器设定的栈结构不尽相同,跨开发平台时由函数调用者清除栈内数据不可行。
  • 某些函数的参数是可变的,如printf函数,这样的函数只能由函数调用者清除栈内数据。
  • 由调用者清除栈内数据时,每次调用都包含清除栈内数据的代码,故可执行文件较大。

4.C语言编译器函数名称修饰规则

  • __stdcall:编译后,函数名被修饰为“_functionname@number”
  • __cdecl:编译后,函数名被修饰为“_functionname”。
  • __fastcall:编译后,函数名给修饰为“@functionname@nmuber”
  • 注:“functionname”为函数名,“number”为参数字节数。
  • 注:函数实现和函数定义时如果使用了不同的函数调用协议,则无法实现函数调用。

(4)arm函数调用约定

ARM和ARM64使用的是ATPCS函数调用约定,具体约定如下——

  • 函数的前4个参数通过R0~R3传递
  • 若形参个数大于4,大于4的部分必须通过堆栈进行传递
  • 参数入栈的顺序为右到左;
  • 被调用者实现栈平衡
  • 返回值由R0传递。

E.C语言相关

(1)memmove与memcpy

函数原型——

void *memcpy(void *restrict s1, const void *restrict s2, size_t n);
void *memmove(void *s1, const void *s2, size_t n);

两个函数都是将s2指向位置的n字节数据拷贝到s1指向的位置,区别就在于关键字restrict, memcpy假定两块内存区域没有数据重叠,而memmove没有这个前提条件。如果复制的两个区域存在重叠时使用memcpy,其结果是不可预知的,有可能成功也有可能失败的,所以如果使用了memcpy,程序员自身必须确保两块内存没有重叠部分。在内存有重叠的情况下,memmove安全性高于memcpy。

(2)有符号数和无符号数的汇编区别

对计算机来说,它根本没有所谓的无符号有符号这样的约定机制,无符号有符号只不过是我们(程序员、学习者)看待二进制数据的方式。无论有符号数还是无符号数,都是以补码(相对真值来说)的形式来存储的,补码在运算时符号位也会参与。

所以有符号数和无符号数的汇编区别主要体现在——

  • 对应的部分操作指令不同——如有符号乘为imul,有符号除为idiv;无符号乘为mul,无符号除为div。
  • 相关计算操作能影响的标志位不同,CF对无符号数运算是有意义的,OF,SF对有符号数而言是有意义的。ZF则都适用。

(3)c语言结构体对齐

  1. 数据成员对齐规则:结构(struct或联合union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储)。

  2. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储。)

  3. 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。

F.漏洞利用相关

(1)shellcode自定位方法

1.CALL/POP型

635D96F1  E8 00000000   call mshtml.635D96F6
635D96F6  58pop eax   
635D96F7  90nop

CALL指令做的操作是压栈下一个地址,跳向指定地址,利用这个特征可以利用CALL/POP操作定位当前位置。

2.CALL/POP改进型

前一种好用但是有缺陷,比如会出现较多的00,可能造成截断,于是有了改进型。

635D96F1E8 FFFFFFFF   call mshtml.635D96F5   //将下一句指令的地址压栈
635D96F6  C2 5890   retn 0x9058
635D96F9  90  nop
跟进635D96F1这个CALL后代码将重新解释如下:
635D96F5  FFC2  inc edx
635D96F7  58    pop eax    //将下一句语句的地址出栈到eax,此时堆栈恢复,edx的值比原来少1,eax为下一句指令的位置。

3.浮点运算型

0013FED0   D9EEfldz
0013FED2   D97424 F4   fstenv (28-byte)ptr ss:[esp-0xC]
0013FED6   5B  pop ebx

浮点运算后位置保存在栈顶,通过POP操作可以获取其位置。metasploit上面用得蛮多的。

4.中断型

使用INT 2c或者INT 2e可以获取下一个执行地址,下一个执行地址将会保存于ebx。

在调试状态无法达到预期的效果,如果想看见效果可以将调试器设置为默认调试器,执行以下代码看见效果:

__asm
{
Int 3
Int 2c
}

5.异常处理型

在shellcode代码中构建一个异常处理函数,再构造一个异常进入异常处理中获取EIP。这种方法编写难度稍微大点,也是可行的。

(2)windows/linux保护机制

windows和Linux下的保护机制
PWN选手的自我修养

windows下的保护机制及绕过

linux下的保护机制及绕过

  • NX/DEP
  • ASLR/PIE
  • Stack Canary/Cookie
  • RELNO(got表不可写)
  • FORTIFY(gcc生成了一些附加代码,通过对数组大小的判断替换strcpy, memcpy, memset等函数名,达到防止缓冲区溢出的作用。)

区别

除了部分安全机制的不同,对于类似的保护机制,区别在于利用方式的不同,具体如下——

  • 在windows下面,对于DEP的绕过技术是ROP;对于ASLR的绕过技术是堆喷射。而在linux下,对于NX的绕过是ret2dll(跳转到libc中的函数执行)、ROP;对于ASLR的绕过是ret2dll, fake linkmap(动态链接就是从函数名到地址的转换过程,所以可以通过动态链接器解析任何函数,无需leak)、Partial Overwrite(PIE开启时,一个32地址的高20位被随机化,低12bit不变)。
  • 对于cookies的绕过,linux下通过leak canary,overwrite canary,改写指针与局部变量绕过。而windows下多了通过SEH链绕过的方式。

(3)漏洞原理

栈溢出

栈溢出的特点就是通过溢出覆盖栈,来控制程序的执行流程:比如改变函数的指针,变量的值,异常处理程序,或者覆盖函数的返回地址,可以得到代码的执行权限。

格式化字符串漏洞

会触发该漏洞的函数很有限,主要就是printf、sprintf、fprintf等print家族函数。具体原理如下图——

需要了解基本的格式化字符串参数;重点关注的是%s(实现任意地址读)、%x和%n(实现任意地址写)、%p(判断可执行文件是否存在格式化字符串漏洞以及爆破偏移)。

堆溢出

应用程序在运行的时候会动态的申请和释放(malloc、realloc、free、new、del等)一块内存区域,这些区域就是堆。堆是一块一块连在一起的,负责存储元数据。当攻击者将数据覆盖到自己申请的堆以外的别的堆的时候,堆 溢出就发生了。接着攻击者能通过覆盖数据改变任何存储在堆上的数据:变量,函数指针,安全令牌,以及各种重要的数据。堆溢出很难被立即的跟踪到,因为被影响的内存快,一般 不会被程序立即的访问,需要等到程序结束之前的某个时间内才有可能访问到,当然也有可 能一直不访问。

linux的堆块结构如下——

(1)UAF

程序创建的结构体中包含指针变量,并且在free了创建的结构体(堆)之后没有将其中的指针置为null。又对此时未置null的Dangling pointer(悬挂指针)进行使用,如指针解引用等。

(2)Double free

双重释放漏洞主要是由对同一块内存进行二次重复释放导致的,利用漏洞可以执行任意代码。是一种特殊的UAF,且可转换为普通的UAF。漏洞代码实例如下——

(4)漏洞利用方式

栈溢出

栈溢出的基本利用是通过程序中的栈溢出,绕过各种防护机制,控制程序的执行流程,以达到控制程序执行其本身已有的代码(或我们填充进去的shellcode——要使shellcode所在内存地址所在段具有可执行权限)的目的。

现代栈溢出利用技术基础——ROP:一种代码复用技术,通过控制栈调用来劫持控制流。(一张图理解ROP原理)

其他的利用技巧和思路如下——

  • 现代栈溢出利用技术基础:ROP
  • 利用signal机制的ROP技术:SROP
  • 没有binary怎么办:BROP 、dump bin
  • 劫持栈指针:stack pivot
  • 利用动态链接绕过ASLR:ret2dl resolve、fake linkmap
  • 利用地址低12bit绕过ASLR:Partial Overwrite
  • 绕过stack canary:改写指针与局部变量、leak canary、overwrite canary
  • 溢出位数不够怎么办:覆盖ebp,Partial Overwrite
  • 程序静态链接库文件时(只开启了NX保护):一种套路是可以通过程序中的静态链接函数mmap()和mprotect()来利用
    ......

堆溢出

堆溢出的利用方式根据具体的漏洞类型而定。利用堆漏洞的关键-了解内存管理的策略。

linux下采用的是Glibc堆内存管理机制。

关键:bins(fast bins、unsorted bins、small bins & large bins、top chunk);malloc chunk的分配策略。

(1)UAF

利用方法:先free,再修改chunk,调用chunk中的函数指针。让两个指针实际指向同一个chunk,一个指针把内存解释为字符串,从而写入任意值,另一个指针把内存解释为函数指针,从而实现控制EIP的目的。如下图——

实例举例——

(2)Double free

对其利用要视具体情况而定,对于fastbin的double free 攻击比较容易,只要double free时中间隔一个bin就不会报错(对于fastbin的double free的利用思路如下图)。

更多的利用方式如unlink(如下图)(free(chunk)时会导致的内存块合并操作,需要先能溢出改变下一个chunk的size字段的in-use位)等,可以参考下列参考博客中的总结。

用图形展示堆利用过程

extend the chunk

(1)off-by-one

通过修改下一个chunk的size,从而得到overlap。

这里有一个trick,一般来说1字节到不了size,前面还有prev_size。

考虑64位,如果malloc的size没有16字节对齐,比如malloc(0x18),系统实际malloc了0x20字节给程序,不够的8字节由后面一个chunk的prev_size提供(共用)。这也很合理,当这个chunk在使用时,prev_size肯定为0,是没用的;当prev_size有用时,这个chunk已经被free了,里面的内容已经无用了。

使用这个trick加off-by-one的溢出,我们刚好可以修改size。

(2)off-by-one null byte

shrink the chunk

和extend the chunk 差不多,都是通过off-by-one null byte来获得overlap。但这个方法对堆布局的构造更加复杂。