查看原文
其他

bpf在android逆向中的辅助效果

lxcoder 看雪学苑 2024-04-20




背景


样本是某比较流行的app,之前听说它有frida检测,正好有空就搂出来看看。




初试(完结)


frida检测常规的手段一般都是:

1.frida_server名字检测
2.端口号检测
3.D-Bus检测
4.maps检测
5.线程检测
6.内存特征检测

而1、2、3都有个特征,那就是在frida_server运行的时候才存在,不论是否附加进程,因为这个app是在附加时退出,所以直接就排除了这几个。

然后就从所有检测的共同点入手了,所有检测的共同点就是字符串检测,而字符串匹配最常见的就是strstr函数。



然后就hook掉修改返回值,之后就尴尬了。



当然还有一些其它的检测,比如里面还对.dex进行了比较,不过目的是处理frida挂不上去的问题,所以就没有细看比较这东西是干嘛的,盲猜可能是检测dex注入的。

上面的几个检测点直接用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"));



结合maps文件就直接定位到libmsaoaidsec.so里面的0x1AEE4方法了,里面一堆的字符串解密函数不用管。



想看每一个代码段都是什么字符串的话可以跑一下脚本自解密即可,或者直接动态打印,不过这里没有必要。

排除这些解密字符串的代码直接找到里面的函数调用的地方:


字符串解密后死循环每隔4秒检测一次,初略看了一下,大概是这么个情况,因为没有验证所以仅供参考。

sub_1A940 线程名检测
sub_1AAEC 通过/proc/self/fd文件的实际路径检测是否存在匹配的文件
sub_1AC00 maps检测
sub_23804 xxx内容有点大,需要具体分析

sub_23804初看貌似是对libart的某些函数的hook检测,有兴趣的可以分析看看,代码不适合直接stalker进行处理,因为里面像这种指令会导致死循环需要特殊处理,在系统的库代码里面这种原子指令到处都是。



再则,里面好像用到了这个:

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的方式调用
...

这时候使用bpf检直就是降维打击,主要用到了bcc里面的opensnoop及trace.py脚本。

python3 opensnoop -u 10094
python3 trace.py --uid 10094 'do_sys_openat2 "%s", arg2@user' -U --address -v -f maps

opensnoop观察访问的文件日志 ,trace查看堆栈。

比如,这里是对这个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的栈信息,我就打开简单看了下:



上下翻了翻,能大致看到有读取maps并手动解析的操作,其它略过~(ps:因为不调试细节看不懂,还有就是懒。

然后试了一下通过bpf修改open函数的返回值,基于opensnoop.py做的尝试性修改:

//在trace_return里面增加代码
if(my_strnstr((const char*)data.name, "maps", 256, 5) != NULL){
bpf_override_return(ctx, -1);
}

其中my_strnstr是自定义实现的strstr函数,因为标准的strstr函数貌似会因为循环问题而过不了验证,所以实现了一个有限循环的strstr,再则,使用bpf_override_return需要内核开启CONFIG_BPF_KPROBE_OVERRIDE选项
开启和关闭效果如下:



bpf很好很强大,强大到颠覆性的改变,可以类比frida对安全行业的影响,而且是完全是高维打击。



看雪ID:lxcoder

https://bbs.kanxue.com/user-home-719948.htm

*本文为看雪论坛优秀文章,由 lxcoder 原创,转载请注明来自看雪社区



# 往期推荐

1、【NKCTF】babyHeap-Off by one&Tcache Attack

2、Qemu源码浅析之v0.1.6

3、堆利用学习:the house of einherjar

4、Bugku CTF安卓逆向LoopAndLoop

5、CVE-2021-32760漏洞分析与复现

6、Hikari源码分析 - AntiClassDump



球分享

球点赞

球在看



点击阅读原文查看更多

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存