
說明
這是一個(gè)arm32的程序,ELF ARM - Basic Crackme
你可以直接從這下載程序
分析
看向流程圖,第一個(gè)塊我一開始以為沒什么好看的,后來越發(fā)覺得這里面做了很多工作,忽略這個(gè)塊就會(huì)搞得你看后面的一頭霧水。
就是獲取argc的長度,看是不是小于2,小于2說明沒有輸入password,直接puts信息提示輸入。

一行一行的分析,這里我會(huì)用到畫圖的方式來更好的理解堆棧
1、首先是往棧中放些東西,注意這里用的不是push,因?yàn)閜ush指令只在thumb模式中有,而這個(gè)程序是arm模式的;r11是棧幀指針,lr是鏈接寄存器,具體可以看我之前的文章 ARM數(shù)據(jù)類型;這條指令首先會(huì)減少sp的值,也就是擴(kuò)大堆棧的空間,然后將r4,r11,r14寄存器以
從右往左的順序放到分配好的棧里(LR -> R11(FP) -> R4)
STMFD SP!, {R4,R11,LR}
堆棧情況如圖
2、這條指令就是改變了下fp的位置,即指向剛剛放入堆棧的LR寄存器(記住一個(gè)寄存器32位,也就是32/8=4個(gè)字節(jié))
ADD R11, SP, #8
3、我們都知道arm有個(gè)函數(shù)調(diào)用約定 (ARM數(shù)據(jù)類型),就是r0-r3用作函數(shù)的前4個(gè)參數(shù),這里用到的是r0和r1;在arm中,有30個(gè)32位的通用寄存器,這意味著每個(gè)寄存器的大小都是4個(gè)字節(jié);
這條指令的主要作用是分配28個(gè)字節(jié)的棧空間來存放局部變量
SUB SP, SP, #0x1C
所以此時(shí)的堆棧應(yīng)該是這樣的
4、分配了??臻g,那肯定要放東西了;這條指令就是在r11 + (-0x20)也就是0xbefff13c + (-0x20) = 0xbefff11c處存放參數(shù)1,這個(gè)地址是在sp指針的下面那個(gè)棧
STR R0, [R11,#var_20]
(因?yàn)檫@里改變不大,我就懶得上堆棧圖了,結(jié)合下面的幾條指令一起上圖)
這條指令是把R1(參數(shù)2)存放到r11 + (-0x24) = 0xbefff118也就是棧頂SP處
STR R1, [R11,#var_24]
然后分別存放0x6、0x0到0xbefff12c、0xbefff128處
MOV R3, #6
STR R3, [R11,#var_10]
MOV R3, #0
STR R3, [R11,#var_14]
假設(shè)我們什么也沒輸入,此時(shí)R0=1,如果要問為什么等于1,那你需要去補(bǔ)充下C語言的知識(shí),因?yàn)閙ain方法的第一個(gè)參數(shù)是第二參數(shù)的長度,第二個(gè)參數(shù)是從命令行接收的用戶輸入;結(jié)合上面的多條指令,所以此時(shí)堆棧應(yīng)該長這樣
5、接著就是取出參數(shù)1,然后比較是否等于2,如果等于就跳轉(zhuǎn)到0x84B0,否則就輸入loser...,然后exit 并傳個(gè)1
LDR R3, [R11,#var_20]
CMP R3, #2
BEQ loc_84B0
直接看true執(zhí)行的塊

來一行一行分析
首先是從
fp+0x24處獲取數(shù)據(jù)到r3寄存器,這個(gè)地址存的是參數(shù)2,自己看上面的代碼就知道了
LDR R3, [R11,#var_24]
然后是從
fp+0x24+0x4處獲取數(shù)據(jù)到r3寄存器,這個(gè)地址是我們?cè)诿钚羞\(yùn)行該程序輸入的內(nèi)容;因?yàn)閞3的地址就是參數(shù)2,再因?yàn)檫@是32位的程序,char*的大小為4個(gè)字節(jié),所以偏移量是+0x4
LDR R3, [R3,#4]
接著看,這行的意思是把r3又存到
fp+0x18地址處
STR R3, [R11,#s]
然后是加載格式化字符串,放在r3寄存器
LDR R3, =aCheckingSForPa
然后又移動(dòng)到r0寄存器來用作printf的第一個(gè)參數(shù)
MOV R0, R3
接著取出輸入的內(nèi)容,放在r1寄存器用作printf的第二參數(shù)
LDR R1, [R11,#s]
然后就是調(diào)用printf
BL printf
接著又從
fp+0x18取出輸入內(nèi)容,因?yàn)閯倓傉{(diào)用了printf
LDR R0, [R11,#s]
然后調(diào)用strlen,參數(shù)是r0,獲取輸入內(nèi)容的長度并把長度放在r3寄存器,記住r0在每個(gè)方法執(zhí)行結(jié)束后就是該方法的返回值
BL strlen
MOV R3, R0
然后把長度r3存到
fp+0x1c地址,接著又取出來放在r3寄存器。。。
STR R3, [R11,#status]
LDR R3, [R11,#status]
然后判斷長度是否小于6,如果小于就跳轉(zhuǎn)執(zhí)行puts輸出"Loser..." (受到了作者的嘲諷
CMP R3, #6
BEQ loc_84F8
接著看 fasle 塊

還是一行一行分析
我們知道r11至始至終都沒變過,所以這里還是取
0xbefff13c+(-0x10)=0xbefff12c處的數(shù)據(jù),這個(gè)地址在第一個(gè)塊也就是我們最開始分析的時(shí)候存過一個(gè)數(shù)據(jù),那就是0x6,方便更好的理解,我把上面的堆棧圖再上一次
LDR R4, [R11,#var_10]
然后又是獲取輸入的字符串,并獲取它的長度放在R3
LDR R0, [R11,#s]
BL strlen
MOV R3, R0
緊接著這里有個(gè)反轉(zhuǎn)減法運(yùn)算操作,反轉(zhuǎn)減法就是把兩個(gè)要算的數(shù)調(diào)換位置算,比如這條指令就是
r3 = r4 - r3,這里開始我們需要仔細(xì)分析,因?yàn)檫@里到了算法部分,這的反轉(zhuǎn)減法是用6 - 字符串長度
RSB R3, R3, R4
接著把運(yùn)算結(jié)果存到剛剛?cè)〕?x6的地址
STR R3, [R11,#var_10]
然后又把輸入的字符串取出來,我先在這說明一下,這里
r11+#s的地址的數(shù)據(jù)還是一個(gè)地址,在這個(gè)地址中的數(shù)據(jù)才是輸入的字符串,我也覺得繞,所以我有個(gè)辦法能幫助我們更好理解
LDR R3, [R11,#s]
首先寫個(gè)仿照這部分簡單寫個(gè)程序,關(guān)于字符串定義啥玩意兒的可以看這里String definition directives
.global main ;程序入口
main:
0x00000000 MOV R1,#0xa ; E3A0100A
0x00000004 MOV R2,#5 ; E30A2005
0x00000008 ADD R3,R1,R2 ; E0813002
0x0000000c LDR R3,=pass ; E59F3008
0x00000010 LDRB R2,[R3] ; E5D32000
0x00000014 LDR R3,=pass ; E51F3000
0x00000018 ADD R3,R3,#5 ; E2833005
0x0000001c LDRB R3, [R3] ; E5D33000
.text
0x00000001c pass: .asciz "122923" ; 39323231
編譯后用十六進(jìn)制打開差不多是這樣,我這里只獲取了代碼和字符串部分,其他的比如格式啥的就不看了;這里字符串存儲(chǔ)用的小端序。
當(dāng)程序運(yùn)行,pc會(huì)指向0x00000000,也就是第一條指令mov r1,#10的位置,然后直到0xc處,這里其實(shí)寫成這樣ldr r3, [pc, #16],就是當(dāng)pc指向這條指令,那么就直接用pc的地址偏移16個(gè)字節(jié),直接到字符串的地址,這里的r3的內(nèi)容就是0x00000001c;接著就是ldrb,這個(gè)指令和ldr的最大區(qū)別就是它只獲取8個(gè)字節(jié)的數(shù)據(jù),比如這里就是獲取的r3的地址所指向的字符串的8個(gè)字節(jié)數(shù)據(jù),也就是0x31;然后這里的ldr可以這樣寫ldr r3, [pc,#8],接著就是r3在原地址上偏移0x5,然后再用ldrb獲取8個(gè)字節(jié)的數(shù)據(jù),那就是最后一個(gè)字符 “3” (0x33)
回到crackme,接下來是ldrb指令,這里就是獲取第一個(gè)字符
LDRB R2, [R3]
然后是獲取最后一個(gè)字符
LDR R3, [R11,#s]
ADD R3, R3, #5
LDRB R3, [R3]
接著就是比較,用cmp來使兩個(gè)字符的ascii碼相減,根據(jù)結(jié)果改變cspr flag,beq是根據(jù)z flag來決定是否跳轉(zhuǎn)的,z flag的條件是兩個(gè)數(shù)相減為0時(shí)置1,也就是相等就跳轉(zhuǎn)
CMP R2, R3
BEQ loc_8538
來看看如果相等會(huì)怎樣;首先從
r11+(-10)地址處取數(shù)據(jù),這個(gè)地址存的是之前6-字符串長度的結(jié)果,然后自增1,接著存到原始地址;所以這里也就是自增1的作用
LDR R3, [R11,#var_10]
ADD R3, R3, #1
STR R3, [R11,#var_10]
從下圖可以看出每個(gè)塊的最后都有一次比較來看,這里應(yīng)該是多個(gè)if else,然后再自增一個(gè)int變量,暫時(shí)不清楚這個(gè)變量有什么用。
直接看下一個(gè)判斷塊
LDR R3, [R11,#s] ;取字符串
LDRB R3, [R3] ;獲取第一個(gè)字符
ADD R2, R3, #1 ;將字符ascii碼加1
LDR R3, [R11,#s] ;取字符串
ADD R3, R3, #1 ;將字符串地址偏移0x1;str: 0x0 34 33 32 31,r3最開始在0x0,偏移一個(gè)字節(jié)就到了0x1,也就是33
LDRB R3, [R3] ;從偏移后的地址處取第一個(gè)字符
CMP R2, R3 ;然后用第一個(gè)字符的ascii+1與第二個(gè)字符的ascii比較
BEQ loc_8564 ;如果相等就自增
然后看第三個(gè)判斷塊
LDR R3, [R11,#s] ;取字符串
ADD R3, R3, #3 ;字符串地址偏移3個(gè)字節(jié),可以寫成r3[3]
LDRB R3, [R3] ;取偏移后的第一個(gè)字符
ADD R2, R3, #1 ;字符ascii+1
LDR R3, [R11,#s] ;取字符串
LDRB R3, [R3] ;取第一個(gè)字符
CMP R2, R3 ;用字符的第四個(gè)字符ascii與第一個(gè)字符ascii比較
BEQ loc_8590 ;相等就自增
第四個(gè)判斷塊
LDR R3, [R11,#s] ;取字符串
ADD R3, R3, #2 ;將字符串地址便宜2個(gè)字節(jié)
LDRB R3, [R3] ;取偏移后第一個(gè)字符
ADD R2, R3, #4 ;字符ascii+4
LDR R3, [R11,#s] ;取字符
ADD R3, R3, #5 ;字符串偏移0x5個(gè)字節(jié),就到了`輸入的`最后一個(gè)字符,要知道字符串最后一個(gè)字符是`\0`
LDRB R3, [R3] ;取偏移后第一個(gè)字符
CMP R2, R3 ;用輸入的字符串中的第三個(gè)字符的ascii+4與輸入的字符串中的最后一個(gè)字符的ascii比較
BEQ loc_85C0 ;相等自增
if else塊到這就完了,根據(jù)上面的邏輯我寫了個(gè)c代碼
#include <stdio.h>
#include <string.h>
int main(int args, char **argv)
{
int len = 0;
char *pass = argv[1];
if (args != 2) {
puts("Loser...");
exit(1);
}
printf("Checking %s for password...\n", pass);
len = strlen(pass)
if (len < tmp) {
puts("Loser...");
exit(len);
}
len = 6 - len;
if (pass[0] != pass[5]) len++;
if (pass[0]+1 != pass[1]) len++;
if (pass[3]+1 != pass[0]) len++;
if (pass[2]+4 != pass[5]) len++;
if (pass[4]+2 != pass[2]) len++;
}
然后分析最后一個(gè)判斷塊
LDR R3, [R11,#s] ;取字符串
ADD R3, R3, #3 ;字符串地址偏移3個(gè)字節(jié)
LDRB R3, [R3] ;取r3[3],第四個(gè)字符串
EOR R3, R3, #0x72 ;邏輯異或,就是二進(jìn)制相同的為0;這里是第四個(gè)字符ascii與0x72邏輯異或運(yùn)算
AND R3, R3, #0xFF ;邏輯與,和異或相反;這里是將異或后的內(nèi)容與0xff邏輯與運(yùn)算
LDR R2, [R11,#var_10] ;應(yīng)該沒忘記,這個(gè)地址存的是那個(gè)自增的數(shù)
ADD R3, R2, R3 ;自增的數(shù)據(jù)與運(yùn)算后的R3相加,結(jié)果還是存在r3
STR R3, [R11,#var_10] ;把結(jié)果存到自增變量的地址
LDR R3, [R11,#s] ;取字符串
ADD R3, R3, #6 ;偏移6個(gè)字節(jié)
LDRB R3, [R3] ;取最后一個(gè)字符終止符`\0`
LDR R2, [R11,#var_10] ;取運(yùn)算結(jié)果
ADD R3, R2, R3 ;相加,運(yùn)算結(jié)果+0
STR R3, [R11,#var_10] ;存
LDR R3, [R11,#var_10] ;取
CMP R3, #0 ;比較
BNE loc_8644 ;不相等跳轉(zhuǎn)輸出loser,相等輸出Success, you rocks
現(xiàn)在可以明確我們需要最后的r3等于0,而r3=與或運(yùn)算+0,所以我們需要之前的與或運(yùn)算結(jié)果為0;
首先是字符串的第四個(gè)字符與0x72('r')異或,然后是與0xff邏輯與
邏輯與的運(yùn)算是兩個(gè)數(shù)的位為1結(jié)果才為1,那么我們就要兩個(gè)數(shù)的沒位都為0就行了,而能達(dá)成這個(gè)條件的的只有數(shù)字0;
而這里的邏輯與運(yùn)算的第二個(gè)數(shù)0xff我們沒法讓它變化,所以從邏輯異或中入手
想要異或的結(jié)果為0,那么就需要兩個(gè)數(shù)相等,也就是說字符串中的第四個(gè)字符串為r
然后后面還有一次運(yùn)算,這個(gè)結(jié)果會(huì)與之前自增的那個(gè)數(shù)相加,所以我們還需要讓那個(gè)自增數(shù)為0,也就是那些判斷都為false
為了方便分析,我更新了下c代碼
#include <stdio.h>
#include <string.h>
void main(int args, char **argv)
{
int tmp;
int len = 0;
char *pass = argv[1];
if (args != 2) {
puts("Loser...");
exit(1);
}
printf("Checking %s for password...\n", pass);
len = strlen(pass);
if (len < tmp) {
puts("Loser...");
exit(len);
}
len = 6 - len;
if (pass[0] != pass[5]) len++;
if (pass[0]+1 != pass[1]) len++;
if (pass[3]+1 != pass[0]) len++;
if (pass[2]+4 != pass[5]) len++;
if (pass[4]+2 != pass[2]) len++;
tmp = ((pass[3]^0x72)&0xFF) + len + pass[6];
if (tmp) {
puts("Loser...");
exit(len);
}
puts("Success, you rocks!");
exit(0);
}
emm...寫了兩種
#include <stdio.h>
#include <string.h>
int main(int args, char **argv)
{
int len = 0;
char *pass = argv[1];
if (args != 2) {
puts("Loser...");
exit(1);
}
printf("Checking %s for password...\n", pass);
len = strlen(pass);
if (len < 6) {
puts("Loser...");
exit(len);
}
len = 6 - len;
if (pass[0] == pass[5]) {
if (pass[0]+1 == pass[1]) {
if (pass[3]+1 == pass[0]) {
if (pass[2]+4 == pass[5]) {
if (pass[4]+2 == pass[2]) {
if (pass[3] == 0x72) {
puts("Success, you rocks!");
exit(len);
}
}
}
}
}
}
puts("Loser...");
exit(len);
}
已知password長度為6,第四個(gè)字符為0x72,然后來分析判斷,初始化
pass = ['', '', '', '0x72', '', '']從已知的地方開始,第四個(gè)字符+1等于第一個(gè)字符,那么
pass = ['0x73', '', '', '0x72', '', '']
接著第一個(gè)字符等于第六個(gè)字符,pass = ['0x73', '', '', '0x72', '', '0x73']
然后第一個(gè)字符+1等于第二個(gè)字符,pass = ['0x73', '0x74', '', '0x72', '', '0x73']
第三個(gè)字符+4等于第六個(gè)字符,pass = ['0x73', '0x74', '', '0x72', '0x67', '0x73']
最后第五個(gè)字符+2等于第三個(gè)字符,pass = ['0x73', '0x74', '0x6f', '0x72', '0x6d', '0x73']
所以結(jié)果為
storms
''.join([chr(int(i,16)) for i in ['0x73', '0x74', '0x6f', '0x72', '0x6d', '0x73']]) # storms
最后
原文:CTF Challenge - ARM Basic Crackme
微信公眾號(hào):the2h0Ng
期間查的資料以及所用的到網(wǎng)頁工具:
arm_instruction_set_reference_guide_100076_0100_00_en
ARM數(shù)據(jù)類型
armclang_reference_guide_100067_0611_00_en
armconverter
onlinedisassembler







