关于作业NeoQUEST-2020示例的反向客户端-服务器apk指南


今天,我们有了一个丰富的程序(一次将有很多网络安全领域!):考虑对Android应用程序进行反编译,拦截流量以获取URL,在没有源代码的情况下重建apk,使用密码分析器等等:

根据传说NeoQUEST-2020,英雄发现了必须用于获取密钥的旧机器人零件。让我们开始吧!

1.逆转apk


因此,在我们设法从半拆卸式机器人中提取一点点东西之前,这是一个可以以某种方式帮助我们获取密钥apk应用程序。让我们做最明显的事情:运行apk并查看其功能。毫无疑问,它不仅具有简约的应用程序界面,而且是一个自定义FileDroid文件客户端,可让您从远程服务器下载文件。好吧,看起来很简单。我们将电话连接到Internet,尝试进行下载(立即为key.txt-好吧,如果呢?)-服务器未成功丢失文件。



就复杂性而言,我们进入下一个事件-我们使用JADX反编译apk并分析应用程序的源代码,幸运的是,这些源代码完全没有被混淆。我们当前的任务是了解远程服务器提供哪些文件下载,并从中选择带有密钥的文件。

我们从com.ctf.filedroid.MainActivity类开始,该类包含onClick()方法,这对我们来说是最有趣的,其中处理“下载”按钮的单击。在此方法内部,两次调用ConnectionHandler类:首先,调用ConnectionHandler.getToken()方法,然后才调用通过用户请求的文件名的ConnectionHandler.getEncryptedFile()。



是的,也就是说,首先我们需要一个令牌!我们将进一步研究获得它的过程。
ConnectionHandler.getToken()方法接受两行输入,然后发送GET请求,并将这些行作为“ crc”和“ sign”参数传递。作为响应,服务器以JSON格式发送数据,我们的应用程序从该数据中提取访问令牌并使用它来下载文件。当然,这一切都很好,但是“ crc”和“ sign”是什么?



