CTF Challenge - ARM Basic Crackme
说明
这是一个arm32的程序,ELF ARM - Basic Crackme
你可以直接从这下载程序
分析
看向流程图,第一个块我一开始以为没什么好看的,后来越发觉得这里面做了很多工作,忽略这个块就会搞得你看后面的一头雾水。
就是获取argc的长度,看是不是小于2,小于2说明没有输入password,直接puts信息提示输入。
一行一行的分析,这里我会用到画图的方式来更好的理解堆栈
1、首先是往栈中放些东西,注意这里用的不是push,因为push指令只在thumb模式中有,而这个程序是arm模式的;r11是栈帧指针,lr是链接寄存器,具体可以看我之前的文章 ARM数据类型;这条指令首先会减少sp的值,也就是扩大堆栈的空间,然后将r4,r11,r14寄存器以
从右往左的顺序
放到分配好的栈里(LR -> R11(FP) -> R4)
1 | STMFD SP!, {R4,R11,LR} |
堆栈情况如图
2、这条指令就是改变了下fp的位置,即指向刚刚放入堆栈的LR寄存器(记住一个寄存器32位,也就是32/8=4个字节
)
1 | ADD R11, SP, #8 |
3、我们都知道arm有个函数调用约定 (ARM数据类型),就是r0-r3用作函数的前4个参数,这里用到的是r0和r1;在arm中,有30个32位的通用寄存器,这意味着每个寄存器的大小都是4个字节;
这条指令的主要作用是分配28个字节的栈空间来存放局部变量
1 | SUB SP, SP, #0x1C |
所以此时的堆栈应该是这样的
4、分配了栈空间,那肯定要放东西了;这条指令就是在r11 + (-0x20)
也就是0xbefff13c + (-0x20) = 0xbefff11c
处存放参数1,这个地址是在sp指针的下面那个栈
1 | STR R0, [R11,#var_20] |
(因为这里改变不大,我就懒得上堆栈图了,结合下面的几条指令一起上图)
这条指令是把R1(参数2)存放到r11 + (-0x24) = 0xbefff118
也就是栈顶SP处
1 | STR R1, [R11,#var_24] |
然后分别存放0x6、0x0到0xbefff12c、0xbefff128处
1 | MOV R3, #6 |
假设我们什么也没输入,此时R0=1,如果要问为什么等于1,那你需要去补充下C语言的知识,因为main方法的第一个参数是第二参数的长度,第二个参数是从命令行接收的用户输入;结合上面的多条指令,所以此时堆栈应该长这样
5、接着就是取出参数1,然后比较是否等于2,如果等于就跳转到0x84B0,否则就输入loser…,然后exit 并传个1
1 | LDR R3, [R11,#var_20] |
直接看true执行的块
来一行一行分析
首先是从
fp+0x24
处获取数据到r3寄存器,这个地址存的是参数2,自己看上面的代码就知道了
1 | LDR R3, [R11,#var_24] |
然后是从
fp+0x24+0x4
处获取数据到r3寄存器,这个地址是我们在命令行运行该程序输入的内容;因为r3的地址就是参数2,再因为这是32位的程序,char*的大小为4个字节,所以偏移量是+0x4
1 | LDR R3, [R3,#4] |
接着看,这行的意思是把r3又存到
fp+0x18
地址处
1 | STR R3, [R11,#s] |
然后是加载格式化字符串,放在r3寄存器
1 | LDR R3, =aCheckingSForPa |
然后又移动到r0寄存器来用作printf的第一个参数
1 | MOV R0, R3 |
接着取出输入的内容,放在r1寄存器用作printf的第二参数
1 | LDR R1, [R11,#s] |
然后就是调用printf
1 | BL printf |
接着又从
fp+0x18
取出输入内容,因为刚刚调用了printf
1 | LDR R0, [R11,#s] |
然后调用strlen,参数是r0,获取输入内容的长度并把长度放在r3寄存器,记住r0在每个方法执行结束后就是该方法的返回值
1 | BL strlen |
然后把长度r3存到
fp+0x1c
地址,接着又取出来放在r3寄存器。。。
1 | STR R3, [R11,#status] |
然后判断长度是否小于6,如果小于就跳转执行puts输出”Loser…” (受到了作者的嘲讽
1 | CMP R3, #6 |
接着看 fasle 块
还是一行一行分析
我们知道r11至始至终都没变过,所以这里还是取
0xbefff13c+(-0x10)=0xbefff12c
处的数据,这个地址在第一个块也就是我们最开始分析的时候存过一个数据,那就是0x6,方便更好的理解,我把上面的堆栈图再上一次
1 | LDR R4, [R11,#var_10] |
然后又是获取输入的字符串,并获取它的长度放在R3
1 | LDR R0, [R11,#s] |
紧接着这里有个反转减法运算操作,反转减法就是把两个要算的数调换位置算,比如这条指令就是
r3 = r4 - r3
,这里开始我们需要仔细分析,因为这里到了算法部分,这的反转减法是用6 - 字符串长度
1 | RSB R3, R3, R4 |
接着把运算结果存到刚刚取出0x6的地址
1 | STR R3, [R11,#var_10] |
然后又把输入的字符串取出来,我先在这说明一下,这里
r11+#s
的地址的数据还是一个地址,在这个地址中的数据才是输入的字符串,我也觉得绕,所以我有个办法能帮助我们更好理解
1 | LDR R3, [R11,#s] |
首先写个仿照这部分简单写个程序,关于字符串定义啥玩意儿的可以看这里String definition directives
1 | .global main ;程序入口 |
编译后用十六进制打开差不多是这样,我这里只获取了代码和字符串部分,其他的比如格式啥的就不看了;这里字符串存储用的小端序。
当程序运行,pc会指向0x00000000,也就是第一条指令mov r1,#10
的位置,然后直到0xc处,这里其实写成这样ldr r3, [pc, #16]
,就是当pc指向这条指令,那么就直接用pc的地址偏移16个字节,直接到字符串的地址,这里的r3的内容就是0x00000001c;接着就是ldrb
,这个指令和ldr的最大区别就是它只获取8个字节的数据,比如这里就是获取的r3的地址所指向的字符串的8个字节数据,也就是0x31;然后这里的ldr可以这样写ldr r3, [pc,#8]
,接着就是r3在原地址上偏移0x5,然后再用ldrb获取8个字节的数据,那就是最后一个字符 “3” (0x33)
回到crackme,接下来是ldrb指令,这里就是获取第一个字符
1 | LDRB R2, [R3] |
然后是获取最后一个字符
1 | LDR R3, [R11,#s] |
接着就是比较,用cmp来使两个字符的ascii码相减,根据结果改变cspr flag,beq是根据z flag来决定是否跳转的,z flag的条件是两个数相减为0时置1,也就是相等就跳转
1 | CMP R2, R3 |
来看看如果相等会怎样;首先从
r11+(-10)
地址处取数据,这个地址存的是之前6-字符串长度
的结果,然后自增1,接着存到原始地址;所以这里也就是自增1的作用
1 | LDR R3, [R11,#var_10] |
从下图可以看出每个块的最后都有一次比较来看,这里应该是多个if else,然后再自增一个int变量,暂时不清楚这个变量有什么用。
直接看下一个判断块
1 | LDR R3, [R11,#s] ;取字符串 |
然后看第三个判断块
1 | LDR R3, [R11,#s] ;取字符串 |
第四个判断块
1 | LDR R3, [R11,#s] ;取字符串 |
if else块到这就完了,根据上面的逻辑我写了个c代码
1 |
|
然后分析最后一个判断块
1 | LDR R3, [R11,#s] ;取字符串 |
现在可以明确我们需要最后的r3等于0,而r3=与或运算+0,所以我们需要之前的与或运算结果为0;
首先是字符串的第四个字符与0x72(‘r’)异或,然后是与0xff逻辑与
逻辑与的运算是两个数的位为1结果才为1,那么我们就要两个数的没位都为0就行了,而能达成这个条件的的只有数字0;
而这里的逻辑与运算的第二个数0xff我们没法让它变化,所以从逻辑异或中入手
想要异或的结果为0,那么就需要两个数相等,也就是说字符串中的第四个字符串为r
然后后面还有一次运算,这个结果会与之前自增的那个数相加,所以我们还需要让那个自增数为0,也就是那些判断都为false
为了方便分析,我更新了下c代码
1 |
|
emm…写了两种
1 |
|
已知password长度为6,第四个字符为0x72,然后来分析判断,初始化
pass = ['', '', '', '0x72', '', '']
从已知的地方开始,第四个字符+1等于第一个字符,那么
pass = ['0x73', '', '', '0x72', '', '']
接着第一个字符等于第六个字符,pass = ['0x73', '', '', '0x72', '', '0x73']
然后第一个字符+1等于第二个字符,pass = ['0x73', '0x74', '', '0x72', '', '0x73']
第三个字符+4等于第六个字符,pass = ['0x73', '0x74', '', '0x72', '0x67', '0x73']
最后第五个字符+2等于第三个字符,pass = ['0x73', '0x74', '0x6f', '0x72', '0x6d', '0x73']
所以结果为
storms
1 | ''.join([chr(int(i,16)) for i in ['0x73', '0x74', '0x6f', '0x72', '0x6d', '0x73']]) # storms |
最后
期间查的资料以及所用的到网页工具:
arm_instruction_set_reference_guide_100076_0100_00_en
ARM数据类型
armclang_reference_guide_100067_0611_00_en
armconverter
onlinedisassembler