hiddenapi运行时监控

目的

为第三方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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值