为了理解这一点,我们进一步走向Checks类,请提供badHash()和badSign()方法。第一个从classes.dex和resources.arsc计算校验和,将这两个值连接起来并包装在Base64中(注意标志10 = NO_WRAP | URL_SAFE,它将派上用场)。那第二种方法呢?并且他对应用程序签名的SHA-256指纹进行了相同的处理。嗯,看来FileDroid并不是真的很想重建:(



好的,假设我们收到了令牌。下一步是什么?我们将其传递给ConnectionHandler.getEncryptedFile()方法的输入,该方法将请求的文件的名称附加到令牌上并生成另一个GET请求,这次使用“ token”和“ file”参数。作为响应的服务器(根据方法的名称判断)发送一个加密的文件,该文件存储在/ sdcard /中。

因此,归纳一下小计:我们有两个新闻,而且...都是不好的。首先,FileDroid并不真正支持我们修改apk的热情(检查校验和和签名),其次,从服务器接收的文件承诺将被加密。

好的,当问题出现时,我们将解决它们,现在的主要问题是我们仍然不知道需要下载哪个文件。但是,在研究ConnectionHandler类时,我们不禁注意到,在getToken()和getEncryptedFile()方法之间,FileDroid开发人员忘记了另一个非常诱人的方法,称为getListing()名称。因此,服务器支持此类功能。。。这似乎就是您所需要的!



要获得列表,我们将需要众所周知的“ crc”和“ sign”-没问题,我们已经知道它们来自何处。我们读取值,发送GET请求,然后...停止。我们将在哪里发送GET请求?最好先获取远程服务器URL。嗯,我们回到MainActivity.onClick(),看看如何生成netPath参数来调用getToken()和getEncryptedFile()方法:

Method getSecureMethod = 
wat.class.getDeclaredMethod("getSecure", new Class[]{String.class});

// . . .

// netPath --> ConnectionHandler.getToken()
(String) getSecureMethod.invoke((Object) null, new Object[]{"fnks"})

// netPath --> ConnectionHandler. getEncryptedFile()
(String) getSecureMethod.invoke((Object) null, new Object[]{"qdkm"})

奇怪的字母组合“ fnks”和“ qdmk”迫使我们转向wat.getSecure()方法的反编译结果。剧透:JADX的结果一般。



经过仔细检查,很明显,该方法的所有这些不太令人满意的内容都可以用这种普通的开关盒代替:

// . . .
switch(CODE)
{
    case «qdkm»: 
        r.2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(2);
        break;
    case «tkog»: 
        r2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(1);
        break;
    case «fnks»: 
        String r2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(0);
	break;
}
java.lang.StringBuilder r1 = new java.lang.StringBuilder 
r1.<init>(r2) 
java.lang.String r0 = r1.toString() 
java.lang.String r1 = radon(r0)
return r1 

由于“ fnks”和“ qdmk”已用于获取令牌并下载文件,因此“ tkog”应提供请求在服务器上列出可用文件所需的URL。似乎希望可以廉价地获得所需的路径。首先,让我们看看URL如何存储在应用程序中。我们打开com.ctf.filedroid.x37AtsW8g.rlieh786d()函数,看到每个URL被保存为一个编码的字节数组,该函数本身从这些字节中形成一个字符串并返回它。



好。但是随后,该行传递给函数com.ctf.filedroid.wat.radon(),该函数的实现提交给本地库libae3d8oe1.so。反向arm64?不错的尝试,FileDroid,但又来一次吗?

2.获取服务器URL


让我们尝试从另一端着手:拦截流量,获取明文形式的URL(并且,还包括校验和和签名值!),将它们与com.ctf.filedroid.x37AtsW8g.rlieh786d()的字节数组进行匹配-可以加密是否是通常的Caesar或XOR密码?..那么恢复第三个URL并执行列表将并不困难。

要重定向流量,您可以使用任何方便的代理(CharlesfiddlerBURP等等。)。我们在移动设备上配置转发,安装适当的证书,验证拦截是否成功,然后启动FileFroid。我们正在尝试下载一个任意文件,然后请参阅“ NetworkError”。此错误是由于存在证书固定引起的(请参阅com.ctf.filedroid.ConnectionHandler.sendRequest方法):文件客户端验证应用程序中“有线”的证书对应于与其交互的服务器。现在很清楚为什么要控制应用程序资源的完整性!



但是,在拦截的流量中,我们至少可以看到文件客户端访问的服务器的域名,这意味着解密URL的希望仍然存在!



让我们返回com.ctf.filedroid.x37AtsW8g.rlieh786d()函数,并注意前几十个字节在所有数组中都重合:

cArr[0] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'i', . . .};

cArr[1] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'j', . . .};

cArr[2] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'j', . . ., '='};

此外,第三个数组的最后一个字节暗示它并非没有base64。让我们尝试用URL的已知部分解码并戳出结果字节:



似乎没有人对ARMag3dd0n如此满意!事情很小:用找到的密钥顺序解码base64 URL和xorim。但是...如果不是XOR,而是一个自制的置换密码,即使尝试100次也无法获得?

3.用Frida重建apk


作为本文的一部分,我们将考虑使用Frida框架,这是一种更轻松(并且我们认为更漂亮)的解决方案方法,该方法将允许我们在运行时使用所需的参数执行任意apk应用程序方法。为此,您需要具有root权限的电话或仿真器。我们假设以下行动计划:

  1. 在PC和测试电话上安装Frida组件。
  2. 恢复与令牌或列表请求匹配的URL,然后下载文件(使用Frida)。
  3. 检索原始应用程序的校验和和签名值。
  4. 获取存储在服务器上的文件列表,并标识所需的文件。
  5. 下载并解密文件。

首先,我们将阐明植根电话和apk之间的关系。我们安装了该应用程序,然后运行它,但是文件客户端并不想完全启动,它只会闪烁并关闭。我们通过logcat检查消息-是的,是的,FileDroid已经感觉到某些东西不对劲并且可以抵抗。



