看雪学院课程《汇编语言详解与二进制漏洞初阶》笔记
前言和声明
安全工程师这条路任重道远。如今国际形势复杂,网络战一旦爆发,安全势力弱的一方很快会处于竞争的下风,加上国家的安全人才缺口过大,我辈则应当肩挑重担,为祖国安全尽一份力。
本博客是博主在学习看雪学院《汇编语言详解与二进制漏洞初阶》课程的笔记,笔记中大部分是对课程作业的解答,少部分是博主补充的知识。
如若转载,请声明出处。谢谢。
与广大有心朝安全方向前进的网友们共勉之。
另注:本博客会持续更新到课程学习完毕。
汇编语言部分
学习汇编语言的意义
- 开发遇到bug时调试更加便利。在用高级语言进行调试时往往会遇到一些非常难以调试出来的bug,此时程序员若能将高级语言代码反汇编成汇编代码来进行调试则可以提高调试效率。
- 逆向分析时的代码阅读。逆向分析时,所分析的软件对于分析者来说其实是一个“黑盒”,此时若不懂汇编语言则将寸步难行。
- 对某些特殊技术的使用。用高级语言编写的程序往往占较大的内存,当程序员编写shellcode、壳等代码时,要尽量压缩其大小,此时汇编语言则大有用处,因为使用汇编语言编写程序时可以精确到字节。
shellcode: 能在一个完整程序中的任意位置运行的代码。
EFLAGS寄存器
EFLAGS寄存器包含了独立的二进制位,用于控制CPU操作,或是反应一些CPU操作的结果。有些指令可以测试和控制这些单独的处理器标志位。
- 中文图
- 英文图
这篇文章对EFLAGS寄存器的讲解非常详细,请点击这里🔗
各寄存器的名称及其主要用途
| 代号 | EAX | EBX | ECX | EDX | ESI | EDI | EBP | ESP |
| 主要用途 | 算术运算、存储中间结果、函数返回值 | 基地址指针 | 循环计数、移位操作计数、重复操作计数 | 乘除运算、存储中间结果 | 存储指针、字符串指令的源操作数指针 | 存储指针、字符串指令的目的操作数指针 | 基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部 | 栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。 |
内存寻址范围
给出几个计算例子。
1、某计算机字长32位,存储容量8MB。按字编址,其寻址范围为(0~2M-1) 计算步骤:8MB字节=810241024*8位。所以8MB/32位=2M.
2、某计算机字长32位,其存储容量为4MB,若按半字编址,它的寻址范围是(0-2M-1)计算步骤:若按半字就是16位了 4MB=410241024*8位,所以4MB/16 = 2M;
3、字长为32位.存储器容量为64KB.按字编址的寻址范围是多少计算步骤:64K字节=64*1024*8位. 所以64KB/32位=(64*1024*8)/32=16*1024=16K 故寻址范围为: 0-16K-1
4、某机字长32位,存储容量1MB,若按字编址,它的寻址范围是什么?
解释:容量1M=210241024 位 一个字长是32 位
所以,寻址范围是二者相除=256K
内存的五种表现形式
声明:图片来源于🔗
若想加深对内存表现形式的理解,请点击这里🔗
数据存储模式(大小端模式)
下面以unsigned int value = 0x12345678为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]来表示value
- Big-Endian: 低地址存放高位,如下:
高地址
---------------
buf[3] (0x78) – 低位
buf[2] (0x56)
buf[1] (0x34)
buf[0] (0x12) – 高位
---------------
低地址 - Little-Endian: 低地址存放低位,如下:
高地址
---------------
buf[3] (0x12) – 高位
buf[2] (0x34)
buf[1] (0x56)
buf[0] (0x78) – 低位
--------------
低地址
| 低地址 | 0x4000 | 0x78 | 0x12 |
| ↓ | 0x4001 | 0x56 | 0x34 |
| ↓ | 0x4002 | 0x34 | 0x56 |
| 高地址 | 0x4003 | 0x12 | 0x78 |
使用VS编写汇编程序
- VS环境配置
本课程中教学用VS2015,具体操作请看🔗。
若使用VS2019则与VS2015操作有所不同,VS2019可以直接在搜索框中搜索“生成自定义”、“属性”等关键词,操作也很方便。 - 一个汇编程序的大致结构
汇编语言中的数学运算
在汇编语言中,有 加 减 乘 除 自增 自减 六种运算。
- 加法
- 减法
- 乘法
- 除法
- 自增
- 自减
堆栈操作
理论
- 什么是栈?
学过数据结构一般都知道,后进先出数据结构者为栈。
千万要牢记,栈的基本特点是:后进先出。
就跟你把羽毛球装在球筒里面一样,最后放进去的肯定是第一个拿出来的。
- 栈操作指令
最基本的无非就是 PUSH 和 POP 。
- 栈的作用
栈的空间有限,所以只能存储少量数据;
所谓保存寄存器环境,也就是说我们对寄存器进行了一些操作,但我想在操作完成之后恢复程序的全部功能,此时寄存器的内容应该也要跟原来一样,故称之为保存寄存器环境。
实践
使用OllyDbg实现栈操作。
-
将任意PE文件拖入OllyDbg,如图修改前四行代码:
-
按F8执行,将eax的内容push进栈
-
再按F8,将ebx的内容push进栈
-
再按F8,将栈顶元素弹出到ecx
-
再按F8,栈顶元素弹出至edx
再强调一次,栈的基本特点是:后进先出。
数据移动指令
- MOV 指令
- LEA 指令
- XCHG 指令
作业:用VS和OllyDbg复现课程中代码,熟练掌握上述三个指令的使用方法。
比较指令
- CMP 指令
- TEST 指令
JCC条件转移指令
-
JCC 表
-
JMP 指令
本例子可供总结 jmp 指令的使用方法。
这段汇编代码给寄存器eax赋初值为0,
然后进入jmpflag段并执行add eax,1语句,
对eax加1之后再跳转(jmp)回jmpflag段起始语句add eax,1,
又对eax加1。
这样子就形成了一个无限循环。 -
JZ/JE 指令
本例子可供总结 jz/je 指令的使用方法。
代码解释:
给eax赋初值为5,
之后使用 cmp 指令(上文有提到其作用)与6比较,
不等,
则不执行 jz 指令。 -
JNE 指令
本例子可供总结 jne 指令的使用方法。
代码解释:
给eax赋初值为5,
之后使用 cmp 指令(上文有提到其作用)与6比较,
不等,
则执行 jz 指令。
串操作指令
汇编中的串应理解为字节串、字串等,应该属于数组的范畴,可以进行扫描查找、比较、传送(填充)等操作。
- MOVS 指令
- STOS 指令
- REP 指令
重复操作字符串指令。
CALL和RETN指令
CALL指令和RETN指令是配合起来写函数的指令。
理论
- CALL 指令
可以调用任意地址,并且将call指令的下一条指令的地址压入栈中。 - RETN 指令
将栈顶元素(该元素是执行call语句时压入的call语句下一条语句的地址)弹出,返回该地址,所以它的功能是使程序执行完call指令继续往下执行。
实践
- 修改程序第1条语句为 call 指令语句,修改77EF3CC3处语句为mov eax,1(修改为其他语句也可以,无所谓的),修改77EF3CC8处语句为 retn
- 执行call 指令
是跳转到红色框框那里,不是跳转到箭头的位置。。。 - 执行retn指令
右边第一个框框中的77EF3CA0已经弹出了,所以在栈中看不到它。
汇编中的函数
过程调用的方式要根据编译器而定,下面图片中的只是一个例子。
寄存器数量有限,为防止出现寄存器不够用的情况,所以出现了栈传参。
- 寄存器传参
可根据此程序代码动手实践,观察各相关寄存器的内容的变化情况。 - 堆栈传参
[esp+4]是第二参数,[esp+8]是第一参数。
为什么呢?因为栈的特点是先进后出,后进先出!所以越靠近栈底的元素(地址越高)的元素越先进栈!
那么为什么要+4和+8呢?因为一个整数占4个字节,所以栈顶元素的首地址应该为esp+4。 - 作业
Win32汇编入门
理论
实践
注意!!!
更改: 函数MessageBoxA参数部分,第1个push 0是将uType的参数值压入栈
作业:
C语言部分
C语言概述
写程序的过程:
定义程序目标
设计程序
编写代码
编译
运行程序
调试程序
维护和修改程序
函数指针
即指向函数的指针。
- 代码
直观展示什么是指向函数的指针。
在上图中,下面两行代码是相等的。
MyAdd是函数add的指针。
- 反汇编
从汇编层面认识什么是指向函数的指针。
补充:
dword ptr [myadd]是什么意思?
dword 双字 就是四个字节
ptr pointer缩写 即指针
[]里的数据是一个地址值,这个地址指向一个双字型数据
比如mov eax, dword ptr [12345678] 把内存地址12345678中的双字型(32位)数据赋给eax
指针函数
即返回值是指针的函数。
函数分析
"Hello world!"程序分析
建立新栈时将新栈刷为CCCCCC…h,很多程序的汇编代码中都能看到CCCCCC…h。
建立新栈是程序健壮性的体现。
作业
记住函数分析的内容。
C语言命名规则
逆向入门
寻找main函数
- 程序版本
- Release
编译器会对程序进行优化,程序占存较小,但调试相对困难。发布程序时一般为此版本。 - Debug
少量优化,程序占存较大,方便调试。
- Release
实践
OllyDebug
Debug版本程序
#include <stdio.h>int main() {printf("das");return 0; }- 以管理员身份运行OllyDebug
Release版本
看视频如何操作
视频 3:14~ 🔗
- OllyDebug
- 第一种方法
- 第一种方法
- 第二种方法
IDA
使用IDA打开,IDA会自动分析出main函数,按F5进入main函数。
双击上图高亮部分,即可查看main函数汇编代码。
修改内存中的数据
- 直接修改待修改内存中的数据
- 修改任意地址处的数据,再跳转到该地址,从而间接修改待修改内存中的数据
思考:
某些地址中的数据不能随便修改,因为可能会导致程序运行时所需的必要数据缺失而损坏程序。
修改跳转
修改跳转进而改变程序执行流程。
直接上实践例子
问题:如何通过修改跳转达到就算键入0也打印"True!"
程序为Release x86版本。
注意!!图中修改汇编代码应该修改为jmp short 008110AE
进行上述操作之后,无论是否键入0,都回跳转到008110AE处,这样就只会打印"True!"了。
有个小问题,我们要做的第一步其实是找到main函数,但在这节课里并没用到上面笔记中的方法,以后补充。
驱动入门
第一个驱动程序
- 创建驱动项目
- 程序编写
- 程序
- 程序的配置
- 寻找驱动程序的存放位置
- 程序运行
第一个驱动程序
代码如下:
#include <ntddk.h>//DriverUnload是驱动的卸载函数,负责清理资源,再驱动卸载的时候调用 VOID myDriverUnload(PDRIVER_OBJECT pDriverObject) {//指明这个参数是我故意不用的,不是我忘了,告知编译器不要警告我UNREFERENCED_PARAMETER(pDriverObject);//打印信息,证明我们已经成功地卸载了这个驱动DbgPrint("Unload success!"); }//DriverEntry相当于三环程序,也就是应用层程序的main函数 //pDriverObject驱动对象指针 //pRegPath注册表路径指针 NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegPath) {//指明这个参数是我故意不用的,不是我忘了,告知编译器不要警告我UNREFERENCED_PARAMETER(pRegPath);//指定驱动卸载函数pDriverObject->DriverUnload = myDriverUnload;//打印信息,证明我们的驱动启动成功了DbgPrint("Hello World!");//返回一个成功的状态函数return STATUS_SUCCESS; } 与50位技术专家面对面20年技术见证,附赠技术全景图总结
以上是生活随笔为你收集整理的看雪学院课程《汇编语言详解与二进制漏洞初阶》笔记的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: 面向智能电网的电力大数据存储与分析应用
- 下一篇: DPDK:不仅是加速