逆向知识第九讲,switch case语句在汇编中表达的方式
一丶Switch Case语句在汇编中的第一种表达方式 (引导性跳转表)
第一种表达方式生成条件:
case 个数偏少,那么汇编中将会生成引导性的跳转表,会做出 if else的情况(类似,但还是能分辨出来的)
1.高级代码:
#include "stdafx.h" int main(int argc, char* argv[]) { switch(argc) { case 0: printf("case 0\n"); break; case 1: printf("case 1\n"); break; case 2: printf("case 2\n"); break; } printf("HelloWorld\n"); return 0; }
2.汇编代码在Debug版本下:
可以看出,生成的跳转表
比较和跳转在一起,而且跳转的时候是一个跳转表. (注意,这里可能不是比较,只要影响标志位即可,也可能是 Dec inc add ....等等但是没有实质性的代码)
注意最后一个跳转 JMP,当JMP的位置,或者代码中JMP的位置(代码中也就是 跳转地址过去后的地方)都是同一个地址,那么则是跳转到SWITCH_END(也就是switch case 语句块结束)
注意这里是代码逻辑分开的.
先判断逻辑,然后进行跳转执行.而在跳转过后的里面判断是否有JMP,有JMP的话,则代表是break
如果内部没有JMP则代表没有Break,那么语法是支持这样写法的.
3.汇编中Release版本
Release版本其实是一样的.只不过需要注意的是,它有代码外提的情况下.
因为我们每一个case里面都有打印输出函数,而且其参数都是一样的,所以跳转过来之后,里面只需要PUSH即可.Switch case完毕之后则会调用printf打印.
4.扩展知识(Default在case上面,或者中间)
4.1直接伪代码:
case 0 ... break;
default .... break
case 2 ....break
4.2汇编代码 Debug
debug版本不用说,直接JMP位置到default位置.
4.3.Release版本下.
对于Release版本,有的时候可能直接变为Default语句
有的时候会变成JNZ执行.这个时候JNZ下面要还原成原来的CASE语句
第一变化的还原手法
第一变化还原挺简单的
1.遇到cmp比较的, 和谁比较,那么跳转的地址变为对应的Case即可.注意,你点击地址过去之后则是Case语句的代码,但是要注意是否代码里面有JMP(没有则没有break)
2.有的时候不是CMP, 有的时候是 dec Reg32,这个时候就要算一下的,比如 dec Reg32结果是多少,则对应还原成多少,比如dec Reg32结果是0,那么其还原成Case0,但是还原第二个的时候要注意
第一个已经Dec了一下,第二个又还原的Dec 所以要和第一个相关联才可以.
二丶Switch Case语句在汇编中的第二种表达方式(简介寻址表)
case 语句块有多个,且其中间隔不算多, 间隔的地方填写Default
2.1高级代码:
#include "stdafx.h" int main(int argc, char* argv[]) { switch(argc) { case 0: printf("case 0\n"); break; case 1: printf("case 1\n"); break; case 2: printf("case 2\n"); break; case 3: printf("case 3\n"); case 5: printf("case 5\n"); break; case 6: printf("case 6\n"); break; case 7: printf("case 7\n"); break; default: printf("default \n"); break; } printf("HelloWorld\n"); return 0; }
2.2 Debug版本下的汇编代码表现形式
首先这里有两个问题
1. 为什么是JA (A是无符号的高于比较)
2. 为什么出现了数组寻址公式.
解答第一个问题:
是这样的,第二种优化方式它会生成跳转表,而跳转表就是在第二种里面出现的寻址.而做表的前提下就是要保证Case语句是排序好的.
所以在这里 case语句首先排序好,而后坐标平移到0位置
什么是坐标平移到0位置?
比如我们有代码
case -1
case 2
case 3
那么此时坐表的条件就是
case -1 变为case 0
case 2 变为 2
这样坐表之后则是从0开始,而里面保存的则是case的地址.
这个时候只会和case最大值作比较.
我们看下动态调试中是什么表现形式把.
坐标平移之后,则可以建立一个表格,用的时候查表即可.所以我们查表的时候可以看到很多case语句的地址
从0开始. 没有的则填写Default即可. 一般JA后面的地址就是Default或者Switch End地址,从表中我们也看出来了.第五项填写的是Switch End的地址.
正好我们的代码中缺少的第五项没有
Debug还原手法:
Debug还原很简单,首先是比较case最大值,(看下是否调整了,也就是坐标平移了,如果平移了,比如以前有个-1,而case 最大值是3,那么会比较 4,因为-1 要+1其余各个的case语句也要加1)
4.3Release版本下的汇编代码
Release版本下也是一样,此时它也是会用 case值和最大值比较,然后去数组中寻址查找表去跳转
注意: 这里数组寻址*4的意思是,地址是4个字节对其
比如我们的表格中存放的是case地址
00401000 case0 Address
00401004 case1 Address
00401008 case 2 Address
首先会和最大值比较,没有超过,那么直接用case的值 *4去表中找地址即可.
比如我们的case值是2, 比较是否大于7,
然后 jA指令判断是否高于7,高于7跳转到 Default或者Switch End,不高于,则 根据case值去表中查找跳转
比如现在是2,那么 2* 4 = 8 + 数组首地址(00401000) = 401008 然后取出内容跳转,此时内容也是case2的地址.
扩展思路:
上面讲的是Switch原型知识,我们要学会扩展.比如如果我case中有个-1怎么办
那么此时-1 +1 坐标平移到0的位置,然后其余各个的case +1
那么比较的时候和case最大的值相比,现在最大的值也+1了. 所以还原的时候,最大值要-1,其余各个case值都要-1则可以.
代码还原方式.
第二种代码还原方式
1.判断是否调整,调整了可以得出最小值,比如 add eax,2 那么得出最小值是-2,因为坐表要从0开始,这里调整则得出-2了,
2.得出case最大值比较,这里也要看是否调整了,没有调整那么最大值是多少就是多少,调整的那么case最大值则是 当前值 - 调整的值
3.得出最小值和最大值之后,去地址表中寻址即可.因为是排好序的,如果没有调整,那么就是从0开始,还原的时候就是case0
调整过后,那么减去调整的值即可,比如调整了2,那么第0项则是 0 - 2 则是case - 2 比较.
三丶Switch Case语句在汇编中的第三种表达方式(多表联合查询)
这种表达式相比前两种有点难理解,但是我们说过,编译器做的越多,那么我们还原的越快.
第三种表达式形成的条件:
当最大case值 - 最小case值的值在255直接,则使用第三种优化方式.
3.1高级代码
#include "stdafx.h" int main(int argc, char* argv[]) { switch(argc) { case 0: printf("case 0\n"); break; case 100: printf("case 100\n"); break; case 200: printf("case 200\n"); break; case 35: printf("case 35\n"); case 45: printf("case 45\n"); break; case 60: printf("case 60\n"); break; case 70: printf("case 70\n"); break; default: printf("default \n"); break; } printf("HelloWorld\n"); return 0; }
3.2 Debug版本下的汇编代码
此时我们可以看出有两次查表的动作
下标表:
这里我们说下第一个表,第一个表我们成为下标表
下标表,里面存放的是Case排好序的值的位置,比如我们的02 在下表中找一下,则是对应45的位置. 那么此时从里面取出来做第二个case,去地址表中寻到case 45 的地址.
下标表的作用,这里的下标表主要是一种优化,一种空间换时间的做法,这里的优化主要是,比如我们屏幕的颜色,白色很多,那么我就优化为,当有白色的时候,去下标表中寻找白色
就和我们的下标表中的07一样,只要知道这个,那么都是default
地址表:
地址表则和第二种优化一样,存放的case的真实地址.
debug还原手法:
1.首先判断是否调整,调整了,可以得出最小case的值, (0 - 调整的值) 没调整则默认从0开始
2.比较的时候会和最大case值比较(没有调整则最大case值就是本身)调整了(最大case值 - 调整后的值 = 真实的最大case值)
3.遇到ja则后边现修改为 default或者switchend
4.从下标表中寻得case当前的位置,(比如存放的是3,其位置在下表表中是35)那么此时代表的就是 case 35 (属于第三个case语句)
5.从下标表中得出case语句的位置,(第几个case)以及case的值是多少(case 35)那么去地址表中寻找其case35的地址即可.
3.3 Release版本下的汇编
和Debug版本下相差无异,只不过有代码外提
此时还原很好还原
1.展开下表表格
2.展开地址表格
展开后则是上面的模样,第一个是地址表,第二个是下标表格.
注意展开的时候可能会遇到问题
1.不是我这种显示,没有这么整齐, 此时 按键盘上的 * 键,可以设置按照数组显示, 设置数组大小,间隔.等等..
2.没有显示 offset addr... 可能显示的直接是一个地址, 此时可以选择快捷键 ctrl + 0 显示成当前段偏移
3.数据类型不对,我的是dd 而你们的可能解释为 db dw 等等.. 此时按键盘快捷键 d即可.可以调整数据类型
还原方法;
和Debug一样, (因为我没有调整所以不用算了,而且从0开始)此时先去下标表中寻找0, 这个0找的的是case语句的位置 也就是第一条case语句是0
然后在寻找在数组的那个位置, 这个位置指的是 case xxx xxx的值是多少
此时我能找到0,那么去数组表中寻找0地址,改为case 0, 按n键修改
修改1的值,也就是找case 第2条语句是什么
在35位置寻找到1,那么此时 第二条语句代表的case则是case 35
利用里面的下边去地址表中寻址,则找到第二个地址则是case 35
我们修改过后则上面会显示了.此时我们正常的还原即可.
四丶Switch Case语句在汇编中的第四种表达方式(二分优化查表,四种变化混合)
第四种表达方式,生成条件,其case值之间的间隔大于255则会使用第四种表达方式.
4.1高级代码:
#include "stdafx.h" int main(int argc, char* argv[]) { switch(argc) { case 0: printf("case 0\n"); break; case 256: printf("case 256\n"); break; case 34: printf("case 34\n"); break; case 500: printf("case 500\n"); break; case 510: printf("case 510\n"); break; default: printf("default \n"); break; } printf("HelloWorld\n"); return 0; }
首先说下如果遇到这种情况,编译器会怎么做.
因为对于编译器来说,不知道你命中率高的那个case语句会执行,所以只能从中间切开
会生成汇编代码 jg 或者 jle (大于或者小于代码 )还有中间判断相等.
比如我们有一组case值
0 |
200 |
300 |
310 |
320 |
此时汇编代码会先判断是否 jz(等于,可能不是jz反正判断相等的跳转即可) 找到中间的case,所以此时还原这个值即可.
不用管大于小于,最后的时候管.
4.2Debug下的汇编代码:
可以看出一大堆的判断 相等的代码.
还原手法:
看到JE比较,那么看上方条件和谁比较(没有调整的情况下)那么还原当前地址为这个case即可.
4.3Release版本下的汇编.
这个是还原过后的,只需要判断 相等即可,根据它的条件来判断,只不过Release版本有代码外提
JNZ先修改为 Default ,此时如果进去每个case语句块中,按到的JMP的跳转地址和JNZ跳转的地址是一样的话,那么就是SWITCH_END
只需要还原相等即可.
注意:
当我还原case 510的时候,有没有发现其上面还原500的时候都是减掉了,所以510的时候又减掉了一次所以此时判断是510
大体的规模已经出来了,下面就是还原具体代码了,代码很简单,不还原了,看下和高级代码对比即可.
看看case是否一样.
转载于:
作者:IBinary
出处:http://www.cnblogs.com/iBinary/