目的
为第三方app提供运行时hidden api访问情况。
google提供了veridex工具,静态分析app所使用的hidden api。但是静态分析的结果可能不全,比如dex动态加载,加固等。
先了解一下开发者是如何访问hiddenapi的
1.通过反射,比如java.lang.Class;->getDeclaredField
2.通过env->GetFieldID等
该篇只谈反射的方案,如何通过ROM定制进行监测
getDeclaredField打点
下面是aosp10.0的源码贴图(art/runtime/native/java_lang_Class.cc)
四个关键点
1. getDeclaredField函数入口处常规打点
2.shouldDenyAccessToMember函数,会进行栈回溯
调用GetReflectionCaller(Thread* self),进行栈回溯
static hiddenapi::AccessContext GetReflectionCaller(Thread* self)
REQUIRES_SHARED(Locks::mutator_lock_) {
// Walk the stack and find the first frame not from java.lang.Class and not
// from java.lang.invoke. This is very expensive. Save this till the last.
struct FirstExternalCallerVisitor : public StackVisitor {
explicit FirstExternalCallerVisitor(Thread* thread)
: StackVisitor(thread, nullptr, StackVisitor::StackWalkKind::kIncludeInlinedFrames),
caller(nullptr) {
}
bool VisitFrame() override REQUIRES_SHARED(Locks::mutator_lock_) {
ArtMethod *m = GetMethod();
if (m == nullptr) {
// Attached native thread. Assume this is *not* boot class path.
caller = nullptr;
return false;
} else if (m->IsRuntimeMethod()) {
// Internal runtime method, continue walking the stack.
LOG(INFO) << "Trace GetReflectionCaller " << m->PrettyMethod();
return true;
}
ObjPtr<mirror::Class> declaring_class = m->GetDeclaringClass();
LOG(INFO) << "Trace GetReflectionCaller " << m->PrettyMethod();
if (declaring_class->IsBootStrapClassLoaded()) {
if (declaring_class->IsClassClass()) {
return true;
}
// Check classes in the java.lang.invoke package. At the time of writing, the
// classes of interest are MethodHandles and MethodHandles.Lookup, but this
// is subject to change so conservatively cover the entire package.
// NB Static initializers within java.lang.invoke are permitted and do not
// need further stack inspection.
ObjPtr<mirror::Class> lookup_class = GetClassRoot<mirror::MethodHandlesLookup>();
if ((declaring_class == lookup_class || declaring_class->IsInSamePackage(lookup_class))
&& !m->IsClassInitializer()) {
return true;
}
}
caller = m;
return false;
}
ArtMethod* caller;
};
FirstExternalCallerVisitor visitor(self);
visitor.WalkStack();
// Construct AccessContext from the calling class found on the stack.
// If the calling class cannot be determined, e.g. unattached threads,
// we conservatively assume the caller is trusted.
ObjPtr<mirror::Class> caller = (visitor.caller == nullptr)
? nullptr : visitor.caller->GetDeclaringClass();
if (caller.IsNull()) {
LOG(ERROR) << "Trace GetReflectionCaller==== failed";
} else {
LOG(INFO) << "Trace GetReflectionCaller==== " << visitor.caller->PrettyMethod();
}
return caller.IsNull() ? hiddenapi::AccessContext(/* is_trusted= */ true)
: hiddenapi::AccessContext(caller);
}
在函数返回前,打印了caller,方便定位代码负责人
3.hidden api访问被block,此处只是为了打印log
标题4.hidden api访问被allowed,此处只是为了打印log
getDeclaredMethodInternal定制与getDeclaredFiled类似
shell脚本对log进行分析
脚本目的:对adb logcat进行解析,统计hiddenapi及其caller
#!/bin/bash
#set -x
declare -i MAX_SEARCH=15
usage() {
echo -e "\033[1;32m"
cat <<EOF
<Author:whulzz@qq.com>
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# apicheck -h
# --file <file>: 日志文件
# --string <string>: 一般是包名得字串
#
# --level <level>: hidden-api等级
# level=1: whitelist
# level=2: greylist
# level=3: blacklist
# --pid <pid>: 只匹配日志中pid
# --output <file>: 将结果输出到文件
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
EOF
echo -e "\033[0m"
exit 1
}
function log_DEBUG() {
echo -e "\033[1;32m\c"
echo -n "$*"
echo -e "\033[0m"
}
function log_SUCCESS() {
echo -e "\033[1;34m\c"
echo -n "$*"
echo -e "\033[0m"
}
function log_WARN() {
echo -e "\033[1;33m\c"
echo -n "WARN:" "$*" >&2
echo -e "\033[0m"
}
function log_ERR() {
echo -e "\033[1;31m\c"
echo -n "ERROR:" "$*" >&2
echo -e "\033[0m"
}
function log_OUT() {
echo "$*" >>${OUTPUT}
}
function parse_arguments() {
while [[ -n "$1" ]]; do
case "$1" in
--string)
shift
STRING=$1
shift
;;
--pid)
shift
PID=$1
shift
;;
--level)
shift
LEVEL=$1
shift
;;
--file)
shift
FILE=$1
shift
;;
--output)
shift
OUTPUT=$1
shift
;;
--help)
usage
shift
exit 0
;;
*)
log_WARN "Unknown option: $1"
usage
exit 1
;;
esac
done
}
function check_args() {
if [[ -z "${FILE}" ]]; then
log_ERR "Unkown file!"
usage
exit 1
fi
if [ ! -f "${FILE}" ]; then
log_ERR "${FILE} not exist!"
exit 1
fi
if [[ -z "${OUTPUT}" ]]; then
OUTPUT="${PWD}/out.txt"
log_WARN "default output: ${OUTPUT}"
fi
FILE1="${PWD}/.tmp.log"
if [[ -f ${FILE1} ]]; then
rm -rf ${FILE1}
fi
touch ${FILE1}
if [[ -z "${STRING}" ]]; then
STRING=".*"
fi
if [[ -z ""${LEVEL} ]]; then
LEVEL=3
fi
if ((${LEVEL} > 3)); then
LEVEL=3
fi
if [[ -n "${PID}" ]]; then
SEARCH="${PID}"
fi
SEARCH="${SEARCH}.*${STRING}.*Trace getDeclared"
if [ ${LEVEL} -eq 3 ]; then
SEARCH="${SEARCH}.*ApiList=blacklist"
elif [ ${LEVEL} -eq 2 ]; then
SEARCH="${SEARCH}.*ApiList=(blacklist|greylist)"
else
SEARCH="${SEARCH}.*ApiList"
fi
}
function nearlyMatch() {
# maxLine=`awk 'END{print NR} ${FILE}'`
# if beg-- is "Trace getDeclared.*++++", pass
# Trace getDeclaredMethodInternal++++
beg=$1
tmpLine=`sed -n "${beg}p" ${FILE1}`
pid=`echo ${tmpLine} | awk '{print $4}'`
tid=`echo ${tmpLine} | awk '{print $5}'`
let beg--
passLine=`echo -e "${pid}( ){1,}${tid}.*Trace getDeclared(Field|MethodInternal)++++"`
matched=`sed -n "${beg}p" ${FILE1} | grep -E $passLine --color`
if [[ -n "${matched}" ]]; then
log_WARN "Ignore curLine: ${tmpLine}"
return
fi
for((i=0;i<${MAX_SEARCH};i++))
do
let curLine=beg-i
line=`sed -n "${curLine}p" ${FILE1}`
matched=`echo ${line}|grep -E "$2"`
if [[ -n "${matched}" ]]; then
caller=`echo ${line}|awk '{for (i=10;i<=NF;i++) printf("%s ", $i);print ""}'`
message="########hiddenapi caller: ${caller}"
log_SUCCESS "${message}"
log_OUT "${message}"
break
fi
done
}
function printTime() {
curtime=$(date "+%Y-%m-%d %H:%M:%S")
if [[ -z "${time}" ]]; then
time=${curTime}
else
time="BEGIN--${time} END--${curTime}"
fi
echo ${time}
}
function main() {
parse_arguments "$@"
check_args
echo "rm -rf ${OUTPUT}"
echo "final search ${SEARCH}"
rm -rf ${OUTPUT}
num=0
grep -E "${SEARCH}" -B ${MAX_SEARCH} ${FILE} > ${FILE1} 2>&1
sp="/-\|"
while read line;
do
let num++
# printf "\b${sp:num%${#sp}:1}"
matched=`echo -e "$line" | grep -E ${SEARCH}`
if [[ -n "${matched}" ]]; then
message="matched line(+${num}) ${line}"
log_DEBUG "${message}"
log_OUT "${message}"
pid=`echo $line | awk '{print $4}'`
tid=`echo $line | awk '{print $5}'`
nearlyMatch ${num} "${pid}( ){1,}${tid}.*${STRING}.*Trace GetReflectionCaller===="
fi
done < ${FILE1}
}
printTime
#fix newline
old_ifs=${IFS}
IFS=$'\n'
main $@
IFS=${old_ifs}
rm -rf ${FILE1}
printTime