一
背景
二
初试(完结)
1.frida_server名字检测
2.端口号检测
3.D-Bus检测
4.maps检测
5.线程检测
6.内存特征检测
然后就从所有检测的共同点入手了,所有检测的共同点就是字符串检测,而字符串匹配最常见的就是strstr函数。
上面的几个检测点直接用frida进行hook就行了:
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"));
排除这些解密字符串的代码直接找到里面的函数调用的地方:
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;
四
尝试bpf定位
接下来假设前面的检测手段都被修正了,那么前面的都得失效,比如:
strstr函数自定义
相应的方法采用svc的方式调用
...
python3 opensnoop -u 10094
python3 trace.py --uid 10094 'do_sys_openat2 "%s", arg2@user' -U --address -v -f maps
比如,这里是对这个app进行进行文件追踪的日志:
既然找到了切入点,那自然是通过trace的栈信息来定位了,trace的用户栈信息有几个问题,一个是栈信息不全,其次就是总是显示unknow的问题,这个有兴趣研究的可以参考大佬的文章https://bbs.kanxue.com/thread-274546.htm。
而我采用的是最笨最简单的一种方式,就是直接解析maps,将文件偏移与文件名关联起来直接替换trace脚本中的相关内容就行了,虽然没有解决特殊情况下栈不全的问题,但也勉强能用了,毕竟我的要求不高。
# 定义
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 = [] # MapsItem
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)
# fname = "maps"
# print("open %s" % fname)
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)
# print("mid %d: %s %s, addr:%s" % (mid, hex(lst[mid].startAddr), hex(lst[mid].endAddr), hex(addr)))
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()
# 使用
# 给_stack_to_string函数增加一个pid参数 def _stack_to_string(self, bpf, stack_id, tgid, pid):
# 在处理栈信息的地方加上下面的代码
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)
然后定位的0x1ac50是哪呢,正好是我们前面分析的0x1AC00函数内部,结合我们前面看过的逻辑,不难看出来这栈信息是不全的,不过其实也无所位了,定位到了一个点,其它的也就不远了,比如就像我们之前的操作,直接hook打印堆栈信息等,或者按大佬的方式去处理也可以。
至于那个libtiny.so的栈信息,我就打开简单看了下:
//在trace_return里面增加代码
if(my_strnstr((const char*)data.name, "maps", 256, 5) != NULL){
bpf_override_return(ctx, -1);
}
开启和关闭效果如下:
看雪ID:lxcoder
https://bbs.kanxue.com/user-home-719948.htm
# 往期推荐
1、【NKCTF】babyHeap-Off by one&Tcache Attack
球分享
球点赞
球在看
点击阅读原文查看更多
原文始发于微信公众号(看雪学苑):bpf在android逆向中的辅助效果