5月5号的时候收到了来自 teamsik 的 local hacker 邀请,不过由于机票太贵,负担不起,就没去了.

背景 && 摘要

本博文是在比赛结束后才发出来的.

前不久在微博上看到有人发 Teamsik Android CTF,于是就放到浏览器书签里了, 由于当时只是说有这么个比赛,什么时候开始还没说,放在书签里好长时间,上周六(2017-04-15)晚上的时候,无事可做,就去翻翻看书签, 突然发现了这个东西, 赶紧打开一看,题目已经给出来了,放在 google drive 上,我看了看提交日期截止五月三日,还好没错过. 于是就做了一下,题目不是很难.在此简单记录一下过程.

环境

Ubuntu 16.04 LTS x64 + apktool 2.1.1 + Android Studio(with smalidea plugin installed) + jadx 0.0.1 + Windows 10 虚拟机(with IDA Pro 6.8 installed)

一共三个apk:

此文按此顺序来写题解.


Easy Writeup

使用 jadx 打开该apk,我们可以看到 主要有两个类: MainActivity,Verifier ,函数代码很简单, 在 MainActivity 中调用了 Verifier 中的 verifyPassword 方法来检测输入. 查看 Verifier 类,可知它的主要方法如下:

public static boolean verifyPassword(Context context, String input) {
    if (input.length() != 4) {
        return false;
    }
    byte[] v = encodePassword(input);
    byte[] p = "09042ec2c2c08c4cbece042681caf1d13984f24a".getBytes();
    if (v.length != p.length) {
        return false;
    }
    for (int i = 0; i < v.length; i++) {
        if (v[i] != p[i]) {
            return false;
        }
    }
    return true;
}

输入长度必须为 4,然后比对输入编码为一个 byte 数组,再和内置字符串生成的 byte 数组比 较,如果相等则成功.这里不是直接去看算法到底做了什么,我们要的是什么才是正确的 flag, 由于是 4 位长度,且都是数字,那么我们采用暴力法,从0000遍历到9999,逐一尝试看哪个正确. 因此我写出了下面的 Java 代码:

import java.io.*;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Jcode
{
    public static void main(String[] args)
    {
        String input = "";
        for(int i = 0;i < 10000; ++i)
        {
            //规格化字符串
            input = "";
            if(i < 10) {input = "000" +Integer.toString(i); continue;}
            if(i < 100) {input = "00" +Integer.toString(i); continue;}
            if(i < 1000) {input = "0" +Integer.toString(i); continue;}
            input = Integer.toString(i);

            //编码输入的字符串
            byte[] v = encodePassword(input);
            byte[] p = "09042ec2c2c08c4cbece042681caf1d13984f24a".getBytes();

            int flag = 1;
            for (int j = 0; j < v.length; j++)
            {
                if(v[j] != p[j]) flag  = 0;
            }

            //判断
            if(flag == 1)
            {
                System.out.printf(input);
                break;
            }
        }

    }

  //-------以下一下代码直接拷贝自 jadx 的反编译结果------
  private static byte[] encodePassword(String input) {
        byte[] SALT = new byte[]{(byte) 95, (byte) 35, (byte) 83, (byte) 73, (byte) 75, (byte) 35, (byte) 95};
        try {
            StringBuilder sb = new StringBuilder();
            sb.append((char) SALT[0]);
            sb.append((char) SALT[1]);
            for (int i = 0; i < input.length(); i++) {
                sb.append((char) input.getBytes("ISO-8859-1")[i]); //这里注意是大写的ISO-8859-4
                sb.append((char) SALT[i + 2]);
            }
            sb.append((char) SALT[6]);
            byte[] result = new byte[0];
            return SHA1(sb.toString()).getBytes("ISO-8859-1");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }
  private static String convertToHex(byte[] data) {
        StringBuffer buf = new StringBuffer();
        for (int i = 0; i < data.length; i++) {
            int halfbyte = (data[i] >>> 4) & 15;
            int two_halfs = 0;
            while (true) {
                if (halfbyte < 0 || halfbyte > 9) {
                    buf.append((char) ((halfbyte - 10) + 97));
                } else {
                    buf.append((char) (halfbyte + 48));
                }
                halfbyte = data[i] & 15;
                int two_halfs2 = two_halfs + 1;
                if (two_halfs >= 1) {
                    break;
                }
                two_halfs = two_halfs2;
            }
        }
        return buf.toString();
    }

    private static String SHA1(String text) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            byte[] sha1hash = new byte[40];
            md.update(text.getBytes("iso-8859-1"), 0, text.length());
            return convertToHex(md.digest());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        } catch (UnsupportedEncodingException e2) {
            e2.printStackTrace();
            return null;
        }
    }
}

