ChCore-lab0

木木ちゃん發表於2024-11-09

Lab 0: 爆弾!!!

這是OS作業系統的前序實驗。。。
(p.s. 怎麼一上來就讓人這麼累。。。)

1. 準備實驗環境

基於Ubuntu 22.04.2 ARM64.
首先,我們將我們的學號填入到student-number.txt中。

其次,在linux上安裝所需要的東西。如果沒有gcc,g++(編譯器),gdb(偵錯程式)則首先執行:

sudo apt-get install -y build-essential
sudo apt-get install -y gcc
sudo apt-get install -y gdb
sudo apt-get install -y g++
sudo apt-get install -y make

其中-y是預設做出yes選擇。如果出現資源佔用,最簡單粗暴的方法就是重啟我們的linux。(doge)
安裝完成後我們檢查一下我們的編譯器。

which命令用於發現編譯器藏在哪裡,剩下的version就是展示我們的版本號。

接下來,我們在終端中輸入

make bomb

這樣就會收穫一個獨屬於自己的專屬炸彈(真好。。。)

介於我的ubuntu本身就是aarch64架構,因此完全不用擔心gdb用不了。這裡還是基於x86/64來進行使用。其實只需要再安裝qemu和gdb-multiarch即可。
輸入:

sudo apt-get install -y qemu
sudo apt-get install -y gdb-multiarch

接下來我們需要開啟兩個終端,分別執行:

make qemu-gdb
make gdb

出現這樣的畫面:

第一個終端:

第二個終端:

就可以準備開始實驗了。

2. 実験が始める!

開始我們的實驗前,我們需要了解一些基本的彙編級別除錯。

  1. disassemble:顯示目前正在執行的函式/模組的機器級原始碼。
  2. nexti:機器級原始碼層面向下執行1步。nexti 5 則代表向下執行5步。但是不會進入新的函式/模組,會繼續在當前模組向下執行。
  3. stepi:機器級原始碼層面向下執行1步。stepi 5 則代表向下執行5步。遇到呼叫新的函式/模組是會進入該模組。
  4. p (char) $x3:列印x3暫存器的值。也可以用p/c $x3來代替p (char).
  5. x $x3:顯示x3所存地址指向的記憶體空間裡的值。x/s 為列印字串, x/c為列印字元。
  6. info register/breakpoints:顯示當前所有暫存器/斷點的情況。有助於我們判斷如何讀取暫存器和管理斷點。
  7. continue/c:繼續執行一直到遇到斷點停止或程式退出。
  8. break main:為main函式打上斷點。沒有符號名時,採用 break *0x400a00來為具體的機器級原始碼打上斷點。

2.1 phase_0

首先我們先給main函式打上斷點。然後執行到main處顯示機器級原始碼。

break main
c
disassemble

我們可以看到main裡的機器級原始碼了!!!

從中我們可以發現我們有6個炸彈需要拆除。我們首先在第一個階段上打上斷點,這樣在拆除炸彈時就可以跳轉到入口處。

在另一個終端上隨便輸入一些東西。我們假設輸入1,然後我們stepi進入到第一個階段的炸彈拆除中。

前兩行函式其實就是正常的入棧操作,將存有呼叫入口的地址和傳入引數分別入棧後,再將棧頂的值存入暫存器中,有助於遞迴回撥函式。

觀察+12到+24處的函式,我們可以發現,函式從我們的輸入中讀入一個int型整數,並將其傳給w0暫存器。w1從一個我們無法看到的記憶體空間(0x4a0084)中讀取了一個數字,並與我們的輸入進行比較。一旦有所不同,我們的炸彈將立刻被引爆。所以在讀入數字後,我們需要檢視w1裡存了什麼。

我們可以看到答案是2022. 一旦炸彈爆炸,我們需要重啟make qemu-gdb 和 make gdb。但是我們可以將答案存入ans.txt中,採用以下命令輸入答案。

make qemu-gdb < ans.txt

這樣就不需要每次用手輸入新的答案。

2.2 phase_1

很明顯它已經暴露了。看到了strcmp就說明我們需要進行字串的比較。

答案已經顯而易見。

2.3 phase_2

打好斷點,我們來看phase_2.

標籤首先提示我們需要讀入8個數。第一個數儲存於棧頂sp+0x20(32)處,一個int型數佔4byte。因此接下來+20到+36就是讀取我們輸入的第一個和第二個數,並且第一和第二個數必須為1.

接下來後面的行為就是:讀取最後一個數和前一個數,新的數相當於這兩個數的和再加上4.很像fibonacci數列。