我们再次转到MainActivity类,发现在onCreate()中调用了doChecks()方法,并且该日志



日志中显示以下错误:此外,onResume()还检查Frida的典型端口是否打开:



我们的文件客户端有点不耐烦进行调试,root和Frida本身。我们的计划中绝对不包括这种反对,因此我们使用apktool实用程序获取了应用程序的smali代码,在任何文本编辑器中打开MainActivity.smali文件,找到onCreate()方法并将doChecks()调用转换为无害的注释:



然后我们剥夺了suicide()方法真正关闭该应用程序的机会:



接下来,让我们再次使用apktool和sign构建我们稍有改进的应用程序通过执行以下命令(可能需要管理员权限)来执行以下操作:

cd "C:\Program Files\Java\jdk-14\bin"
.\keytool -genkey -v -keystore filedroid.keystore -alias filedroid_alias -keyalg RSA -keysize 2048 -validity 10000
.\jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore  filedroid.keystore filedroid_patched.apk filedroid_alias
.\jarsigner -verify -verbose -certs filedroid_patched.apk

我们在手机上重新安装该应用程序,然后运行它-欢呼,下载顺利进行,日志很干净!



我们继续在PC和移动设备上安装Frida框架:

$ sudo pip3 install frida-tools
$ wget https://github.com/frida/frida/releases/download/$(frida --version)/frida-server-$(frida --version)-android-arm.xz
$ unxz frida-server-$(frida --version)-android-arm.xz
$ adb push frida-server-$(frida --version)-android-arm /data/local/tmp/frida-server

在移动设备上启动Frida框架服务器:

$ adb shell su - "chmod 755 /data/local/tmp/frida-server"
$ adb shell su - "/data/local/tmp/frida-server &"  

我们正在准备一个简单的get-urls.js脚本,它将为所有受支持的请求服务器调用wat.getSecure():

Java.perform(function () 
{
     const wat = Java.use('com.ctf.filedroid.wat');
	console.log(wat.getSecure("fnks"));
	console.log(wat.getSecure("qdmk"));
	console.log(wat.getSecure("tkog"));
});

我们在移动设备上启动FileDroid,并使用脚本将相应的过程“粘贴”:



4.获取服务器上的文件列表


最终,远程服务器离我们越来越近了!现在我们知道服务器以下列方式支持请求:

  1. filedroid.neoquest.ru/api/verifyme?crc= {crc}&sign = {sign}
  2. filedroid.neoquest.ru/api/list_post_apocalyptic_collection?crc= {crc}&sign = {sign}
  3. filedroid.neoquest.ru/api/file?file= {file}和令牌= {token}

为了获得可用文件的列表,仍然需要计算原始应用程序的校验和和签名值,然后将它们编码为base64。

python3中的此类脚本将允许您执行以下操作:

扰流板
import hashlib
import binascii
import base64
from asn1crypto import cms, x509
from zipfile import ZipFile


def get_info(apk):
    with ZipFile(apk, 'r') as zipObj:
        classes = zipObj.read("classes.dex")
        resources = zipObj.read("resources.arsc")
        cert = zipObj.read("META-INF/CERT.RSA")
        crc = "%s%s" % (get_crc(classes), get_crc(resources))
        return get_full_crc(classes, resources).decode("utf-8"), get_sign(cert).decode("utf-8")


def get_crc(file):
    crc = binascii.crc32(file) & 0xffffffff
    return crc


def get_full_crc(classes, resources):
    crc = "%s%s" % (get_crc(classes), get_crc(resources))
    return base64.urlsafe_b64encode(bytes(crc, "utf-8"))


def get_sign(file):
    pkcs7 = cms.ContentInfo.load(file)
    data = pkcs7['content']['certificates'][0].chosen.dump()
    sha256 = hashlib.sha256()
    sha256.update(data)
    return base64.urlsafe_b64encode(sha256.digest())  

get_info('filedroid.apk')


