ARM汇编语言,PPC破解基础。
ARM汇编语言(1)大家知道平常我们用的windows程序是XX,修改字体等都是通过反汇编,调试等手段来实现的,常用的工具有Ollydbg,SoftICE,IDAPro等,其中又分静态分析跟动态调试,不过对于PPC的分析,动态就甭指望了,因为调试要求至少程序能运行起来,PPC程序基于ARM,在PC上根本没法运行,所以……(除非有在PPC上运行的调试器,除非调试器自带虚拟机)
只能静态分析了,现在就我所知,IDAPro是能反汇编ARM程序的,EVC也有这能力(甚至能调试,不过这家伙在我的机器上调试起来就像蜗牛……)所以跟专业的IDAPro是更好的选择。其他我暂时还没发现有能反汇编ARM程序的软件。IDApro支持的程序种类非常多。有兴趣可以看看IDA的支持列表,IDA的反汇编速度非常慢,不过这似乎不是问题,因为PPC程序也都非常小:)
具体的ARM指令集,自己去看手册,有中文的,我也不熟,毕竟我以前搞的是x86汇编,跟ARM指令集没什么相似之处的,所以以前的经验就一切索那浮云了,我也是从头开始摸索。其实ARM是精简指令集,指令很少,也很简单。掌握他们并不困难,不过掌握了他们并不代表就是能改程序了,因为如果想改程序,更重要的是知道PPC的编程结构,也就是OS怎么组织程序的,如参数调用约定啦,WINAPI啦之类的,ARM的指令集非常简单,甚至没有CALL指令。不过很可惜,现在这方面的书非常少,并且,也似乎没有深究的必要,因为同Native程序相比,.NET CF程序有着更多的优点。那是未来的趋势。
现在就探索一下PPC的程序,其实探索这东西,最好的办法是分析自己写的程序。因为别人的程序你没有源代码,并且没法按你的要求去组织代码片断分析,所以写点小程序分析之还是很有必要的。好在微软的EVC++是免费下载的并且微软自己都提供了XXX(看来MS是非常急迫的想抢占市场啊),所以每个人都能获得免费的开发环境,至于PPC的SDK,就看需要下了,因为本质上PPC的程序跟wince的程序是没区别的,只不过通过AYGSHELL等库来实现了跟PPC平台在界面上更紧密的集成,这不是我们的重点。其他基本没啥区别,尤其在反汇编代码上:)
我先建了一个mytest的工程,是SDK的Simple工程,这种工程的好处就是反汇编代码异常简洁。没有MFC的干扰代码。然后我只加了一句代码总的代码如下
#include "stdafx.h"
int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
// TODO: Place code here.
MessageBox(0,_T("helloWorld"),_T("hello"),MB_OK);
return 0;
}
程序特简单,一次编译通过。然后打开IDA反汇编,主要反汇编代码如下:
WinMain ; CODE XREF: start+34 p
. ; DATA XREF: .pdata:00014000 o
var_4 = -4
STR LR, [SP,#var_4]!
LDR R2, =aHello ; lpCaption
LDR R1, =aHelloworld ; lpText
MOV R3, #0 ; uType
MOV R0, #0 ; hWnd
BL MessageBoxW
MOV R0, #0
LDMFD SP!, {PC}
.text:0001102C ; End of function WinMain
框架先不管,我们先看看我加的那句MessageBox调用的反汇编代码
LDR R2, =aHello ; lpCaption
LDR R1, =aHelloworld ; lpText
MOV R3, #0 ; uType
MOV R0, #0 ; hWnd
BL MessageBoxW
可以看出,参数还是从右到左传入的,但是却没有使用堆栈,而是用的R3,R2,R1,R0,
AMR没有x86那些eax,ebx等的寄存器,只有R0~R15 16个寄存器,然后我贴出ARM汇编手册的相关介绍
[引用]
ARM 处理器有二十七个寄存器,其中一些是在一定条件下使用的,所以一次只能使用十六个...
· 寄存器 0 到寄存器 7 是通用寄存器并可以用做任何目的。不象 80x86 处理器那样要求特定寄存器被用做栈访问,或者象 6502 那样把数学计算的结果放置到一个累加器中,ARM 处理器在寄存器使用上是高度灵活的。
· 寄存器 8 到 12 是通用寄存器,但是在切换到 FIQ 模式的时候,使用它们的影子(shadow)寄存器。
· 寄存器 13 典型的用做 OS 栈指针,但可被用做一个通用寄存器。这是一个操作系统问题,不是一个处理器问题,所以如果你不使用栈,只要你以后恢复它,你可以在你的代码中自由的占用(corrupt)它。每个处理器模式都有这个寄存器的影子寄存器。
· 寄存器 14 专职持有返回点的地址以便于写子例程。当你执行带连接的分支的时候,把返回地址存储到 R14中。同样在程序第一次运行的时候,把退出地址保存在 R14 中。R14的所有实例必须被保存到其他寄存器中(不是实际上有效)或一个栈中。这个寄存器在各个处理器模式下都有影子寄存器。一旦已经保存了连接地址,这个寄存器就可以用做通用寄存器了。
· 寄存器 15 是程序计数器。它除了持有指示程序当前使用的地址的二十六位数之外,还持有处理器的状态。
[引用结束]
可能有些术语会看不懂,没关系,有些地方我也看不懂,慢慢来嘛。像这个例子,四个参数完全用寄存器来传递,不必说是效率非常之高了,不过参数多了必定用栈,因为不就才16个寄存器嘛就用玩了怎么办?虽然在CPU中都是通用寄存器,但在OS里边就不是通用的了,有些是有特殊意义的,上边引用的那段文字就有提及,如上边的R13,R14.
在上边的例子中,MessageBox一共有4个参数,其中两个是字串,两个是常数,所以写入寄存器的方式也不相同。常数直接MOV进去,字串取地址用的是LDR指令
下边是指零手册中的介绍
MOV{条件}{S} ,
dest = op_1
MOV 从另一个寄存器、被移位的寄存器、或一个立即值装载一个值到目的寄存器。你可以指定相同的寄存器来实现 NOP 指令的效果,你还可以专门移位一个寄存器:
MOV R0, R0 ; R0 = R0... NOP 指令
MOV R0, R0, LSL#3 ; R0 = R0 * 8
LDR{条件} Rd,
LDR指令我也不是很太懂,根据指令手册,这个指令是移动相应地址中的内容去寄存器,所以我分析了这两了LDR的指令,IDA反汇编的代码无疑更优秀,不过一句LDR R2,=ahello确实使人不知所以然,是地址?还是其他?下边看看EVC里边的反汇编代码吧:
26011010 mov r3, #0
26011014 ldr r2, [pc, #0x30]
26011018 ldr r1, [pc, #0x28]
2601101C mov r0, #0
26011020 bl |MessageBoxW (2601108c)|
哦,这句的指令是把PC+30地址的内容放到r2里边啊,PC+30时多少?就是现在的26011014+30就是IDA反汇编的00011020里边的内容了,为什么这两个东西反汇编地址不一样呢?因为VC是在调试,所以程序被重定位了。00011020里边是什么呢?经过分析IDA的反汇编代码,就是字串“hello”的首地址了也就是指针。如此看来,r2,r1仍然是放的字串的指针,这跟win32的API实现是完全相同的。就上边看来,似乎EVC反汇编的代码更容易分析,其实不然。因为EVC我用的是DEBUG版,是有调试信息的。如果换REL版则EVC只会反汇编指令,而无任何信息,效果比IDA差得多。
[[i] 本帖最后由 冰川 于 2008-5-14 10:57 编辑 [/i]] ARM汇编语言(2)
下边我再探索一下API函数参数传递的内部处理方式,以及如何返回值。我在这个工程里边添加了以下代码
#include "stdafx.h"
int WINAPI MyFunc(int,int,int);
int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
// TODO: Place code here.
MessageBox(0,_T("helloWorld"),_T("hello"),MB_OK);
MyFunc(1,2,3);
return 0;
}
int WINAPI MyFunc(int a,int b,int c)
{
int d =a+b+c;
return d;
}
可以看出,我在原来的基础上加了一个函数的调用,并且这个函数是声明为WINAPI,即跟API是一种调用方式,最后返回了这三个输入参数的和。其反汇编代码如下:
调用部分:
14: MyFunc(1,2,3);
26011024 mov r2, #3
26011028 mov r1, #2
2601102C mov r0, #1
26011030 bl |MyFunc (26011050)|
这里跟上边分析的一板一眼,把三个参数分别传入r2,r1,r0,然后bl到我们的函数。BL类似于x86中的Call指令,不过动作比Call简单的多。在指令手册里边是这么描述这个函数的:
BL 是另一个分支指令。就在分支之前,在寄存器 14 中装载上 R15 的内容。你可以重新装载 R14 到 R15 中来返回到在这个分支之后的那个指令,它是子例程的一个基本但强力的实现。
R15是什么呢?就是PC!即程序计数器,看到这里,不用说也知道如果程序返回只要把R14的值再装回R15子程序也就返回了。实际是不是这样呢?我们来看子程序的反汇编代码:为了使大家能看懂这段反汇编程序,我加了部分注释。
18: int WINAPI MyFunc(int a,int b,int c)
19:
20: {
26011050 mov r12, sp ;先把sp的内容装入r12
26011054 stmdb sp!, {r0 - r2} ;stm的作用是复制寄存器的内容到栈,为什么要这样呢?
;刚才说过参数是通过Rx寄存器传递的参数,现在复制到了栈上,不
;过这好像抵消了用寄存器传递带来的性能的提升……考虑到这是
;debug版,release版本应该情况会好一些吧,这个一会儿再分析。
26011058 stmdb sp!, {r12, lr} ;把r12根lr复制到堆栈,r12存的是刚才保存的sp即栈顶指针
;lr则是r14的别名,即函数的返回点。我倒,现在的栈不就跟
;用完全用栈传递完全相同了么……
2601105C sub sp, sp, #8 ;建立了两个中间变量,但为什么是建2个呢?我可只建了一个
;d啊555。难道是保留的?
21: int d =a+b+c; ;
26011060 ldr r0, [sp, #0x10] ;
26011064 ldr r1, [sp, #0x14] ;
26011068 add r2, r0, r1 ;这一段就是做加法了。
2601106C ldr r3, [sp, #0x18] ;
26011070 add r0, r2, r3 ;加法完成,结果存在r0
26011074 str r0, [sp] ;结果存入d
22: return d; ;再看看是怎么返回的。
26011078 ldr r1, [sp] ;下三句作用是把结果放r0,代码比较垃圾
2601107C str r1, [sp, #4] ;这里r1,r0里边的值都是结果6,为什么我说返回值在r0里边呢?
26011080 ldr r0, [sp, #4] ;这是我调整了主函数的代码,分析出的结果。
23: }
26011084 add sp, sp, #8 ;释放建的中间变量
26011088 ldmia sp, {sp, pc} ;现在栈顶即是保存的PC跟SP,分别弹出,相当于以前x86的
;mov esp,ebp
;ret
我的修改后的测试主函数是:
int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
int a;
int b=4;
// TODO: Place code here.
MessageBox(0,_T("helloWorld"),_T("hello"),MB_OK);
int i=MyFunc(1,2,3);
a=b+i;
return 0;
}
下边再看看rel版的程序,因为一般发行的就是rel版,所以探索之更有意义。下边是反汇编代码。
WinMain ; CODE XREF: start+34 p
. ; DATA XREF: .pdata:00014000 o
var_4 = -4
STR LR, [SP,#var_4]!
LDR R2, =aHello ; lpCaption
LDR R1, =aHelloworld ; lpText
MOV R3, #0 ; uType
MOV R0, #0 ; hWnd
BL MessageBoxW
MOV R2, #3
MOV R1, #2
MOV R0, #1
BL sub_11038
MOV R0, #0
LDMFD SP!, {PC}
.text:0001102C ; End of function WinMain
; CODE XREF: WinMain+24 p
ADD R0, R0, R1
ADD R0, R0, R2
RET
. End of function sub_11038
代码无疑比debug模式更简洁易懂,至少在代码方面简直最小代码了,我就不做过多的解释了,不明白的话看上边那个debug的注释,我感兴趣的是:这儿居然有个ret指令,是新加的指令还是IDA为了易懂而弄得伪指令?因为我的指令手册没有该指令的解释。再看看EVC的反编译结果。
add r0, r0, r1
add r0, r0, r2
mov pc, lr
ldr r12, [pc]
ldr pc, [r12]
andeq r3, r1, r12
andeq r1, r1, r4, lsl r2
andeq r2, r1, r0
这是什么乱七八糟了^^,看看返回那个地方,好像就一句
mov pc, lr
就返回了。下边的好像都没有用,我调试了一下,果然如此,在这一句就返回了。看来IDA为了使代码易懂,有点不尊重原著了:)现在似乎只有EVC能在模拟器上或者连上机器能调试,不过代码异常混乱,比ida分析困难的多。不推荐使用了,感兴趣可以自己摸索。 ARM汇编语言(3)
这次我们一起探索ARM程序的更多细节。上边我们分析到了一个函数,看到是用寄存器来传递参数。但寄存器总是有限不可能总用寄存器,以前fast调用程序好像前3个参数使用寄存器,在PPC上,这应该是类似的。为此,我改了上次那个函数,因为MessageBoxW函数4个参数仍然用寄存器传递,所以直接从5个参数的程序开始分析。修改后代码如下:
int WINAPI MyFunc(int,int,int,int,int);
int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
int a;
int b=4;
// TODO: Place code here.
MessageBox(0,_T("helloWorld"),_T("hello"),MB_OK);
int i=MyFunc(1,2,3,4,5);
a=b+i;
return 0;
}
int WINAPI MyFunc(int a,int b,int c,int d2,int e)
{
int d =a+b+c+d2+e;
return d;
}
先看看调用部分代码:
var_4 = -4
MOV R3, #5
STR R3, [SP,#4+var_4] ;开始操作栈了
MOV R2, #3 ;前四个参数还是用寄存器传递
MOV R3, #4
MOV R1, #2
MOV R0, #1
BL sub_1104C ;Call我们的子程序
MOV R0, #0
我们看到,第5个参数已经开始使用堆栈做传递了
STR R3, [SP,#4+var_4]
这句的作用是将R3的值放入SP+#4+var_4所指的地址中。var_4是个常数是-4,所以这句等价于STR R3,[SP]
然后让我们看子程序部分的反汇编代码
sub_1104C ; CODE XREF: WinMain+34 p
arg_0 = 0
ADD R0, R0, R1
ADD R1, R0, R2
LDR R0, [SP,#arg_0]
ADD R2, R1, R3
ADD R0, R2, R0
RET
; End of function sub_1104C
结果多少让我有些惊讶,因为子程序跟主程序公用了一个堆栈,并且切入子程序时也没有保存栈顶指针,这样如果子程序崩溃,则整个程序的栈操作必然乱套。难道不会出现不稳定的情况?或许是没有操作堆栈的缘故?并且几乎所有的运算的结果都使用的寄存器。代码相当高效(甚至我想不出如何写比这更高效的程序)。这(RISC代码容易优化)就是RISC的优势所在吧,总之代码比x86下边VC++编译器优化的要好。
为了难为一下编译器,我改的代码稍微麻烦了一点:
int WINAPI MyFunc(int,int,int,int,int,int);
int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
int a;
int b=4;
// TODO: Place code here.
MessageBox(0,_T("helloWorld"),_T("hello"),MB_OK);
int i=MyFunc(1,2,3,4,5,6);
a=b+i;
return 0;
}
int WINAPI MyFunc(int a,int b,int c,int d2,int e,int f)
{
int g=3;
int h=4;
int i=6;
int j=9;
int k=i*j;
int d =a+b+c+d2+e+f+g+h;
return (d+k);
}
现在参数我改成了6个,并且我加了几个中间变量,以便把栈操作逼出来,调用处的代码在意料之中,最后2个参数使用了栈传递,而前四个参数使用了寄存器传递。看来可确定,PPC的API调用约定是:前四个参数分别用R0,R1,R2,R3,多余的参数从右向左依次入栈。
.text:0001101C MOV R3, #6
.text:00011020 MOV R0, #5
.text:00011024 STR R3, [SP,#8+var_4]
.text:00011028 STR R0, [SP,#8+var_8]
.text:0001102C MOV R3, #4
.text:00011030 MOV R2, #3
.text:00011034 MOV R1, #2
.text:00011038 MOV R0, #1
.text:0001103C BL sub_11054
.text:00011040 MOV R0, #0
而接下来子程序的反汇编代码,多少让我有些吃惊:
.text:00011054 sub_11054
.text:00011054
.text:00011054 arg_0 = 0
.text:00011054 arg_4 = 4
.text:00011054
.text:00011054 ADD R0, R0, R1
.text:00011058 ADD R1, R0, R2
.text:0001105C LDR R0, [SP,#arg_0]
.text:00011060 ADD R2, R1, R3
.text:00011064 LDR R1, [SP,#arg_4]
.text:00011068 ADD R3, R2, R0
.text:0001106C ADD R0, R3, R1
.text:00011070 ADD R0, R0, #0x3D
.text:00011074 RET
.text:00011074 ; End of function sub_11054
太智能了!不但没把栈操作逼出来,而且在编译时自动作了尽可能多的运算大大减少了实际运行时的效率!但这也让我想到了让编译器操作栈的方法,我把子程序代码改为如下:
int WINAPI MyFunc(int a,int b,int c,int d2,int e,int f)
{
int g=3;
int h=4;
int i=6;
int j=9*c;
int k=i*f;
int d =a+b+c+d2+e+f+g+h;
return (d+k);
}
这次编译器撑不住了,终于开始使用栈:
sub_11054 ; CODE XREF: WinMain+3C p
arg_0 = 0
arg_4 = 4
STMFD SP!, {R4-R6,LR} ;首先r4,r5,r6,lr分别入栈(保存现场)
MOV R4, R0 ;用r4,r5,r6暂存r0-r2的值
MOV R5, R1
MOV R6, R2
LDR R0, [SP,#0x10+arg_4]
ADD R1, R0, #1
RSB R2, R1, R1,LSL#3 ;r2=(r1<<3)-r1
ADD R0, R2, R4
ADD R1, R0, R5
LDR R0, [SP,#0x10+arg_0]
ADD R2, R1, R6
ADD R3, R2, R3
ADD R0, R3, R0
LDMFD SP!, {R4-R6,PC} ;恢复r4-r6的值并离开函数。
; End of function sub_11054
算法我就没必要写分析了,我说一下这次的堆栈操作:
这次仍然没有用栈做中间变量,而是多用了3个通用寄存器。而只有在进入与离开时操作下内存。这样能大大的减少内存的访问率,加快运行效率,微软的代码优化做的确实非常的好。
基本上函数的入口与出口就是这种形式了,下篇研究下字串的调用。 ARM指令第二操作数#immed_8r详解
大多数ARM通用数据处理指令有一个灵活的第2操作数(flexible second operand),
这里这解释一下其中的一种格式,
#immed_8r
常量的表达式。常量必须对应于8位位图(pattern)。该位图在32位字中,被循环移位偶数位(0,2,4,...28,30)
合法常量0xff,0xff000,0xf000000f。非法常量:0x101,0xff04
ARM 在32位模式下,一条指令长度为32位,在上述数据处理指令中,操作数2为12位。
所以像0x7f02这样的数,要两条指令才能完成。
MOV R3, #0x7F00 ;E3 A0 3C 7F 该指令自己完成0x7f移位
ORR R1, R3, #2
所以直接是找不到0x7f02的 侦探,讲讲怎样破解混淆过的.net程序吧,尤其是.net和非.net程序相互调用,不知从何入手了:不干 :不干 :不干
海绵宝宝滚雪球小游戏
*** 作者被禁止或删除 内容自动屏蔽 ***页:
[1]