近期学习ios逆向,也为了熟悉一下iOS开发正向。用objective-c整了一个越狱检测的crackme,然后用frida过一遍自己写的检测(真是没事找事)。代码粗糙,请见谅。
克隆项目,再用xcode安装到手机上
利用stat检查一些越狱后才有的敏感路径,如:/Applications/Cydia.app 和 /usr/sbin/sshd,以此来判断是否越狱。stat判断文件是否存在, 返回0则为获取成功,-1为获取失败。可通过hook stat,过掉检测
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
function hook_stat(is_pass){
var stat
=
Module.findExportByName(
'libSystem.B.dylib'
,
'stat'
);
Interceptor.attach(stat, {
onEnter: function(args) {
/
/
这里是方法被调用时的处理逻辑
/
/
args[
0
] 是 stat 方法的第一个参数,通常是文件路径
/
/
args[
1
] 是 stat 方法的第二个参数,这里可以添加其他参数的处理
console.log(
'stat is hooked: '
);
},
onLeave: function(retval){
if
(is_pass){
retval.replace(
-
1
);
console.log(`stat retval: ${Number(retval.toString())}
-
>
-
1
`);
}
}
});
}
|
越狱后会产生一些特殊的链接库,ipa可以通过_dyld_get_image_name来获取所有的链接库,再遍历匹配,判断是否为越狱设备。
可以通过分析找到ipa检测的dylib,再hook _dyld_get_image_name,将返回替换为合法dylib,过掉检测。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
function hook_dyld_get_image_name(is_pass){
let cheek_paths
=
[
"/Library/MobileSubstrate/MobileSubstrate.dylib"
,
]
let NSString
=
ObjC.classes.NSString;
let true_path
=
NSString.stringWithString_(
"/System/Library/Frameworks/Intents.framework/Intents"
);
let _dyld_get_image_name
=
Module.findExportByName(null,
"_dyld_get_image_name"
);
Interceptor.attach(_dyld_get_image_name, {
onEnter: function(args){
console.log(
"_dyld_get_image_name is hooked."
)
this.idx
=
eval
(args[
0
]).toString(
10
);
},
onLeave: function(retval){
let rtnStr
=
retval.readCString();
if
(is_pass){
for
(let i
=
0
;i<cheek_paths.length;i
+
+
){
if
(cheek_paths[i]
=
=
=
rtnStr.toString()){
retval.replace(true_path);
console.log(`replace: (${this.idx}) ${rtnStr}
=
> ${true_path}`)
}
}
}
}
})
}
|
越狱后会在手机上安装越狱设备,如cydia。可以通过 -[UIApplication canOpenURL:] 来检测是否能启动app。
可hook -[UIApplication canOpenURL:] 替换返回过掉检测。但canOpenURL方法 返回是个 BOOL,即YES/NO,也就是1和0的宏。但在Interceptor.attach里用 retval.replace()总是会导致app崩溃(不知道原理,望大佬指点)。
所以使用 Interceptor.replace() + NaviteCallback, 替换掉方法,使其固定返回 0,也就是 NO。但这个解法,也不能算是好方法。。。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function hook_canopenurl(is_pass){
let api
=
new ApiResolver(
"objc"
);
api.enumerateMatches(
"-[UIApplication canOpenURL:]"
).forEach((matche)
=
> {
console.log(
"canOpenURL is hooked."
);
if
(is_pass){
Interceptor.replace(matche.address, new NativeCallback((url_obj)
=
> {
return
0
;},
"int"
, [
"pointer"
]))
}
})
}
|
越狱后会产生特殊的文件和目录,可以通过 fileExistsAtPath 来检测,直接hook过掉
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/
/
-
[NSFileManager fileExistsAtPath:isDirectory:]
function hook_fileExistsAtPath(is_pass){
let api
=
new ApiResolver(
"objc"
);
let matches
=
api.enumerateMatches(
"-[NSFileManager fileExistsAtPath:isDirectory:]"
)
matches.forEach((matche)
=
> {
console.log(
"fileExistsAtPath is hooked."
);
if
(is_pass){
Interceptor.replace(matche.address, new NativeCallback((path, is_dir)
=
> {
console.log(ObjC.
Object
(path).toString(), is_dir)
return
0
;
},
"int"
, [
"pointer"
,
"bool"
]))
}
})
}
|
越狱后为root权限,可以在私有路径如 /private/ 下创建文件。如果创建文件无异常则越狱,反之。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
function hook_writeToFile(is_pass){
let api
=
new ApiResolver(
"objc"
);
api.enumerateMatches(
"-[NSString writeToFile:atomically:encoding:error:]"
).forEach((matche)
=
> {
Interceptor.attach(matche.address, {
onEnter: function(args){
this.error
=
args[
5
];
this.path
=
ObjC.
Object
(args[
2
]).toString();
console.log(
"writeToFile is hooked"
);
},
onLeave: function(retval){
if
(is_pass){
let err
=
ObjC.classes.NSError.alloc();
Memory.writePointer(this.error, err);
}
}
})
})
}
|
越狱后有些文件会被移动,但这个文件路径又必须存在,所以可能会创一个文件链接。ipa可以检测一些敏感路径是否是链接来判断是否越狱。
这里仅过掉路径检测(符号链接不会过T.T)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
/
/
oc 检测函数
+
(Boolean)isLstatAtLnk{
/
/
检测文件路径是否存在,是否是路径链接
Boolean result
=
FALSE;
NSArray
*
jbPaths
=
@[
@
"/Applications"
,
@
"/var/stash/Library/Ringtones"
,
@
"/var/stash/Library/Wallpaper"
,
@
"/var/stash/usr/include"
,
@
"/var/stash/usr/libexec"
,
@
"/var/stash/usr/share"
,
@
"/var/stash/usr/arm-apple-darwin9"
,
];
struct stat stat_info;
for
(NSString
*
jbPath
in
jbPaths){
char jbPathChar[jbPath.length];
memcpy(jbPathChar, [jbPath cStringUsingEncoding:NSUTF8StringEncoding], jbPath.length);
if
(lstat(jbPathChar, &stat_info)){
NSLog(@
"stat_info.st_mode: %hu, S_IFLNK: %d, %d"
, stat_info.st_mode, S_IFLNK, stat_info.st_mode & S_IFLNK);
if
(stat_info.st_mode & S_IFLNK){
result
=
TRUE;
NSLog(@
"是路径链接>> %@"
, jbPath);
}
}
else
{
NSLog(@
"路径不存在>> %@"
, jbPath);
result
=
TRUE;
}
}
return
result;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/
/
过lstat
function hook_lstat(is_pass){
var stat
=
Module.findExportByName(
'libSystem.B.dylib'
,
'lstat'
);
Interceptor.attach(stat, {
onEnter: function(args) {
console.log(
'lstat is hooked: '
);
},
onLeave: function(retval){
if
(is_pass){
retval.replace(
1
);
console.log(`lstat retval: ${Number(retval.toString())}
-
>
1
`);
}
}
});
}
|
未越狱的设备是无法fork子进程
hook fork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
function hook_fork(is_pass){
let fork
=
Module.findExportByName(null,
"fork"
);
if
(fork){
console.log(
"fork is hooked."
);
Interceptor.attach(fork, {
onLeave: function(retval){
console.log(`fork
-
> pid:${retval}`);
if
(is_pass){
retval.replace(
-
1
)
}
}
})
}
}
|
查看是否有注入异常的类,比如HBPreferences 是越狱常用的类,再用 NSClassFromString 判断类是否存在
通过分析找出检测的类名,再去hook NSClassFromString
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
function hook_NSClassFromString(is_pass){
let clses
=
[
"HBPreferences"
];
var foundationModule
=
Process.getModuleByName(
'Foundation'
);
var nsClassFromStringPtr
=
Module.findExportByName(foundationModule.name,
'NSClassFromString'
);
if
(nsClassFromStringPtr){
Interceptor.attach(nsClassFromStringPtr, {
onEnter: function(args){
this.
cls
=
ObjC.
Object
(args[
0
])
console.log(
"NSClassFromString is hooked"
);
},
onLeave: function(retval){
if
(is_pass){
clses.forEach((ck_cls)
=
> {
if
(this.
cls
.toString().indexOf(ck_cls) !
=
=
-
1
){
console.log(`nsClassFromStringPtr
-
> ${this.
cls
}
-
${ck_cls}`)
retval.replace(ptr(
0x00
))
}
})
}
}
})
}
}
|
通过getenv函数,查看环境变量DYLD_INSERT_LIBRARIES来检测是否越狱
hook getenv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
function hook_getenv(is_pass){
let getenv
=
Module.findExportByName(null,
"getenv"
);
Interceptor.attach(getenv, {
onEnter: function(args){
console.log(
"getenv is hook"
);
this.env
=
ObjC.
Object
(args[
0
]).toString();
},
onLeave: function(retval){
if
(is_pass && this.env
=
=
"DYLD_INSERT_LIBRARIES"
){
console.log(`env: ${this.env}
-
${retval.readCString()}`)
retval.replace(ptr(
0x0
))
}
}
})
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
|
function hook_stat(is_pass){
var stat
=
Module.findExportByName(
'libSystem.B.dylib'
,
'stat'
);
Interceptor.attach(stat, {
onEnter: function(args) {
/
/
这里是方法被调用时的处理逻辑
/
/
args[
0
] 是 stat 方法的第一个参数,通常是文件路径
/
/
args[
1
] 是 stat 方法的第二个参数,这里可以添加其他参数的处理
console.log(
'stat is hooked: '
);
},
onLeave: function(retval){
if
(is_pass){
retval.replace(
-
1
);
console.log(`stat retval: ${Number(retval.toString())}
-
>
-
1
`);
}
}
});
}
function hook_dyld_get_image_name(is_pass){
let cheek_paths
=
[
"/Library/MobileSubstrate/MobileSubstrate.dylib"
,
]
let NSString
=
ObjC.classes.NSString;
let true_path
=
NSString.stringWithString_(
"/System/Library/Frameworks/Intents.framework/Intents"
);
let _dyld_get_image_name
=
Module.findExportByName(null,
"_dyld_get_image_name"
);
Interceptor.attach(_dyld_get_image_name, {
onEnter: function(args){
console.log(
"_dyld_get_image_name is hooked."
)
this.idx
=
eval
(args[
0
]).toString(
10
);
},
onLeave: function(retval){
let rtnStr
=
retval.readCString();
if
(is_pass){
for
(let i
=
0
;i<cheek_paths.length;i
+
+
){
if
(cheek_paths[i]
=
=
=
rtnStr.toString()){
retval.replace(true_path);
console.log(`replace: (${this.idx}) ${rtnStr}
=
> ${true_path}`)
}
}
}
}
})
}
function hook_canopenurl(is_pass){
let api
=
new ApiResolver(
"objc"
);
api.enumerateMatches(
"-[UIApplication canOpenURL:]"
).forEach((matche)
=
> {
console.log(
"canOpenURL is hooked."
);
if
(is_pass){
Interceptor.replace(matche.address, new NativeCallback((url_obj)
=
> {
return
0
;},
"int"
, [
"pointer"
]))
}
})
}
/
/
-
[NSFileManager fileExistsAtPath:isDirectory:]
function hook_fileExistsAtPath(is_pass){
let api
=
new ApiResolver(
"objc"
);
let matches
=
api.enumerateMatches(
"-[NSFileManager fileExistsAtPath:isDirectory:]"
)
matches.forEach((matche)
=
> {
console.log(
"fileExistsAtPath is hooked."
);
if
(is_pass){
Interceptor.replace(matche.address, new NativeCallback((path, is_dir)
=
> {
console.log(ObjC.
Object
(path).toString(), is_dir)
return
0
;
},
"int"
, [
"pointer"
,
"bool"
]))
}
})
}
function hook_writeToFile(is_pass){
let api
=
new ApiResolver(
"objc"
);
api.enumerateMatches(
"-[NSString writeToFile:atomically:encoding:error:]"
).forEach((matche)
=
> {
Interceptor.attach(matche.address, {
onEnter: function(args){
this.error
=
args[
5
];
this.path
=
ObjC.
Object
(args[
2
]).toString();
console.log(
"writeToFile is hooked"
);
},
onLeave: function(retval){
if
(is_pass){
let err
=
ObjC.classes.NSError.alloc();
Memory.writePointer(this.error, err);
}
}
})
})
}
function hook_lstat(is_pass){
var stat
=
Module.findExportByName(
'libSystem.B.dylib'
,
'lstat'
);
Interceptor.attach(stat, {
onEnter: function(args) {
console.log(
'lstat is hooked: '
);
},
onLeave: function(retval){
if
(is_pass){
retval.replace(
1
);
console.log(`lstat retval: ${Number(retval.toString())}
-
>
1
`);
}
}
});
}
function hook_fork(is_pass){
let fork
=
Module.findExportByName(null,
"fork"
);
if
(fork){
console.log(
"fork is hooked."
);
Interceptor.attach(fork, {
onLeave: function(retval){
console.log(`fork
-
> pid:${retval}`);
if
(is_pass){
retval.replace(
-
1
)
}
}
})
}
}
function hook_NSClassFromString(is_pass){
let clses
=
[
"HBPreferences"
];
var foundationModule
=
Process.getModuleByName(
'Foundation'
);
var nsClassFromStringPtr
=
Module.findExportByName(foundationModule.name,
'NSClassFromString'
);
if
(nsClassFromStringPtr){
Interceptor.attach(nsClassFromStringPtr, {
onEnter: function(args){
this.
cls
=
ObjC.
Object
(args[
0
])
console.log(
"NSClassFromString is hooked"
);
},
onLeave: function(retval){
if
(is_pass){
clses.forEach((ck_cls)
=
> {
if
(this.
cls
.toString().indexOf(ck_cls) !
=
=
-
1
){
console.log(`nsClassFromStringPtr
-
> ${this.
cls
}
-
${ck_cls}`)
retval.replace(ptr(
0x00
))
}
})
}
}
})
}
}
function hook_getenv(is_pass){
let getenv
=
Module.findExportByName(null,
"getenv"
);
Interceptor.attach(getenv, {
onEnter: function(args){
console.log(
"getenv is hook"
);
this.env
=
ObjC.
Object
(args[
0
]).toString();
},
onLeave: function(retval){
if
(is_pass && this.env
=
=
"DYLD_INSERT_LIBRARIES"
){
console.log(`env: ${this.env}
-
${retval.readCString()}`)
retval.replace(ptr(
0x0
))
}
}
})
}
setImmediate(()
=
> {
hook_stat(true);
hook_dyld_get_image_name(true)
hook_canopenurl(true);
hook_fileExistsAtPath(true);
hook_writeToFile(true);
hook_lstat(true);
hook_fork(true);
hook_NSClassFromString(true);
hook_getenv(true)
})
|
检测的正向代码在项目的 JailBreakCheek 类下。单独过这些检测基本没啥难度,直接hook。但在真实app中还是重点在分析中,如何找到这些具体检测的点。这次分享的frida代码有点粗糙,啊哈哈,要实际使用还得再优化一下。并且可以多看看frida官方的脚本网站https://codeshare.frida.re/
后面有时间的话,再分享些其他类型的检测如frida检测,混淆代码或加固之类。
更多【 iOS越狱检测app及frida过检测】相关视频教程:www.yxfzedu.com