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. 実験が始める!
開始我們的實驗前,我們需要了解一些基本的彙編級別除錯。
- disassemble:顯示目前正在執行的函式/模組的機器級原始碼。
- nexti:機器級原始碼層面向下執行1步。nexti 5 則代表向下執行5步。但是不會進入新的函式/模組,會繼續在當前模組向下執行。
- stepi:機器級原始碼層面向下執行1步。stepi 5 則代表向下執行5步。遇到呼叫新的函式/模組是會進入該模組。
- p (char) $x3:列印x3暫存器的值。也可以用p/c $x3來代替p (char).
- x $x3:顯示x3所存地址指向的記憶體空間裡的值。x/s 為列印字串, x/c為列印字元。
- info register/breakpoints:顯示當前所有暫存器/斷點的情況。有助於我們判斷如何讀取暫存器和管理斷點。
- continue/c:繼續執行一直到遇到斷點停止或程式退出。
- 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.我們發現,在遞迴呼叫函式後,每次返回時分別有以下兩種操作:
- 輸入的數字大於當前節點數字,w0=w0*2,w0=w0+1.
- 輸入的數字小於當前節點數字,w0=w0*2.
樹只有三層,因此我們遞迴時也只能選擇以上操作三次。我們則需要進行操作:2->1->1.由於這是函式返回時進行的操作,所以我們應該選擇數字,使得他能夠在遞迴呼叫時採取1->1->2的方式行動。這樣一來我們有(假設輸入數字為a):
- a>49.
- a>88.
- 88<a<91.
所以合理的答案就應該為:89,90這兩個數中的任意一個。
拆彈結束。
Edited by mumujun12345. 20240918
勿忘國恥。