目的
為第三方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:[email protected]>
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# 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