前言和聲明
安全工程師這條路任重道遠。如今國際形勢複雜,網絡戰一旦爆發,安全勢力弱的一方很快會處於競爭的下風,加上國家的安全人才缺口過大,我輩則應當肩挑重擔,爲祖國安全盡一份力。
本博客是博主在學習看雪學院《彙編語言詳解與二進制漏洞初階》課程的筆記,筆記中大部分是對課程作業的解答,少部分是博主補充的知識。
如若轉載,請聲明出處。謝謝。
與廣大有心朝安全方向前進的網友們共勉之。
另注:本博客會持續更新到課程學習完畢。
彙編語言部分
學習彙編語言的意義
- 開發遇到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(修改爲其他語句也可以,無所謂的),修改77EF3CC4處語句爲 retn
- 執行call 指令
是跳轉到紅色框框那裏,不是跳轉到箭頭的位置。。。
- 執行retn指令
彙編中的函數
過程調用的方式要根據編譯器而定,下面圖片中的只是一個例子。
寄存器數量有限,爲防止出現寄存器不夠用的情況,所以出現了棧傳參。
- 寄存器傳參
可根據此程序代碼動手實踐,觀察各相關寄存器的內容的變化情況。 - 堆棧傳參
[esp+4]是第二參數,[esp+8]是第一參數。
爲什麼呢?因爲棧的特點是先進後出,後進先出!所以越靠近棧底的元素(地址越高)的元素越先進棧!
那麼爲什麼要+4和+8呢?因爲一個整數佔4個字節,所以棧頂元素的首地址應該爲esp+4。 - 作業
Win32彙編入門
理論
實踐
注意!!!
更改: 函數MessageBoxA參數部分,第1個push 0是將uType的參數值壓入棧
作業:
C語言部分
C語言概述
寫程序的過程:
定義程序目標
設計程序
編寫代碼
編譯
運行程序
調試程序
維護和修改程序
函數指針
即指向函數的指針。
- 代碼
直觀展示什麼是指向函數的指針。
在上圖中,下面兩行代碼是相等的。
MyAdd是函數add的指針。
int add(inta, int b)
int (*MyAdd)(int a, int b)
- 反彙編
從彙編層面認識什麼是指向函數的指針。
補充:
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;
}