pop pc 到底做了什么
如果想要了解更多細節(jié)臀规,請閱讀CPU手冊。簡而言之栅隐,CPU將棧頂?shù)?個字節(jié)放入pc塔嬉,接下來的一個字節(jié)放入csr,最后一個字節(jié)忽略租悄,然后將sp增加4
注意:盡管CPU支持16個段谨究,計算器只使用了2個, 所以只有lcsr和csr的最低位有意義,其他位永遠是0泣棋。 除此之外胶哲,由于內(nèi)存對齊的特性,pc寄存器的最低位永遠是0
所以潭辈,這對實現(xiàn)“編程”有什么用途呢鸯屿?
我們很容易發(fā)現(xiàn),代碼中存在大量的pop pc
(大多數(shù)函數(shù)的末尾都是pop pc
),這條指令會把棧頂?shù)?個字節(jié)以上述方式放入pc寄存器中并執(zhí)行把敢。因此寄摆,如果我們控制好棧中的數(shù)據(jù),就可以跳轉(zhuǎn)到任意位置技竟,執(zhí)行任意位置的代碼冰肴。
剩下的問題就是如何寫ROP程序了
例子
我們來看看這個hackstring(適用于991ES PLUS):
52個任意字符 cv24 M 1 - 0 cv26 X - Int cs23 0 - cv24 M 1 - 0 cv26 cs4 - A 4 0 - ! cs32 0 - 20個任意字符
注意:cv指的是單位轉(zhuǎn)換屈藐,cs指的是科學常數(shù)榔组,后面數(shù)字是對應編號熙尉。
我們先對照符號表把它轉(zhuǎn)化成16進制的形式:
?? ?? ?? ... (52個任意字符) ... ?? ?? ??
ee 54 31 ?? 30 f0 58 ?? 6a 27 30 ??
ee 54 31 ?? 30 f0 04 ?? 41 34 30 ?? 57 b6 30 ??
??的意思是這個位置的數(shù)值不重要。
這個hackstring如何運作搓扯?
首先检痰,基本溢出會導致這100字節(jié)在內(nèi)存中循環(huán)出現(xiàn)(具體原因在前幾節(jié)中)
具體地說, 地址x (8154h <= x < 8e00h) 具有hackstring中第 (x - 8154h) mod 100個字節(jié)的值。
最終锨推,根據(jù)計算铅歼,可知當關鍵的pop pc
執(zhí)行時,棧頂?shù)乃膫€字節(jié)恰好是hackstring的第53-56字節(jié)换可。
也就是說椎椰,當pop oc
被執(zhí)行后,pc=0x54EE沾鳄,csr=1慨飘,CPU執(zhí)行1:54EE處的代碼,這個地方的代碼是:
pop xr0 ; 154EE
pop pc ; 154F0
我們把類似于 1:54EE這樣位置的代碼叫做gadget译荞,這是ROP領域的一個術語瓤的,這些地方往往位于某個函數(shù)的末尾。因為函數(shù)返回之前要將開頭備份的寄存器的值還原吞歼,所以存在大量的類似于上面這樣的位置圈膏,可以實現(xiàn)在ROP中控制寄存器的值,甚至向某個內(nèi)存單元中寫入特定的值等操作篙骡。
計算器將棧頂?shù)?個字節(jié)pop到xr0中稽坤,然后將棧頂?shù)?個字節(jié)pop到pc中(注意每次從棧中彈出值都會使sp自動增加,指向后面的值),現(xiàn)在er0=0xF030 r2=0x58
糯俗,CPU又把6A 27 30 ??這四個字節(jié)放到pc寄存器中慎皱,即CPU又跳到了0:276A處執(zhí)行代碼:
st r2, [er0] ; 0276A
pop pc ; 0276C
接下來就不說了,跟前面一樣叶骨。關鍵處在這hackstring的最后8個字節(jié):
41 34 30 ?? 57 b6 30 ??
.
要理解這8個字節(jié)茫多,你得了解nX/U8函數(shù)調(diào)用的規(guī)范。
首先忽刽,對于會調(diào)用子函數(shù)的函數(shù)天揖,我們知道它們以push lr
開頭,以pop pc
結尾
這是一些函數(shù)的位置及功能(991ES PLUS)
-
0:343Eh
: 一個函數(shù), 接受一個地址er0和一個數(shù)字r2, 輸出位于地址er0處的r2行字符 -
0:B654h
: 一個函數(shù)跪帝,不接受參數(shù)也不返回值 (void f(void)
)今膊,閃爍光標,并等待用戶按下SHIFT鍵后返回伞剑。
這8個字節(jié)
41 34 30 ?? 57 b6 30 ??
按順序調(diào)用了這兩個函數(shù)
注意我提到了多行打印的函數(shù)位于0:343e斑唬,為什么我要跳轉(zhuǎn)到0:3441呢?
首先,實際pc的值其實是3440(內(nèi)存對齊),選擇41只是因為輸入方便而已。
現(xiàn)在恕刘,假設我們跳轉(zhuǎn)到了0:343E缤谎,那么CPU會先執(zhí)行push lr
,最后函數(shù)末尾執(zhí)行pop pc
時褐着,pc
的值會被設定為最開始入棧的lr坷澡,但這并不是我們想要的結果(我們無法操縱lr
的值)。我們的目的是想讓CPU繼續(xù)把棧頂?shù)闹祻棾龅?code>pc中含蓉,以便繼續(xù)控制程序流频敛。
因此,我們跳轉(zhuǎn)到push lr
后的一條指令馅扣,而不是函數(shù)開頭斟赚。這樣就可以避免函數(shù)保存lr,最后pop pc
就會繼續(xù)彈出棧頂?shù)闹档?strong>pc寄存器中差油,從而繼續(xù)控制程序流汁展。
(總結:跳到緊跟著push lr
后的那條指令大多數(shù)情況下都是正確的,但在某些情況下厌殉,也可以有別的選項)
- 如果我們跳轉(zhuǎn)到
push lr
前面一條或若干條指令食绿,那么程序往往會執(zhí)行到一些無法預料的地方,因為push lr
前面往往是另一個函數(shù)的末尾公罕,即pop pc
或rt
器紧。 - 如果我們跳轉(zhuǎn)到
push lr
指令,那么最后在pop pc
時楼眷,CPU會跳轉(zhuǎn)到開始時lr中的地址執(zhí)行塔沃,這只有在我們能夠控制寄存器lr
的內(nèi)容時才可以控制程序流欢摄。
一個類似的情況是:如果我們跳轉(zhuǎn)到一個以rt結尾的函數(shù)開頭或中間。我們也需要保證寄存器lr的值是可控的。 - 如果我們跳轉(zhuǎn)到
push lr
后的一條指令持偏,CPU會執(zhí)行完函數(shù)本身景鼠,然后彈出棧頂?shù)?個字節(jié)到pc寄存器中霞势。這是最理想的情況油额。 - 如果我們跳轉(zhuǎn)到
push lr
后的若干條指令,CPU會執(zhí)行函數(shù)體的大部分指令肮蛹,除了開頭的幾條指令(往往是備份寄存器)勺择,這往往會破壞棧平衡,因此需要仔細研究伦忠。
一般情況下我們在調(diào)用函數(shù)時只使用選項3省核,在某些情況下也會用到2和4,這些例子會另行說明昆码。
循環(huán)
唯一在ROP中實現(xiàn)循環(huán)的方法就是修改sp的值气忠,如果你在源代碼中搜索sp邻储,你會發(fā)現(xiàn)修改了sp的值而又離rt
或pop pc
足夠近的指令只有:
-
mov sp, er14
(后面往往是pop er14
和pop pc
) -
add sp, #...
(正數(shù)表示從棧中彈出相應字節(jié))
目前只有第一種用于循環(huán):
所以,為了循環(huán)我們需要:
- 保證沒有執(zhí)行一些會破壞棧的函數(shù)(幾乎所有函數(shù)都會使用到棧用作臨時存儲)
- 執(zhí)行gadget
pop er14; pop pc
然后把 A-2 放在這個gadget的后面(A就是sp的新值). - 執(zhí)行gadget
mov sp, er14; pop er14; pop pc
.
(在有函數(shù)破壞了棧的情況下旧噪,循環(huán)依然是有可能的吨娜,在后面會提及)
當最后一個gadget mov sp, er14; pop er14; pop pc
被執(zhí)行時,發(fā)生了:
-
mov sp, er14
: sp 的值變?yōu)?A - 2舌菜。 -
pop er14
: sp 增加 2 字節(jié). er14 就是那兩個字節(jié)的內(nèi)容(不重要)萌壳。 -
pop pc
: pc 的值現(xiàn)在變成了 A處的4個字節(jié)亦镶。
PS:可以認為這一串10個字節(jié)一起構成了goto A
這樣一個gadget日月。
例子:
<52個任意符號> cv24 M 1 - Fvar cv26 cv40 - Int cs23 0 - tan-1 D 0 - cs26 cv26 cs16 D 1 - cv12 = 0 - sin 2 0 - 0 cv34 Int cs23 0 - (-) cs32 0 - frac Ans ^ cs32 0 - <后面填滿100個字節(jié)>
16進制表示:
?? ... (52 bytes) ... ??
ee 54 31 ?? 46 f0 fe ?? 6a 27 30 ?? b2 44 30 ?? 40 f0
1d 44 31 ?? e2 3d 30 ?? a0 32 30 ?? 30 f8
6a 27 30 ?? 60 b6 30 ?? ae 8b 5e b6 30 ??
?? ??
只有最后10個字節(jié)實現(xiàn)了循環(huán)
60 b6 30 ?? 跳轉(zhuǎn)到 0:b660h。 從 0:b660h 到 0:b662h 的指令被執(zhí)行缤骨。
接下來爱咬,ae 8b 被彈出到 er14. (也就是0x8BAE)
5e b6 30 ?? 跳轉(zhuǎn)到 0:b65eh。與上述過程類似绊起,只不過多執(zhí)行了mov sp, er14
精拟。
含有修改棧的循環(huán)
注意:
-
pop
指令只會增加sp的值,不會修改棧本身 -
push
指令大多數(shù)時候都會修改棧的內(nèi)容虱歪,只有當被push
的值能夠確保恰好跟棧頂前的值相同的時候才不會修改棧的內(nèi)容蜂绎。(包括push lr
)
所以,要在這種情況下循環(huán)笋鄙,我們需要在之前的循環(huán)方式之前加上還原堆棧的部分
大多數(shù)時候师枣,我們通過調(diào)用strcpy
函數(shù)還原棧,在前面我們已經(jīng)知道內(nèi)存中全部是輸入?yún)^(qū)100字節(jié)的拷貝萧落,那么只要隨便把一個拷貝還原到剛才的地方就可以了践美。