其实,这是一篇水文,没什么技术含量,都是些基础东西。样本是某比较流行的app,之前听说它有frida检测,正好有空就搂出来看看了。
frida检测常规的手段一般都是
而1、2、3都有个特征,那就是在frida_server运行的时候才存在,不论是否附加进程,因为这个app是在附加时退出,所以直接就排除了这几个。
然后就从所有检测的共同点入手了,所有检测的共同点就是字符串检测,而字符串匹配最常见的就是strstr函数
然后就hook掉修改返回值,之后就尴尬了。。。
当然还有一些其它的检测,比如里面还对.dex进行了比较,不过目的是处理frida挂不上去的问题,所以就没有细看比较这东西是干嘛的,盲猜可能是检测dex注入的。
上面的几个检测点直接用frida进行hook就行了
定位检测代码的位置还是简单的采用了打印栈的方式
结合maps文件就直接定位到libmsaoaidsec.so里面的0x1AEE4方法了,里面一堆的字符串解密函数不用管
想看每一个代码段都是什么字符串的话可以跑一下脚本自解密即可,或者直接动态打印,不过这里没有必要。
排除这些解密字符串的代码直接找到里面的函数调用的地方:
字符串解密后死循环每隔4秒检测一次,初略看了一下,大概是这么个情况,因为没有验证所以仅供参考
sub_23804初看貌似是对libart的某些函数的hook检测,有兴趣的可以分析看看,代码不适合直接stalker进行处理,因为里面像这种指令会导致死循环需要特殊处理,在系统的库代码里面这种原子指令到处都是
再则,里面好像用到了这个玩意
参考
接下来假设前面的检测手段都被修正了,那么前面的都得失效,比如:
这时候使用bpf检直就是降维打击,主要用到了bcc里面的opensnoop及trace.py脚本
opensnoop观察访问的文件日志 ,trace查看堆栈
比如,这里是对这个app进行进行文件追踪的日志
你会频繁的看到这类日志,而循环读取这类文件的,那最有可能就是检测点了
既然找到了切入点,那自然是通过trace的栈信息来定位了,trace的用户栈信息有几个问题,一个是栈信息不全,其次就是总是显示unknow的问题,这个有兴趣研究的可以参考大佬的文章 https://bbs.kanxue.com/thread-274546.htm
而我采用的是最笨最简单的一种方式,就是直接解析maps,将文件偏移与文件名关联起来直接替换trace脚本中的相关内容就行了,虽然没有解决特殊情况下栈不全的问题,但也勉强能用了,毕竟我的要求不高
效果如下:
然后定位的0x1ac50是哪呢,正好是我们前面分析的0x1AC00函数内部,结合我们前面看过的逻辑,不难看出来这栈信息是不全的,不过其实也无所位了,定位到了一个点,其它的也就不远了,比如就像我们之前的操作,直接hook打印堆栈信息等,或者按大佬的方式去处理也可以。
至于那个libtiny.so的栈信息,我就打开简单看了下:
上下翻了翻,能大致看到有读取maps并手动解析的操作,其它略过~(ps:因为不调试细节看不懂,还有就是懒!)
然后试了一下通过bpf修改open函数的返回值,基于opensnoop.py做的尝试性修改
其中my_strnstr是自定义实现的strstr函数,因为标准的strstr函数貌似会因为循环问题而过不了验证,所以实现了一个有限循环的strstr,再则,使用bpf_override_return需要内核开启CONFIG_BPF_KPROBE_OVERRIDE选项
开启和关闭效果如下:
bpf很好很强大,强大到颠覆性的改变,可以类比frida对安全行业的影响,而且是完全是高维打击,目前已经发现有很多大佬在搞这方面的研究了,有兴趣的可以报虫佬的课程系统的学一学,个人觉得很值得学
1.frida_server
名字检测
2.
端口号检测
3.D
-
Bus检测
4.maps
检测
5.
线程检测
6.
内存特征检测
1.frida_server
名字检测
2.
端口号检测
3.D
-
Bus检测
4.maps
检测
5.
线程检测
6.
内存特征检测
var ph
=
Module.findExportByName(null,
"strstr"
);
Interceptor.attach(ptr(ph), {
onEnter: function (args) {
this.filename
=
args[
0
]
this.checkname
=
args[
1
]
/
/
console.log(this.filename.readCString(), ptr(args[
1
]).readCString())
}, onLeave: function (retval) {
var s
=
ptr(this.filename).readCString()
if
(s.indexOf(
"gmain"
) >
=
0
){
console.log(
"gmain anti."
)
retval.replace(
0
)
}
else
if
(s.indexOf(
"gum-js-loop"
) >
=
0
){
console.log(
"gum-js-loop anti."
)
retval.replace(
0
)
}
else
if
(s.indexOf(
"linjector"
) >
=
0
){
console.log(
"linjector anti."
)
retval.replace(
0
)
}
else
if
(s.indexOf(
"/data/local/tmp"
) >
=
0
){
console.log(
"/data/local/tmp anti."
)
retval.replace(
0
)
}
}
})
var ph
=
Module.findExportByName(null,
"strstr"
);
Interceptor.attach(ptr(ph), {
onEnter: function (args) {
this.filename
=
args[
0
]
this.checkname
=
args[
1
]
/
/
console.log(this.filename.readCString(), ptr(args[
1
]).readCString())
}, onLeave: function (retval) {
var s
=
ptr(this.filename).readCString()
if
(s.indexOf(
"gmain"
) >
=
0
){
console.log(
"gmain anti."
)
retval.replace(
0
)
}
else
if
(s.indexOf(
"gum-js-loop"
) >
=
0
){
console.log(
"gum-js-loop anti."
)
retval.replace(
0
)
}
else
if
(s.indexOf(
"linjector"
) >
=
0
){
console.log(
"linjector anti."
)
retval.replace(
0
)
}
else
if
(s.indexOf(
"/data/local/tmp"
) >
=
0
){
console.log(
"/data/local/tmp anti."
)
retval.replace(
0
)
}
}
})
console.log(Thread.backtrace(this.context, Backtracer.FUZZY).
map
(DebugSymbol.fromAddress).join(
"\n"
));
console.log(Thread.backtrace(this.context, Backtracer.FUZZY).
map
(DebugSymbol.fromAddress).join(
"\n"
));
sub_1A940 线程名检测
sub_1AAEC 通过
/
proc
/
self
/
fd文件的实际路径检测是否存在匹配的文件
sub_1AC00 maps检测
sub_23804 xxx内容有点大,需要具体分析
sub_1A940 线程名检测
sub_1AAEC 通过
/
proc
/
self
/
fd文件的实际路径检测是否存在匹配的文件
sub_1AC00 maps检测
sub_23804 xxx内容有点大,需要具体分析
v2
=
dlopen(
"libart.so"
,
0
);
if
( !v2 )
goto LABEL_23;
v3
=
v2;
...
off_47728[
0
]
=
(
int
*
)dlsym(v3, (const char
*
)&v38);
dlclose(v3);
goto LABEL_23;
...
LABEL_23:
v0
=
off_47728[
0
];
if
( off_47728[
0
] )
return
(unsigned
int
)
*
v0 >>
1
=
=
0x2C000028
;
return
0LL
;
v2
=
dlopen(
"libart.so"
,
0
);
if
( !v2 )
goto LABEL_23;
v3
=
v2;
...
off_47728[
0
]
=
(
int
*
)dlsym(v3, (const char
*
)&v38);
dlclose(v3);
goto LABEL_23;
...
LABEL_23:
v0
=
off_47728[
0
];
if
( off_47728[
0
] )
return
(unsigned
int
)
*
v0 >>
1
=
=
0x2C000028
;
return
0LL
;
strstr函数自定义
相应的方法采用svc的方式调用
...
strstr函数自定义
相应的方法采用svc的方式调用
...
python3 opensnoop
-
u
10094
python3 trace.py
-
-
uid
10094
'do_sys_openat2 "%s", arg2@user'
-
U
-
-
address
-
v
-
f maps
python3 opensnoop
-
u
10094
python3 trace.py
-
-
uid
10094
'do_sys_openat2 "%s", arg2@user'
-
U
-
-
address
-
v
-
f maps
class
MapsItem:
def
__init__(
self
, startAddr, endAddr, fOffset, path):
self
.startAddr
=
int
(startAddr,
16
)
self
.endAddr
=
int
(endAddr,
16
)
self
.fileOffset
=
int
(fOffset,
16
)
self
.path
=
path
def
update(
self
, startAddr
=
None
, endAddr
=
None
, path
=
None
):
if
startAddr
is
not
None
:
self
.startAddr
=
startAddr
if
endAddr
is
not
None
:
self
.endAddr
=
endAddr
if
path
is
not
None
:
self
.path
=
path
def
__str__(
self
):
return
"MapsItem{%s-%s %s %s}"
%
(
hex
(
self
.startAddr),
hex
(
self
.endAddr),
hex
(
self
.fileOffset),
self
.path)
class
PidItem(
object
):
pid
=
0
lst_segments
=
[]
def
__init__(
self
, pid):
self
.pid
=
pid
def
printLst(
self
):
for
item
in
self
.lst_segments:
print
(
"%s"
%
(item))
class
KKStackCache:
pidmap
=
{}
def
__init__(
self
, pid):
if
pid
in
KKStackCache.pidmap:
pass
else
:
KKStackCache.parserMaps(pid)
@staticmethod
def
parserMaps(pid):
try
:
fname
=
"/proc/%d/maps"
%
(pid)
f
=
open
(fname)
pattern
=
re.
compile
(r
'([\w]+)-([\w]+)\s+([\w\-]+)\s+([\w\-]+)\s+([\w\:]+)\s+([\d]+)\s+(\S+(?:\s+\S+)*?)*\s*$'
)
KKStackCache.pidmap[pid]
=
PidItem(pid)
mapsdata
=
[]
lines
=
f.readlines()
for
line
in
lines:
match
=
re.match(pattern, line)
if
match:
mapsItem
=
MapsItem(match.group(
1
), match.group(
2
), match.group(
4
), match.group(
7
))
mapsdata.append(mapsItem)
else
:
print
(
"No match:"
+
line)
KKStackCache.pidmap[pid].lst_segments
=
sorted
(mapsdata, key
=
lambda
x: x.startAddr)
f.close()
except
:
pass
print
(
"parser %d over."
%
pid)
@staticmethod
def
addrInfo(pid, addr):
pidmap
=
KKStackCache.pidmap.get(pid)
if
pidmap
is
not
None
:
lst
=
pidmap.lst_segments
bg
=
0
;
end
=
len
(lst)
-
1
while
bg <
=
end:
mid
=
int
((bg
+
end
+
1
)
/
2
)
if
addr < lst[mid].startAddr:
end
=
mid
-
1
elif
addr >
=
lst[mid].endAddr:
bg
=
mid
+
1
else
:
itm
=
lst[mid]
path
=
itm.path
offset
=
itm.fileOffset
+
(addr
-
itm.startAddr)
return
True
, addr, offset, path
return
False
,
None
,
None
,
None
@staticmethod
def
printLst(pid):
pidmap
=
KKStackCache.pidmap.get(pid)
if
pidmap
is
not
None
:
pidmap.printLst()
if
pid >
0
:
name, offset, module
=
BPF._sym_cache(tgid).resolve(addr,
False
)
if
name
is
None
:
KKStackCache(pid)
isExist, baseAddr, offset, path
=
KKStackCache.addrInfo(pid, addr)
if
not
isExist:
KKStackCache.parserMaps(pid)
isExist, baseAddr, offset, path
=
KKStackCache.addrInfo(pid, addr)
if
isExist:
if
path
is
not
None
:
symstr
=
"%s %s"
%
(
hex
(offset), path)
class
MapsItem:
def
__init__(
self
, startAddr, endAddr, fOffset, path):
self
.startAddr
=
int
(startAddr,
16
)
self
.endAddr
=
int
(endAddr,
16
)
self
.fileOffset
=
int
(fOffset,
16
)
self
.path
=
path
def
update(
self
, startAddr
=
None
, endAddr
=
None
, path
=
None
):
if
startAddr
is
not
None
:
self
.startAddr
=
startAddr
if
endAddr
is
not
None
:
self
.endAddr
=
endAddr
if
path
is
not
None
:
self
.path
=
path
def
__str__(
self
):
return
"MapsItem{%s-%s %s %s}"
%
(
hex
(
self
.startAddr),
hex
(
self
.endAddr),
hex
(
self
.fileOffset),
self
.path)
class
PidItem(
object
):
pid
=
0
lst_segments
=
[]
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!