编译并执行上述代码,我们可以得到flag为: 9083

Trivial Writeup

安装 smalidea

这里我将展示如何无源码调试 apk. 首先下载 smalidea , 你会得到一个 zip 包,打开 Android Studio,依次 Configure/Settings/Plugins/Install plugin from disk..., 选择下载的 smalidea 压缩包即可安装.

新建smali工程并设置断点

我们使用 apktool 反编译 Qualification-1.3-trivial-release.apk , 得到一个 目录 Qualification-1.3-trivial-release,打开 Android Studio, 然后 Import project(Eclipse ADT,Gradle,etc)选择刚刚 apktool 生成 的目录,接着选择 Create project from existing sources,其余的都按默 认就可以.这样的话,我们就新建了一个 smali 工程,如下图1所示:

图1:新建 smali 工程

我们单击图1中红框处,在下拉列表中选择Project,然后就变成了工程结构,如图2所示:

图2:smali 工程的 Project 视图

接下来我们可以使用 jadx 打开apk,对照这java代码,在smali里面找到关键地方,按下 Ctrl+F8 添加断点,

安装apk并开始调试

在安装前,应该修改 AndroidManifest.xml文件,添加 android:debuggable="true", 当你修改完后,需要重新打包,签名安装等,我写了个自动化脚本完成这些工作:

#! /bin/bash
#---------------------------------------------
# script name:apksign
# usage:apksign apk_project_folder
# 2016-07-27 Wed 11:24 AM
# by bugnofree  www.ahageek.com
# 上面的工程文件夹是apktool反编译之后生成的文件夹,
# 第一次使用 apksign 时会要求输入一些信息,
# 关键是秘钥值,为and.key,其他随意输入,
# 执行一次后,以后打包只需要输入秘钥and.key即可自动打包.
#---------------------------------------------
# Android Applications Repack Script
# You should setup the following PATH variables:
#   export PATH=${PATH}:${ANDROID_SDK}/tools
#   export PATH=${PATH}:${ANDROID_SDK}/platform-tools
#   export PATH=${PATH}:${ANDROID_SDK}/build-tools/23.0.2


