-
-
[原创]2015 MSC(第二届)iOS 1,2,3解题思路
-
发表于: 2015-10-21 00:03 13683
-
第一题 -------------------->
第一题出的可能有点坑人,倒不是技术的难题。我首先拿道题后,用IDA打开后,扫了扫,这个题挺简单啊,只需要写个tweak,就把密码爆出来,不是很复杂啊。但是在我的iPad min 2 3G版本运行崩溃,我刚开始还怀疑我下的包是不是正确,下了第二遍还是崩溃啊,不知道是本来设计就是这样还是因为我的设备问题。看看注意事项:“iOS赛题的运行需要iOS v8.4及以上版本,需iphone4s及以上机型“,没有说可以用iPad,难不成主办方还非限制不能使用iPad解题,最后无奈将LP的iPhone 5s(为了安全期间,手机是不一般不越狱的)越狱,安装AppSync后,运行又崩溃。不是吧,还以为是AppSync出什么问题,折腾了很久,最终放弃直接硬编码来破解了。哎,不知道是出题就是这么设计的,还是其他原因,这个题有点太。。。
好了,下面进入主题。
方法一:如果这个题能够能够正确运行,写个tweak如下,程序中的循环解密调用AESCrypt类decrypt最后的结果,就暴漏出答案了。
%hook AESCrypt
+ (NSString *)decrypt:(NSString *)message password:(NSString *)password
{
NSLog(@"message=%@", message) ;
NSLog(@"password=%@", password) ;
NSString* answer = %orig;
NSLog(@"answer=%@", answer) ;
return answer ;
}
%end
方法二:编码破解,其中用到的类NSData+Base64、NSData+CommonCrypto和Ceasar_CipherModel这个在github上很容易就搜索到了
NSString *message =
@"mrMZAbjtZozDOGI9UeeH6g0iLHNnTNsFyzS0tYca4R3KkaQ0doxdDVuxZ7HoqYOcxFhgDiEvdGKix95VJNEUP8rdox4cm7GHVkbVcTJPmrTtH7hompW+xjTgGg2zQhs0tUGQ8lCggev2SNoWcaUOUU==" ;
int count = 5 ;
NSString* result = @"" ;
do {
Ceasar_CipherModel * ceasar = [[Ceasar_CipherModel alloc] initWithCipherKey:--count] ;
[ceasar setCodedMessage:message] ;
[ceasar decrypt] ;
message = [AESCrypt decrypt:[ceasar originalMessage] password:@"ZGlhb2RhX2ppYW5rYW5nCg=="] ;
} while (count > 0) ;
NSLog(@"%s", [message UTF8String]) ;
@implementation AESCrypt
+ (NSString *)decrypt:(NSString *)message password:(NSString *)password
{
NSData* data = [NSData base64DataFromString:message] ;
NSData* hash = [[password dataUsingEncoding:NSUTF8StringEncoding] SHA256Hash] ;
return [[NSString alloc] initWithData:[data decryptedAES256DataUsingKey:hash error:nil] encoding:NSUTF8StringEncoding] ;
}
@end
第二题 -------------------->
找出kernelcache中的/dev/random,/dev/pf和/dev/ptmx在kernelcache中的d_read, d_write, d_ioctl地址。
这些都是字符型设备文件,如果对linux驱动了解的话,很容易联想到,cdev_init,cdev_add,cdev_del函数,这些*nix都是相通的。所以在函数表中找找cdev看看是否存在_cdevsw_add,可以通过这个函数找出所有设备文件定义。因为这儿bin是被strip掉的,需要定位这几个设备文件分别对应的是哪款代码,可以通过devfs_make_node函数的倒数第二个参数完全判断出来。找到字符设备添加的位置,只需要分析_cdevsw_add函数传入的第二个结构体,得到答案,至于那个d_read, d_write, d_ioctl的地址,也可以进入相应指针地址的函数内稍微瞅瞅就可以区分出来。当然也有偷懒的方式,毕竟XNU苹果是开源的,直接可以通过查找opensource.apple.com找到对应源码。
互相并以学习的态度,我就啰嗦点多写点。下面是我找的地址,以供大家学习参考交流:
#/dev/random
http://www.opensource.apple.com/source/xnu/xnu-2422.1.72/bsd/dev/random/randomdev.c
static struct cdevsw random_cdevsw =
{
random_open, /* open */
random_close, /* close */
random_read, /* read */
random_write, /* write */
random_ioctl, /* ioctl */
(stop_fcn_t *)nulldev, /* stop */
(reset_fcn_t *)nulldev, /* reset */
NULL, /* tty's */
eno_select, /* select */
eno_mmap, /* mmap */
eno_strat, /* strategy */
eno_getc, /* getc */
eno_putc, /* putc */
0 /* type */
};
#在IDA中的内容
__DATA:__data:803BDE74 off_803BDE74 DCD sub_800C0E18+1 ; open
__DATA:__data:803BDE78 DCD sub_800C0E34+1 ; close
__DATA:__data:803BDE7C DCD sub_800C0EA0+1 ; read
__DATA:__data:803BDE80 DCD sub_800C0E38+1 ; write
__DATA:__data:803BDE84 DCD sub_800C0E04+1 ; ioctl
其他/dev/pf和/dev/ptmx就把啰嗦列举了。源码地址分别如下
http://www.opensource.apple.com/source/xnu/xnu-2782.30.5/bsd/net/pf_ioctl.c
http://www.opensource.apple.com/source/xnu/xnu-2782.30.5/bsd/kern/tty_ptmx.c
至于相对kernelcache有更多了解的朋友,可以参见
https://www.nowsecure.com/blog/2014/04/14/ios-kernel-reversing-step-by-step/
第三题 -------------------->
第三题虽然没有做出来,与大家分享一下我的分析过程,互相学习并望指点一下有没有更精妙的方法(感觉自己的方案有点太人肉计算机了)。
第三题倒是在我的ipad上能够运行。拿到第三题后感觉确实就难度增加了,这个就不能简单的用静态分析来解决问题了,需要使用lldb来动态调试跟踪了。我对arm7汇编还是熟悉,在这种疯跳的代码面前,还是选自己熟悉的汇编格式来分析更省事,所以搞了个iPhone 5c来,越狱装AppSync有是一顿折腾。外加Xcode6的lldb对armv7调试与IDA不同显示相同,从苹果网站下个Xcode5.0.2那个慢,又是一顿折腾。。
闲话少说,开始真题。这道题我是两个方案并行进行。一个是暴力破解方法,另外一个就是人工分析。
方案一:暴力破解
__text:0000B070 ; void __cdecl -[ViewController onClick](struct ViewController *self, SEL)
__text:0000B0C2 ADD R0, PC ; selRef_UTF8String
__text:0000B0C4 LDR R1, [R0] ; "UTF8String"
__text:0000B0C6 MOV R0, R6 ; R0为输入文本
__text:0000B0C8 BLX _objc_msgSend
__text:0000B0CC BL sub_1DCB8 ; checkPassword
其中sub_1DCB8是关键函数,返回1表示成功(“呵呵!正式在下!”),返回0表示失败(“大人,冤枉啊!不是我!”)
所以写个tweak,hook到sub_1DCB8,当然这个方案是下下策,也就是赌一下,毕竟人工分析与机器碰运气也是互相不耽误的。呵呵
下面是tweak大体内容如下:
int (*original_checkPassword)(const char*);
int replaced_checkPassword(const char* password) {
for (char* word in 暴力字典表) {
retValue = original_checkPassword(word);
if (retValue == 1) {
return retValue ;//爆破成功
}
}
}
%ctor
{
@autoreleasepool
{
MSImageRef image = MSGetImageByName("/var/mobile/Containers/Bundle/Application/076C360A-AA7C-473B-AF88-C5A1171D91AB/iOS题3.app/iOS题3") ;
//Look for our target method
unsigned int *sym = (unsigned int*)0x1dcb9 ;//0x1dcb8 + 1
MSHookFunction((void *)sym, (void *)replaced_checkPassword, (void **)&original_checkPassword);
}
}
可惜最终也没有爆破出来。
方案二:人工分析
下面是我的分析过程,可能写的会比较啰嗦,大家不要介意,也是希望这个帖子公布出来后,有大牛能够给我指点一二或给大家提个思路,我能提高学习一下。lldb调试就不在累述了,需要帮助的朋友可以参见《iOS应用逆向工程(第2版)》
首先,在
__text:0000B0CC BL sub_1DCB8 ; checkPassword
下断点,
(lldb) br s -a 0xb0cc
运行起来随便输入点,比如"wang",此时R0保存的是输入字符串的地址0x003660e0(以下地址或者内存内容为我当时分析时数据,因时因输入而异,仅供以下分析参考)。因为输入毕竟要使用,所以可以在输入地址0x003660e0下个观察点的断点。看看代码是从哪里开始处理这个字符串,关键也是这个代码跳的厉害,一步一步追踪很难找到关键点。
----下个读断点
(lldb) wa s e -w read -- 0x003660e0
Watchpoint created: Watchpoint 1: addr = 0x003660e0 size = 4 state = enabled type = r
new value: 1735287159
然后运行起来后,在这里断到读观察点,
__text:00020F76 STR.W R3, [R7,#-0x7C]
__text:00020F7A BLX _strlen ; check input words length
在这里下断点,查看一下数据,这个就是刚才输入的“wang",在根据_strlen,这不是求长度吗? 差不多找到只能说一部分关键点了,下来就跟着往下追踪了。看看有什么可获取的。解决这个程序中有两个地方是决定跳转到下一条地址的代码,在这两个地方下个断点,并加个打印条件打出下一条执行的代码段地址,这样就不会跑飞了。
片段1:
__text:00013ED8 LDR R0, [SP,#arg_8]
__text:00013EDC STR LR, [SP,#arg_8] ; LR = return address
__text:00013EE0 MOV LR, R0
片段2:
__text:00013ED8 LDR R0, [SP,#arg_8]
__text:00013EDC STR LR, [SP,#arg_8] ; LR = return address
__text:00013EE0 MOV LR, R0
(lldb) br s -a 0x13ee0
5: address = 0x00013ee0, ....
(lldb) br com add 5 <- 这个5是上面断点编号
# p/x $lr
# DONE
(lldb) br s -a 0x13f08
6: address = 0x00013f08, ....
(lldb) br com add 6 <- 这个5是上面断点编号
# p/x $lr
# DONE
知道关键函数开始点后,又能取到跳转下一条跳转地址后,那么下面就是开始单步。模式如下:
__text:00020F88 LDR.W R1, [R7,#-0x6C] <----我们关系的执行代码
__text:00020F8C PUSH {R0,R1,LR} <----从这里开始就开始调用调用地址重算函数了,所以在这个地址执行c就可以了
__text:00020F8E MOVW R0, #0x21D
__text:00020F92 BL LocationPCPosition
如下是我分析的讲字符串"wang"加密成数字的片段:
计算输入长度
__text:00020F76 STR.W R3, [R7,#-0x7C] r3=0xa3610b54 ro=输入地址
__text:00020F7A BLX _strlen ; check input words length
判断输入长度是否为零
__text:00020F96 CMP R0, #0 ; r0 为输入长度
__text:00021072 MOVW R1, #0 ; 此时r1为0
__text:00020FE4 IT EQ
__text:00020FE6 MOVEQ R1, #1 ; 如果输入长度为0,则R1赋值为1
__text:00020FD4 CMP R1, #0 ;如果r1=0, 长度不为0,否则长度为0,失败
__text:00020FD6 LDR.W R1, [R7,#-0x7C] ;r1 = 0xa3610b54
__text:00020FC6 LDR.W R2, [R7,#-0x78] ;r2 = 0xf029035e
__text:000211CA IT NE
__text:000211CC MOVNE R1, R2 ;r1=0xa3610b54 r2 = 0xf029035e
;保存初始密钥key
__text:00020FB4 STR.W R1, [R7,#-0x40] ;[R7,#-0x40]=0xa3610b54
;取第一个字符存[R7,#-0x41]
__text:00020FB8 LDRB.W R1, [R7,#-0x32] ;[R7,#-0x32]=0x00000077 r1=0x00000077 <-第一个字符'w' 取第一个字符
__text:00020FA2 STRB.W R1, [R7,#-0x41] ;[R7,#-0x41]=0x610b5477
;保存原始输入字符指针和长度到 [R7,#-0x48] 和 [R7,#-0x4C]
__text:00020FA6 LDR.W R1, [R7,#-0x68] ;r1 = 0x0030d5c0 <-输入字符串的地址 “wang”
__text:0002112E STR.W R1, [R7,#-0x48] ;[R7,#-0x48]=0x0030d5c0 <-输入字符串的地址 “wang”
__text:00021132 STR.W R0, [R7,#-0x4C] ;[R7,#-0x4C]=0x00000004 <-输入字符串的地址 “wang” 的长度
;初始化一个相关的KEY,从[R7,#-0x74]转存到[R7,#-0x50]
__text:00021204 LDR.W R0, [R7,#-0x74] ;[R7,#-0x74] = 0xffffffff 设置R0为0xffffffff
__text:00020F64 STR.W R0, [R7,#-0x50] ;[R7,#-0x50] = 0xffffffff
;初始化一个相关的KEY,从[R7,#-0x70]转存到[R7,#-0x54]
__text:00020F68 LDR.W R3, [R7,#-0x70] ;设置R3为 0x00000000
__text:0002119A STR.W R3, [R7,#-0x54] ;[R7,#-0x54] = 0x00000000
;找到初始化KEY是0xa3610b54的地方开始执行代码,并且将Key暂存在[R7,#-0x6C]
__text:000200C6 LDR.W R0, [R7,#-0x40] ;[R7,#-0x40] = 0xa3610b54, R0取值0xa3610b54
__text:000206A2 MOVW R1, #0xC15 ;
__text:00020722 MOVT.W R1, #0xAC47 ; R1=0xac470c15
__text:00020716 CMP R0, R1 ; R0=0xa3610b54 R1=0xac470c15
__text:000206FC STR.W R0, [R7,#-0x6C] ;[R7,#-0x6C]=0xa3610b54
__text:000206EE MOVW R0, #0xB54
__text:00020930 MOVT.W R0, #0xA361 ;R0 = 0xa3610b54
__text:00020934 LDR.W R1, [R7,#-0x6C] ;R1 = 0xa3610b54
__text:000206B0 CMP R1, R0 :R0 = 0xa3610b54, R1 = 0xa3610b54
__text:00021120 MOVW R0, #0x986
__text:00020F24 MOVT.W R0, #0x7C4A ; R0 = 0x7c4a0986
__text:00020F28 MOVW R1, #0xDC4D
__text:00020F56 MOVT.W R1, #0xAEFF ; R1 = 0xaeffdc4d
__text:00020F44 MOV R2, #0xFFFFFFFF ; R2 = 0xffffffff
__text:00020F48 MOVW R3, #0x98B0 ;
__text:0002125C MOVT.W R3, #0 ; R3 = 0x000098b0
__text:00021260 ADD R3, PC ; PC = 0x00021260, R3 = 0x000098b0 之后R3=0x0002ab14
;R9一个变动的KEY值,初始化的时候为0xffffffff
__text:0002126C LDR.W R9, [R7,#-0x50] ; [R7,#-0x50] 为-1,R9也变成R9 = 0xffffffff
;R12为输入字符串的长度
__text:00021270 LDR.W R12, [R7,#-0x4C] ; [R7,#-0x4C] 为0x00000004,R12也变成0x00000004 <-输入字符串的地址 “wang” 的长度
;LR为输入字符串在内存中的地址
__text:000212EC LDR.W LR, [R7,#-0x48] ; LR = 0x0030d5c0 <-输入字符串的地址 “wang”
;取的第一个字符
__text:000212F0 LDRB.W R4, [R7,#-0x41] ; R4 = 0x00000077
; 输入的字符串长度减一
__text:000212AC MOV R5, #0x3EE9B11C ; R5 = 0x3ee9b11c
__text:000212D0 SUB.W R12, R12, R5 ; R12 = 0x00000004, R5 = 0x3ee9b11c R12 = R12 - R5 = 0xc1164ee8
__text:000212DE ADD.W R12, R12, #0xFFFFFFFF ; 0xFFFFFFFF 也等于0 R12 = 0xc1164ee7
__text:000212BE MOV R5, #0x3EE9B11C ; R5 = 0x3ee9b11c
__text:0002133A ADD R12, R5 ; R12 = 0xc1164ee7 R5 = 0x3ee9b11c R12 = R12 + R5 = 0x00000003
;降减一后的字符长度,保存在[R7,#-0x30],并且下一个字符地址保存在[R7,#-0x2C]
__text:00021318 STR.W R12, [R7,#-0x30] ; [R7,#-0x30] = 0x00000003
;去第二个字符地址保存在[R7,#-0x2C]
__text:0002131C ADD.W R12, LR, #1 ; LR=0x0030d5c0 <-输入字符串的地址 R12 = LR + 1 = 0x0030d5c1 <- "ang"
__text:0002130A STR.W R12, [R7,#-0x2C] ; R12 = 0x0030d5c1 <- 即"ang"的地址, [R7,#-0x2C]= 0x0030d5c1
;R4=第一个字符, LR=初始化KEY异或#0xFFFFFFFF,R2为0x20F44地方初始化的常量0xFFFFFFFF
__text:0002129A AND.W R12, R4, #0xFF ; R4 = 0x00000077, R12 = R4 & 0xFF = 0x00000077
__text:0002129E EOR.W LR, R9, #0xFFFFFFFF ; R9 = 0xffffffff LR = R9 ^ 0xFFFFFFFF = 0x00000000
__text:000213F4 EOR.W R4, R2, #0xFF ; R2 = 0xffffffff, R4 = R2 ^ 0xFF = 0xffffff00
__text:000213F8 MOVW R5, #0x3A26
__text:0002127E MOVT.W R5, #0xA4D7 ; R5 = 0xa4d73a26
__text:00021282 EORS R5, R2 ; R5 = 0xa4d73a26, R2 = 0xffffffff R5 = R5 ^ R2 = 0x5b28c5d9
__text:000213B6 ORR.W LR, LR, R4 ; LR = 0x00000000, R4 = 0xffffff00, LR = LR | R4 = 0xffffff00
__text:000213BA MOVW R4, #0x3A26
__text:000213D6 MOVT.W R4, #0xA4D7 ; R4 = 0xa4d73a26
__text:000213DA ORRS R4, R5 ; R4 = 0xa4d73a26, R5 = 0x5b28c5d9, R4 = R4 | R5 = 0xffffffff
__text:000213E6 EOR.W LR, LR, #0xFFFFFFFF ; LR = 0xffffff00, LR = LR ^ 0xFFFFFFFF = 0x000000ff
__text:000213C8 AND.W LR, LR, R4 ; LR = 0x000000ff, LR = LR & R4 = 0x000000ff & 0xffffffff = 0x000000ff
;第一个字符R12
__text:0002138E EOR.W R4, R12, #0xFFFFFFFF ; R4 = 0xffffff88 R4=‘w' ^ 0xFFFFFFFF
__text:00021392 AND.W R4, R4, LR ; R4 = 0x00000088
;根据第一个字符和R3,计算出地址,然后取出来放R3,看来R3有大用
__text:00020F36 EOR.W LR, LR, #0xFFFFFFFF ; LR = 0x000000ff ^ 0xFFFFFFFF = 0xffffff00
__text:00020DDC AND.W R12, R12, LR ; R12 = 0x00000077 & 0xffffff00 = 0x00000000
__text:00020DE0 ORR.W R12, R12, R4 ; R12 = 0x00000088
__text:00020DCC MOV.W R12, R12,LSL#2 ; R12 = 0x00000220
__text:00020DD0 ADD R3, R12 ; R3 = 0x0002ab14, R3 = R3 + R12 = 0x0002ad34
__text:0002135E LDR R3, [R3] ; R3 = 0xe3630b12
__text:00020DBE MOVW R12, #8 ;
__text:00020CAE MOVT.W R12, #0 ; R12 = 0x00000008
__text:00020CB2 LSR.W R9, R9, R12 ; R9 = 0xffffffff R9 = R9 >> R12 = 0xffffffff >> 0x00000008 = 0x00ffffff
;R3为输入的字符进行相应操作,然后再取的一个值,对他进行0xFFFFFFFF EOR操作保存在R12
__text:00020CC0 EOR.W R12, R3, #0xFFFFFFFF ; R3 =0xe3630b12 , R12 = R3 ^ 0xFFFFFFFF = 0x1c9cf4ed
;然后与0x3802bd30进行 and 操作
__text:00020CC4 MOVW LR, #0xBD30
__text:00020D5C MOVT.W LR, #0x3802 ; LR = 0x3802bd30
__text:00020D60 AND.W R12, R12, LR ; R12 = R12 & LR = 0x0002112f
;R2为0x20F44地方初始化的常量0xFFFFFFFF
__text:00020CD2 MOVW LR, #0xBD30
__text:00020CF0 MOVT.W LR, #0x3802 ; LR = 0x3802bd30
__text:00020CF4 EOR.W R2, R2, LR ; R2 = 0xffffffff R2 = R2 ^ LR = 0xc7fd42cf
;R3为上面那个根据第一个字母算出来,然后查表的R3
__text:00020D6E ANDS R3, R2 ; R3 = 0xe3630b12 R3 = R3 & R2 = 0xc3610202
__text:00020D4A EOR.W LR, R9, #0xFFFFFFFF ; R9 = 0x00ffffff, LR = R9 ^ 0xFFFFFFFF = 0xff000000
__text:00020D4E MOVW R4, #0xBD30
__text:00020D3C MOVT.W R4, #0x3802 ; R4 = 0x3802bd30
__text:00020D2A AND.W LR, LR, R4 ; LR = 0x38000000
__text:00020D2E AND.W R2, R2, R9 ; R2 = 0x00fd42cf
__text:00020D7A ORR.W R3, R3, R12 ; R3 = 0xdb61b622S
__text:00020D7E ORR.W R2, R2, LR ; R2 = 0x38fd42cf
__text:00020D02 EORS R2, R3 ; R2 = 0xe39cf4ed
;上面进行一系列操作后,保存在[R7,#-0x28],这个地方以后估计要进行比较,下个读监听就知道在哪里判断了
__text:00020D1C STR.W R2, [R7,#-0x28] ; [R7,#-0x28] 中保存 0xe39cf4ed
.........
一堆无用代码或跳转代码,到下面
.........
; R0=剩余长度
__text:0001FEB0 LDR.W R0, [R7,#-0x30] ; R0 = 0x00000003
; R2=剩余字符串的字符串地址
__text:0001FE90 LDR.W R2, [R7,#-0x2C] ; R2 = 0x0030d5c1
; 第一个字符计算的结果
__text:0001FE94 LDR.W R3, [R7,#-0x28] ; R3 = 0xe39cf4ed
;;;;;;;;;
;;;;;;;;;第二个字符开始,后面的都是循环从这里开始处理的
;;;;;;;;;
; 保存第二个字符到[R7,#-0x41]
__text:0001FE82 STRB.W R1, [R7,#-0x41] ; R1 = 0x00000061 就是'a' 保存
; 保存第二个字符地址到[R7,#-0x48]
__text:00020092 STR.W R2, [R7,#-0x48] ; R2 = 0x0030d5c1
; 保存剩余长度到[R7,#-0x4C]
__text:0001FBCE STR.W R0, [R7,#-0x4C] ; R0 = 0x00000003
; 保存一个字符进行加密计算的结果到[R7,#-0x50]
__text:0001FBD2 STR.W R3, [R7,#-0x50] ; R3 = 0xe39cf4ed
以上是对输入字符进行加密的核心代码,循环调用上面的算法,进行运算。当找到关键代码前半部分执行开始和结束地址后,下面只需要在下两个端点,下次调试的时候,就可以跳过分析这段了,分别可以在如下两个地方下断点:
; R4为当前要处理的那个字符开始
__text:0002129A AND.W R12, R4, #0xFF ; R4 = 0x00000077, R12 = R4 & 0xFF = 0x00000077
; R2为上面R4字符处理完成后,运算结果保存地址。
__text:00020D1C STR.W R2, [R7,#-0x28] ; [R7,#-0x28] 中保存 0xe39cf4ed
通过处理"wang"过程中,[R7,#-0x28]分别被变更为:
w = 0xe39cf4ed
a = 0xe4ed53ff
n = 0x87ec4e81
g = 0x49eeab03
好在每次调试这几个字符,数值都不变化,也减少了调试的难度。
下来执行就一顿乱跳了,不知道所云,好在秉着生成的数据总是要用的原则,处理完输入字符后,给[R7,#-0x28]的地址下读观察点断点,来定位哪里开始使用这个关键数据的。
下个watchpoint,继续运行,会再一段代码断下来。由于所有程序在规定是时间内我没有分析完,而且在这个地方我也没有分析太多,所以就不在罗列了。
不知不觉,啰嗦了这么多,也算是对自己的这几天参加比赛的总结。其中肯定有比我分析方法高明的地方,只是罗列一下自己的愚见,望大家指点,共同提高。
也非常感谢看雪与主办方举行的第一和第二次比赛,对我帮助很大,尤其是对我这种对破解感兴趣的业余人员。提供了学习的目标和指引的方向。
再次感谢看雪论坛,见谅我平时只看帖不回贴的行为,呵呵
第一题出的可能有点坑人,倒不是技术的难题。我首先拿道题后,用IDA打开后,扫了扫,这个题挺简单啊,只需要写个tweak,就把密码爆出来,不是很复杂啊。但是在我的iPad min 2 3G版本运行崩溃,我刚开始还怀疑我下的包是不是正确,下了第二遍还是崩溃啊,不知道是本来设计就是这样还是因为我的设备问题。看看注意事项:“iOS赛题的运行需要iOS v8.4及以上版本,需iphone4s及以上机型“,没有说可以用iPad,难不成主办方还非限制不能使用iPad解题,最后无奈将LP的iPhone 5s(为了安全期间,手机是不一般不越狱的)越狱,安装AppSync后,运行又崩溃。不是吧,还以为是AppSync出什么问题,折腾了很久,最终放弃直接硬编码来破解了。哎,不知道是出题就是这么设计的,还是其他原因,这个题有点太。。。
好了,下面进入主题。
方法一:如果这个题能够能够正确运行,写个tweak如下,程序中的循环解密调用AESCrypt类decrypt最后的结果,就暴漏出答案了。
%hook AESCrypt
+ (NSString *)decrypt:(NSString *)message password:(NSString *)password
{
NSLog(@"message=%@", message) ;
NSLog(@"password=%@", password) ;
NSString* answer = %orig;
NSLog(@"answer=%@", answer) ;
return answer ;
}
%end
方法二:编码破解,其中用到的类NSData+Base64、NSData+CommonCrypto和Ceasar_CipherModel这个在github上很容易就搜索到了
NSString *message =
@"mrMZAbjtZozDOGI9UeeH6g0iLHNnTNsFyzS0tYca4R3KkaQ0doxdDVuxZ7HoqYOcxFhgDiEvdGKix95VJNEUP8rdox4cm7GHVkbVcTJPmrTtH7hompW+xjTgGg2zQhs0tUGQ8lCggev2SNoWcaUOUU==" ;
int count = 5 ;
NSString* result = @"" ;
do {
Ceasar_CipherModel * ceasar = [[Ceasar_CipherModel alloc] initWithCipherKey:--count] ;
[ceasar setCodedMessage:message] ;
[ceasar decrypt] ;
message = [AESCrypt decrypt:[ceasar originalMessage] password:@"ZGlhb2RhX2ppYW5rYW5nCg=="] ;
} while (count > 0) ;
NSLog(@"%s", [message UTF8String]) ;
@implementation AESCrypt
+ (NSString *)decrypt:(NSString *)message password:(NSString *)password
{
NSData* data = [NSData base64DataFromString:message] ;
NSData* hash = [[password dataUsingEncoding:NSUTF8StringEncoding] SHA256Hash] ;
return [[NSString alloc] initWithData:[data decryptedAES256DataUsingKey:hash error:nil] encoding:NSUTF8StringEncoding] ;
}
@end
第二题 -------------------->
找出kernelcache中的/dev/random,/dev/pf和/dev/ptmx在kernelcache中的d_read, d_write, d_ioctl地址。
这些都是字符型设备文件,如果对linux驱动了解的话,很容易联想到,cdev_init,cdev_add,cdev_del函数,这些*nix都是相通的。所以在函数表中找找cdev看看是否存在_cdevsw_add,可以通过这个函数找出所有设备文件定义。因为这儿bin是被strip掉的,需要定位这几个设备文件分别对应的是哪款代码,可以通过devfs_make_node函数的倒数第二个参数完全判断出来。找到字符设备添加的位置,只需要分析_cdevsw_add函数传入的第二个结构体,得到答案,至于那个d_read, d_write, d_ioctl的地址,也可以进入相应指针地址的函数内稍微瞅瞅就可以区分出来。当然也有偷懒的方式,毕竟XNU苹果是开源的,直接可以通过查找opensource.apple.com找到对应源码。
互相并以学习的态度,我就啰嗦点多写点。下面是我找的地址,以供大家学习参考交流:
#/dev/random
http://www.opensource.apple.com/source/xnu/xnu-2422.1.72/bsd/dev/random/randomdev.c
static struct cdevsw random_cdevsw =
{
random_open, /* open */
random_close, /* close */
random_read, /* read */
random_write, /* write */
random_ioctl, /* ioctl */
(stop_fcn_t *)nulldev, /* stop */
(reset_fcn_t *)nulldev, /* reset */
NULL, /* tty's */
eno_select, /* select */
eno_mmap, /* mmap */
eno_strat, /* strategy */
eno_getc, /* getc */
eno_putc, /* putc */
0 /* type */
};
#在IDA中的内容
__DATA:__data:803BDE74 off_803BDE74 DCD sub_800C0E18+1 ; open
__DATA:__data:803BDE78 DCD sub_800C0E34+1 ; close
__DATA:__data:803BDE7C DCD sub_800C0EA0+1 ; read
__DATA:__data:803BDE80 DCD sub_800C0E38+1 ; write
__DATA:__data:803BDE84 DCD sub_800C0E04+1 ; ioctl
其他/dev/pf和/dev/ptmx就把啰嗦列举了。源码地址分别如下
http://www.opensource.apple.com/source/xnu/xnu-2782.30.5/bsd/net/pf_ioctl.c
http://www.opensource.apple.com/source/xnu/xnu-2782.30.5/bsd/kern/tty_ptmx.c
至于相对kernelcache有更多了解的朋友,可以参见
https://www.nowsecure.com/blog/2014/04/14/ios-kernel-reversing-step-by-step/
第三题 -------------------->
第三题虽然没有做出来,与大家分享一下我的分析过程,互相学习并望指点一下有没有更精妙的方法(感觉自己的方案有点太人肉计算机了)。
第三题倒是在我的ipad上能够运行。拿到第三题后感觉确实就难度增加了,这个就不能简单的用静态分析来解决问题了,需要使用lldb来动态调试跟踪了。我对arm7汇编还是熟悉,在这种疯跳的代码面前,还是选自己熟悉的汇编格式来分析更省事,所以搞了个iPhone 5c来,越狱装AppSync有是一顿折腾。外加Xcode6的lldb对armv7调试与IDA不同显示相同,从苹果网站下个Xcode5.0.2那个慢,又是一顿折腾。。
闲话少说,开始真题。这道题我是两个方案并行进行。一个是暴力破解方法,另外一个就是人工分析。
方案一:暴力破解
__text:0000B070 ; void __cdecl -[ViewController onClick](struct ViewController *self, SEL)
__text:0000B0C2 ADD R0, PC ; selRef_UTF8String
__text:0000B0C4 LDR R1, [R0] ; "UTF8String"
__text:0000B0C6 MOV R0, R6 ; R0为输入文本
__text:0000B0C8 BLX _objc_msgSend
__text:0000B0CC BL sub_1DCB8 ; checkPassword
其中sub_1DCB8是关键函数,返回1表示成功(“呵呵!正式在下!”),返回0表示失败(“大人,冤枉啊!不是我!”)
所以写个tweak,hook到sub_1DCB8,当然这个方案是下下策,也就是赌一下,毕竟人工分析与机器碰运气也是互相不耽误的。呵呵
下面是tweak大体内容如下:
int (*original_checkPassword)(const char*);
int replaced_checkPassword(const char* password) {
for (char* word in 暴力字典表) {
retValue = original_checkPassword(word);
if (retValue == 1) {
return retValue ;//爆破成功
}
}
}
%ctor
{
@autoreleasepool
{
MSImageRef image = MSGetImageByName("/var/mobile/Containers/Bundle/Application/076C360A-AA7C-473B-AF88-C5A1171D91AB/iOS题3.app/iOS题3") ;
//Look for our target method
unsigned int *sym = (unsigned int*)0x1dcb9 ;//0x1dcb8 + 1
MSHookFunction((void *)sym, (void *)replaced_checkPassword, (void **)&original_checkPassword);
}
}
可惜最终也没有爆破出来。
方案二:人工分析
下面是我的分析过程,可能写的会比较啰嗦,大家不要介意,也是希望这个帖子公布出来后,有大牛能够给我指点一二或给大家提个思路,我能提高学习一下。lldb调试就不在累述了,需要帮助的朋友可以参见《iOS应用逆向工程(第2版)》
首先,在
__text:0000B0CC BL sub_1DCB8 ; checkPassword
下断点,
(lldb) br s -a 0xb0cc
运行起来随便输入点,比如"wang",此时R0保存的是输入字符串的地址0x003660e0(以下地址或者内存内容为我当时分析时数据,因时因输入而异,仅供以下分析参考)。因为输入毕竟要使用,所以可以在输入地址0x003660e0下个观察点的断点。看看代码是从哪里开始处理这个字符串,关键也是这个代码跳的厉害,一步一步追踪很难找到关键点。
----下个读断点
(lldb) wa s e -w read -- 0x003660e0
Watchpoint created: Watchpoint 1: addr = 0x003660e0 size = 4 state = enabled type = r
new value: 1735287159
然后运行起来后,在这里断到读观察点,
__text:00020F76 STR.W R3, [R7,#-0x7C]
__text:00020F7A BLX _strlen ; check input words length
在这里下断点,查看一下数据,这个就是刚才输入的“wang",在根据_strlen,这不是求长度吗? 差不多找到只能说一部分关键点了,下来就跟着往下追踪了。看看有什么可获取的。解决这个程序中有两个地方是决定跳转到下一条地址的代码,在这两个地方下个断点,并加个打印条件打出下一条执行的代码段地址,这样就不会跑飞了。
片段1:
__text:00013ED8 LDR R0, [SP,#arg_8]
__text:00013EDC STR LR, [SP,#arg_8] ; LR = return address
__text:00013EE0 MOV LR, R0
片段2:
__text:00013ED8 LDR R0, [SP,#arg_8]
__text:00013EDC STR LR, [SP,#arg_8] ; LR = return address
__text:00013EE0 MOV LR, R0
(lldb) br s -a 0x13ee0
5: address = 0x00013ee0, ....
(lldb) br com add 5 <- 这个5是上面断点编号
# p/x $lr
# DONE
(lldb) br s -a 0x13f08
6: address = 0x00013f08, ....
(lldb) br com add 6 <- 这个5是上面断点编号
# p/x $lr
# DONE
知道关键函数开始点后,又能取到跳转下一条跳转地址后,那么下面就是开始单步。模式如下:
__text:00020F88 LDR.W R1, [R7,#-0x6C] <----我们关系的执行代码
__text:00020F8C PUSH {R0,R1,LR} <----从这里开始就开始调用调用地址重算函数了,所以在这个地址执行c就可以了
__text:00020F8E MOVW R0, #0x21D
__text:00020F92 BL LocationPCPosition
如下是我分析的讲字符串"wang"加密成数字的片段:
计算输入长度
__text:00020F76 STR.W R3, [R7,#-0x7C] r3=0xa3610b54 ro=输入地址
__text:00020F7A BLX _strlen ; check input words length
判断输入长度是否为零
__text:00020F96 CMP R0, #0 ; r0 为输入长度
__text:00021072 MOVW R1, #0 ; 此时r1为0
__text:00020FE4 IT EQ
__text:00020FE6 MOVEQ R1, #1 ; 如果输入长度为0,则R1赋值为1
__text:00020FD4 CMP R1, #0 ;如果r1=0, 长度不为0,否则长度为0,失败
__text:00020FD6 LDR.W R1, [R7,#-0x7C] ;r1 = 0xa3610b54
__text:00020FC6 LDR.W R2, [R7,#-0x78] ;r2 = 0xf029035e
__text:000211CA IT NE
__text:000211CC MOVNE R1, R2 ;r1=0xa3610b54 r2 = 0xf029035e
;保存初始密钥key
__text:00020FB4 STR.W R1, [R7,#-0x40] ;[R7,#-0x40]=0xa3610b54
;取第一个字符存[R7,#-0x41]
__text:00020FB8 LDRB.W R1, [R7,#-0x32] ;[R7,#-0x32]=0x00000077 r1=0x00000077 <-第一个字符'w' 取第一个字符
__text:00020FA2 STRB.W R1, [R7,#-0x41] ;[R7,#-0x41]=0x610b5477
;保存原始输入字符指针和长度到 [R7,#-0x48] 和 [R7,#-0x4C]
__text:00020FA6 LDR.W R1, [R7,#-0x68] ;r1 = 0x0030d5c0 <-输入字符串的地址 “wang”
__text:0002112E STR.W R1, [R7,#-0x48] ;[R7,#-0x48]=0x0030d5c0 <-输入字符串的地址 “wang”
__text:00021132 STR.W R0, [R7,#-0x4C] ;[R7,#-0x4C]=0x00000004 <-输入字符串的地址 “wang” 的长度
;初始化一个相关的KEY,从[R7,#-0x74]转存到[R7,#-0x50]
__text:00021204 LDR.W R0, [R7,#-0x74] ;[R7,#-0x74] = 0xffffffff 设置R0为0xffffffff
__text:00020F64 STR.W R0, [R7,#-0x50] ;[R7,#-0x50] = 0xffffffff
;初始化一个相关的KEY,从[R7,#-0x70]转存到[R7,#-0x54]
__text:00020F68 LDR.W R3, [R7,#-0x70] ;设置R3为 0x00000000
__text:0002119A STR.W R3, [R7,#-0x54] ;[R7,#-0x54] = 0x00000000
;找到初始化KEY是0xa3610b54的地方开始执行代码,并且将Key暂存在[R7,#-0x6C]
__text:000200C6 LDR.W R0, [R7,#-0x40] ;[R7,#-0x40] = 0xa3610b54, R0取值0xa3610b54
__text:000206A2 MOVW R1, #0xC15 ;
__text:00020722 MOVT.W R1, #0xAC47 ; R1=0xac470c15
__text:00020716 CMP R0, R1 ; R0=0xa3610b54 R1=0xac470c15
__text:000206FC STR.W R0, [R7,#-0x6C] ;[R7,#-0x6C]=0xa3610b54
__text:000206EE MOVW R0, #0xB54
__text:00020930 MOVT.W R0, #0xA361 ;R0 = 0xa3610b54
__text:00020934 LDR.W R1, [R7,#-0x6C] ;R1 = 0xa3610b54
__text:000206B0 CMP R1, R0 :R0 = 0xa3610b54, R1 = 0xa3610b54
__text:00021120 MOVW R0, #0x986
__text:00020F24 MOVT.W R0, #0x7C4A ; R0 = 0x7c4a0986
__text:00020F28 MOVW R1, #0xDC4D
__text:00020F56 MOVT.W R1, #0xAEFF ; R1 = 0xaeffdc4d
__text:00020F44 MOV R2, #0xFFFFFFFF ; R2 = 0xffffffff
__text:00020F48 MOVW R3, #0x98B0 ;
__text:0002125C MOVT.W R3, #0 ; R3 = 0x000098b0
__text:00021260 ADD R3, PC ; PC = 0x00021260, R3 = 0x000098b0 之后R3=0x0002ab14
;R9一个变动的KEY值,初始化的时候为0xffffffff
__text:0002126C LDR.W R9, [R7,#-0x50] ; [R7,#-0x50] 为-1,R9也变成R9 = 0xffffffff
;R12为输入字符串的长度
__text:00021270 LDR.W R12, [R7,#-0x4C] ; [R7,#-0x4C] 为0x00000004,R12也变成0x00000004 <-输入字符串的地址 “wang” 的长度
;LR为输入字符串在内存中的地址
__text:000212EC LDR.W LR, [R7,#-0x48] ; LR = 0x0030d5c0 <-输入字符串的地址 “wang”
;取的第一个字符
__text:000212F0 LDRB.W R4, [R7,#-0x41] ; R4 = 0x00000077
; 输入的字符串长度减一
__text:000212AC MOV R5, #0x3EE9B11C ; R5 = 0x3ee9b11c
__text:000212D0 SUB.W R12, R12, R5 ; R12 = 0x00000004, R5 = 0x3ee9b11c R12 = R12 - R5 = 0xc1164ee8
__text:000212DE ADD.W R12, R12, #0xFFFFFFFF ; 0xFFFFFFFF 也等于0 R12 = 0xc1164ee7
__text:000212BE MOV R5, #0x3EE9B11C ; R5 = 0x3ee9b11c
__text:0002133A ADD R12, R5 ; R12 = 0xc1164ee7 R5 = 0x3ee9b11c R12 = R12 + R5 = 0x00000003
;降减一后的字符长度,保存在[R7,#-0x30],并且下一个字符地址保存在[R7,#-0x2C]
__text:00021318 STR.W R12, [R7,#-0x30] ; [R7,#-0x30] = 0x00000003
;去第二个字符地址保存在[R7,#-0x2C]
__text:0002131C ADD.W R12, LR, #1 ; LR=0x0030d5c0 <-输入字符串的地址 R12 = LR + 1 = 0x0030d5c1 <- "ang"
__text:0002130A STR.W R12, [R7,#-0x2C] ; R12 = 0x0030d5c1 <- 即"ang"的地址, [R7,#-0x2C]= 0x0030d5c1
;R4=第一个字符, LR=初始化KEY异或#0xFFFFFFFF,R2为0x20F44地方初始化的常量0xFFFFFFFF
__text:0002129A AND.W R12, R4, #0xFF ; R4 = 0x00000077, R12 = R4 & 0xFF = 0x00000077
__text:0002129E EOR.W LR, R9, #0xFFFFFFFF ; R9 = 0xffffffff LR = R9 ^ 0xFFFFFFFF = 0x00000000
__text:000213F4 EOR.W R4, R2, #0xFF ; R2 = 0xffffffff, R4 = R2 ^ 0xFF = 0xffffff00
__text:000213F8 MOVW R5, #0x3A26
__text:0002127E MOVT.W R5, #0xA4D7 ; R5 = 0xa4d73a26
__text:00021282 EORS R5, R2 ; R5 = 0xa4d73a26, R2 = 0xffffffff R5 = R5 ^ R2 = 0x5b28c5d9
__text:000213B6 ORR.W LR, LR, R4 ; LR = 0x00000000, R4 = 0xffffff00, LR = LR | R4 = 0xffffff00
__text:000213BA MOVW R4, #0x3A26
__text:000213D6 MOVT.W R4, #0xA4D7 ; R4 = 0xa4d73a26
__text:000213DA ORRS R4, R5 ; R4 = 0xa4d73a26, R5 = 0x5b28c5d9, R4 = R4 | R5 = 0xffffffff
__text:000213E6 EOR.W LR, LR, #0xFFFFFFFF ; LR = 0xffffff00, LR = LR ^ 0xFFFFFFFF = 0x000000ff
__text:000213C8 AND.W LR, LR, R4 ; LR = 0x000000ff, LR = LR & R4 = 0x000000ff & 0xffffffff = 0x000000ff
;第一个字符R12
__text:0002138E EOR.W R4, R12, #0xFFFFFFFF ; R4 = 0xffffff88 R4=‘w' ^ 0xFFFFFFFF
__text:00021392 AND.W R4, R4, LR ; R4 = 0x00000088
;根据第一个字符和R3,计算出地址,然后取出来放R3,看来R3有大用
__text:00020F36 EOR.W LR, LR, #0xFFFFFFFF ; LR = 0x000000ff ^ 0xFFFFFFFF = 0xffffff00
__text:00020DDC AND.W R12, R12, LR ; R12 = 0x00000077 & 0xffffff00 = 0x00000000
__text:00020DE0 ORR.W R12, R12, R4 ; R12 = 0x00000088
__text:00020DCC MOV.W R12, R12,LSL#2 ; R12 = 0x00000220
__text:00020DD0 ADD R3, R12 ; R3 = 0x0002ab14, R3 = R3 + R12 = 0x0002ad34
__text:0002135E LDR R3, [R3] ; R3 = 0xe3630b12
__text:00020DBE MOVW R12, #8 ;
__text:00020CAE MOVT.W R12, #0 ; R12 = 0x00000008
__text:00020CB2 LSR.W R9, R9, R12 ; R9 = 0xffffffff R9 = R9 >> R12 = 0xffffffff >> 0x00000008 = 0x00ffffff
;R3为输入的字符进行相应操作,然后再取的一个值,对他进行0xFFFFFFFF EOR操作保存在R12
__text:00020CC0 EOR.W R12, R3, #0xFFFFFFFF ; R3 =0xe3630b12 , R12 = R3 ^ 0xFFFFFFFF = 0x1c9cf4ed
;然后与0x3802bd30进行 and 操作
__text:00020CC4 MOVW LR, #0xBD30
__text:00020D5C MOVT.W LR, #0x3802 ; LR = 0x3802bd30
__text:00020D60 AND.W R12, R12, LR ; R12 = R12 & LR = 0x0002112f
;R2为0x20F44地方初始化的常量0xFFFFFFFF
__text:00020CD2 MOVW LR, #0xBD30
__text:00020CF0 MOVT.W LR, #0x3802 ; LR = 0x3802bd30
__text:00020CF4 EOR.W R2, R2, LR ; R2 = 0xffffffff R2 = R2 ^ LR = 0xc7fd42cf
;R3为上面那个根据第一个字母算出来,然后查表的R3
__text:00020D6E ANDS R3, R2 ; R3 = 0xe3630b12 R3 = R3 & R2 = 0xc3610202
__text:00020D4A EOR.W LR, R9, #0xFFFFFFFF ; R9 = 0x00ffffff, LR = R9 ^ 0xFFFFFFFF = 0xff000000
__text:00020D4E MOVW R4, #0xBD30
__text:00020D3C MOVT.W R4, #0x3802 ; R4 = 0x3802bd30
__text:00020D2A AND.W LR, LR, R4 ; LR = 0x38000000
__text:00020D2E AND.W R2, R2, R9 ; R2 = 0x00fd42cf
__text:00020D7A ORR.W R3, R3, R12 ; R3 = 0xdb61b622S
__text:00020D7E ORR.W R2, R2, LR ; R2 = 0x38fd42cf
__text:00020D02 EORS R2, R3 ; R2 = 0xe39cf4ed
;上面进行一系列操作后,保存在[R7,#-0x28],这个地方以后估计要进行比较,下个读监听就知道在哪里判断了
__text:00020D1C STR.W R2, [R7,#-0x28] ; [R7,#-0x28] 中保存 0xe39cf4ed
.........
一堆无用代码或跳转代码,到下面
.........
; R0=剩余长度
__text:0001FEB0 LDR.W R0, [R7,#-0x30] ; R0 = 0x00000003
; R2=剩余字符串的字符串地址
__text:0001FE90 LDR.W R2, [R7,#-0x2C] ; R2 = 0x0030d5c1
; 第一个字符计算的结果
__text:0001FE94 LDR.W R3, [R7,#-0x28] ; R3 = 0xe39cf4ed
;;;;;;;;;
;;;;;;;;;第二个字符开始,后面的都是循环从这里开始处理的
;;;;;;;;;
; 保存第二个字符到[R7,#-0x41]
__text:0001FE82 STRB.W R1, [R7,#-0x41] ; R1 = 0x00000061 就是'a' 保存
; 保存第二个字符地址到[R7,#-0x48]
__text:00020092 STR.W R2, [R7,#-0x48] ; R2 = 0x0030d5c1
; 保存剩余长度到[R7,#-0x4C]
__text:0001FBCE STR.W R0, [R7,#-0x4C] ; R0 = 0x00000003
; 保存一个字符进行加密计算的结果到[R7,#-0x50]
__text:0001FBD2 STR.W R3, [R7,#-0x50] ; R3 = 0xe39cf4ed
以上是对输入字符进行加密的核心代码,循环调用上面的算法,进行运算。当找到关键代码前半部分执行开始和结束地址后,下面只需要在下两个端点,下次调试的时候,就可以跳过分析这段了,分别可以在如下两个地方下断点:
; R4为当前要处理的那个字符开始
__text:0002129A AND.W R12, R4, #0xFF ; R4 = 0x00000077, R12 = R4 & 0xFF = 0x00000077
; R2为上面R4字符处理完成后,运算结果保存地址。
__text:00020D1C STR.W R2, [R7,#-0x28] ; [R7,#-0x28] 中保存 0xe39cf4ed
通过处理"wang"过程中,[R7,#-0x28]分别被变更为:
w = 0xe39cf4ed
a = 0xe4ed53ff
n = 0x87ec4e81
g = 0x49eeab03
好在每次调试这几个字符,数值都不变化,也减少了调试的难度。
下来执行就一顿乱跳了,不知道所云,好在秉着生成的数据总是要用的原则,处理完输入字符后,给[R7,#-0x28]的地址下读观察点断点,来定位哪里开始使用这个关键数据的。
下个watchpoint,继续运行,会再一段代码断下来。由于所有程序在规定是时间内我没有分析完,而且在这个地方我也没有分析太多,所以就不在罗列了。
不知不觉,啰嗦了这么多,也算是对自己的这几天参加比赛的总结。其中肯定有比我分析方法高明的地方,只是罗列一下自己的愚见,望大家指点,共同提高。
也非常感谢看雪与主办方举行的第一和第二次比赛,对我帮助很大,尤其是对我这种对破解感兴趣的业余人员。提供了学习的目标和指引的方向。
再次感谢看雪论坛,见谅我平时只看帖不回贴的行为,呵呵
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
赞赏
看原图
赞赏
雪币:
留言: