【入門筆記】CSE 365 - Fall 2024之Computing 101(pwn.college)
Your First Program 你的第一個程式
Your First Register 你的第一個暫存器
CPU的思維方式非常簡單。 它移動資料,更改資料,基於資料做出決策,並基於資料採取行動。 大多數情況下,這些資料儲存在暫存器中。
簡而言之,暫存器是資料的容器。 CPU可以將資料放入暫存器,在暫存器之間移動資料,等等。 在硬體層面上,這些暫存器是使用非常昂貴的晶片實現的,它們被塞在令人震驚的微觀空間中,以甚至連光速這樣的物理概念都影響它們效能的頻率訪問。 因此,CPU可以擁有的暫存器數量是非常有限的。 不同的CPU體系結構有不同數量的暫存器,這些暫存器的名稱也不同,等等。通常,程式程式碼可以出於任何原因使用10 ~ 20個“通用”暫存器,以及多達幾十個用於特殊目的的暫存器。
在x86的現代版本x86_64中,程式可以訪問16個通用暫存器。 在這個挑戰中,我們將學習我們的第一個: rax
。 你好啊,rax!(擴充套件累加器暫存器)
rax
,一個單獨的x86暫存器,只是x86 CPU龐大複雜設計中的一小部分,但這是我們將要開始的地方。 與其他暫存器類似, rax
是一個儲存少量資料的容器。 使用 mov
指令將資料移動到。 指令被指定為運算子(在這個例子中是 mov
)和運算元,它們表示額外的資料(在這個例子中,它將是指定的 rax
作為目標,以及我們要儲存在那裡的值)。
例如,如果你想將值 1337
儲存到 rax
中,x86程式集看起來是這樣的:
mov rax, 1337
你可以看到以下幾點:
- 目的節點(
rax
)在源節點(值1337
)的前面 - 兩個運算元之間用逗號分隔
- 這真是太簡單辣!
在這個挑戰中,您將編寫您的第一個彙編程式。 必須將 60
的值移動到 rax
。 將你的程式寫在一個具有 .s
副檔名的檔案中,例如 rax-challenge.s
(雖然不是強制的, .s
是彙編程式檔案的典型副檔名),並將它作為引數傳遞給 /challenge/check
檔案(例如 /challenge/check rax-challenge.s
)。 你可以使用你最喜歡的文字編輯器,也可以使用pwn.college中的文字編輯器——VSCode工作空間來實現你的 .s
檔案!
勘誤: 如果你以前見過x86彙編,那麼你可能見過它的一種稍微不同的方言。 pwn的方言。學院是“英特爾語法”,這是編寫x86彙編的正確方式(提醒一下,x86是英特爾創造的)。 一些課程錯誤地教授“AT&T語法”的使用,造成了大量的困惑。 我們將在下一模組中稍微涉及到這個,然後,希望再也不用考慮AT&T語法了。
檢視解析
mov rax, 60
Your First Syscall 你的第一次系統呼叫
你的第一個程式崩潰了…… 別擔心,它總會發生的! 在這個挑戰中,你將學習如何讓程式乾淨地退出,而不是崩潰。
啟動程式和乾淨利落地停止程式是計算機作業系統處理的操作。 作業系統管理程式的存在、程式之間的互動、硬體、網路環境,等等。
你的程式使用匯編指令與CPU“互動”,例如你之前編寫的 mov
指令。 類似地,你的程式使用 syscall
(系統呼叫)指令與作業系統互動(當然是透過CPU)。
就像你可能使用電話與當地的餐館進行互動來點餐一樣,程式使用系統呼叫來請求作業系統代表程式執行操作。 稍微泛化一下,程式所做的任何不涉及資料計算的工作都是透過系統呼叫完成的。
程式可以呼叫很多不同的系統呼叫。 例如,Linux大約有330個不同的系統呼叫,但這個數字會隨著時間的推移而變化,因為系統呼叫的增加和廢棄。 每個系統呼叫都由一個系統呼叫編號表示,從0開始計數,程式透過將其系統呼叫編號移動到 rax
暫存器並呼叫 syscall
指令來呼叫特定的系統呼叫。 例如,如果我們想呼叫syscall 42(你稍後會了解的一個系統呼叫!),可以編寫兩條指令:
mov rax, 42
syscall
非常酷,而且超級簡單!
在這個挑戰中,我們將學習我們的第一個系統呼叫: exit
。
exit
系統呼叫導致程式退出。 透過顯式退出,我們可以避免前一個程式遇到的崩潰!
現在, exit
的系統呼叫編號是 60
。 現在開始編寫你的第一個程式:它應該將60移動到 rax
,然後呼叫 syscall
以乾淨利落地退出!
檢視解析
mov rax, 60
syscall
Exit Codes 退出程式碼
現在你可能知道了,每個程式在結束時都有一個退出程式碼。 這是透過向 exit
系統呼叫傳遞一個引數來完成的。
類似於在 rax
變數中指定系統呼叫編號(例如,60表示 exit
),引數也透過暫存器傳遞到系統呼叫。 系統呼叫可以接受多個引數,但 exit
只接受一個引數:退出程式碼。 系統呼叫的第一個引數是透過另一個暫存器傳遞的: rdi
。 rdi
是本次挑戰的重點。
在這個挑戰中,你必須讓你的程式以 42
的退出程式碼退出。 因此,你的程式需要3條指令:
- 設定程式的退出程式碼(將其移動到
rdi
)。 - 設定
exit
系統呼叫(mov rax, 60
)的系統呼叫號碼。 syscall
!
檢視解析
mov rdi, 42
mov rax, 60
syscall
Building Executables 構建可執行檔案
你寫了第一個程式? 但到目前為止,我們已經處理了將其構建為CPU可以實際執行的可執行檔案的實際工作。 在這個挑戰中,你將構建它!
要構建可執行二進位制檔案,你需要:
- 將程式集寫入檔案(通常使用
.S
或.s
語法)。在這個例子中,我們使用asm.s
)。 - 將二進位制檔案彙編成可執行的目標檔案(使用
as
命令)。 - 連結一個或多個可執行目標檔案到最終的可執行二進位制檔案(使用
ld
命令)!
讓我們一步一步來:
彙編檔案包含您的彙編程式碼。 對於上一關,這可能是:
hacker@dojo:~$ cat asm.s
mov rdi, 42
mov rax, 60
syscall
hacker@dojo:~$
但是它需要包含更多的資訊。 我們提到在本課程中使用英特爾彙編語法,我們需要讓彙編程式知道這一點。 你可以透過在彙編程式碼的開頭新增一個指令來實現這一點,例如:
hacker@dojo:~$ cat asm.s
.intel_syntax noprefix
mov rdi, 42
mov rax, 60
syscall
hacker@dojo:~$
.intel_syntax noprefix
告訴彙編器你將使用英特爾彙編語法,特別是它的變體,你不必為每條指令新增額外的字首。 稍後我們將討論這些,但現在,我們讓彙編程式來解決這個問題!
接下來,我們將彙編程式碼。 這是使用匯編器 as
完成的,如下所示:
hacker@dojo:~$ ls
asm.s
hacker@dojo:~$ cat asm.s
.intel_syntax noprefix
mov rdi, 42
mov rax, 60
syscall
hacker@dojo:~$ as -o asm.o asm.s
hacker@dojo:~$ ls
asm.o asm.s
hacker@dojo:~$
這裡, as
工具讀取 asm.s
,將其彙編成二進位制程式碼,並輸出一個名為 asm.o
的目標檔案。 這個目標檔案實際上已經彙編了二進位制程式碼,但還沒有準備好執行。 首先,我們需要連結它。
在典型的開發工作流程中,原始碼被編譯,然後彙編成目標檔案,通常有很多這樣的檔案(通常,程式中的每個原始碼檔案都會編譯成自己的目標檔案)。 然後將它們連結在一起,形成一個單獨的可執行檔案。 即使只有一個檔案,我們仍然需要連結它,以準備最終的可執行檔案。 這是透過 ld
(源於術語“link editor連結編輯器”)命令完成的,如下所示:
hacker@dojo:~$ ls
asm.o asm.s
hacker@dojo:~$ ld -o exe asm.o
ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000
hacker@dojo:~$ ls
asm.o asm.s exe
hacker@dojo:~$
這將建立一個 exe
檔案,然後我們可以執行它! 如下所示:
hacker@dojo:~$ ./exe
hacker@dojo:~$ echo $?
42
hacker@dojo:~$
針不戳! 現在可以構建程式了。 在這個挑戰中,繼續並自己完成這些步驟。 構建你的可執行檔案,並將其傳遞給 /challenge/check
作為flag!
_star
細心的學習者可能已經注意到 ld
列印出關於 entry symbol _start
的警告。 _start
符號本質上是 ld
的註釋,說明ELF(可執行檔案)執行時程式執行應該從哪裡開始。 警告指出,如果沒有指定的 _start
,執行將從程式碼的開頭開始。 這對我們來說並沒有關係!
如果想消除錯誤,可以在程式碼中指定 _start
符號,如下所示:
hacker@dojo:~$ cat asm.s
.intel_syntax noprefix
.global _start
_start:
mov rdi, 42
mov rax, 60
syscall
hacker@dojo:~$ as -o asm.o asm.s
hacker@dojo:~$ ld -o exe asm.o
hacker@dojo:~$ ./exe
hacker@dojo:~$ echo $?
42
hacker@dojo:~$
這裡還有兩行。 第二個引數 _start:
新增了一個名為start的標籤,指向程式碼的起始位置。 第一個是 .global _start
,它指示 as
使 _start
標籤在連結器級別全域性可見,而不僅僅是在目標檔案級別區域性可見。 由於 ld
是連結器,因此該指令對於 _start
標籤是必要的。
對於這個dojo中的所有挑戰,從檔案的開頭開始執行是很好的,但是如果您不想看到彈出這些警告,現在您知道如何防止它們了!
檢視解析
.intel_syntax noprefix
mov rdi, 42
mov rax, 60
syscall
as -o
ld -o
Tracing Syscalls 系統呼叫追蹤
當你編寫越來越大的程式時,你(是的,你也可以!)可能會在實現某些功能時犯錯誤,從而在程式中引入bug。 在這個模組中,我們將介紹一些用於除錯程式的工具和技術。 第一個非常簡單:系統呼叫跟蹤程式 strace
。
給定一個要執行的程式, strace
將使用Linux作業系統的功能來自檢並記錄程式呼叫的每個系統呼叫及其結果。 例如,來看一下上一個挑戰中的程式:
hacker@dojo:~$ strace /tmp/your-program
execve("/tmp/your-program", ["/tmp/your-program"], 0x7ffd48ae28b0 /* 53 vars */) = 0
exit(42) = ?
+++ exited with 42 +++
hacker@dojo:~$
如你所見, strace
報告了哪些系統呼叫被觸發了,傳遞了哪些引數,以及返回了哪些資料。 這裡用於輸出的語法是 system_call(parameter, parameter, parameter, ...)
。 這種語法借用了一種名為C語言的程式語言,但我們現在不用擔心。 只要記住如何閱讀這個特定的語法即可。
在這個例子中, strace
報告了兩個系統呼叫:第二個是程式用來請求終止的 exit
系統呼叫,你可以看到傳遞給它的引數(42)。 第一個是 execve
系統呼叫。 我們稍後將瞭解這個系統呼叫,但它有點像 exit
的反面:它用於啟動一個新程式(在情景中,啟動的程式是 your-program
)。 在這種情況下,它實際上並不是由 your-program
呼叫的:它被strace檢測到是strace工作方式的一個奇怪特徵,我們稍後會研究。
在最後一行中,你可以看到 exit(42)
的結果,即程式退出時的退出程式碼為 42
!
現在, exit
系統呼叫很容易進行內部審查,而無需使用 strace
——畢竟, exit
的部分意義是為您提供一個可以訪問的退出程式碼。 但其他系統呼叫就不那麼明顯了。 例如, alarm
系統呼叫(系統呼叫編號37)將在作業系統中設定一個定時器,當許多秒過去時,Linux將終止該程式。 alarm
的作用例如:在程式凍結時終止程式,但在本例中,我們將使用 alarm
來練習我們的 strace
窺探!
在這個挑戰中,你必須 strace
/challenge/trace-me
程式找出它傳遞給 alarm
系統呼叫的引數是什麼值,然後呼叫 /challenge/submit-number
以獲取到的引數值。 好運!
檢視解析
strace /challenge/trace-me
Moving Between Registers 暫存器之間的移動
好的,讓我們再學習一個暫存器: rsi
! 就像 rdi
, rsi
是一個可以存放一些資料的地方。 例如:
mov rsi, 42
當然,您也可以在暫存器之間移動資料! 看:
mov rsi, 42
mov rdi, rsi
就像第一行將 42
移動到 rsi
,第二行將 rsi
的值移動到 rdi
。 在這裡,我們必須提及一個複雜的問題:這裡的移動實際上指的是集合。 在上述片段之後, rsi
和 rdi
將會是 42
。 為什麼選擇 mov
,而不是像 set
這樣合理的東西,這是一個謎(即使非常博學的人在被問到時也會訴諸各種各樣的猜測),但它確實是,而我們就是這樣約定俗成了。
不管怎樣,迎接挑戰吧! 在這個挑戰中,我們將在 rsi
暫存器中儲存一個秘密值,程式必須以該值作為返回碼退出。 因為 exit
使用儲存在 rdi
中的值作為返回碼,所以你需要將 rsi
中的秘密值移動到 rdi
中。 好運!
檢視解析
.intel_syntax noprefix
mov rdi, rsi
mov rax, 60
syscall
as -o
ld -o
Loading From Memory 從記憶體中載入資料
正如你的程式所看到的,計算機記憶體是一個巨大的儲存資料的地方。 就像街上的房子一樣,記憶體的每個部分都有一個數字地址,就像街上的房子一樣,這些數字(大部分)是連續的。 現代計算機有大量的記憶體,而典型的現代程式的記憶體檢視實際上有很大的差距(想象一下:街道的一部分沒有建房子,所以這些地址被跳過了)。 但這些都是細節:關鍵是,計算機在記憶體中儲存資料,主要是按順序儲存。
在這一關卡,我們將練習訪問儲存在記憶體中的資料。 我們該怎麼做呢? 回想一下,為了將一個值移動到暫存器中,我們這樣做:
mov rdi, 31337
之後, rdi
的值為 31337
。 酷。 好吧,我們可以使用相同的指令來訪問記憶體! 該命令還有另一種格式,它使用第二個引數作為訪問記憶體的地址!思考一下我們的記憶體看起來像這樣:
地址 │ 內容
+────────────────────+
│ 31337 │ 42 │
+────────────────────+
要訪問記憶體地址31337處的記憶體內容,可以這樣做:
mov rdi, [31337]
當CPU執行這條指令時,它當然知道 31337
是一個地址,而不是一個原始值。 如果你把指令想象成一個人告訴CPU該做什麼,我們堅持我們的“街上的房子”類比,那麼指令/人不是僅僅向CPU傳遞資料,而是指向街上的房子。 然後CPU會去那個地址,按門鈴,開啟前門,把裡面的資料拖出來,放入 rdi
。 因此,這裡的 31337
是一個記憶體地址,指向儲存在該記憶體地址中的資料。 這條指令執行後,儲存在 rdi
中的值將是 42
!
讓我們把它付諸實踐! 我在記憶體地址 133700
中儲存了一個秘密數字,如下所示:
地址 │ 內容
+────────────────────+
│ 133700 │ ??? │
+────────────────────+
您必須取得這個秘密數字,並將其用作程式的退出程式碼。 要做到這一點,你必須將它讀入 rdi
,如果你還記得的話,它的值是 exit
的第一個引數,並用作退出程式碼。 好運!
檢視解析
.intel_syntax noprefix
mov rdi, [133700]
mov rax, 60
syscall
as -o
ld -o
More Loading Practice 更多的載入練習
你看起來還需要再多練習一下。 在這一關卡中,我們將secret值放在 123400
,而不是 133700
,如下所示:
Address │ Contents
+────────────────────+
│ 123400 │ ??? │
+────────────────────+
去把它載入 rdi
和 exit
,以此作為退出程式碼!
檢視解析
.intel_syntax noprefix
mov rdi, [123400]
mov rax, 60
syscall
as -o
ld -o
你更喜歡訪問 133700
還是 123400
? 你的回答可能會涉及到你的性格,但從技術角度來看,這並不是非常相關。 事實上,在大多數情況下,編寫程式時根本不需要處理實際的記憶體地址!
這怎麼可能呢? 通常,記憶體地址儲存在暫存器中,我們使用暫存器中的值來指向記憶體中的資料! 讓我們從這個記憶體配置開始:
Address │ Contents
+────────────────────+
│ 133700 │ 42 │
+────────────────────+
思考下面的彙編程式碼片段:
mov rdi, 133700
現在,你有以下情況:
Address │ Contents
+────────────────────+
│ 133700 │ 42 │◂┐
+────────────────────+ │
│
Register │ Contents │
+────────────────────+ │
│ rdi │ 133700 │─┘
+────────────────────+
rdi
現在儲存了一個值,對應於要載入的資料的地址! 讓我們載入它:
mov rdi, [rax]
這裡,我們正在訪問記憶體,但不是為記憶體讀取指定固定地址(如133700),而是使用儲存在rax中的值作為記憶體地址。透過包含記憶體地址,rax是一個指向我們想要訪問的資料的指標!當我們使用rax代替直接指定它所儲存的地址來訪問它所引用的記憶體地址時,我們稱之為指標解引用。在上面的示例中,我們解引用rax以將它指向的資料(地址為133700的值42)載入到rdi中。針不戳!
這也說明了另一點:這些暫存器是通用的! 到目前為止,我們一直使用 rax
作為系統呼叫索引,這並不意味著它不能有其他用途。 在這裡,它被用作指向記憶體中秘密資料的指標。
類似地,暫存器中的資料也沒有隱含的用途。 如果 rax
包含 133700
的值,我們寫 mov rdi, [rax]
, CPU將該值作為記憶體地址進行解引用。 但是如果我們在同樣的條件下寫 mov rdi, rax
, CPU就會很高興地把 133700
變成 rdi
。 對CPU來說,資料就是資料;只有以不同的方式使用時,它才會變得不同。
在這個挑戰中,我們初始化 rax
,以包含儲存在記憶體中的秘密資料的地址。 解引 rax
,將秘密資料轉換為 rdi
,並使用它作為程式的退出程式碼來獲取flag!
檢視解析
.intel_syntax noprefix
mov rdi, [rax]
mov rax, 60
syscall
as -o
ld -o
Dereferencing Yourself 自我解引用
在上一關卡中,我們解除了 rax
的引用,將資料讀入 rdi
。 有趣的是我們對 rax
的選擇是任意的。 我們可以使用任何其他指標,甚至 rdi
本身! 沒有什麼能阻止你解除暫存器的引用,用解除引用的值重寫它自己的內容!
例如,我們用 rax
。 我給每一行都加了註釋:
mov [133700], 42
mov rax, 133700 # after this, rax will be 133700
mov rax, [rax] # after this, rax will be 42
在這段程式碼中, rax
從用作指標變成用於儲存從記憶體中讀取的資料。 CPU使這一切工作!
在這個挑戰中,你將探索這個概念。 我們沒有像之前那樣初始化 rax
,而是將 rdi
作為指向秘密值的指標! 你需要解除它的引用,將該值載入到 rdi
中,然後將該值作為退出程式碼載入到 exit
中。 好運!
檢視解析
.intel_syntax noprefix
mov rdi, [rid]
mov rax, 60
syscall
as -o
ld -o
Dereferencing with Offsets
所以現在你可以像專業人士一樣在記憶體中解引用指標了! 但是指標並不總是直接指向你需要的資料。 例如,有時一個指標可能指向一個資料集合(比如一整本書),你需要在這個集合中引用其中所需的特定資料。
例如,如果指標(例如 rdi
)指向記憶體中的一個數字序列,如下所示:
Address │ Contents
+────────────────────+
│ 133700 │ 50 │◂┐
│ 133701 │ 42 │ │
│ 133702 │ 99 │ │
│ 133703 │ 14 │ │
+────────────────────+ │
│
Register │ Contents │
+────────────────────+ │
│ rdi │ 133700 │─┘
+────────────────────+
如果想得到該序列的第二個數字,可以這樣做:
mov rax, [rdi+1]
哇,超級簡單! 在記憶體術語中,我們稱這些數字為插槽位元組:每個記憶體地址代表記憶體的一個特定位元組。 上面的例子訪問的是 rax
指向的記憶體地址後1位元組的記憶體。 在記憶體中,我們稱這1位元組的差值為偏移量,因此在這個例子中,與 rdi
所指向的地址之間有一個偏移量為1。
讓我們來實踐這個概念。 和之前一樣,我們將初始化 rdi
指向秘密值,但不是直接指向它。 這一次,secret值與 rdi
點的偏移量是8位元組,類似於下面這樣:
Address │ Contents
+────────────────────+
│ 31337 │ 0 │◂┐
│ 31337+1 │ 0 │ │
│ 31337+2 │ 0 │ │
│ 31337+3 │ 0 │ │
│ 31337+4 │ 0 │ │
│ 31337+5 │ 0 │ │
│ 31337+6 │ 0 │ │
│ 31337+7 │ 0 │ │
│ 31337+8 │ ??? │ │
+────────────────────+ │
│
Register │ Contents │
+────────────────────+ │
│ rdi │ 31337 │─┘
+────────────────────+
當然,實際的記憶體地址不是 31337
。 我們隨機選擇它,並將其儲存在 rdi
。 用偏移量8去解引用rdi
並獲得flag!
檢視解析
.intel_syntax noprefix
mov rdi, [rdi+8]
mov rax, 60
syscall
as -o
ld -o
Stored Addresses 儲存地址
指標可以變得更有趣! 想象一下,你的朋友住在同一條街上的另一所房子裡。 與其記住他們的地址,你可以把它寫下來,然後把這張紙記錄著他們的房子地址一起放在你的房子裡。 然後,為了從朋友那裡獲取資料,需要將CPU指向你家,讓它進入你家並找到朋友的地址,並將該地址用作指向他家的指標。
類似地,由於記憶體地址實際上只是值,它們可以儲存在記憶體中,稍後再檢索! 讓我們探索一個場景,我們將值 133700
儲存在地址 123400
,將值 42
儲存在地址 133700
。 請看下面的指令:
mov rdi, 123400 # after this, rdi becomes 123400
mov rdi, [rdi] # after this, rdi becomes the value stored at 123400 (which is 133700)
mov rax, [rdi] # 這裡我們解引用rdi,將42讀入rax!
哇! 這種地址儲存在程式中非常常見。 地址和資料會被儲存、載入、移動,有時甚至會相互混淆! 當這種情況發生時,可能會出現安全問題,並且您將在pwn.college的旅程期間輕鬆處理許多此類問題。
現在,讓我們練習解除記憶體中地址的引用。 我將一個秘密值儲存在一個秘密地址,然後將這個秘密地址儲存在地址 567800
。 你必須讀取地址,解引用它,獲取秘密值,然後用它作為退出程式碼 exit
。 你能行的!
檢視解析
.intel_syntax noprefix
mov rdi, [567800]
mov rdi, [rdi]
mov rax, 60
syscall
as -o
ld -o
Double Dereference 雙重解引用
在最後的幾個關卡中,你將:
- 使用我們告訴你的地址(在一個關卡中,
133700
,在另一個關卡中,123400
)從記憶體中載入一個秘密值。 - 使用我們放入
rax
中的地址,以便您從記憶體中載入秘密值。 - 使用我們告訴你的地址(在最後一關卡中,
567800
)將秘密值的地址從記憶體載入到暫存器中,然後使用該暫存器作為指標從記憶體中檢索秘密值!
讓我們把最後兩個放在一起。 在這個挑戰中,我們將 SECRET_VALUE
儲存在記憶體中的地址 SECRET_LOCATION_1
,然後將 SECRET_LOCATION_1
儲存在記憶體中的地址 SECRET_LOCATION_2
。 然後,我們將 SECRET_ADDRESS_2
轉化為 rax
! 使用 133700
表示 SECRET_LOCATION_1
,使用123400表示 SECRET_LOCATION_2
,得到的結果看起來像這樣(在真正的挑戰中,這些值是不一樣的,而且是不可見的!)
Address │ Contents
+────────────────────+
┌─│ 133700 │ 123400 │◂┐
│ +────────────────────+ │
└▸│ 123400 │ 42 │ │
+────────────────────+ │
│
│
│
Register │ Contents │
+────────────────────+ │
│ rdi │ 133700 │─┘
+────────────────────+
這裡,你需要執行兩次記憶體讀取:一次解引用 rax
,從 rax
所指向的位置讀取 SECRET_LOCATION_1
(也就是 SECRET_LOCATION_2
),另一次解引用現在儲存 SECRET_LOCATION_1
的暫存器,將 SECRET_VALUE
讀取 rdi
,這樣你就可以將它用作退出程式碼了!
聽起來很多,但基本上你已經完成了所有這些。 去把它彙編起來!
檢視解析
.intel_syntax noprefix
mov rdi, [rax]
mov rdi, [rdi]
mov rax, 60
syscall
as -o
ld -o
Triple Dereference 三重解引用
好的,讓我們把它擴充套件到一個更深的深度! 在這個挑戰中,我們增加了一個額外的間接關卡,所以現在需要三次解引用才能找到秘密值。 像這樣:
Address │ Contents
+────────────────────+
┌─│ 133700 │ 123400 │◂──┐
│ +────────────────────+ │
└▸│ 123400 │ 100000 │─┐ │
+────────────────────+ │ │
│ 100000 │ 42 │◂┘ │
+────────────────────+ │
│
│
Register │ Contents │
+────────────────────+ │
│ rdi │ 133700 │───┘
+────────────────────+
如您所見,我們將放置您必須解引用到rdi的第一個地址。 去獲取值吧!
檢視解析
.intel_syntax noprefix
mov rdi, [rdi]
mov rdi, [rdi]
mov rdi, [rdi]
mov rax, 60
syscall
as -o
ld -o
Hello Hackers
Writing Outpt 寫出輸出
讓我們一起學習寫文字吧!
不出所料,你的程式透過呼叫系統呼叫向螢幕寫入文字。 具體來說,這是 write
系統呼叫,它的系統呼叫編號為 1
。 但write
系統呼叫還需要透過其引數指定要寫入的資料以及寫入的位置。
您可能還記得,在Linux Luminarium 學習的管道模組實踐中,檔案描述符(FDs)的概念。 提醒一下,每個程序從3個FD開始。
- FD 0:標準輸入是程序接收輸入的通道。例如,shell使用標準輸入來讀取您輸入的命令。
- FD 1:標準輸出是處理輸出正常資料的通道,例如在前面的挑戰中列印給你的flag或實用程式的輸出,如
ls
- FD 2:標準錯誤是處理輸出錯誤細節的通道。例如,如果你鍵入了錯誤的命令,shell會以標準錯誤的形式輸出該命令不存在。
事實證明,在 write
系統呼叫中,這就是指定資料寫入位置的方式! exit
系統呼叫的第一個(也是唯一的)引數是退出程式碼( mov rdi, 42
),而 write
的第一個(但在這個例子中,不僅如此!)引數是檔案描述符。 如果想寫入標準輸出,需要將 rdi
設定為1。 如果想寫入標準錯誤,需要將 rdi
設定為2。 超級簡單!
這就剩下我們要寫什麼了。 現在,你可以想象一個場景,我們透過另一個暫存器引數指定要寫入的內容到 write
系統呼叫。 但是這些暫存器並不能容納大量的資料,並且要寫出像這個挑戰描述這樣長的內容,您需要多次呼叫 write
系統呼叫。 相對而言,這有很大的效能成本——CPU需要從執行程式的指令切換到執行Linux本身的指令,進行大量的日常計算,與硬體互動以獲得在螢幕上顯示的實際畫素,然後再切換回來。 這是很慢的,所以我們儘量減少呼叫系統呼叫的次數。
當然,解決這個問題的方法是同時編寫多個字元。 write
系統呼叫透過兩個引數來表示這些:a從(記憶體中的)哪裡開始寫,以及要寫多少個字元。 這些引數作為第二個和第三個引數傳遞給 write
。 在我們從 strace
中學到的類C語法中,這將是:
write(file_descriptor, memory_address, number_of_characters_to_write)
舉一個更具體的例子,如果你想從記憶體地址1337000寫入10個字元到標準輸出(檔案描述符1),可以這樣做:
write(1, 1337000, 10);
哇,很簡單! 現在,我們實際上如何指定這些引數?
- 如上所述,我們將在
rdi
暫存器中傳遞系統呼叫的第一個引數。 - 我們將透過
rsi
暫存器傳遞第二個引數。 Linux中公認的慣例是,rsi
用作系統呼叫的第二個引數。 - 我們將透過
rdx
暫存器傳遞第三個引數。 這是整個模組中最令人困惑的部分:rdi
(儲存第一個引數的暫存器)與rdx
非常相似,很容易混淆,不幸的是,由於歷史原因,這種命名方式一直存在。 哦…… 只是我們得小心點。 也許像這樣的助記法“rdi
是初始引數,rdx
是第三引數”? 或者只是把它想成是要追蹤不同的名字相似的朋友,沒什麼大不了的。
當然, write
系統呼叫指數變為 rax
本身: 1
。 除了 rdi
和 rdx
的混淆之外,這真的很簡單!
現在,您知道如何將暫存器指向記憶體地址(從記憶體模組!),您知道如何設定系統呼叫編號,以及如何設定其餘的暫存器。 所以,這應該是非常簡單的!
與之前類似,我們將一個秘密字元值寫入記憶體,地址為 1337000
。 對單個字元呼叫 write
(就現在而言!我們稍後會做多字元寫入)值到標準輸出,然後我們會給你一個flag!
檢視解析
.intel_syntax noprefix
mov rdi, 1
mov rsi, 1337000
mov rdx, 1
mov rax, 1
syscall
#上面的彙編程式碼進行偽c語言編碼後得到`write(1, 1337000, 1)`
as -o
ld -o
Chaining Syscalls 鏈式系統呼叫
好吧,我們之前的解決方案寫了輸出,但後來崩潰了。 在這個關卡,您將寫入輸出,然後不會崩潰!
我們將透過呼叫 write
系統呼叫來做到這一點,然後呼叫 exit
系統呼叫來乾淨地退出程式。 我們如何呼叫兩個系統呼叫? 就像你呼叫兩個指令一樣! 首先,你設定必要的暫存器並呼叫 write
,然後你設定必要的暫存器並呼叫exit
!
你之前的解決方案有5個指令(設定 rdi
,設定 rsi
,設定 rdx
,設定 rax
,和 syscall
)。 這個應該有這5個,再加上3個用於退出的 exit
(將 rdi
設定為退出程式碼,將 rax
設定為系統呼叫索引60,以及 syscall
)。 對於這個關卡,讓我們以退出程式碼 42
退出!
檢視解析
.intel_syntax noprefix
mov rdi, 1
mov rsi, 1337000
mov rdx, 1
mov rax, 1
syscall
mov rdi, 42
mov rax, 60
syscall
#上面的彙編程式碼進行偽c語言編碼後得到`write(1, 1337000, 1);exit`
as -o
ld -o
Writing Strings 寫入字串
好了,我們還有一件事要做。 你已經寫出了一個位元組,現在我們來練習寫出多個位元組。 我在記憶體位置 1337000
中儲存了一個14字元的秘密字串。 你能把它寫出來嗎?
提示: 與之前的解決方案相比,唯一需要修改的是 rdx
中的值!
檢視解析
.intel_syntax noprefix
mov rdi, 1
mov rsi, 1337000
mov rdx, 14
mov rax, 1
syscall
mov rdi, 42
mov rax, 60
syscall
#上面的彙編程式碼進行偽c語言編碼後得到`write(1, 1337000, 14);exit`
as -o
ld -o
Assembly Crash Course 組合語言速成課程
set-register 設定暫存器
在這一關卡,您將使用暫存器。您將被要求修改或讀取暫存器。
在這一關卡,你將使用暫存器!請設定如下:
rdi = 0x1337
檢視解析
.intel_syntax noprefix
mov rdi, 0x1337
as -o
ld -o
set-multiple-register 設定多個暫存器
在這一關卡,您將使用暫存器。您將被要求修改或讀取暫存器。
在這一關卡,您將使用多個暫存器。請設定如下:
rax = 0x1337
r12 = 0xCAFED00D1337BEEF
rsp = 0x31337
檢視解析
.intel_syntax noprefix
mov rax, 0x1337
mov r12, 0xCAFED00D1337BEEF
mov rsp, 0x31337
as -o
ld -o
add-to-register 暫存器執行加法操作
在這一關卡,您將使用暫存器。您將被要求修改或讀取暫存器。
我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器執行一些公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
x86中有許多指令可以讓你在暫存器和記憶體上執行所有普通的數學操作。
作為簡寫,當我們說 A += B
時,實際上是指 A = A + B
。
下面是一些有用的指令。
add reg1, reg2
<=>reg1 += reg2
sub reg1, reg2
<=>reg1 -= reg2
imul reg1, reg2
<=>reg1 *= reg2
div
更復雜,我們稍後討論。注意:所有 regX
(正規表示式)都可以用常量或記憶體位置替換。
執行如下操作:
- 將
0x331337
加進rdi
檢視解析
.intel_syntax noprefix
add rdi, 0x331337
as -o
ld -o
linear-equation-registers 線性方程暫存器
在這一關卡,您將使用暫存器。您將被要求修改或讀取暫存器。
現在,我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器進行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
用你學到的知識,計算如下:
f(x) = mx + b
,其中:
m = rdi
x = rsi
b = rdx
將結果放入 rax
。
注意:在使用哪些暫存器方面, mul
(無符號乘法)和 imul
(有符號乘法)之間有重要的區別。
在這種情況下,你需要使用 imul
。
檢視解析
.intel_syntax noprefix
imul rsi, rdi
add rsi, rdx
mov rax, rsi
as -o
ld -o
integer-division 整數的除法
在這一關卡,您將使用暫存器。您將被要求修改或讀取暫存器。
我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器執行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡,通常是 rax
。
x86中的除法比普通數學中的除法更特殊。這裡稱為整數數學(integer Math),意思是每個值都是整數。
例如: 10 / 3 = 3
。
為什麼?
因為 3.33
向下取整為整數。
此關卡的相關指令如下:
mov rax, reg1
div reg2
筆記:div
是一個特殊的指令,它可以將128位的被除數除以64位的除數,同時儲存商和餘數,只使用一個暫存器作為運算元。
這個複雜的div
指令是如何在128位的被除數(是暫存器的兩倍大)上工作和操作的?
對於 div reg
指令,將執行以下操作:
rax = rdx:rax / reg
rdx = remainder
rdx:rax
表示 rdx
為128位被除數的上64位, rax
為128位被除數的下64位。
在呼叫 div
之前,你必須仔細看看 rdx
和 rax
中的內容。
請計算以下內容:
speed = distance / time
,其中:
distance = rdi
time = rsi
speed = rax
注意,distance
值最多為64位,因此 rdx
應該為0。
檢視解析
.intel_syntax noprefix
mov rax, rdi
div rsi
#將被除數放入rax,除法計算完成後將結果放入rax中
as -o
ld -o
modulo-operation 模運算
在這一關卡,您將使用暫存器。您將被要求修改或讀取暫存器。
我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器執行公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
彙編中的模運算是另一個有趣的概念!
X86允許你得到 div
操作後的餘數。
例如: 10 / 3
導致餘數 1
。
餘數與求模相同,也稱為“取模”運算子。
在大多數程式語言中,我們使用符號 %
來表示mod。
請計算如下: rdi % rsi
將值置於 rax
。
檢視解析
.intel_syntax noprefix
mov rax, rdi
div rsi
mov rax, rdx
#將被除數放入rax,除法計算完成後餘數放在了rdx暫存器中
as -o
ld -o
set-upper-byte 設定高位元組
在這一關卡,您將使用暫存器。您將被要求修改或讀取暫存器。
我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器進行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡,通常是 rax
。
x86中另一個很酷的概念是能夠獨立訪問較低的暫存器位元組。
x86_64中的每個暫存器大小為64位(bits),在前面的關卡中,我們使用 rax
、 rdi
或 rsi
來訪問完整的暫存器。
我們還可以使用不同的暫存器名稱訪問每個暫存器的較低位元組。
例如 rax
的下32位可透過 eax
訪問,下16位可透過 ax
訪問,下8位可透過 al
訪問。
MSB LSB
+----------------------------------------+
| rax |
+--------------------+-------------------+
| eax |
+---------+---------+
| ax |
+----+----+
| ah | al |
+----+----+
低暫存器位元組訪問適用於幾乎所有暫存器。
僅使用一條移動指令,請將 ax
暫存器的高8位設定為 0x42
。
檢視解析
.intel_syntax noprefix
mov ah, 0x42
#注意審題,ax暫存器的高8位是ah
as -o
ld -o
efficient-modulo 高效模運算
在這一關卡,您將使用暫存器。您將被要求修改或讀取暫存器。
我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器執行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
事實證明,使用 div
運算子來計算求模運算是很慢的!
我們可以使用一個數學技巧來最佳化求模運算子( %
)。編譯器經常使用這個技巧。
如果我們有 x % y
,而 y
是2的冪,如 2^n
,則結果將是 x
中較低的 n
位。
因此,我們可以使用較低的暫存器位元組訪問來高效地實現模運算!
只使用以下指令:
mov
請計算以下內容:
rax = rdi % 256
rbx = rsi % 65536
檢視解析
.intel_syntax noprefix
mov al, dil ; rdi 的低 8 位移動到 rax 的低 8 位 (rax = rdi % 256)
mov bx, si ; rsi 的低 16 位移動到 rbx (rbx = rsi % 65536)
as -o
ld -o
MSB LSB
+----------------------------------------+
| rax |
+--------------------+-------------------+
| eax |
+---------+---------+
| ax |
+----+----+
| ah | al |
+----+----+
MSB LSB
+----------------------------------------+
| rdi |
+--------------------+-------------------+
| edi |
+---------+---------+
| di |
+----+----+
| dih| dil|
+----+----+
byte-extraction 位元組提取
在這一關卡,您將使用暫存器。您將被要求修改或讀取暫存器。
我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器執行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
在這一關卡,你將處理位邏輯和操作。這將涉及大量直接與儲存在暫存器或記憶體位置中的位進行互動。你可能還需要使用x86中的邏輯指令: and
, or
, not
, xor
。
在彙編中移動位是另一個有趣的概念!
X86允許你在暫存器中“移動”位。
例如, al
,即 rax
的最低8位。
al
(以位元為單位)的值為:
rax = 10001010
如果我們使用 shl
指令向左移動一次:
shl al, 1
新值為:
al = 00010100
所有元素都向左移動,最高的位元位脫落,而右邊增加了一個新的0。
你可以用它對你關心的部分做一些特殊的事情。
移位運算有一個很好的副作用,可以快速完成乘法(乘以2)或除法(除以2)運算,它也可以用於求模。
以下是重要的使用指令:
shl reg1, reg2
<=> 將reg1
向左移動reg2
中的位數shr reg1, reg2
<=> 將reg1
向右移動reg2
中的位數
注意:reg2
可以被一個常量或記憶體位置替換。
只使用以下指令:
mov
,shr
,shl
請執行以下操作: 設定 rax
為 rdi
的第5位最低有效位元組。
例如:
rdi = | B7 | B6 | B5 | B4 | B3 | B2 | B1 | B0 |
Set rax to the value of B4
檢視解析
.intel_syntax noprefix
shr rdi, 32 ; 右移 32 位,保留 B4 及其以下位
mov al, dil ; 只保留最低有效位元組8bit
as -o
ld -o
bitwise-and 按位與運算
在這一關卡,您將使用暫存器。您將被要求修改或讀取暫存器。
我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器執行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
在這一關卡,你將處理位邏輯和操作。這將涉及大量直接與儲存在暫存器或記憶體位置中的位進行互動。你可能還需要使用x86中的邏輯指令: and
, or
, not
, xor
。
彙編中的位邏輯是另一個有趣的概念!X86允許你對暫存器逐位執行邏輯操作。
在這個例子中,假設暫存器只儲存8位。
rax
和 rbx
中的值為:
rax = 10101010
rbx = 00110011
如果我們使用 and rax, rbx
指令對 rax
和 rbx
執行按位和,結果將透過逐個和每個位元對來計算,這就是為什麼它被稱為按位邏輯。
從左到右:
- 1 AND 0 = 0
- 0 AND 0 = 0
- 1 AND 1 = 1
- 0 AND 1 = 0
- ... …
最後,我們將結果組合在一起得到:
rax = 00100010
下面是一些真值表供參考:
-
AND 與
A | B | X ---+---+--- 0 | 0 | 0 0 | 1 | 0 1 | 0 | 0 1 | 1 | 1
-
OR 或
A | B | X ---+---+--- 0 | 0 | 0 0 | 1 | 1 1 | 0 | 1 1 | 1 | 1
-
XOR 異或
A | B | X ---+---+--- 0 | 0 | 0 0 | 1 | 1 1 | 0 | 1 1 | 1 | 0
如果不使用以下指令: mov
, xchg
,(xchg
是一種彙編指令,用於交換兩個運算元的值)請執行以下操作:
設定 rax
為 (rdi AND rsi)
的值
檢視解析
.intel_syntax noprefix
and rdi, rsi ; 將 rdi 和 rsi 進行 AND 運算,結果儲存在 rdi 中
xor rax, rax ; 清除 rax,確保初始值為 0
or rax, rsi ; 將 rdi OR rax(此時 rax 為 0,rdi 不變)
as -o
ld -o
check-even 檢查是否為偶數
在這一關卡,您將使用暫存器。您將被要求修改或讀取暫存器。
我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器執行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下是 rax
。
在這一關卡,你將處理位邏輯和操作。這將涉及大量直接與儲存在暫存器或記憶體位置中的位進行互動。你可能還需要使用x86中的邏輯指令: and
, or
, not
, xor
。
只使用以下指令:
and
or
xor
實現以下邏輯:
if x is even then
y = 1
else
y = 0
其中:
x = rdi
y = rax
檢視解析
要實現這個邏輯,判斷一個數 x 是否為偶數,可以檢查其最低位(LSB)。如果最低位為 0,則 x 為偶數;如果最低位為 1,則 x 為奇數。
.intel_syntax noprefix
and rdi, 1 ; 檢查 rdi 的最低位(rdi & 1,只檢測最低位,若rdi為偶數則rdi為0)
xor rax, rax ; 清除 rax,初始化為 0
or rax, rdi ; 如果最低位為 1,則 rax 變為 1(rdi 是奇數),否則 rax 仍為 0
xor rax, 1 ; 將結果反轉:如果是偶數,rax = 1;如果是奇數,rax = 0
as -o
ld -o
memory-read 讀取記憶體
現在,我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器進行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
在這個關卡中,你將使用記憶體。這需要你對記憶體中線性儲存的資料進行讀寫。你可能還會被要求多次解除對我們動態放入記憶體供你使用的東西的引用。
到目前為止,你已經使用暫存器作為儲存東西的唯一方式,本質上是像數學中的x
這樣的變數。
然而,我們也可以將位元組儲存到記憶體中!
回想一下,記憶體是可以定址的,每個地址在該位置都包含一些內容。請注意,這類似於現實生活中的地址!
例如:真實地址“699 S Mill Ave, Tempe, AZ 85281”對應於“ASU Brickyard”。我們也會說它指向“ASU Brickyard”。我們可以這樣表示:
['699 S Mill Ave, Tempe, AZ 85281'] = 'ASU Brickyard'
這個地址是特殊的,因為它是唯一的。但這也並不意味著其他地址不能指向同一個東西(因為一個人可以擁有多套房子)。
記憶體是完全一樣的!
例如,你的程式碼儲存在記憶體中的地址(當我們從你那裡獲取它時)是 0x400000
。
在x86中,我們可以訪問記憶體位置中的東西,稱為解引用,如下所示:
mov rax, [some_address] <=> Moves the thing at 'some_address' into rax
這也適用於暫存器中的內容:
mov rax, [rdi] <=> Moves the thing stored at the address of what rdi holds to rax
這和寫入記憶體是一樣的:
mov [rax], rdi <=> Moves rdi to the address of what rax holds.
因此,如果 rax
地址為 0xdeadbeef
,那麼 rdi
將儲存在地址 0xdeadbeef
:
[0xdeadbeef] = rdi
注意:記憶體是線性的,在x86_64中,它從 0
到 0xffffffffffffffff
(是的,非常巨大)。
請執行如下操作:將儲存在 0x404000
的值放入 rax
。確保 rax
中的值是儲存在 0x404000
中的原始值。
檢視解析
.intel_syntax noprefix
mov rax, [0x404000]
as -o
ld -o
memory-write 寫入記憶
現在,我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器進行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
在這個關卡中,你將使用記憶。這需要你對記憶體中線性儲存的資料進行讀寫。如果你感到困惑你可能還會被要求多次解除對我們動態放入記憶體供你使用的東西的引用。
請執行以下操作: 將儲存在 rax
中的值放置到 0x404000
中。
檢視解析
.intel_syntax noprefix
mov [0x404000], rax
as -o
ld -o
memory-increment 記憶體增量
現在,我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器進行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
在這個關卡中,你將使用記憶。這需要你對記憶體中線性儲存的資料進行讀寫。你可能還會被要求多次解除對我們動態放入記憶體供你使用的東西的引用。
請執行以下操作:
- 將儲存在
0x404000
中的值放入rax
中。 - 將儲存在地址
0x404000
中的值增加0x1337
。
確保 rax
中的值是儲存在 0x404000
中的原始值,並確保 [0x404000]
現在是增加後的值。
檢視解析
.intel_syntax noprefix
mov rax, [0x404000]
add qword ptr [0x404000], 0x1337
#`qword` 是 "quad word" 的縮寫,表示 64 位(8 位元組)資料。使用 ptr 關鍵字是為了告訴彙編器接下來的運算元是一個指標,指向記憶體地址中的資料。`qword ptr`明確指示運算元的大小,確保在執行加法時不會產生歧義。
as -o
ld -o
byte-access 位元組訪問
現在,我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器進行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
在這個關卡中,你將使用記憶。這需要你對記憶體中線性儲存的資料進行讀寫。如果你感到困惑你可能還會被要求多次解除對我們動態放入記憶體供你使用的東西的引用。
回想一下,x86_64中的暫存器是64位寬的,這意味著它們可以儲存64位。類似地,每個記憶體位置都可以視為64位值。我們將64位(8位元組)的東西稱為四字詞。
下面是記憶體大小名稱的分類:
- Quad Word = 8 Bytes = 64 bits
- Double Word = 4 bytes = 32 bits
- Word = 2 bytes = 16 bits
- Byte = 1 byte = 8 bits
Bit——位
Byte——位元組
Word——字
Double Word——雙字
Quad Word——四字
在x86_64中,你可以在解引用地址時訪問這些長度中的每一個,就像使用更大或更小的暫存器訪問一樣:
mov al, [address]
<=>將最低有效位元組從地址移到rax
mov ax, [address]
<=>將最低有效字從地址移到rax
mov eax, [address]
<=>將最低有效的雙字從地址移到rax
mov rax, [address]
<=>將完整的四字內容從地址移動到rax
請記住,遷移到 al
並不完全清除上層位元組。
請執行以下操作: 設定 rax
為 0x404000
的最低有效位元組。
檢視解析
.intel_syntax noprefix
mov al, [0x404000]
as -o
ld -o
memory-size-access 記憶體大小訪問
現在,我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器執行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
在這個關卡中,你將使用記憶。這需要你對記憶體中線性儲存的資料進行讀寫。如果你感到困惑你可能還會被要求多次解除對我們動態放入記憶體供你使用的東西的引用。
回想一下:
- 記憶體大小名稱的分解:
- Quad Word = 8 Bytes = 64 bits
- Double Word = 4 bytes = 32 bits
- Word = 2 bytes = 16 bits
- Byte = 1 byte = 8 bits
在x86_64中,你可以在解引用地址時訪問這些長度中的每一個,就像使用更大或更小的暫存器訪問一樣:
mov al, [address]
<=>將最低有效位元組從地址移到rax
mov ax, [address]
<=>將最低有效字從地址移到rax
mov eax, [address]
<=>將最低有效的雙字從地址移到rax
mov rax, [address]
<=>將完整的四字從地址移動到rax
請執行以下操作:
- 設定
rax
為0x404000
的最低有效位元組。 - 設定
rbx
為0x404000
的最低有效字。 - 設定
rcx
為0x404000
的最低有效雙字。 - 設定
rdx
為0x404000
的完整四字。
檢視解析
.intel_syntax noprefix
mov al, [0x404000]
mov bx, [0x404000]
mov ecx, [0x404000]
mov rdx, [0x404000]
as -o
ld -o
little-endian-write 小端寫入
現在,我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器進行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
在這個關卡中,你將使用記憶。這需要你對記憶體中線性儲存的資料進行讀寫。你可能還會被要求多次解除對我們動態放入記憶體供你使用的東西的引用。
值得注意的是,你可能已經注意到,值的儲存順序與我們表示它們的順序相反。
舉個例子:
[0x1330] = 0x00000000deadc0de
如果你檢查它在記憶體中的實際樣子,你會看到:
[0x1330] = 0xde
[0x1331] = 0xc0
[0x1332] = 0xad
[0x1333] = 0xde
[0x1334] = 0x00
[0x1335] = 0x00
[0x1336] = 0x00
[0x1337] = 0x00
這種“反向”儲存格式在x86中是有意為之的,它被稱為“小端序”。
對於這個挑戰,我們將為您提供每次執行時動態建立的兩個地址。
第一個地址將放置在 rdi
。 第二個被放在 rsi
。
利用前面提到的資訊,執行如下操作:
- Set
[rdi] = 0xdeadbeef00001337
- Set
[rsi] = 0xc0ffee0000
提示:將一個大常量賦值給一個未引用的暫存器可能需要一些技巧。嘗試將一個暫存器設定為該常量的值,然後將該暫存器賦值給解除引用的暫存器。
檢視解析
為了將一個常量寫入記憶體地址,你通常需要先將常量載入到一個暫存器中,然後使用這個暫存器的值去寫入記憶體。
.intel_syntax noprefix
mov rax, 0xdeadbeef00001337
mov [rdi], rax
mov rax, 0xc0ffee0000
mov [rsi], rax
#將兩個 64 位的常量分別寫入由 rdi 和 rsi 指向的記憶體地址,使用小端序儲存資料。
as -o
ld -o
memory-sum 記憶體求和
現在,我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器執行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
在這個關卡中,你將使用記憶。這需要你對記憶體中線性儲存的資料進行讀寫。你可能還會被要求多次解除對我們動態放入記憶體供你使用的東西的引用。
回想一下,記憶體是線性儲存的。
這是什麼意思?
假設我們訪問 0x1337
處的完整四字內容:
[0x1337] = 0x00000000deadbeef
記憶體的實際佈局方式是逐位元組排列,小端序:
[0x1337] = 0xef
[0x1337 + 1] = 0xbe
[0x1337 + 2] = 0xad
...
[0x1337 + 7] = 0x00
這對我們有什麼用?
好吧,這意味著我們可以使用偏移量來訪問相鄰的元素,就像上面顯示的那樣。
假設你想要一個地址的第5個位元組,你可以這樣訪問它:
mov al, [address+4]
記住,偏移量從0開始。
執行如下操作:
- 從
rdi
中儲存的地址中載入兩個連續的四字。 - 計算前面步驟中的四詞之和。
- 將和儲存在
rsi
的地址。
檢視解析
.intel_syntax noprefix
mov rax, [rdi] ; 載入第一個四字到 rax
mov rbx, [rdi + 8] ; 載入第二個四字到 rbx
add rax, rbx ; 將兩個值相加
mov [rsi], rax ; 將結果儲存到 rsi 指向的地址
as -o
ld -o
stack-subtraction 棧減法
現在,我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器進行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
在這一關卡,您將使用棧,動態擴充套件和收縮的記憶體區域。你需要對棧進行讀寫,這可能需要使用 pop
和 push
指令。你可能還需要使用棧指標暫存器( rsp
)來知道棧指向哪裡。
在這些關卡次中,我們將介紹棧。
棧是記憶體中的一個區域,可以儲存稍後使用的值。
要在棧中儲存一個值,我們使用 push
指令,而要取得一個值,我們使用 pop
指令。
棧是一種後進先出(LIFO)的記憶體結構,這意味著最後推入的值就是第一個彈出的值。
想象一下把盤子從洗碗機裡拿出來。假設有1個紅,1個綠,1個藍。首先,我們把紅色的放在櫃子裡,然後把綠色的放在紅色的上面,然後是藍色的。
我們的盤子堆看起來像這樣:
Top ----> Blue
Green
Bottom -> Red
現在,如果我們想要一個盤子來做三明治,我們會從一堆盤子中取出最上面的那個,也就是最後一個進入櫥櫃的藍色盤子,也就是第一個出來的盤子。
在x86上, pop
指令將從棧頂取出值並將其放入暫存器。
類似地, push
指令會將暫存器中的值壓入棧頂。
使用這些指令,取棧頂的值,減去 rdi
,然後放回去。
檢視解析
.intel_syntax noprefix
pop rax
sub rax, rdi
push rax
as -o
ld -o
swap-stack-values 交換棧值
現在,我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器進行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
在這一關卡,您將使用棧,動態擴充套件和收縮的記憶體區域。你需要對棧進行讀寫,這可能需要使用 pop
和 push
指令。你可能還需要使用棧指標暫存器( rsp
)來知道棧指向哪裡。
在這一關卡,我們將探索棧的後進先出(LIFO)屬性。
只使用以下說明:
push
pop
交換 rdi
和 rsi
中的值。
例子:
- 開始時
rdi = 2
和rsi = 5
- 請在結束時滿足
rdi = 5
和rsi = 2
檢視解析
.intel_syntax noprefix
push rdi
push rsi
pop rdi
pop rsi
as -o
ld -o
average-stack-values 棧值平均
現在,我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器進行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
在這一關卡,您將使用棧,動態擴充套件和收縮的記憶體區域。你需要對棧進行讀寫,這可能需要使用 pop
和 push
指令。你可能還需要使用棧指標暫存器( rsp
)來知道棧指向哪裡。
在前面的關卡中,你使用 push
和 pop
來從棧中儲存和載入資料。但是,你也可以直接使用棧指標訪問棧。
在x86上,堆疊指標儲存在特殊暫存器 rsp
中。 rsp
總是儲存堆疊頂部的記憶體地址,即最後一個壓入值的記憶體地址。
與記憶體關卡類似,我們可以使用 [rsp]
來訪問 rsp
中的記憶體地址的值。
在不使用 pop
的情況下,計算棧上連續儲存的4個四字的平均值。將平均值壓入棧。
提示:
RSP+0x??
Quad Word ARSP+0x??
Quad Word BRSP+0x??
Quad Word CRSP
Quad Word D
檢視解析
.intel_syntax noprefix
mov rax, [rsp]
mov rbx, [rsp+8]
mov rcx, [rsp+16]
mov rdx, [rsp+24]
add rax, rbx
add rax, rcx
add rax, rdx
shr rax, 2 ; 右移 2 位,即除以 4
push rax
as -o
ld -o
absolute-jump 絕對跳轉
現在,我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器進行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
在這個關卡,您將處理控制流操作。這涉及到使用指令來間接和直接控制特殊暫存器 rip
(指令指標)。你將使用諸如 jmp
、 call
、 cmp
以及它們的替代指令來實現所請求的行為。
前面,我們學習瞭如何以偽控制的方式運算元據,但是x86提供了直接操作控制流的實際指令。
操縱控制流有兩種主要方法。
- Through a jump 透過跳轉
- Through a call 透過呼叫
在這個關卡中,你將使用跳轉。
有兩種型別的跳轉:
- Unconditional jumps 無條件的跳轉
- Conditional jumps 條件跳轉
無條件跳轉總是會觸發,並且不基於前面指令的結果。
如你所知,記憶體位置可以儲存資料和指令。你的程式碼將儲存在 0x400042
(每次執行都會改變)。
對於所有的跳轉,有三種型別:
- 相對跳轉:跳轉到下一條指令的正或負偏移。
- 絕對跳轉:跳轉到特定地址。
- 間接跳轉:跳轉到暫存器中指定的記憶體地址。
在x86中,絕對跳轉(跳轉到特定地址)是透過首先將目標地址放入暫存器 reg
,然後執行 jmp reg
來完成的。
x86 指令集是變長指令集,這意味著每條指令的長度是變化的,取決於運算元的大小。絕對跳轉通常涉及到 32 位或 64 位的目標地址,但指令長度有限,不能在一條指令中直接攜帶如此大的目標地址。
- 在 x86 32 位模式下,
jmp
指令通常是 5 位元組(包括操作碼和 32 位的偏移量)。 - 在 x86-64 64 位模式下,
jmp
指令則通常是 10 位元組(包括操作碼和 64 位的偏移量)。
雖然 jmp
可以直接處理較短的相對跳轉(透過偏移量),但要處理一個 64 位的絕對跳轉(在 x86-64 中),無法透過單個 jmp
指令直接指定一個完整的目標地址。於是就需要一種間接的方式:將目標地址載入到暫存器中,再透過 jmp reg
執行跳轉。
在這個關卡中,我們會要求你做一個絕對的跳轉。執行如下操作:跳轉到絕對地址 0x403000
。
檢視解析
.intel_syntax noprefix
mov rax, 0x403000
jmp rax
as -o
ld -o
relative-jump 相對跳轉
現在,我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器執行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
在這個關卡,您將處理控制流操作。這涉及到使用指令來間接和直接控制特殊暫存器 rip
(指令指標)。你將使用諸如 jmp
、 call
、 cmp
以及它們的替代指令來實現所請求的行為。
回想一下,對於所有跳轉,有三種型別:
- Relative jumps 相對跳轉
- Absolute jumps 絕對的跳轉
- Indirect jumps 間接的跳轉
在這個關卡中,我們將要求你進行一個相對跳轉。你需要在程式碼中填充一些空間,以便實現這個相對跳轉。我們建議使用 nop
指令。它長度為 1 位元組,並且非常可預測。
事實上,我們正在使用的彙編器有一個方便的 .rept
指令,您可以使用它來重複一些次彙編指令:GNU彙編器手冊
這個關卡的有用說明:
jmp (reg1 | addr | offset)
nop
提示:對於相對跳轉,請思考如何在x86中使用 labels
。
利用上述知識,執行如下操作:
- 程式碼中的第一條指令是
jmp
。 - 將
jmp
跳轉指令改為相對跳轉,跳轉到當前指令位置向後偏移 0x51 位元組。 - 在相對跳轉將重定向控制流的程式碼位置,將
rax
設定為0x1。
檢視解析
.intel_syntax noprefix
jmp skip ; 直接跳轉到標籤 skip,開始相對跳轉
nop_padding:
.rept 0x51 ; 重複 0x51 次
nop ; 每次重複的指令
.endr ; 結束重複指令
skip: ; 這裡的程式碼將在 jmp 跳轉後執行
mov rax, 0x1 ; 將 rax 設定為 0x1
as -o
ld -o
+------------------+
| |
| Start |
| |
+--------+---------+
|
v
+------------------+
| |
| jmp skip | <--- 相對跳轉到 skip 標籤
| |
+--------+---------+
|
v
+------------------+
| |
| nop_padding | <--- 這裡的 nop 指令會被執行
| |
+--------+---------+
|
v
+------------------+
| |
| nop | <--- 多個 nop 指令(填充)
| |
+--------+---------+
|
v
+------------------+
| |
| skip | <--- 到達 skip 標籤
| |
+--------+---------+
|
v
+------------------+
| |
| mov rax, 0x1 | <--- 設定 rax 為 0x1
| |
+------------------+
|
v
+------------------+
| |
| End |
| |
+------------------+
在彙編中,jmp skip
是一種相對跳轉,它的目標是 skip
標籤,而不是直接跳過 nop_padding
。在跳轉指令中,目標地址是相對於當前指令的偏移量計算的。
編譯器會根據 jmp
指令的位置和 skip
標籤的位置計算出偏移量。即使 jmp
指令出現在 nop_padding
的前面,指令會根據偏移量跳轉到 skip
。
相對跳轉:中間的指令會被執行,因為控制流是基於當前位置的偏移量進行計算的。
絕對跳轉:中間的指令不會被執行,因為它直接跳轉到指定地址,跳過了所有位於這個地址之前的指令。
jump-trampoline 跳轉跳板
現在,我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器進行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
在這個關卡,您將處理控制流操作。這涉及到使用指令來間接和直接控制特殊暫存器 rip
(指令指標)。你將使用諸如 jmp
、 call
、 cmp
以及它們的替代指令來實現所請求的行為。
現在,我們將前兩個關卡結合起來執行以下操作:
- 建立一個兩次跳轉的跳板:
- 程式碼中的第一條指令是
jmp
。 - 使
jmp
從當前位置相對跳轉到0x51位元組。 - 在0x51處,編寫如下程式碼:
- 將棧上的最高值放入暫存器
rdi
。 jmp
到絕對地址0x403000
- 將棧上的最高值放入暫存器
- 程式碼中的第一條指令是
檢視解析
.intel_syntax noprefix
jmp skip ; 直接跳轉到標籤 skip,開始相對跳轉
nop_padding:
.rept 0x51 ; 重複 0x51 次
nop ; 每次重複的指令
.endr ; 結束重複指令
skip: ; 這裡的程式碼將在 jmp 跳轉後執行
mov rdi, rsp ; 將棧上的最高值放入暫存器 rdi
mov rax, 0x403000
jmp rax ; jmp 到絕對地址0x403000
as -o
ld -o
conditional-jump 條件跳轉
在這個關卡,您將處理控制流操作。這涉及到使用指令來間接和直接控制特殊暫存器 rip
(指令指標)。你將使用諸如 jmp
、 call
、 cmp
以及它們的替代指令來實現所請求的行為。
我們將在這個關卡中使用動態值多次測試您的程式碼!這意味著我們將以各種隨機的方式執行你的程式碼,以驗證邏輯是否足夠穩固,可以正常使用。
現在我們將介紹條件跳轉——x86中最有價值的指令之一。在高階程式語言中,if-else結構的作用如下:
if x is even:
is_even = 1
else:
is_even = 0
這看起來應該很熟悉,因為它只能以位邏輯實現,你在之前的版本中已經實現過。在這些結構中,我們可以根據提供給程式的動態值來控制程式的控制流。
用jmp
實現上述邏輯可以這樣做:
; 假設 rdi = x, rax 為輸出
; rdx = rdi 除以 2 的餘數
mov rax, rdi
mov rsi, 2
div rsi
; 如果餘數是 0,則為偶數
cmp rdx, 0 ; cmp指令執行的是減法操作,計算兩個運算元的差值,但不會儲存計算結果,只修改標誌暫存器
; 如果餘數不為 0,則跳轉到 not_even 部分
jne not_even ; jne全稱是 "Jump if Not Equal"(如果不相等則跳轉)
; 如果是偶數,則繼續執行下面的程式碼
mov rbx, 1
jmp done
; 只有在不是偶數時跳轉到這裡
not_even:
mov rbx, 0
done:
mov rax, rbx
; 後面還有更多指令
但通常情況下,你需要的不僅僅是一個if-else
。有時你需要兩個if檢查,後面跟著一個else。要做到這一點,你需要確保你的控制流在失敗後fall- through
到下一個 if
。All語句在執行後必須跳轉到相同的 done
,以避免else語句。
x86中有很多跳轉型別,瞭解它們的使用方法將會很有幫助。它們幾乎都依賴於一種叫做ZF的東西,即零標誌。當a cmp
等於時,ZF設為1,否則設為0。
利用上面的知識,實現如下程式碼:
if [x] is 0x7f454c46:
y = [x+4] + [x+8] + [x+12]
else if [x] is 0x00005A4D:
y = [x+4] - [x+8] - [x+12]
else:
y = [x+4] * [x+8] * [x+12]
其中:
x = rdi
、y = rax
。
假設每個解引用的值都是有符號dword。這意味著在每個記憶體位置上,這些值都可以從負值開始。
一個有效的解決方案將至少使用以下一次:
jmp
(任意變體),cmp
檢視解析
.intel_syntax noprefix
cmp dword ptr [rdi], 0x7f454c46
je yes
cmp dword ptr [rdi], 0x00005A4D
je yesyes
mov eax, [rdi+4]
imul eax, [rdi+8]
imul eax, [rdi+12]
jmp done
yes:
mov eax, [rdi+4]
add eax, [rdi+8]
add eax, [rdi+12]
jmp done
yesyes:
mov eax, [rdi+4]
sub eax, [rdi+8]
sub eax, [rdi+12]
done:
as -o
ld -o
indirect-jump 間接跳轉
在這個關卡,您將使用控制流操作。這涉及到使用指令來間接或直接控制特殊暫存器 rip
(指令指標)。你將使用諸如 jmp
、 call
、 cmp
以及它們的替代指令來實現所請求的行為。
我們將在這個關卡中使用動態值多次測試您的程式碼!這意味著我們將以各種隨機的方式執行你的程式碼,以驗證邏輯是否足夠穩固,可以正常使用。
最後一種跳轉型別是間接跳轉,通常用於switch語句。Switch語句是一種特殊的if語句,它只使用數字來確定控制流的走向。
下面是一個例子:
switch(number):
0: jmp do_thing_0
1: jmp do_thing_1
2: jmp do_thing_2
default: jmp do_default_thing
本例中的開關工作在 number
上,它可以是0、1或2。如果 number
不是這些數字之一,則觸發預設(default)。你可以認為這是一個簡化的else-if型別結構。在x86中,你已經習慣了使用數字,所以你可以基於一個精確的數字來編寫if語句也就不足為奇了。此外,如果你知道數字的範圍,switch語句就可以很好地工作。
以跳轉表的存在為例。跳轉表是一個連續的記憶體段,其中儲存了要跳轉的地址。
在上面的例子中,跳轉表如下所示:
[0x1337] = address of do_thing_0
[0x1337+0x8] = address of do_thing_1
[0x1337+0x10] = address of do_thing_2
[0x1337+0x18] = address of do_default_thing
使用跳轉表,可以大大減少 cmps
的使用量。現在我們需要檢查的是 number
是否大於2。如果是,就一直這樣做:
jmp [0x1337+0x18]
否則:
jmp [jump_table_address + number * 8]
使用上述知識,實現以下邏輯:
if rdi is 0:
jmp 0x40301e
else if rdi is 1:
jmp 0x4030da
else if rdi is 2:
jmp 0x4031d5
else if rdi is 3:
jmp 0x403268
else:
jmp 0x40332c
請按照以下約束條件執行上述操作:
- 假設
rdi
不為負。 - 使用不超過1條
cmp
指令。 - 使用不超過3個跳躍(任何變體)。
- 我們將在
rdi
中為您提供要“接通”的數字。 - 我們將為您提供
rsi
中的跳轉表基址。
下面是一個示例表:
[0x40427c] = 0x40301e (addrs will change)
[0x404284] = 0x4030da
[0x40428c] = 0x4031d5
[0x404294] = 0x403268
[0x40429c] = 0x40332c
檢視解析
.intel_syntax noprefix
cmp rdi, 3 ; 比較 rdi 和 3
jg default ; 如果 rdi > 3,跳轉到預設跳轉
; 如果 rdi 在 0 到 3 之間,使用跳轉表
mul rdi, 8
jmp [rsi + rdi * 8] ; 使用 rsi 作為跳轉表基地址,透過 rdi 計算偏移量進行跳轉
default:
jmp qword ptr [rsi + 32] ; 預設跳轉到跳轉表最後一個地址
done:
as -o
ld -o
average-loop 迴圈計算平均值
現在,我們將在每次執行之前在記憶體中動態設定一些值。每次執行時,這些值都會改變。這意味著你需要對暫存器執行某種公式化的操作。我們會告訴你哪些暫存器是預先設定的,以及你應該把結果放在哪裡。大多數情況下,它是 rax
。
在這個關卡,您將處理控制流操作。這涉及到使用指令來間接和直接控制特殊暫存器 rip
(指令指標)。你將使用諸如 jmp
、 call
、 cmp
以及它們的替代指令來實現所請求的行為。
在上一關卡中,你計算了4個整數四字的平均值,這是要計算的固定數量的東西。但如何處理程式執行時得到的尺寸呢?
在大多數程式語言中,都存在一種稱為for迴圈的結構,它允許你在有限的時間內執行一組指令。限定的數值可以在程式執行之前或執行期間確定,“during”表示這個值是動態給定的。
例如,可以使用for迴圈計算數字1 ~ n的和:
sum = 0
i = 1
while i <= n:
sum += i
i += 1
計算 n
連續四詞的平均值,其中:
rdi
=第一個四字記憶體地址rsi
=n
(迴圈的次數)rax
=計算平均值
檢視解析
.intel_syntax noprefix
mov rax, 0 ; 清空 rax (用來儲存累加和)
mov rbx, rsi ; 將 n (rsi) 複製到 rbx,作為迴圈計數器
; 迴圈開始
.loop:
cmp rbx, 0 ; 檢查是否還有迭代次數
je .done ; 如果 rcx == 0, 結束迴圈
add rax, [rdi] ; 累加當前四字到 rax
add rdi, 8 ; 將 rdi 指向下一個四字 (8 位元組對齊)
dec rbx ; 減少迭代次數
jmp .loop ; 跳回迴圈
.done:
; 計算平均值 (除以 n)
; 將 rax 中的值除以 rsi (n)
div rsi ; 將 rax 除以 n
; 結果儲存在 rax 中
as -o
ld -o
count-non-zero 計數非零值
在這個關卡,您將處理控制流操作。這涉及到使用指令來間接和直接控制特殊暫存器 rip
(指令指標)。你將使用諸如 jmp
、 call
、 cmp
以及它們的替代指令來實現所請求的行為。
我們將在這個關卡中使用動態值多次測試您的程式碼!這意味著我們將以各種隨機的方式執行你的程式碼,以驗證邏輯是否足夠穩固,可以正常使用。
在前面幾關卡中,你發現for迴圈可以迭代很多次,動態和靜態都是已知的,但如果你想迭代直到滿足某個條件,會發生什麼呢?
存在另一種迴圈結構,稱為while-loop,可以滿足這種需求。在while迴圈中,迭代直到滿足某個條件。
舉個例子,假設我們在記憶體中有一個相鄰數字的位置,我們想要得到所有數字的平均值,直到找到一個大於或等於 0xff
:
average = 0
i = 0
while x[i] < 0xff:
average += x[i]
i += 1
average /= i
利用以上知識,請執行以下操作:
在記憶體的連續區域中計算連續的非零位元組,其中:
rdi
=第1位元組記憶體地址rax
=連續非零位元組數
另外,如果 rdi = 0
,則設定 rax = 0
(我們將檢查)!
下面是一個測試用例的例子:
rdi = 0x1000
[0x1000] = 0x41
[0x1001] = 0x42
[0x1002] = 0x43
[0x1003] = 0x00
設 rax = 3
。
檢視解析
.intel_syntax noprefix
mov rax, 0 ; 初始化 rax 為 0,作為非零位元組的計數器
mov rsi, 0 ; 初始化 rsi 為 0,用於計數連續的非零位元組
; 檢查 rdi 是否為零,如果是,則直接跳轉到 done
test rdi, rdi ; test 只是檢查 operand1 和 operand2 的按位與結果,更新標誌暫存器,但不修改運算元本身。檢查 rdi 是否為零
jz .done ; 如果 rdi 為零,則跳轉到 done(設定 rax = 0)
.loop:
cmp byte ptr [rdi], 0 ; 比較當前位元組是否為零
je .done ; 如果是零,跳轉到 done(結束迴圈)
add rax, 1 ; 非零位元組計數器加 1
add rdi, 1 ; 移動到下一個位元組
jmp .loop ; 繼續迴圈
.done:
; rax 中儲存的是連續非零位元組的數量
; 結束程式
as -o
ld -o
string-lower 字串轉小寫
我們將在這個關卡中使用動態值多次測試您的程式碼!這意味著我們將以各種隨機的方式執行你的程式碼,以驗證邏輯是否足夠穩固,可以正常使用。
在這一關卡,你將使用函式!這將涉及操作指令指標(rip),以及執行比通常更難的任務。您可能會被要求使用堆疊來儲存值或呼叫我們提供的函式。
在前面的關卡中,我們實現了一個while迴圈來計算記憶體連續區域中連續非零位元組的數量。
在這一關卡,使用者將再次獲得一個連續的記憶體區域,並迴圈遍歷每個執行條件操作的區域,直到達到0位元組。所有這些都將包含在一個函式中!
函式是一段不會破壞控制流的可呼叫程式碼。
函式使用“call”和“ret”指令。
“call”指令將下一條指令的記憶體地址壓入棧中,然後跳轉到儲存在第一個引數中的值。
讓我們以下面的指令為例:
0x1021 mov rax, 0x400000
0x1028 call rax
0x102a mov [rsi], rax
call
將下一條指令的地址0x102a
壓入棧中。call
跳轉到0x400000
,儲存在rax
中。
“ret”指令與“call”指令相反。
ret
彈出棧頂值並跳轉到它。
讓我們以下面的指令和棧為例:
Stack ADDR VALUE
0x103f mov rax, rdx RSP + 0x8 0xdeadbeef
0x1042 ret RSP + 0x0 0x0000102a
這裡, ret
將跳轉到 0x102a
。
請實現以下邏輯:
str_lower(src_addr):
i = 0
if src_addr != 0:
while [src_addr] != 0x00:
if [src_addr] <= 0x5a:
[src_addr] = foo([src_addr])
i += 1
src_addr += 1
return i
foo
為 0x403000
。 foo
接受一個引數作為值,並返回一個值
所有函式(foo
和str_lower
)必須遵循Linux amd64呼叫約定(也稱為System V amd64 ABI): System V AMD64 ABI
因此,你的函式 str_lower
應該在 rdi
中尋找 src_addr
,並將函式返回值放在 rax
中。
需要注意的是, src_addr
是記憶體中的一個地址(字串所在的位置),而 [src_addr]
指的是存在於 src_addr
的位元組。
因此, foo
函式接受一個位元組作為它的第一個引數,並返回一個位元組。
檢視解析
.intel_syntax noprefix
str_lower:
mov rbx, 0x403000 ; 將 foo 函式的地址 (0x403000) 載入到 rbx 暫存器中
xor rcx, rcx ; 清空 rcx 暫存器,用作計數器,記錄轉換為小寫字母的位元組數
test rdi, rdi ; 檢查傳入的字串地址 (rdi) 是否為空
jz done ; 如果 rdi 為 0,跳轉到 done,表示字串為空,無需處理
process_string:
mov al, byte ptr [rdi] ; 將當前位元組(rdi 指向的地址的值)載入到 al 暫存器中
test al, al ; 檢查當前位元組是否為 NULL 字元(0x00),即字串結束標誌
jz done ; 如果是 NULL 字元(0x00),跳轉到 done,表示字串結束
cmp al, 0x5A ; 比較當前字元是否大於 'Z' (0x5A)
jg skip_conversion ; 如果字元大於 'Z'(即不是大寫字母),跳到 skip_conversion,跳過轉換
mov rsi, rdi ; 儲存當前字串地址 (rdi) 到 rsi,稍後恢復
mov dil, al ; 將當前位元組(大寫字母)存入 rdi 暫存器的低位元組部分(dil),作為 foo 函式的引數
call rbx ; 呼叫 foo 函式,轉換位元組為小寫字母,轉換結果將返回在 al 暫存器
mov rdi, rsi ; 恢復原始的 rdi 地址
mov byte ptr [rdi], al ; 將轉換後的位元組(儲存在 al 中)寫回原地址,替換原來的字元
inc rcx ; 增加轉換計數器 rcx,記錄已經轉換為小寫字母的位元組數
skip_conversion:
inc rdi ; 移動到下一個字元(增加 rdi,指向下一個位元組)
jmp process_string ; 跳回 process_string 繼續處理下一個字元
done:
mov rax, rcx ; 將轉換的位元組數(rcx)存入 rax,作為返回值
ret ; 返回,rax 包含轉換的小寫位元組數量
as -o
ld -o
most-common-byte 最常見位元組
我們將在這個關卡中使用動態值多次測試您的程式碼!這意味著我們將以各種隨機的方式執行你的程式碼,以驗證邏輯是否足夠穩固,可以正常使用。
在這一關卡,你將使用函式!這將涉及操作指令指標(rip),以及執行比通常更難的任務。您可能會被要求使用堆疊來儲存值或呼叫我們提供的函式。
在前一關卡中,你學習瞭如何建立第一個函式以及如何呼叫其他函式。現在我們將使用具有函式棧幀的函式。
函式棧幀是一組壓入棧中的指標和值的集合,用於儲存內容以供以後使用,並在棧上為函式變數分配空間。
首先,讓我們討論一下特殊暫存器 rbp
,即棧基指標。
rbp
暫存器用於告訴我們棧幀第一次從哪裡開始。舉個例子,假設我們想構建一個只在函式中使用的列表(一個連續的記憶體空間)。這個列表有5個元素,每個元素是一個dword。一個包含5個元素的列表已經佔用了5個暫存器,因此,我們可以在堆疊上騰出空間!
程式集看起來像這樣:
; 將棧基址設定為當前棧頂
mov rbp, rsp
; 將棧向下移動 0x14 位元組 (5 * 4)
; 這相當於分配空間
sub rsp, 0x14
; 將 list[2] 賦值為 1337
mov eax, 1337
mov [rbp-0x8], eax
; 對列表進行更多操作...
; 恢復已分配的空間
mov rsp, rbp
ret
請注意, rbp
總是用於將棧恢復到最初的位置。如果我們在使用後不恢復堆疊,我們最終將耗盡。另外,注意我們是如何從 rsp
減去的,因為棧是向下增長的。為了讓堆疊有更多的空間,我們減去所需的空間。 ret
和 call
仍然起作用。
再一次,請建立實現以下功能的函式:
most_common_byte(src_addr, size):
i = 0
while i <= size-1:
curr_byte = [src_addr + i]
[stack_base - curr_byte] += 1
i += 1
b = 0
max_freq = 0
max_freq_byte = 0
while b <= 0xff:
if [stack_base - b] > max_freq:
max_freq = [stack_base - b]
max_freq_byte = b
b += 1
return max_freq_byte
假設:
- 任何位元組的長度都不會超過0xffff
- 這個長度永遠不會大於0xffff
- 這個列表至少有一個元素
約束:
- 你必須把“計數列表”放在堆疊上
- 您必須像恢復普通函式一樣恢復堆疊
- 不能修改
src_addr
處的資料
檢視解析
.intel_syntax noprefix
.intel_syntax noprefix
most_common_byte:
push rbp ; 儲存呼叫者的基址暫存器(rbp),為函式的棧幀做好準備
mov rbp, rsp ; 設定當前函式的棧幀基址
sub rsp, 256 ; 為位元組計數器陣列分配 256 位元組空間(用來記錄每個位元組出現的次數)
xor rcx, rcx ; 將 rcx 清零,用作計數器(用於迴圈)
initialize_counting_list_with_zero:
mov byte ptr [rbp + rcx - 256], 0 ; 將計數陣列的當前位元組位置初始化為 0
inc rcx ; 增加 rcx,移動到下一個位元組位置
cmp rcx, 256 ; 檢查 rcx 是否達到 256(即陣列的末尾)
jl initialize_counting_list_with_zero ; 如果 rcx 小於 256,繼續初始化下一個位元組
xor rcx, rcx ; 清零 rcx,準備計數字節的出現次數
count_bytes:
movzx eax, byte ptr [rdi + rcx] ; 載入當前位元組(rdi 是字串的起始地址)到 eax 暫存器
inc byte ptr [rbp + rax - 256] ; 將該位元組在計數陣列中的計數加 1,陣列位置為 rax-256
inc rcx ; 移動到下一個位元組
cmp rcx, rsi ; 比較 rcx 和 rsi,rsi 是字串的長度
jl count_bytes ; 如果 rcx 小於 rsi,繼續處理下一個位元組
init_b_max_freq_max_freq_byte:
xor rcx, rcx ; 清零 rcx,用作索引
xor rdx, rdx ; 清零 rdx,用來儲存當前位元組出現的最大頻率
xor rbx, rbx ; 清零 rbx,用來儲存具有最大頻率的位元組
find_most_common_byte:
movzx eax, byte ptr [rbp + rcx - 256] ; 載入計數陣列中的當前位元組頻率到 eax
cmp al, dl ; 比較當前位元組的頻率與當前最大頻率(dl)
jle next_byte ; 如果當前位元組的頻率小於等於最大頻率,跳到下一個位元組
update_max_freq_and_max_freq_byte:
mov dl, al ; 更新最大頻率(dl)
mov bl, cl ; 更新最大頻率位元組(bl)
next_byte:
inc rcx ; 移動到下一個位元組位置
cmp rcx, 256 ; 檢查是否已遍歷所有位元組
jl find_most_common_byte ; 如果 rcx 小於 256,繼續檢查下一個位元組
return:
mov al, bl ; 將具有最大頻率的位元組存入 al,作為返回值
restore:
mov rsp, rbp ; 恢復棧指標
pop rbp ; 恢復基址暫存器
ret ; 返回
as -o
ld -o
Debugging Refresher 除錯進修
level1
執行/challenge/embryogdb_level1
程式即會自動開啟搭載了/challenge/embryogdb_level1
程式的GDB(像是套娃),先是向我們顯示了GDB程式的版權開頭內容,我們輸入run
或r
在GDB中執行/challenge/embryogdb_level1
程式
執行後,會出現以下內容:
GDB是一個非常強大的動態分析工具,您可以使用它來了解程式在整個執行過程中的狀態。在本模組中,您將熟悉gdb的一些功能。
您正在執行gdb!程式當前處於暫停狀態。這是因為它在這裡設定了自己的斷點。
您可以使用‘ start ’命令啟動程式,並在‘ main ’上設定斷點。
您可以使用‘ starti ’命令來啟動一個程式,在‘ _start ’上設定斷點。
你可以使用命令“run”來啟動一個程式,不帶斷點集。
您可以使用命令“attach <PID>”來附加到其他已經執行的程式。
可以使用命令‘ core <PATH> ’分析已執行程式的核心轉儲。
在啟動或執行程式時,您可以以與在shell上幾乎完全相同的方式指定引數。
例如,您可以使用‘ start <ARGV1> <ARGV2> <ARGVN> <STDIN_PATH> ’。
使用命令‘ continue ’,或簡稱‘ c ’,以繼續程式執行。
我們為了從斷點處繼續執行/challenge/embryogdb_level1
程式,輸入以下命令
continue
或
c
/challenge/embryogdb_level1
程式繼續執行,我們得到flag
level2
GDB是一個非常強大的動態分析工具,您可以使用它來了解程式在整個執行過程中的狀態。在本模組中,您將熟悉gdb的一些功能。
你可以用‘ info registers ’檢視所有暫存器的值。
或者,您也可以使用‘ print ’命令或簡稱‘ p ’命令列印特定暫存器的值。例如,‘ p $rdi ’將以十進位制形式列印$rdi的值。你也可以用十六進位制‘ p/x $rdi ’列印它的值。
為了解決這個問題,你必須用十六進位制計算出暫存器r12的當前隨機值。
隨機值已設定!
為了檢視暫存器r12的當前隨機值我們輸入以下命令:
info registers #可以檢視所有暫存器的值
p $r12 #以十進位制形式列印$r12的值
p/x $r12 #以十六進位制形式列印$r12的值
得到r12的值後,我們從斷點處繼續執行/challenge/embryogdb_level2
程式,它會問我們r12暫存器中隨機的值是多少,我們輸入即可得到flag
level3
GDB是一個非常強大的動態分析工具,您可以使用它來了解程式在整個執行過程中的狀態。在本模組中,您將熟悉gdb的一些功能。
您可以使用‘ x/<n><u><f> <address> ’引數化命令檢查記憶體的內容。
在這種格式中,‘ <u> ’是要顯示的單位大小,‘ <f> ’是要顯示的格式,‘ <n> ’是要顯示的元素數量。有效單元大小為‘ b ’(1位元組),‘ h ’(2位元組),‘ w ’(4位元組)和‘ g ’(8位元組)。有效的格式是‘ d ’(十進位制),‘ x ’(十六進位制),‘ s ’(字串)和‘ i ’(指令)。地址可以使用暫存器名、符號名或絕對地址來指定。
此外,您可以在指定地址時提供數學表示式。
例如,‘ x/8i $rip ’將從當前指令指標中列印接下來的8條指令。‘ x/16i main ’將列印main的前16條指令。你也可以使用‘ disassemble main ’,或者簡稱‘ disas main ’,來列印main的所有指令。或者,‘ x/16gx $rsp ’將列印堆疊上的前16個值。‘ x/gx $rbp-0x32 ’將列印儲存在堆疊上的本地變數。
您可能希望使用正確的程式集語法檢視指令。你可以用命令“set disassembly-flavor intel”來做。
為了解決這個問題,你必須找出堆疊上的隨機值(從‘ /dev/urandom ’讀取的值)。考慮一下read系統呼叫的引數是什麼。
為了檢視堆疊上的隨機值,我們應該從棧頂指標暫存器rsp
入手,我們輸入以下命令:
x/16gx $rsp
c #繼續程序程序設定隨機值
x/16gx $rsp
複製與舊值不同的新值(上面的舊值和相應的新值大部分相同)即可得到flag
level4
GDB是一個非常強大的動態分析工具,您可以使用它來了解程式在整個執行過程中的狀態。在本模組中,您將熟悉gdb的一些功能。
動態分析的一個關鍵部分是將程式執行到您感興趣的狀態。到目前為止,這些挑戰已經為您自動設定了斷點,以便在您可能感興趣的狀態下暫停執行。自己能夠做到這一點非常重要。
在程式的執行過程中,有多種方式可以向前移動。
您可以使用`stepi <n>`命令或`si <n>`命令(簡寫)向前單步執行一條指令。
您可以使用`nexti <n>`命令或`ni <n>`命令(簡寫)向前單步執行一條指令,同時跳過任何函式呼叫。`<n>`引數是可選的,但可以讓您一次執行多個步驟。
您可以使用`finish`命令來完成當前正在執行的函式。
您可以使用`break *<address>`引數化命令在指定的地址設定斷點。
您已經使用了`continue`命令,它將繼續執行,直到程式遇到斷點。
在逐步執行程式時,您可能會發現始終顯示一些值對您很有用。有多種方法可以做到這一點。
最簡單的方法是使用`display/<n><u><f>`引數化命令,其格式與`x/<n><u><f>`引數化命令完全相同。
例如,`display/8i $rip`將始終顯示接下來的8條指令。
另一方面,`display/4gx $rsp`將始終顯示堆疊上的前4個值。
另一個選項是使用`layout regs`命令。這將GDB切換到TUI模式,並顯示所有暫存器的內容,以及附近的指令。
為了解決這個問題,您必須找出一系列將放置在堆疊上的隨機值。強烈建議您嘗試使用`stepi`、`nexti`、`break`、`continue`和`finish`的組合,以確保您對這些命令有良好的內部理解。這些命令在導航程式的執行過程中是絕對關鍵的。
為了更好地理解程式執行,我們可以使gdb進入TUI模式,顯示所有暫存器的內容以及附近的指令
layout regs
如果顯示出錯了可以
為了檢視堆疊上的隨機值,我們應該從棧頂指標暫存器rsp
入手,我們輸入以下命令:
x/16gx $rsp
ni 18 #繼續程序程序設定隨機值,這一次沒有斷點處所以需要手動向前單步執行命令,這裡執行了17次才設定了隨機變數,ni 18指向前單步執行到第18條指令(包括當前)
x/16gx $rsp
複製與舊值不同的新值(上面的舊值和相應的新值大部分相同)會再次設定新的隨機值並且跳轉到下一個輸入隨機值的地方
(未完待續……)