if [ $# != 1 ]
then
    echo "USAGE:$0 APK_PROJECT_DIR"
    exit 1
fi
key_name="and.key"
key_psd="and.key"
key_alias="andkey"
#Rebuild Applications 
apk_path=${1}"/dist/"
apk=${apk_path}${1}".apk"
apktool b ${1} -o ${apk}
#generate sign key
if [ ! -f "$key_name" ]
then 
    keytool -genkey -v -keystore ${key_name} -alias ${key_alias} -keyalg RSA -keysize 2048 -validity 20000
fi

echo "Sign the apk..."
#to sign the apk
jarsigner -verbose -sigalg MD5withRSA -digestalg SHA1 -keystore ${key_name} ${apk} ${key_alias}
jarsigner -verify -certs ${apk}

echo Get package name of $apk ...
#Uinstall firstly
#The "-r" option in sed is used to enable extend Reguler Expression which
#could make your life more convient when you write regular expression.
#Command "s" is used to replace the output with the matched strings.
#Format:sed option s/pattern/replace/soption
pkg_name=$(aapt dump badging "${apk}" | grep package | sed -r "s/package: name='([a-z0-9.]*)'.*/\1/")

echo "Uninstall the apk..."

adb uninstall ${pkg_name}

echo "Install the apk..."
#Install The Newer Apk Agin
adb install ${apk}

你需要适当的做出一些修改,比如环境配置,才可以让该脚本正常运行. 在手机上打开程序使其运行,然后回到 studio,在studio的工具栏有一个按钮 Attach debugger to Android process, 点击后如下图3所示:

图3:Android Studio附加进程

附加后,在studio底部栏中的console TAB中将会看到以下输出:

 Connected to the target VM, address: 'localhost:8600', transport: 'socket' 

然后在手机上输入一些字符,将会在 studio 中断下,如下图4所示:

图4:断点

于是很快的我们就得到了本apk的flag为: AndroidHackingIsFun

Intermediate writeup

调试本 APK 请务必用真机调试,因为我试了原生的诸多版本Android模拟器在IDA中都会出现 SIGIIL 错误.用真机则没有这个问题.这个问题暂时不知道原因,刚开始以为是Android SDK模拟器 的版本不对,于是就根据 APK 编译的目标平台重新建立了一个模拟器,测试仍然是无效的. 因此,调试该 so 文件我使用的真机.

由于我的工作环境略微有点复杂:windows10的虚拟机中安装了IDA Pro 6.8,然后在主机上安装了 Android SDK,也就是说adb是在我主机上起作用的,模拟器/手机是直接和主机相连接的, 因此对于 android_server 所用的 TCP 协议以及 jdwp 协议需要进行两次端口转发, 每一次都要做挺多步骤,因此我写了个 bash 脚本来自动化完成这些繁琐的工作.

#! /bin/bash

#文件说明
#IDA动态调试so库半自动化脚本
#针对每个应用程序,你需要修改 activity,filter,andsrv 变量


#apk的主activity名称
activity=org.teamsik.ahe17.qualification.intermediate/org.teamsik.ahe17.qualification.MainActivity
#进程的可唯一过滤的字符串
filter=intermedia
# android_server 路径
andsrv=/opt/idasodebug/


echo "Please confirm jobs listed as follows is done:"
echo "+1:Start android emulator/phone (only one instance)"
echo "+2:Install the target application in your android emulator/phone"
read -p  "Press any key to continue if you are done...."

#-------------清理进程-----------
echo "#1: Clean progress...."

ps -elf | grep adb | awk '{print $4}'| xargs kill -9 $1
ps -elf | grep redir | awk '{print $4}'| xargs kill -9 $1
ps -elf | grep jdb | awk '{print $4}'| xargs kill -9 $1
#终结android_server
andsrv_pid=$(adb shell ps |grep android_server |awk '{print $2}')
adb shell "su -c 'kill -9 $andsrv_pid' "
adb start-server
sleep 1

#-------------启动android_server-----------
echo "# Start up android_server..."
adb push $andsrv/android_server /data/local/tmp/
#注意,必须以root权限运行android_server,否则IDA在附加进程时不显示其他进程
#特别是针对真机调试时,通过adb shell执行root权限的命令,方法如下:
#adb shell "su -c 'command'"
adb shell "su -c '/data/local/tmp/android_server' " &




#-------------协议转发,使得虚拟机中的IDA可以连接宿主机中的虚拟机-------------
echo "#2:ADB port forward IDA Pro...."
#获取主机IP地址(暂时不知awk的语法)
ip=$(ip route get 1 | awk '{print $NF;exit}')
echo "Your host IP address is $ip "
adb forward tcp:23946 tcp:23946
#放到子shell中执行,(每个)命令必须以分号结尾
redir --lport=23946 --laddr=$ip --cport=23946 --caddr=localhost &



#--------------启动apk-------------
echo "#3:Start up  activity...."
adb shell am start -D -n $activity

read -p "Go on after you attach your IDA to your target application...ensure you have setup Debugger Option!"



#--------------获取apk的pid然后连接java调试器-------------
#取得pid所在行
pidline=$(adb shell ps | grep $filter)
#分割字符串
IFS=' ' read -a pidsplit <<< "${pidline}"
#目标进程的pid
pid=${pidsplit[1]}
echo "The PID of your target application is $pid"
echo "#4:ADB port forward for jdwp...."
adb forward tcp:7788 jdwp:$pid
#后台执行
redir --lport=7788 --laddr=$ip  --cport=$pid --caddr=localhost &
echo "#5:Now go to your IDA and setup the Debugger Option!Press F9 to run the program until you encounter your target so lib then begin your debug travel..."
echo "  jdb will give ouput \"Initializing jdb ...\" if you are do in the right way..."
jdb -connect com.sun.jdi.SocketAttach:port=7788,hostname=localhost
exit

上面说一下如何使用,在命令行上运行该脚本(名称为: idaso), 首先它会让你确认两件事情:

如果以上两件事情都完成了,那么按回车键继续.之后它将会清理进程,做的事情无非就是杀掉 adb什么的再重启,然后会上传并启动 android_server(android_server应该和 idaso 脚本位于同一目录 ),这些准备完后,将会以debug模式启动apk,启动后会要求你去设置IDAPro的远程连接,IP已经输出 了,此时进入IDA Pro,一次 Debugger/Attach/Remote Android Debugger,按如下图5设置:

图5:IDA Pro设置

之后在弹出的界面中选择要attach的进程,如下图6设置:

图6:IDA Pro选择要attach的进程

Attach之后就可以回到那个脚本界面,按回车继续.之后就可以再回到 IDA Pro里进行调试了. 在 IDA 中,我们需要再次设置调试选项,然后按下 F9 运行,这个apk需要 F9 多次,然后会弹出如 下图7对话框:

图7:lib缺失

点击取消即可,直到程序跑起来,然后在程序中输入一个测试字符串,如123456789,然后按下 VERIFY PASSWORD按钮,IDA将会断下病弹出如下图7类似的对话框,我们还是点击取消.然后目标库 就会被载入,如下图8所示:

图8:目标so库加载

之后,在标记1处双击进入,在目标函数处下断点,然后F9运行程序,之后既可以正常调试了.

一般的,我们会动静结合来分析,由于本apk带有多个so库:

lib/
├── arm64-v8a
│   └── libnative-lib.so
├── armeabi
│   └── libnative-lib.so
├── armeabi-v7a
│   └── libnative-lib.so
├── mips
│   └── libnative-lib.so
├── mips64
│   └── libnative-lib.so
├── x86
│   └── libnative-lib.so
└── x86_64
    └── libnative-lib.so

我们需要查看android设备的架构,然后得知到底加载的是哪一个so,通过命令 adb shell getprop可以获得系统的属性,其中ro.product.cpu.abi, 便是获得系统的构架: adb shell getprop ro.product.cpu.abi 我这里是armeabi-v7a,因此静态分析时应分析目录 armeabi-v7a 下的so库进行静态分析. 在目标库加载完后,查看模块列表中的so库基地址,在静态DIA中依次 Edit/Segment/Rebase,在打开的界面中,勾选Image Base,然后输入上面的基地址,点击确定. 这样静态分析的地址就和动态分析的地址一致了.在静态IDA中找到目标JNI函数,然后去动态IDA中下断点即可.

最后经过分析,实际检验过程在函数 int __fastcall Java_org_teamsik_ahe17_qualification_Verifier_checkPasswordByJNI(JNIEnv *env, jobject a2, jstring input) 中,有很多if语句判断,代码流程比较简单,两行python代码给出flag:

for num in "78 65 84 73 86 69  65 72 69".split():
    print(chr(int(num)))

所以最后的 flag 是 NATIVEAHE.

最后补充一些东西如下:

附件下载:链接:http://pan.baidu.com/s/1jIDmtCi 密码:defm

Reference

  1. Attacking Android Applications With Debuggers




如有问题可以通过我的邮箱dXAyZ2Vla0AxNjMuY29tCg==或者新浪微博:bugnofree联系我.