0x4007b8 <+48>:    add     x19, sp, #0x20 		// 儲存第一個數的地址
0x4007bc <+52>:    add     x20, sp, #0x38 		// 儲存最後一個數的地址
0x4007c0 <+56>:    b       0x4007d0 <phase_2+72> 	// 跳轉。
0x4007c4 <+60>:    add     x19, x19, #0x4 		// 將指標移到第二個數。
0x4007c8 <+64>:    cmp     x19, x20 			// 檢查是否到了結尾,如果結尾則返回。
0x4007cc <+68>:    b.eq    0x4007f4 <phase_2+108>  
0x4007d0 <+72>:    ldr     w0, [x19] 			// 讀入第一個數的內容。
0x4007d4 <+76>:    ldr     w1, [x19, #4] 		// 讀入第二個數的內容。
0x4007d8 <+80>:    add     w0, w0, w1 		// 兩數相加
0x4007dc <+84>:    add     w0, w0, #0x4 		// 再加4
0x4007e0 <+88>:    ldr     w1, [x19, #8] 
0x4007e4 <+92>:    cmp     w1, w0 			// 比較第三個數是不是按照前面規則得到的數字。
0x4007e8 <+96>:    b.eq    0x4007c4 <phase_2+60>  
0x4007ec <+100>:   bl      0x400af4 <explode>
0x4007f0 <+104>:   b       0x4007c4 <phase_2+60>
0x4007f4 <+108>:   ldp     x19, x20, [sp, #16] 
0x4007f8 <+112>:   ldp     x29, x30, [sp], #64
0x4007fc <+116>:   ret

這樣我們需要填入的數就是
1 1 6 11 21 36 61 101

phase_3

難度有些上升了,不過不怕,慢慢來看。
(真長啊,以前的人是怎麼用匯編寫遊戲的。。。)

這個__isoc99_sscanf函式用於識別引數個數並將輸入的數壓入棧中。我們需要清醒地認識到,入棧時,棧頂指標的地址將減小。因此先壓入第一個引數,再壓入第二個引數,則第二個引數的地址要小於第一個引數的地址。在這裡的機器語言中,我們可以發現[$sp+28]為第一個引數的位置,[$sp+24]是第二個引數的位置。

從+36行開始讀入第一個數,我們可以從後面看見,讀入的數可以有3種選擇。2,3,4均可。否則我們就會粉身碎骨。

我們首先來看首位數字是2的情況。

0x400844 <+68>:    ldp     x29, x30, [sp], #32
0x400848 <+72>:    ret
...
0x400884 <+132>:   ldr     w0, [sp, #24]		//讀入第二個數字,第一個數字存在sp+28處
0x400888 <+136>:   eor     w0, w0, w0, asr #3		//將w0進行算術右移3位後,加上w0,並與自己進行異或,存入w0。
0x40088c <+140>:   and     w0, w0, #0x7		//取消高位1,即將其處理為不大於7的正數。
0x400890 <+144>:   ldr     w1, [sp, #28]		// 將得到的數與第一個輸入的數字比較,相等則成功拆彈。
0x400894 <+148>:   cmp     w0, w1
0x400898 <+152>:   b.eq    0x400844 <phase_3+68>
0x40089c <+156>:   bl      0x400af4 <explode>

這樣我們很明顯可以發現,我們有多個答案。這裡我們給出一個最簡單的答案:2 2.這樣在+132處得到的w0即為2,與w1相同。

接下來來看首位數字是3的情況。

0x400844 <+68>:    ldp     x29, x30, [sp], #32
0x400848 <+72>:    ret
...
0x400854 <+84>:    ldr     w2, [sp, #24]
0x400858 <+88>:    mov     w0, #0x6667		// #26215存入w0中
0x40085c <+92>:    movk    w0, #0x6666, lsl #16 	// w0=0x66666667.
0x400860 <+96>:    smull   x0, w2, w0			// w2與w0相乘,存入x0。
0x400864 <+100>:   asr     x0, x0, #34		// 將x0算術右移34位。
0x400868 <+104>:   sub     w0, w0, w2, asr #31	// w0=w0-w2算術右移31位。
0x40086c <+108>:   add     w1, w0, w0, lsl #2		// w1=w0+w0邏輯左移兩位
0x400870 <+112>:   sub     w1, w2, w1, lsl #1		// w2-w1邏輯左移兩位
0x400874 <+116>:   add     w0, w1, w0			// w0=w1+w0,並比較w0是否為3.
0x400878 <+120>:   cmp     w0, #0x3
0x40087c <+124>:   b.eq    0x400844 <phase_3+68>
0x400880 <+128>:   bl      0x400af4 <explode>

很明顯最簡單的答案為3 3.

最後是首位數字為4:

0x400884 <+132>:   ldr     w0, [sp, #24]
0x400888 <+136>:   eor     w0, w0, w0, asr #3
0x40088c <+140>:   and     w0, w0, #0x7
0x400890 <+144>:   ldr     w1, [sp, #28]
0x400894 <+148>:   cmp     w0, w1
0x400898 <+152>:   b.eq    0x400844 <phase_3+68> 
0x40089c <+156>:   bl      0x400af4 <explode>

和之前的套路類似,我們就不在贅述了。最簡單的答案為4 4.

phase_4

壞了,看到了encrypt,還進行了兩段加密。我們慢慢來解開其到底是如何進行加密的。
首先我們先隨便輸入一串文字:ABCDEFGHIJK。

執行到+16處,我們觀察一下x0(也就是w0,只是w0為x0的低32位)是什麼。可以看到,在+12處我們將x0指向的字串的地址複製給了x19。現在x19指向的是我們剛剛輸入的字串。

執行到+24處,我們觀察一下存入x20的是什麼,也就是x0發生了什麼變化。

可以看到,x0=11,也就是我們輸入的字串的長度。那麼剛剛在+16呼叫的函式就是統計輸入字串的長度,並且其長度不能大於10.一旦大於10將粉身碎骨。

於是我們將輸入改成ABCDEFGHIJ。

x20存入的是我們的字串長度,x19指向我們輸入字串的第一個地址。這樣我們將這兩個作為引數傳入到encrypt_method1當中。
step進入其中:

0x4008e8 <+32>:    mov     x4, x2
0x4008ec <+36>:    mov     x2, #0x0        
//進入迴圈
0x4008f0 <+40>:    lsl     x5, x2, #1
0x4008f4 <+44>:    ldrb    w5, [x0, x5]
0x4008f8 <+48>:    strb    w5, [x4], #1
0x4008fc <+52>:    add     x2, x2, #0x1
0x400900 <+56>:    cmp     w3, w2
0x400904 <+60>:    b.gt    0x4008f0 <encrypt_method1+40>

這個迴圈就是提取字串中的奇數位的字元,並將其儲存在一塊記憶體中。

0x400918 <+80>:    sub     w4, w1, w5
0x40091c <+84>:    sub     w2, w5, w3
0x400920 <+88>:    add     x2, x0, w2, sxtw #1
0x400924 <+92>:    add     x2, x2, #0x1
0x400928 <+96>:    mov     x1, #0x0                       
0x40092c <+100>:   add     x3, sp, #0x10
0x400930 <+104>:   add     x5, x3, w5, sxtw
0x400934 <+108>:   lsl     x3, x1, #1
0x400938 <+112>:   ldrb    w3, [x2, x3]
0x40093c <+116>:   strb    w3, [x5, x1]
0x400940 <+120>:   add     x1, x1, #0x1
0x400944 <+124>:   cmp     x1, x4
0x400948 <+128>:   b.ne    0x400934 <encrypt_method1+108> 
0x40094c <+132>:   add     x1, sp, #0x10
0x400950 <+136>:   bl      0x421cc0 <strcpy>

完成這一部分之後,我們的字串將會重新變成奇數位拼接偶數位,即ACEGIBDFHJ。
在encrypt_method2打上斷點,continue跑步進入encrypted_method2!


前面從+0到+40全部都是讀入資料。在讀入資料完成後,我們需要關注的是$x19,$x20,$x21,$x22這四個暫存器的值,因為他們是除了幀指標x29和棧指標x30以外儲存了有關x0和x1兩個引數的相關指標。

我們可以看到,x19指向我們前面處理過的字串,x20儲存了字串的長度資料。x21和x22目前我們還不知道是用來做什麼的,但是彆著急,我們接著往下。

我們發現這裡進行了一些操作,首先將x19存有的指向字串的首地址存於x20,然後獲取首個字元,檢查該字元ascii是否大於0x61(97).那麼我們透過man ascii可以看到:

必須是小寫字母即之後的一些符號,炸了。😦
那我們重新再來,輸入abcdefghij試一試。

透過像486一樣的能力,我們再一次回到了被炸死的地方。這一次,我們成功透過,並來到了+44處。

0x400990 <+44>:    ldrb    w1, [x20]
0x400994 <+48>:    ldr     x0, [x22, #8]
0x400998 <+52>:    add     x0, x0, x1
0x40099c <+56>:    ldurb   w0, [x0, #-97]
0x4009a0 <+60>:    strb    w0, [x20]
0x4009a4 <+64>:    add     x19, x19, #0x1
0x4009a8 <+68>:    cmp     x19, x21

我們來觀察一下這段。首先,我們從x20處載入一個位元組,也就是一個字元。接著,我們載入了前面神秘的x22+8處記憶體的內容到x0。這到底是什麼呢?

可以看見這是一個字串!所以此時x0儲存的是這個字串的地址。那麼接下來的三行就很明顯了,我們建立了一個如下的對映:
新字元=原字元+x0字串-97.用人話來講就是:

原字元:abcdefghijklmnopqrstuvwxyz
新字元:qwertyuiopasdfghjklzxcvbnm

回到phase_4.觀察到在進入strcmp之前,x1獲得了某個神秘的地址:

我們可以看到加密後的密文:

經過我們剛剛發現的加密過程,逆向思考可以得到:

isggstsvke
hloolelwrc
helloworlc

因此我們的答案即為:helloworlc.

phase_5

終於來到了最後一個實驗。老規矩,先看看這個模組裡是什麼:

短,好,喜歡。首先,提示我們要讀入一個整數。然後我們會讓x1獲得某個地址(說明該地址是某個資料結構/字串的開始,然後進入一個名為func_5的函式,最終的結果是讓x0記憶體有3這樣一個數。否則粉身碎骨。。。

接下來我們進入func_5.

從+0到+28,我們獲得神秘的資料結構的第一個內容,並將其與我們的輸入比較。如果相等,我們就功虧一簣。如果等於0,說明我們遍歷了整個資料結構。返回。

首個儲存的數是49.我們的輸入為5.因此沒有爆炸!

那麼我們就很好奇這個資料結構裡面儲存著什麼?我們需要檢視一下這部分的組合語言。我們先以十進位制列印一下x19的第一個資料,發現其位於一個叫做search_tree的資料結構中。

採用disassemble命令。我們透過試探法得知search_tree資料結構的邊緣在+168.因此我們列印:

我們可以發現有以下比較重要的部分:

0x4a0070 <search_tree+0>:  udf     #49
0x4a0088 <search_tree+24>: udf     #20
0x4a00a0 <search_tree+48>: udf     #88
0x4a00b8 <search_tree+72>: udf     #3
0x4a00d0 <search_tree+96>: udf     #37
0x4a00e8 <search_tree+120>:        udf     #55
0x4a0100 <search_tree+144>:        udf     #91

在arm彙編指令中,.inst指令可以強制干涉PC的指向,使其直接指向所需要指向的地址。

接著我們對比上面func_5中的指令:

<func_5>
...	// 檢查是否有相等,出現則爆炸,當出現讀取到0的時候返回。
0x400a74 <+40>:    cmp     w0, w20
0x400a78 <+44>:    b.le    0x400aa0 <func_5+84>
0x400a7c <+48>:    ldr     x1, [x19, #8]
0x400a80 <+52>:    mov     w0, w20
0x400a84 <+56>:    bl      0x400a4c <func_5>
0x400a88 <+60>:    lsl     w0, w0, #1
...
ret
...
0x400aa0 <+84>:    ldr     x1, [x19, #16]
0x400aa4 <+88>:    mov     w0, w20
0x400aa8 <+92>:    bl      0x400a4c <func_5>
0x400aac <+96>:    lsl     w0, w0, #1
0x400ab0 <+100>:   add     w0, w0, #0x1
...
ret

結合前面的指令我們可以發現:
當我們輸入的數大於當前節點的數時,我們尋找的是當前節點+16的節點,進入遞迴。當輸入的數小於當前節點的數時,我們尋找當前節點+8的節點,進入遞迴。當輸入等於當前節點的數時,雖然進入了+16的遞迴但是很快就會爆炸。這很像是“遍歷AVL樹”。我們可以畫出這樣一個樹:

那麼接下來我們就要思考如何才能使得w0最終等於3.我們發現,在遞迴呼叫函式後,每次返回時分別有以下兩種操作:

  1. 輸入的數字大於當前節點數字,w0=w0*2,w0=w0+1.
  2. 輸入的數字小於當前節點數字,w0=w0*2.

樹只有三層,因此我們遞迴時也只能選擇以上操作三次。我們則需要進行操作:2->1->1.由於這是函式返回時進行的操作,所以我們應該選擇數字,使得他能夠在遞迴呼叫時採取1->1->2的方式行動。這樣一來我們有(假設輸入數字為a):

  1. a>49.
  2. a>88.
  3. 88<a<91.
    所以合理的答案就應該為:89,90這兩個數中的任意一個。

拆彈結束。


Edited by mumujun12345. 20240918
勿忘國恥。