您也可以手动。我们认为class.dex和resources.arsc中的CRC32是任何方便的工具(例如,对于Linux-标准crc32实用程序),我们分别获得值1276945813和2814166583,将它们连接起来(将出现12769458132814166583)并在base64中进行编码,例如,在这里



为了执行类似的步骤来对应用程序进行签名,请在JADX窗口中转到“ APK签名”部分,复制“ SHA-256指纹”值,并将其在base64中编码为字节数组:



重要说明:在原始apk中,base64编码是通过URL_SAFE标志执行的,即 分别使用“-”和“ _”代替字符“ +”和“ /”。有必要确保通过自编码也可以观察到这一点。为此,在联机编码时,可以使用64 script.jp64.jp64.jp64.jpcrqvqcdvcdb将使用“ ABCDEFGHIJKLMNOPQRSTUVWXYZabc script64”替换为“ ABCDEFGHIJKLMNOPQRSTUVWXYZabcde fghijklmnopqrstuvwxyz0123456789 +/-”。

最后,我们具备成功列出文件的所有要素:

  1. filedroid.neoquest.ru/api/list_post_apocalyptic_collection?crc= {crc}&sign = {sign}
  2. crc:MTI3Njk0NTgxMzI4MTQxNjY1ODM =
  3. 符号:HeiTSPWdCuhpbmVxqLxW-uhrozfG_QWpTv9ygn45eHY =

我们执行GET请求-并为我们的清单加油!而且,其中一个文件的名称不言自明-“如果您要转义,请打开”-似乎我们需要它。



接下来,我们请求一次性访问令牌并下载文件:

import requests

response = requests.get('https://filedroid.neoquest.ru/api/verifyme', 
    		params={    'crc': 'MTI3Njk0NTgxMzI4MTQxNjY1ODM=', 
'sign': HeiTSPWdCuhpbmVxqLxW-uhrozfG_QWpTv9ygn45eHY=},
verify=False)
    
token = response.json()['token']
print(token)
response = requests.get('https://filedroid.neoquest.ru/api/file', 
params={'token': token, 'file': '0p3n1fuw4nt2esk4p3.jpg'}, verify=False)

with open("0p3n1fuw4nt2esk4p3.jpg", 'wb') as fd:
        fd.write(response.content)

我们打开下载的文件,并回忆起我们留给以后的一种小情况:



5.添加少量加密...


嗯,为时过早,我们推迟了FileDroid。让我们回到JADX,看看文件客户端开发人员是否对我们有用。是的,在代码清理显然不受欢迎的情况下就是这种情况:未使用的cryptoFile()方法在ConnectionHandler类中静静地等待我们的注意。我们有什么?

加密AES模式CBC,sinhroposylka占据前16个字节...惰性-进步的引擎,最好再次使用Frida并毫不费力地解密我们的0p3n1fuw4nt2esk4p3.jpg。但是只是通过作为加密密钥吗?选项不多,但是考虑到存在另一个“被遗忘”的savePlainFile(字符串文件,字符串令牌)方法,选择是显而易见的。

准备以下crypto.js脚本(作为令牌) 指示实际值,例如'HoHknc572mVpZESSQN1Xa7S9zOidxX1PMbykdoM1EXI ='):

Java.perform(function () {
    const JavaString = Java.use('java.lang.String');
    const file_name = JavaString.$new('0p3n1fuw4nt2esk4p3.jpg');
    const ConnectionHandler = Java.use('com.ctf.filedroid.ConnectionHandler');
    const result = ConnectionHandler.savePlainFile(file_name, <token>);
    console.log(result);
});

我们将加密文件0p3n1fuw4nt2esk4p3.jpg放在/ sdcard /上,运行FileDroid并使用Frida注入crypto.js脚本。脚本运行后,plainfile.jpg文件将出现在/ sdcard /上。我们打开它,...就解决了!



这项艰巨的任务要求参与者同时具备多个信息安全领域的知识和技能,我们很高兴大多数竞争对手都能成功应对!

我们希望那些在没有足够时间或知识之前没有收到密钥的人现在可以成功地通过任何CTF中的类似任务:)

All Articles