记录一下在攻防世界Mobile方向中做过的题。
1. app1(版本号)
这道题的一个知识点就是程序自身的版本号、版本名在BuildConfig.class
中存储。这里的版本号、版本名与我们在应用程序上看到的版本号、版本名不是同一个东西。
1 | package v5le0n9; |
另外,版本号、版本名还能通过以下方式查看:
2. app2(广播接收器)
程序一进去就到了MainActivity,输入一点东西后点击登陆去到SecondActivity。用jadx查看一下这两个Activity都做了什么。
在SencondActivity中又启动了MoniterInstallService。
有点混乱了,可以先求出口令和用户名试试。将x86目录下的so文件载入IDA32,在Exports表里找到doRawData()
函数,查看伪代码。
python3解决from Crypto.Cipher import AES报错问题
1 | import base64 |
发现这个也不是flag。当我们把用户名tencent,口令aimage输入进去,点击登陆发现崩溃了。查看一下XML文件,还有FileDataActivity我们还没有分析。
1 | import base64 |
其实还有更简便的方法,直接去到FileDataActivity页面查看。
这道题就没想着要我们跟着代码分析App去到FileDataActivity,不过也算收获到一点知识,显式Intent和隐式Intent,广播自定义意图,动静态注册广播接收器 。
3. app3(备份文件ab)
下载下来是一个.ab
文件,.ab
文件是 Android 系统的备份文件格式,它分为加密和未加密两种类型。.ab
文件的前 24 个字节是类似文件头的东西,如果是加密的,在前 24 个字节中会有 AES-256 的标志,如果未加密,则在前 24 个字节中会有 none 的标志。安卓备份文件.AB的解包方法
下载abe.jar
,将.ab
文件解压。在abe目录下运行cmd。
1 | java -jar abe.jar unpack C:\Users\dell\Desktop\app3.ab app3.tar |
.tar
文件在Windows也可解压。找到.apk
文件安装到模拟器,又运行不了。
因为没有跳转语句,无论输入什么都是出来这句话。再看看有什么隐藏的<activity>
,发现也只有我们分析过的那两个。
再去那两个<activity>
仔细分析我们刚才没有分析的,肯定是有什么遗漏了。
SHA1=”Stra1234” + “44e2e4457d4e252ca5b9fe9d20b3fea5” + “yaphetshan”= ae56f99638285eb0743d8bf76d2b0c80e5cbb096,取前7位就是ae56f99。
我们刚才说的有加密,是用SqlCipher加密的,而这个就是数据库的密码。
解压包除.apk
文件之外,还有两个.db
文件,一个Demo.db
,另一个是Encrypt.db
。两个都试试。
安装SQLite数据库用来解密数据库,上面分析写着版本为3.4.0
,Linux注意对应版本,Windows下载最新也就3.0.1
,所以没问题。
两个加密的.db
文件使用SQLCipher打开。发现Encryto.db
中有东西。
很明显的Base64,拿去解码得:
1 | VGN0ZntIM2xsMF9Eb19ZMHVfTG92M19UZW5jM250IX0= |
4. easy_apk(变种base64)
载入AK没发现lib文件,载入jeb查看源码:
天真的我就拿去Base64解码了,结果解码失败。回来再看,它是新Base64,点进去查看它的算法,发现它是把索引表给替换了。
拿Base64变种脚本替换索引表即可。
1 | import base64 |
1 | flag{05397c42f9b6da593a3644162d36eb01} |
5. RememberOther(MD5)
运行一下程序。
拿去AK,没有看到需要动态调试的so文件。
拿去jeb分析代码:
那我们试试直接点注册。
MD5的奇数位为b216ebb92fa5caf6
,再将MD5值解密,解出来为YOU_KNOW_
(究竟哪个网站可以免费解MD5,我还是看牛牛们的wp才知道是这个答案)。拿到程序去验证没错。YOU_KNOW_
很明显是flag形式,后面还缺了些东西,出题人给了剩下的线索。
你懂!你懂安卓!
1 | YOU_KNOW_ANDROID |
6. easyjni(算法分析&变种base64)
这道题看名字就知道考察jni。
载入AK,看到lib目录下有libnative.so
文件,是armeabi-v7a架构的。载入jeb,代码混淆了,没关系一个个方法分析。
在判断语句,调用了MainActivity.a方法,其中一个实参就是我们输入的内容。MainActivity.a方法返回到私有a方法里,形参是我们输入的内容。再看私有a方法,调用了ncheck()方法。
调用ncheck()方法前,还创建了一个a类对象,先进去看看a类的a方法。发现是变种Base64加密。
将apk文件解包,将里面的so文件载入IDA。
1 | C:\Users\dell\Desktop>apktool d easyjni.apk |
在Export模块找到ncheck()方法。
哇这就有点恶心人了哈,我从没见过等号在中间的。仔细分析一下IDA源码,原来不过如此。
1 | MbT3sQgX039i3g==AQOoMQFPskB1Bsc7 |
再用4中的变种Base64脚本解决。
1 | import base64 |
7. easy-so(算法分析)
运行程序。
看题目,肯定有so文件,载入AK发现4种架构都有,可以快乐地玩耍了。
载入jeb分析源码。
我们需要做的就是将CheckString方法的返回值为1。解包,用x86架构的so文件载入IDA。因为我用的模拟器是x86架构的。
这个与6的题目考点几乎一模一样,只是没了变种Base64的过程。
1 | f72c5a36569418a20907b55be5bf95ad |
拿去程序里运行验证一下,验证通过。
1 | flag{90705bb55efb59da7fc2a5636549812a} |
8. easyjava(算法分析)
运行一下程序。
载入jeb分析:
将b类和a类所有方法都分析一遍,反正后面也要用到。
回到MainActivity类,b.b和a.a的作用一样,都是以参数为边界,对换各自的c整形列表,所以v4和v5都应该是对换后的列表。经过分析a类和b类发现并没有增加或减少原本的长度,所以v2的长度就应该等于v3的长度,v3的长度从字符串中可以计算到是12,所以循环要经历12次。
以v2索引值为0的值为例,将v2索引值为0的值传入b.a方法,如果这个值可以转换成小写字母,则把b.b小写字母表中相同的小写字母的索引值取出,与a列表(也就是v4)的数组元素对比,如果取出的索引值与a列表中某个元素相同,则返回元素的索引值。更新b.a列表和b.b小写字母表,都是首位放最末位。将返回的a列表元素的索引值作为参数传入a.a方法,如果这个形参与a列表中(也就是v5)的元素相同,取出这个元素的索引值,作为a.b小写字母表中的索引,返回该索引的元素,也就是“w”。
用Python写出来相当于a_alphabet[v5.index(v4.index(b_alphabet.index(ans[0])))] = w
现在我们要做的就是把这个过程逆回来。
1 | v4 = [17,23,7,22,1,16,6,9,21,0,15,5,10,18,2,24,4,11,3,14,19,12,20,13,8,25] |
嘤嘤嘤终于成功了,在做的时候看漏了很多细节,所以正确的flag一直出不来,还是要耐心一点!
1 | flag{venividivkcr} |
9. Ph0en1x-100(动态调试so)
载入AK,看到有lib文件。载入jeb分析源码。
而getFlag和encrypt都是Native方法,需要在so文件查看详细内容。将程序解包,拿x86目录下的so文件载入IDA,查看getFlag和encrypt方法。
encrypt方法就是将输入的字符串中的每个字符的ASCII码都减1。
getFlag方法看得就有点懵逼了,如果静态分析实在困难的话,可以考虑动态调试。
动态调试需要满足android:debuggable="true"
,没有就要在AK中编辑添加,重新编译签名。
动态调试方法可看 https://v5le0n9.github.io/posts/15be101a.html?highlight=and#9-IDA%E5%8A%A8%E6%80%81%E7%A0%B4%E8%A7%A3%E7%99%BB%E5%BD%95%E9%AA%8C%E8%AF%81 我就在这偷下懒了。
刚才静态分析中,很明显看到一个循环,那我们就在循环的下一句下断点,在程序中输入字符串点击按钮,IDA停在断点处。此时,查看寄存器窗口EDI的值,点击小箭头,跟随地址。
到了这里还没行,循环解决了但getFlag方法还未结束。F8步过观察字符串的变化。运行到快要结束时字符串已经不再变化了。在F8步过的过程中,发现.
变成了e
。所以正确的getFlag的返回值应该为:
1 | ek`fz@q2^x/t^fn0mF^6/^rb`qanqntfg^E`hq| |
所以我们输入的字符串中每个字符-1就是上面这个字符串。编写脚本:
1 | flag = "ek`fz@q2^x/t^fn0mF^6/^rb`qanqntfg^E`hq|" |
10. 黑客精神(JNI_OnLoad)
运行程序,注册码保存后直接退出。
载入AK,有so文件,因为没有x86目录,如果要动态调试的话就要开我那个慢得要死的AS原生模拟器了。
载入jeb,分析源码:
看到有Log语句,赶紧打开AS连接上模拟器运行一下看看Log。程序一进去就显示m=0,然后再m=Xman。
说明程序在打开窗口前就已经载入so文件读取m的值,但它是静态的,如果我们注册过的话,程序会读取m=1,不会让我们再次注册。so文件的onCreate方法中,第一句就是初始化SN,所以m的值应该是在native方法initSN中存着。
将so文件载入IDA,在导出表里找不到initSN方法,也找不到所有native方法,但看到了JNI_Onload,说明整个过程都是在JNI_Onload里动态完成的。
点进JNI_Onload函数,查看源码:
所以EoPAoY62@ElRD
应该就是注册码,但输入发现不对…Logcat窗口还是显示m=0。
那继续分析saveSN。
现在可以捋一下思路了,W3_arE_whO_we_ARE
是要加密的字符串,EoPAoY62@ElRD
相当于密钥,作为参数传入saveSN。v7存着的就是密钥。
把这个加密算法用Python写一遍:
1 | flag = "W3_arE_whO_we_ARE" |
最后一个work方法,查看它的内存,里面包含着flag的格式。
1 | xman{201608Am!2333} |
11. APK逆向(算法分析)
同Bugku的mobile1
12. 人民的名义-抓捕赵德汉1-200(jar包)
下载下来的是jar包,使用命令jar -xvf 1-200.jar
解压jar包。
1 | C:\Users\dell\Desktop>jar -xvf 1-200.jar |
在AS或eclipse中创建一个项目,将jar包中的.class
文件放到项目的class目录下即可查看源码。(jar包可直接用jadx或jd-gui工具查看源码)
这个MD5解出来是monkey99
,拿去试了一下,发现flag就是这个。
1 | flag{monkey99} |
13. 基础android(广播接收器)
载入AK没什么发现,载入jeb分析源码。
1 | import string |
拿到密码后继续去到第二关。
那就再看看这个程序还开了哪个类我们还没有分析。GetAndChange
和NextContent
我们还没有分析,进去看看。
之后再也没有任何操作了,我们已经看到图片了,所以NextContent
不是关键类。返回到GetAndChange
类,找一下BroadcastReceiver
是干什么用的。
BroadcastReceiver
详解:https://blog.csdn.net/huiblog/article/details/53234544
回到Manifest
发现果然有静态注册。
将android.is.very.fun
作为显示码输入,但我点击按钮没有任何反应…但是之前不是分析time_2.zip
转换为图片嘛,直接解压不行,因为它本来就不是zip压缩包。所以将后缀名修改为jpg就可以看到图片了。
1 | flag{08067-wlecome} |
14. easy-dex(解密dex & Twofish算法)
在模拟器运行,点一下屏幕就一直在闪,五颜六色地闪。
载入AK,没有找到smali文件,但有一个so文件。并且在AndroidManifest.xml
文件中能看到有两个丢失的但程序需要的smali文件。MainActivity
和NativeActivity
。MainActivity
大家都知道啦,一般是安卓程序的入口。但我们发现这个程序的入口是NativeActivity
,这个是什么呢?
写android纯C++的程序需要用到NativeActivity
,这个NativeActivity
就是一个一般的java类, 和普通的activity没有区别。NativeActivity
是android sdk自带的一个activity。android的纯C++的程序也是需要一个java虚拟机来运行的。NativeActivity
通过native_app_glu
来启动我们的C++线程,传递各种activity事件给C++代码。native_app_glu
在ndk的sources\android
目录里面,将native_app_glu
当作我们工程的静态库,这个静态库里面封装好了,会创建一个线程,这个线程里面会调用一个android_main(android_app* pApplication)
的函数,因此,我们C++这边的入口函数就是android_main()
。我们在这个android_main()
函数里面的任务就是进行消息循环,做各种任务。
解包,载入IDA查看so文件。找到android_main
函数,点进去。
也就是说要在10s内要摇100次手机。
看了个大概,再详细分析代码。
所以我们首先取出unk_7004
里面的加密数据,再将它解密,再解压缩得出dex内容将它写入文件中。
File -> Script commond,选择Python,编写ida dump脚本将数据提取出来:
1 | from idaapi import * |
在so文件的同目录下就会生成一个cipherdata
文件。接下来给这个文件进行解密操作。
1 | import zlib |
将dex文件载入jeb,就可以查看这个程序的MainActivity
类了。
那么,先找密钥。密钥的id号为2131099683
,转换成十六进制为0x7F060023
。将dex重命名为classes.dex
放到解包目录下,重打包。解包目录下就会生成一个bulid
目录。
将resources.arsc
载入jadx,查找id号。
继续查找two_fish
,找到一串字符串,这个就是密钥。
1 | I have a male fish and a female fish. |
Twofish是什么?它其实是一个分组加密算法!Twofish是布鲁斯·施奈尔带领的项目组于1998年研发的区块加密算法。美国国家标准技术研究所(NIST)公开招募的高级加密标准(AES)算法最终候选算法之一,但最终并未当选高级加密标准算法。双鱼算法的标志性特点是它采用了和密钥相关的替换盒(S盒)。密钥输入位的一半被用于“真正的”加密流程进行编排并作为Feistel的轮密钥使用,而另一半用于修改算法所使用的S盒。双鱼算法的密钥编排非常复杂。软件实现的128位双鱼算法在大多数平台上的运行速度不及最终获胜的128位的AES标准算法Rijndael,不过,256位的双鱼算法运行速度却较AES-256稍快。包括Twofish-ECB, Twofish-CBC, Twofish-CTR, Twofish-OFB, Twofish-CFB。
所以再来捋一下思路。我们输入的字符串与密钥进行Twofish加密的结果是字节数组m。密钥经过&0xFF转换不需要我们手动操作,因为网上的解密算法已经包含在内了。但网上的加密算法得出来的不是带负数的字节数组,而是Base64或hex。所以我们可以先将字节数组转换成Base64的形式,再拿去解密。
1 | import base64 |
1 | qwb{TH3y_Io<e_EACh_OTh3r_FOrEUER} |
15. 你是谁(算法分析)
载入AK,有so文件。载入jeb,找到MainActivity
中的onCreate
方法。
1 | 11 12 17 18 20 23 26 29 30 34 35 39 40 49 51 58 62 67 73 76 84 85 |
按照上面点位点好后,弹出Right design
,点击按钮,弹出通过爱的验证
。好像并没有什么用。再往上找找源码,发现有flag字样。
中文意思是“你获得了已经排序过的flag”。
1 | print(chr(20667)) |
结合题目和代码,这个重新排序应该是“我是傻逼”。而它说了,那个是排序过的flag,而正确的flag应该为25105 26159 20667 36924
。
1 | flag{25105 26159 20667 36924} |
16. Android2.0(算法分析)
载入AK发现有so文件,载入jeb分析源码。
解包,将so文件载入IDA,分析getResult
方法。
尝试编写Python脚本:
1 | v5 = "LN^dl" |
提交flag发现不对,看看哪里漏了。最后发现Init
函数不是简单的平均分成3组,而是对正确的flag的每个索引除以3取余得到fgorll{sraasoy}
。
1 | ans = "fgorll{sraasoy}" |
17. boomshakalaka-3(共享文件)
啊好讨厌为什么它是游戏!!我已经玩了好几分钟了!
载入AK看到它有so文件,载入jeb分析源码:
这个base64解码得bazingaaaa
。诶结果不是这个。进去a类看看吧。
SharedPreferences是一个轻量级的存储类,特别适合用于保存软件配置参数。使用SharedPreferences保存数据,其背后是用xml文件存放数据,文件存放在/data/data/程序包名/shared_prefs目录下。
1 | public abstract SharedPreferences getSharedPreferences(String name, int mode); |
第一个参数是存储时的名称,第二个参数则是文件的打开方式。
那我们先找找它的xml文件吧。这个程序的包名为com.example.plane
,包名可在AndroidManifest.xml
的manifest
标签中找到。
所以可以推测N0
和MG
被写进了Cocos2dxPrefsFile.xml
文件中。
但打开Cocos2dxPrefsFile.xml
文件却发现不止这两个字符串。我刚才玩了两次,出现了两个极为相似的字符串。
那就再玩几次试试。我发现每次关闭程序再打开又重新写入MGN0
,而不关闭程序重新玩不会写入MGN0
,每次结束都会以dz99
为结束标志。**里面的是每个串的区别。
1 | MGN0ZntDMGNvUzJkX0FuRHJvMW*Rf*dz99 |
这些星号里面的串有些区别,但又是固定出现的,比如都是Rf
、RV
等等。说明在某个内存中存有这些字符。解包将so文件载入IDA,在函数名窗口搜索score
,发现有好多这些字符串。
原来MW
其实也是包含在里面的。将这些字符串组合起来MWRfRzBtRV9Zb1VfS24w
,再将前缀和后缀加上MGN0ZntDMGNvUzJkX0FuRHJvMWRfRzBtRV9Zb1VfS24wdz99
。用Base64解码得0ctf{C0coS2d_AnDro1d_G0mE_YoU_Kn0w?}
。为什么用Base64,其实题目上面的flag.xml中的Base64字符串已经暗示得很清楚了。
1 | 0ctf{C0coS2d_AnDro1d_G0mE_YoU_Kn0w?} |
18. Illusion(ARM汇编)
运行程序。
载入AK,有一个Flag文件,打开出现一串字符串Ku@'G_V9v(yGS
。
载入jeb,分析源码。
将so文件载入IDA查看CheckFlag
方法。
如果要将算法逆过来,就需要将Flag里的字符串减32再左移32位。
1 | flag = list("Ku@'G_V9v(yGS") |
所以sub_10C0
每次循环得出的值就是上面这一串数字。进去sub_10C0
看看算法。
(说着随机选取,结果还是认真算了)Flag字符串长度为13,所以在aE116c5c66e7b37
数组中只取前13个字符。
1 | .rodata:000023C8 ; ORG 0x23C8 |
前13个字符为e116c5c66e7b3
,其中最大的ASCII码为e
(101),最小是1
(49)。而我们可以输入的可视化字符的ASCII码范围是32~126。所以v9[i]+aE116c5c66e7b37[i]-64
的范围应该在17~163。照着IDA的代码抄一遍:
1 | for a1 in range(17,164): |
在这个范围完全没有这么大的数值…而且在这个范围要不就返回0要不就返回1。肯定是哪里出问题了。呜呜我看了牛牛们的wp说这个是假的,真的CheckFlag
在JNI_Onload
函数里,我就知道!!我就说导出表都有CheckFlag
了怎么还有个JNI_Onload
!以后记住从JNI_Onload
进去准没错,还是没能抵挡住CheckFlag
的诱惑。
可以继续试试参数范围。aLjavaLangStrin_0
存的字符串为(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
。取前13个字符为(Ljava/lang/S
。最小的ASCII码为40
((
),最大的ASCII码为118
(v)。所以v10[i]+aLjavaLangStrin_0[i]-64
的范围在8~180。
1 | ''' |
还是要不就是0要不就是1。这个时候就不要想是不是你的问题了!肯定是IDA反汇编的错!所以接下来要看汇编代码找到正确的逻辑。
对于栈的立即数,可以右键 -> Q算得栈的偏移值。
到这已经将所有细节都分析了,除了sub_1028
,如果它反编译没错的话那答案基本就已经出来了。
1 | flag = ((输入的字符串+内存字符串-64) - sub_1028(输入的字符串+内存字符串-64, 93) * 93) + 32 |
编写脚本:
1 | import string |
19. APK逆向-2(XML文件)
在模拟器上安装失败,解包不能解包,用AK不能反编译。那就把它后缀改为.zip
,解压发现可以解压。没有找到smali目录,但有classes.dex
和resources.arsc
文件。这两个文件在jadx都可以打开,但AndroidManifest.xml
文件显示乱码。
其实我们解压时就可以知道是AndroidManifest.xml
的问题了。
那么接下来就是要仔细分析AndroidManifest.xml
文件。
AndroidManifest.xml
文件采用小端模式存储,可以大体分为四个部分:
- Header:头文件
String Chunk:存储字符串资源的程序块
ResourceId Chunk:存储资源id的程序块
XmlContent Chunk:存储xml内容程序块,其中包含了五个部分,Start Namespace Chunk 、End Namespace Chunk 、Start Tag Chunk 、End Tag Chunk 、Text Chunk
Header
magicnumber:魔数,固定值 0x0008003(16进制),占四个字节。
filesize:xml文件总字节数 ,占四个字节。
String Chunk
ChunkType:StringChunk类型,4个字节 ,固定值 0x001c0001
ChunkSize:StringChunk大小 ,4个字节
StringCount:StringChunk字符串的个数,4个字节
StyleCount:StringChunk样式的个数,4个字节,固定值 0x00000000
Unkown: 位置区域,4个字节,固定值 0x00000000解析时候需要略过4个字节
StringPoolOffset:字符串池偏移量,4个字节,偏移量相对StringChunk头部位置
StylePoolOffset:样式池偏移量,4个字节,偏移量相对于StringChunk头部位置,固定值 0x00000000 ,这个字段基本没用到过
StringOffsets:每个字符串在字符串池中的相对偏移量,int数组,它的大小是 StringCount*4 个字节
StyleOffsets:每个样式的偏移量,int数组,它的大小是 StyleCount*4 个字节
String Pool:字符串池,存储了所有的字符串
Style Pool:样式池,存储了所有的样式,一般为0
ResourceId Chunk
ChunkType:ResourceldChunk的类型,占4个字节,固定值 0x00080180
ChunkSize:ResourceldChunk的大小,占4个字节
ResourceIds:int数组,大小为(ChunkSize - 8) / 4 ,减 8是减去头部大小的8个字节(ChunkType和ChunkSize)
XmlContent Chunk
XmlContentChunk 这部分表示的是存储了清单文件的详细信息,包含的5项,其中Start Namespace Chunk 和End Namespace Chunk 这两个可以合并一个来说明, 因为它们的结构完全一致,解析过程也是一样的。至于End Tag Chunk一共有6个数据,也就是 Start Tag Chunk 的前 6 项,这里不做单独解析和说明。End Tag Chunk这个跟清单文件标签一样的,就是给解析出来的标签加上结束标签一样。Text Chunk这个模块在010 Editor模板里并没有用到过。
Start Namespace Chunk主要包含一个清单文件的命令空间内容
ChunkType:Chunk的类型,4个字节 ,固定值 0x00100100
ChunkSize:Chunk的大小 ,4个字节
LineNumber:清单文件中的行号, 4个字节
Unknown:未知区域, 4个字节
Prefix:命名空间的前缀, 4个字节
Uri:命名空间的URI, 4个字节
Start Tag Chunk主要存放清单文件中的标签信息
ChunkType:Chunk的类型,4个字节 ,固定值 0x00100102
ChunkSize:Chunk的大小 ,4个字节
LineNumber:清单文件中的行号, 4个字节
Unknown:未知区域, 4个字节
Namespace Uri:命名空间用到的url在字符串的索引,值为 -1 表示没有用到命名空间 uri。标签的一般都没有使用到命名空间,4个字节
Name:标签名称(在字符串中的索引值),4个字节
Flags:标签类型例如开始标签还是结束标签,固定值0x00140014,4个字节
Attribute Count :标签包含的属性个数,4个字节
Class Attribute :标签包含的类属性,此项值常为 0,4个字节
Attributes :属性内容集合,每个属性固定 20 个字节,包含 5 个字段,每个字段都是 4 字节无符号 int,解析的时候需要注意Type这个值做一次处理需要右移24位。各个字段含义如下:
- NamespaceUri:属性的命名空间uri 在字符串池中的索引
- Name:属性名称在字符串池中的索引
- ValueStr:属性值
- Type:属性类型
- Data:属性数据
首先查看xml的固定值是否有误。ChunkType应该为01 00 1c 00。而且StylePoolOffset应该为00 00 00 00。修改完后一定要点保存,而不是另存为,因为我发现另存为后有些其它值也被修改了,导致重打包后再解包时出现错误!
忽然发现解包时异常已经告诉我有其中一个错误了…
将它重打包时出现没有apktool.yml
文件错误,将其它apk里面的apktool.yml
复制一份下来,将apk文件名改了即可。
这次终于成功解包了。
所以xml有什么什么神秘的呢?发现中间那个字符串就是flag。
1 | 8d6efd232c63b7d2 |
20. ill-intentions(Native hook)
翻译一下就是,选择您希望与之交互的MainActivity,待办事项:添加按钮来选择活动,现在使用的是Send_to_Activity。
载入AK看可不可以直接修改AndroidManifest.xml
的入口,不行。程序还是那个界面。
那就载入jeb分析看看吧。
这些方法都是在so文件里面。解包先看看so文件。导出表刚好有这三个函数。如果导出表有native函数,说明这些是导出函数;如果没有,而只有JNI_Onload,说明那些是未导出函数。
可以确定DefinitelyNotThisOne
函数肯定没有flag。剩下那两个函数看得我头大,而且传入的参数也有些是在Java层加密过的。
因为你!我去学了Frida so hook!Frida超详细安装实战教程
hook一个so方法需要知道:
- 程序的名字(Frida-ps -U查看程序名字):CTF Application
- so文件名:libhello-jni.so
- so方法名:Java_com_example_application_ThisIsTheRealOne_orThat
编写hook脚本:
1 | import frida |
开启Frida服务:
1 | C:\Users\dell>adb shell |
打开另一命令窗口开启端口转发,Frida默认端口27042:
1 | C:\Users\dell>adb forward tcp:27042 tcp:27042 |
等等,突然想起来我们还没去到我们想要进去的Activity,修改xml文件我是不会修改了,但可以利用objection运行指定的Activity。
安装objection:pip install objection
在模拟器运行程序,在终端输入需要调试的程序的包名:objection -g com.example.hellojni explore
1 | C:\Users\dell>objection -g com.example.hellojni explore |
列出app所有的Activity:android hooking list activities
1 | com.example.hellojni on (Meizu: 7.1.2) [usb] # android hooking list activities |
启动指定Activity:android intent launch_activity com.example.application.ThisIsTheRealOne
1 | com.example.hellojni on (Meizu: 7.1.2) [usb] # android intent launch_activity com.example.application.ThisIsTheRealOne |
可以看到程序页面已经变了:
运行脚本,点击程序中间的按钮,打印返回值。
1 | C:\Users\dell\Desktop>python hookso.py |
它说这个不是我要找的Activity,换一个。
1 | com.example.hellojni on (Meizu: 7.1.2) [usb] # android intent launch_activity com.example.application.IsThisTheRealOne |
修改脚本:
1 | import frida |
运行,点击按钮,打印返回值。
1 | C:\Users\dell\Desktop>python hookso.py |
1 | CTF{IDontHaveABadjokeSorry} |
21. LoopCrypto
将用户输入传到a()
方法中,当点击“CHECK”时执行onClick()
方法。取出包中的签名并对其进行MD5加密,再转换成16进制字符串。将输入与加密后的签名一同传入到JNI层的check()
方法中。
使用jeb动调可以得到签名的MD5值吗?由于APK的debuggable属性为false,那我们修改系统为全局可调试。有两个进程,选第一个进程发现变量全为空,选第二个进程发现不可调试。
行吧,真不知道什么原因,或许到时候可以hook check()
函数得到签名的MD5值。进入So,没有找到check()
,去看看JNI_OnLoad()
。
进入sub_88C4()
,sub_8740()
调用了Decode.a()
方法,将这三块内存数据解密成字符串。
所以sub_87FC()
就是我们要找的check()
函数。进入check()
函数:
尝试使用Frida hook sub_87FC()
,要先在内存中找到So的地址,再通过check()
函数在So中的偏移得到它的参数。
1 | function hook_check(){ |
呃…竟然没有,是因为我用的是x86架构模拟器,而So是ARM架构的吗,那怎么办…使用Android 8.1.0真机启动不了这个App。
使用objection查看内存中加载的库确实没有找到libcheck.so
。回看代码,发现是在Decode类中进行So加载操作,当我们点击按钮时应该会执行到Decode.check()
方法,也就是会让So加载起来,所以为什么在内存中找不到呢?
将off_12004中的622个字节dump下来,File -> Script command,编写脚本以二进制的形式dump出来。
进入sub_85E0()
函数:
所以APK签名的MD5值是多少呢?
F8 C4 90 56 E4 CC F9 A1 1E 09 0E AF 47 1F 41 8D
进入sub_84D0()
:
1.2.11算是一个提示,对加密后的数据使用zlib-1.2.11压缩算法进行压缩。
22. 从林的秘密(wasm & z3)
查看布局发现ID都对应不上,其实是在这个APK中内置了WebView页面,文字、输入框、按钮这些都不是在Java代码中定义的,而是由加载的URL显示出来的。所以这个Activity中定义的文字、输入框、按钮这些控件都是用来迷惑人的,点击事件自然也没有用。
URL在So中的sayHello()
方法里,为http://127.0.0.1:8000。在浏览器中输入网址也可以得到这个HTML页面。
check_key()
方法被一个不会发生的点击事件调用,所以逆向check_key()
只是在浪费时间。我们知道So在加载进内存后会依次执行init()
、init_array()
以及JNI_OnLoad()
函数,查找导出表只看到JNI_OnLoad()
,看看它都干了什么。
这个mm0数组大小为0x85E0,但从伪代码中可以看到还有用到下标为0x85F2的,所以取最大值0x85F3。使用IDA Python脚本将这块内存dump下来,顺便异或0x67。
1 | static main(){ |
打开文件就可以看到解密后的HTML源码了。
JavaScript中有个check_flag()
函数,要求输入字符串长度为32,并调用check_key()
函数,可是并没有看到check_key()
函数,应该是从那一大串二进制数据中动态加载出来的。而那一大串二进制数据是WebAssembly,即WASM。WebAssembly是一种新的编码格式并且可以在浏览器中运行,WASM可以与JavaScript并存,WASM更类似一种低级的汇编语言。
将二进制数据粘贴到记事本,并用010 Editor导入十六进制数据(File -> Import Hex),然后另存为check.wasm
文件。由于它是汇编语言,转为C语言或直接生成可执行文件更容易理解,推荐一个工具wasm一键转c。将可执行文件载入IDA查看check_key()
函数。
这些o()
函数的主要作用是对memory的偏移存的值,也就是我们输入的字符串进行异或。
xxx()
函数是对异或后的值进行线性运算:
复杂的线性运算、按位运算都可以使用z3库来求解。python z3库学习
由于偏移1024~1055与v2~v33并不是对应关系,所以还要调整对应关系后再进行异或运算。
得到flag:
1 | K9nXu3_2o1q2_w3bassembly_r3vers3 |
23. 变形金刚(字符串加密 & RC4 & 变种Base64)
运行一下程序发现提示不同,程序的执行流程走到了父类AppCompiatActivity里面,分析代码可知,只要对So中的eq()
方法分析得到mPassword,就相当于得到了flag。
查看So的导出表,只有以下两个函数:
datadiv_decodexxx()
是So进行字符串加密的特征函数,进去发现它做了一些异或操作。
将它们拷贝出来进行异或还原字符串:
进入JNI_OnLoad()
查看eq()
方法:
进入eq()
:
继续分析代码,对输入字符串进行RC4加密后,还使用到了byte4050,byte4050是一个字符表,长度为65。根据代码中右移2、4、6可以猜测是Base64的变种,也就是将3个字符经编码后变成4个字符。byte24E8中最后两个字符是“;;”,而byte4050字符表最后一个字符也是“;”,说明这个字符相当于Base64的“=”,印证我们之前的推测。
此时我们已经得到编码后的字符串,现在要将字符串解码。byte24E8的两个占位符是最后才加上去的,所以不参与异或。
红框中,如果直接打印,可见字符会被输出;如果需要数据转成16进制形式,需要使用binascii库。现在已知key,原始S盒,加密后的16进制字符串,就可以进行RC4解密了。
这…很明显不对,原始S盒不可能出错,加密后的16进制字符串应该没错,所以可能是解key时出了错。遇到移位运算,不要使用负数,不要太相信结果,要结合上下文分析。
结果发现并没有用到“-”,所以还是不对。如果将字符串全部逆序根本不用将“-”提取出来,可能是“-”的位置不变。最后转换出来的key如下图:
更改key值再次进行RC4解密,哎这个就像样了,是正常字符串,fu0kzHp2aqtZAuY6
。
输入密码得到flag。
1 | flag{android4-9} |
24. 一统天下
在说明中可以知道加密逻辑已经被去掉了,也就是说这个APK文件无害的,需要我们研究其中的加密算法。载入jeb,加密算法如下图,但是类和方法混淆得太离谱了,如果jeb没有自动解密功能还会更夸张。
这个App名是吃鸡辅助,点进去后配置完文件自动退出,生成另一个名为QQ的App,卸载吃鸡辅助App。点进QQ图标重新进入勒索软件界面,找到关键类。
每次加载APK到jeb,this.O000O0OO = "" + 10169905;
中的常量值都会改变,使用jadx工具发现这里进行了一个随机数选择。
进入new OO00O0()
,找到显示主页面执行逻辑。定位关键字符串,找到与特征码、解密口令相关语句。
接下来就是找OO0OOOO()
函数。我们发现lib目录下有两个So文件,libmydvp.so
和libmyqtest.so
。主页面加载了libmyqtest.so
,导出表中没有OO0OOOO()
函数和JNI_OnLoad()
,看到So字符串加密和一个Native层函数Java_android_support_v4_app_o000000o_o000000o0()
,该函数是在主页面加载的。
猜测要找的函数应该在libmydvp.so
中,程序又是如何加载libmydvp.so
到内存的呢?在Java代码中查找loadLibrary()
函数。
根据继承关系找到OO0OOOO0.class
,它在AndroidManifest.xml
中,被provider标签调用。因为provider会在入口类走之前运行,因此第一个执行的类为android.support.v4.app.OO0OOOO0
,也就是这个程序先加载libmydvp.so
再加载libmyqtest.so
。
这个So文件使用到了OLLVM混淆。
JNI_OnLoad()
函数:
24.1 反字符串加密
So中的字符串解密有两种情况,一种是在执行JNI_OnLoad()
前就将所有字符串解密,解密算法在.init_array
中;另一种是使用时再解密。加密字符串数据存储在.data
节区。
先将所有加密字符串解密,这里如何快速获得所有的解密字符串?一个个敲代码或在010 Editor中异或都太慢了。。。找到OO0OOOO()
函数,并且看字符串还有TracerPid检测。
果然,被我找到一些方法。使用IDA插件uEmu对加密字符串进行解密;使用Frida hook方式得到解密字符串;使用AndroidEmu或unidbg对抗字符串混淆;也可以将So加载到内存后,dump .data
节区中的数据。
Android APP漏洞之战(14)——Ollvm混淆与反混淆这篇文章几乎涵盖这道题所要用到的知识。
24.1.1 IDA插件uEmu
由于我下载的IDA Pro自带Python,所以要将unicorn下载到相应的Python环境中,在载入uEmu.py
脚本时才不会报没有这个模块的错:
1 | pip install unicorn --target="D:\CTF\tools\IDA_Pro_v7.5\python\3\" |
在解密函数的首尾下断点,在首断点右键 -> uEmu -> Start,在尾断点右键 -> uEmu -> Run,右键 -> uEmu -> Show Memory Range 查看byte_E3004解密后的结果。
24.1.2 Frida hook
1 | function hook_native(){ |
24.1.3 AndroidEmu
批量解密可以用AndroidNativeEmu:
1 | pip install androidemu |
采用上面文章的解密脚本,但不知道为什么出错,暂时不管了。
1 | import logging |
24.1.4 从内存中dump .data
节区
查看应用程序的进程与PID:
1 | > adb shell "ps | grep {PackageName}" |
在/proc/{PID}/maps
中得到libmydvp.so
的内存映像地址:
1 | # cat /proc/11082/maps |grep libmydvp.so |
.data
节区可读可写,所以将最后一段dump出来。skip是内存起始地址,count是这个节的大小:
1 | # dd if=/proc/11082/mem of=/data/local/tmp/dump.so skip=3575353344 bs=1 count=4096 |
将dump.so
赋予755权限再pull出来。使用010 Editor将适当的字符串数据拷贝到libmydvp.so
中的.data
节区。
重新加载libmydvp.so
,就可以看到解密后的字符串。
24.2 反虚假控制流
分析完.init_array
的字符串解密,在.init_array
中还有一些函数。
进入sub_1AEE8()
,可看到pthread_create()
函数和getpid()
函数,还有/proc/
和/status
、TracerPid
字样,可以确定这个函数是在进行TracerPid检测。
进入sub_1ED54()
,创建管道,创建线程,同样是进行了TracerPid检测。
进入JNI_OnLoad()
,分析代码。
ssspbahh.so
是以NISLFILE开头的,并没有这种魔数类型。但我们在解密字符串是发现NISLFILE字样,并且被JNI_OnLoad()
中的sub_86128()
调用。然而这个函数被混淆得离谱。
尝试通过符号执行来解决虚假控制流和控制流平坦化。Kali安装angr环境教程,在Windows安装就像shit一样。ida pro6.4 linux安装使用,用来查看函数地址。利用angr符号执行去除虚假控制流,使用deflat脚本。
进入虚拟Python环境:
1 | source /root/angr/bin/activate |
执行命令:
1 | python3 deflat.py -f samples/bin/check_passwd_x8664_flat --addr 0x400530 |
退出虚拟环境:
1 | deactivate |
不知道为什么去不了…明明例子中的ELF文件可以去掉的。
通过native hook方式找到动态注册函数?但是有反调试,这个方法应该不可行。
25. Phishing is not a crime-2
你能用Instagram for Android移动应用程序APK v7.9.0在signed_bodyPOST参数中对传出的JSON数据进行签名吗?提示:我喜欢早上的炒蛋!
下载下来是一个伪Instagram,抓包没抓到东西,连不上网。点击登录或注册根本没请求到服务器。
26. Flag_system(备份文件ab)
下载下来的是一个安卓备份文件,具体看题3。
直接使用abe解压出来的TAR文件损坏,发现版本与是否压缩都不一样,修改一下该文件的版本与是否压缩参数,并将后缀名改为.ab
。此时解压出来的TAR文件正常。
TAR文件中主要有两个APK文件和一个DB文件,DB文件被加密。其中一个APK非常简单,再看另一个。
并没有什么特殊的信息,我们知道数据库名为BOOKS.db
,找到建立数据库的地方。
此时数据库密码可以通过Log打印,也可以通过动调或hook方式得到。由于程序没有设置debuggable,所以通过设置ro.debuggable=1,再进行动调比较方便。由于getSign()
函数在程序运行前就执行了,所以要用debug模式才能断到getSign()
函数处。
得到数据库密码:320b42d5771df37906eee0fff53c49059122eeaf。但我使用DB Browser for SQLCipher for Windows v3.12.99打不开数据库,用sqlcipher 2还是不能打开。
此时可以换一种方法解题。查看getReadableDatabase()
函数,它返回的是一个SQLiteDatabase类型的对象,而这个数据类型中有query()
与rawQuery()
方法,都是用来查询数据库内容的。
对比之下发现rawQuery()
方法更符合我们编写SQL语句的习惯,我们可以用它来查询数据库中的信息。hook getReadableDatabase()
,得到SQLiteDatabase对象,然后在getReadableDatabase()
方法中调用rawQuery()
方法来获得想要的数据。
为什么只有自己增加的数据…
27. Where(炸弹引爆 & DEX修复)
下载下来不知道什么格式文件,拿去WinHex或者010 Editor,文件头为50 4B 03 04,标准的ZIP文件格式,APK文件本质上也是个ZIP文件,所以将后缀名修改为.apk
。
/assets
目录下还有一个abc文件,打开发现是一个DEX文件头,在/original
目录下找到一个y文件,应该是DEX文件的一部分。
查看abc文件,由于DEX文件头描述了整个DEX文件的分布,所以可以看到DexClassDef与数据段是有数据的,其余都为0。
但y文件只有0x94大小,根本不能构成一个DEX文件。与它同目录的CERT.RSA
文件异常的大,考虑是使用了炸弹引爆。
已知DEX文件总大小为0x1549F0,DEX文件头大小为0x70,所以DEX body的大小为0x154980。DEX body埋在了CERT.RSA
的末尾,它的总大小为0x154EAB,所以CERT.RSA
的大小应该最大为0x52B。
但在提取前发现前面有“KEY”和“DEX”字样,后面有“aes-128-cbc”,也就是说DEX body有可能进行了AES加密。
aes-128-cbc是对称加密算法,加解密使用的KEY是一样的,明密文长度有可能不一致。将加密过后的DEX body取出,使用解密算法解密。
1 | openssl enc -d -aes-128-cbc -k 'Misc@inf0#fjhx11' -nosalt -in encrypt_dex -out decrypted |
解密失败,这是由于openssl高版本加密时对密码使用sha256进行加密;低版本使用md5加密。如果需要兼容,则需要指定参数-md
的值,确保两边使用相同的摘要算法。使用opensll加解密压缩文件1
openssl enc -d -aes-128-cbc -md md5 -k 'Misc@inf0#fjhx11' -nosalt -in encrypt_dex -out decrypted
将decrypted
中的数据追加到abc文件的尾部,大小恰好是0x1549F0。
由于现在已经有DEX body了,string_ids_size、type_ids_size等这些肯定不全为0。以string_ids_size为例,它的偏移为0x70,type_ids_size的偏移为0x91DC,这两个地址中间存放的全都是string的偏移地址。一个地址占4个字节,所以string_ids_size应该为(0x91DC - 0x70) / 4 = 0x245B。同理可得type_ids_size为0x484,proto_ids_size为0x12CF,field_ids_size为0x1BA4,method_ids_size为0x4404。
将修改后的abc文件载入jadx发现checksum错误,猜测DEX文件还没完全修复,但现在只能修改checksum查看DEX文件被我们修改成什么样了。
发现onCreate()
方法还没被修复出来。查看abc文件中的onCreate()
方法,刚好全0的数据大小为0x94。
将y文件中的数据复制到这个地方,最后修改checksum(和最开始的checksum也不一样)。载入jadx查看onCreate()
方法,得到最终flag:
28. TryGetFlag1
看起来是函数抽取型壳,使用FART脱壳,应用没有存储权限,要在AndroidManifest.xml
中增加读写存储权限。
1 | <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> |
在application标签中添加属性:
1 | android:requestLegacyExternalStorage="true" |
但我们发现重新安装APK后APK不能正常运行,也不能使用FART进行脱壳,这不死循环了吗…猜测这道题根本不用脱壳解题。
由于知道一些明显的方法和成员变量,可以使用Frida hook这些方法与成员变量查看其值。
GetFlag()
方法返回的字符串提示我们这里有可利用的bug。MainActivity.class
中还有一个静态成员变量,也将它hook出来看看。
将字符串作为GetFlag()
的参数传入,却发现它说我们得到了一个错误的flag。
1 | CISCN{You.Got.It.11a15e} |
提交上去不行,我实在不知道怎么解了。
29. Load
APK安装上了但闪退。
救命真的好难