wmctf-2023
WMCTF2023
8.20的比赛,拖了快两个月,我是懒狗,呜呜呜。
0x00 ezAndroid
1 | 耐心搜索 |
Android killer与PKID显示未加壳。jeb打开:
跟踪this.CheckUsername与this.check2函数。发现是naive层的函数,如下所示:
(1)找到java函数在native层对应的函数符号,并以此找到函数地址。(包名:com.wmctf.ezandroid
)
(a)objection注入失败(frida反调试),但是幸运的是,关键函数CheckUsername与check2都使用动态注册,根据frida_hook_libart,有:
1 | frida -U -f com.wmctf.ezandroid -l hook_RegisterNatives.js --no-pause |
(2)可以得到CheckUsername和check2在libezandroid.so中的名字:0x35a4
与0x3f0c
。如下所示:
sub_35a4-CheckUsername
Step1
分析libezandroid.so,对于sub_35a4(CheckUsername),其中首先调用了sub_3EC0(a1, name, 0LL),其中a1为JNIEnv,name为用户名。跟进后,发现是JNIEnv+0x548函数。
因此,打算hook sub_3EC0函数,并打印其输入与返回值。但是首先,先绕过反调试(因为frida注入时总会退出
)。
Step2
反调试绕过,见链接。首先查看open了哪些文件:
1 | // look_open.js |
发现断在了:
/proc/self/maps用于访问当前进程的内存映射信息。查看此映射信息:
1 | // look_maps.js |
当挂上frida之后,如下所示:
要绕过此检测,就要备份一个正常启动的maps文件,如下所示:
1 | // bypass.js |
Step3
快乐的hook sub_3EC0():
1 | // hook_sub_3EC0.js |
发现返回值ret为用户名,但是args[1]并不是想象中的,为用户名,猜测应该是用户名的jstring对象。
Step4
紧接着分析,发现sub_35a4函数貌似经过ollvm的混淆,有很多while循环,如下所示:
使用项目,将deflat.py代码改为:
即可扫描到sub_35a4函数,运行指令:
1 | python3 deflat.py -f libezandroid.so --addr 0x4035A4 |
但是效果不尽人意。
Step5
经过艰苦卓绝的手动测试,终于得到如下结论:
- 一共三轮大循环,前两轮大循环无用。
- 用户名长度为10,如下所示,其中v47为用户名长度:
memcmp(v30, &byte_A138, 0xAu)
应总返回0,即比较成功。
接下来,重点关注以下部分(仅执行一次):
钩取sub_60A0函数,得到返回值为12345678
。
钩取sub_6C2C函数,其参数分别为:用户名,返回字符串(要返回),用户名长度,字符串”12345678”,某个值(要返回)。
钩取memcmp函数,但是系统可能一直在调用memcmp,且Interceptor.attach开销很大,导致钩取时直接卡住。
Step6
分析sub_6C2C函数,首先使用deflat olllvm反混淆,但失败。之后,将程序划分为多个Block,分别为:
1 | // block 1 |
block的运行逻辑为:先256次 block3->block10->block6
,block3,block1,再256次 block8->block9->block11
,block8,block7,最后10次block5->block2->block4
。经过分析,属于RC4算法
,密钥为12345678
。RC4魔改了一下,最后异或j。
Step7
这一步主要是获取memcmp的第2个参数byte_A138
。前面钩取memcmp的方法行不通,那么找到byte_A138在哪儿赋值的。找到相关函数,如下所示:
得到byte_A138 = [0xe9,0x97,0x64,0xe6,0x7e,0xeb,0xbd,0xc1,0x0,0x1e]
。解密脚本出不了,猜测是不是byte_A138的计算有问题,frida钩取一下。打印内存的脚本:
1 | function print_mem(){ |
发现byte_A138
应该为:[0xe9,0x97,0x64,0xe6,0x7e,0xeb,0xbd,0xc1,0xab,0x43]
。
针对用户名的解密脚本:
1 | from Crypto.Cipher import ARC4 |
sub_3f0c-check2
再来看验证密码的函数,与上个函数的分析类似,密码长度为16。重点关注sub_AFC与unk_A158。
Step1
钩取sub_AFC,发现参数为:密码、0x10、Re_1s_eaSy123456。分析sub_AFC,划分block(化简版):
1 | // block1 |
计算过程:block1、block6、block8、9次block5->block3->block4
、block5、block2、block7、block6。化简后:block1、block8、9次block3->block4
、block2、block7。
分析block1中的sub_11A0,参数分别为:-2263271711、Re_1s_eaSy123456,猜测是初始化参数,在此跳过。
再分析block8中的sub_165C,是将用户输入的密码传递到另一个变量中,例如存到变量里是abcdefghijklmnoz
,那么变量则为aeimbfjncgkodhlz
。
1 | d = {"0":[], "1":[], "2":[], "3":[]} |
block8中的sub_1990,逻辑为(轮密钥加):
1 | byte_A178 = [...] |
block3中的sub_1CD8,是字节替换。block3中的sub_2000,是行移位。sub_23FC是列混淆。sub_2ADC则是重新得到密文。综上,属于AES加密流程。
既然,AES是对称加密,那么我们把要比较的密文扔进去,再过一遍,不就得到明文了吗?(我真傻逼
)
hook得到要比较的密文:
1 | 2bc8208b5c0da79b2a513ad27171ca50 |
主动调用sub_afc函数(不要忘记前面的bypass哦):
1 | function bypass() { |
Step2
与正常的AES加密结果不同,猜测更换了S盒。表面上没改,实际用的时候改了,真狗啊。下面是抓取S盒的脚本:
1 | function bypass() { |
抓到的S盒为:
1 | [0x29, 0x40, 0x57, 0x6e, 0x85, 0x9c, 0xb3, 0xca, 0xe1, 0xf8, 0x0f, 0x26, 0x3d, 0x54, 0x6b, 0x82,0x99, 0xb0, 0xc7, 0xde, 0xf5, 0x0c, 0x23, 0x3a, 0x51, 0x68, 0x7f, 0x96, 0xad, 0xc4, 0xdb, 0xf2,0x09, 0x20, 0x37, 0x4e, 0x65, 0x7c, 0x93, 0xaa, 0xc1, 0xd8, 0xef, 0x06, 0x1d, 0x34, 0x4b, 0x62,0x79, 0x90, 0xa7, 0xbe, 0xd5, 0xec, 0x03, 0x1a, 0x31, 0x48, 0x5f, 0x76, 0x8d, 0xa4, 0xbb, 0xd2,0xe9, 0x00, 0x17, 0x2e, 0x45, 0x5c, 0x73, 0x8a, 0xa1, 0xb8, 0xcf, 0xe6, 0xfd, 0x14, 0x2b, 0x42,0x59, 0x70, 0x87, 0x9e, 0xb5, 0xcc, 0xe3, 0xfa, 0x11, 0x28, 0x3f, 0x56, 0x6d, 0x84, 0x9b, 0xb2,0xc9, 0xe0, 0xf7, 0x0e, 0x25, 0x3c, 0x53, 0x6a, 0x81, 0x98, 0xaf, 0xc6, 0xdd, 0xf4, 0x0b, 0x22,0x39, 0x50, 0x67, 0x7e, 0x95, 0xac, 0xc3, 0xda, 0xf1, 0x08, 0x1f, 0x36, 0x4d, 0x64, 0x7b, 0x92,0xa9, 0xc0, 0xd7, 0xee, 0x05, 0x1c, 0x33, 0x4a, 0x61, 0x78, 0x8f, 0xa6, 0xbd, 0xd4, 0xeb, 0x02,0x19, 0x30, 0x47, 0x5e, 0x75, 0x8c, 0xa3, 0xba, 0xd1, 0xe8, 0xff, 0x16, 0x2d, 0x44, 0x5b, 0x72,0x89, 0xa0, 0xb7, 0xce, 0xe5, 0xfc, 0x13, 0x2a, 0x41, 0x58, 0x6f, 0x86, 0x9d, 0xb4, 0xcb, 0xe2,0xf9, 0x10, 0x27, 0x3e, 0x55, 0x6c, 0x83, 0x9a, 0xb1, 0xc8, 0xdf, 0xf6, 0x0d, 0x24, 0x3b, 0x52,0x69, 0x80, 0x97, 0xae, 0xc5, 0xdc, 0xf3, 0x0a, 0x21, 0x38, 0x4f, 0x66, 0x7d, 0x94, 0xab, 0xc2,0xd9, 0xf0, 0x07, 0x1e, 0x35, 0x4c, 0x63, 0x7a, 0x91, 0xa8, 0xbf, 0xd6, 0xed, 0x04, 0x1b, 0x32,0x49, 0x60, 0x77, 0x8e, 0xa5, 0xbc, 0xd3, 0xea, 0x01, 0x18, 0x2f, 0x46, 0x5d, 0x74, 0x8b, 0xa2,0xb9, 0xd0, 0xe7, 0xfe, 0x15, 0x2c, 0x43, 0x5a, 0x71, 0x88, 0x9f, 0xb6, 0xcd, 0xe4, 0xfb, 0x12] |
S盒求逆,得到:
1 | [65, 232, 143, 54, 221, 132, 43, 210, 121, 32, 199, 110, 21, 188, 99, 10, 177, 88, 255, 166, 77, 244, 155, 66, 233, 144, 55, 222, 133, 44, 211, 122, 33, 200, 111, 22, 189, 100, 11, 178, 89, 0, 167, 78, 245, 156, 67, 234, 145, 56, 223, 134, 45, 212, 123, 34, 201, 112, 23, 190, 101, 12, 179, 90, 1, 168, 79, 246, 157, 68, 235, 146, 57, 224, 135, 46, 213, 124, 35, 202, 113, 24, 191, 102, 13, 180, 91, 2, 169, 80, 247, 158, 69, 236, 147, 58, 225, 136, 47, 214, 125, 36, 203, 114, 25, 192, 103, 14, 181, 92, 3, 170, 81, 248, 159, 70, 237, 148, 59, 226, 137, 48, 215, 126, 37, 204, 115, 26, 193, 104, 15, 182, 93, 4, 171, 82, 249, 160, 71, 238, 149, 60, 227, 138, 49, 216, 127, 38, 205, 116, 27, 194, 105, 16, 183, 94, 5, 172, 83, 250, 161, 72, 239, 150, 61, 228, 139, 50, 217, 128, 39, 206, 117, 28, 195, 106, 17, 184, 95, 6, 173, 84, 251, 162, 73, 240, 151, 62, 229, 140, 51, 218, 129, 40, 207, 118, 29, 196, 107, 18, 185, 96, 7, 174, 85, 252, 163, 74, 241, 152, 63, 230, 141, 52, 219, 130, 41, 208, 119, 30, 197, 108, 19, 186, 97, 8, 175, 86, 253, 164, 75, 242, 153, 64, 231, 142, 53, 220, 131, 42, 209, 120, 31, 198, 109, 20, 187, 98, 9, 176, 87, 254, 165, 76, 243, 154] |
根据项目,快乐得到:_eZ_Rc4_@nd_AES!
。最后flag:Re_1s_eaSy_eZ_Rc4_@nd_AES!
。
0x01 ez_v1deo
1 | 视频好像被L1near弄坏掉了 |
7s的avi视频,92.8MB,感觉离谱,肯定藏了什么东西。根据AVI格式看了看,感觉没啥问题,单纯就是觉得strl块(存放avi数据流)太大了,92MB。但是看了一下,链接说avi文件体积本来就很大,所以应该也没啥问题,还推荐了MSU VideoStego工具来分析avi文件。
详细的avi格式:链接。
MSU VideoStego工具解压缩需要密码,试了几个说不行。最后使用binwalk提取出了sit类型的文件。但是感觉方向错了,看wp。
wp
(1)首先使用ffmpeg将avi转为帧格式。
1 | ffmpeg -i flag.avi -r 20 ./images/image-%3d.png |
(2)使用lsb提取。
1 | from PIL import Image |
最终得到flag:WMCTF{5b658ab9-946c-3869-fc21-6ad99b3bc714}
。虽然和逆向没太大关系,但是学到了。
发现我好像把题目记录错了,日了狗了,以下是更改版本。
0x02 gohunt
1 | wmctf{...} |
一个flag.jpg,一个gohunt。jpg扫出来数据为:
1 | YMQHsYFQu7kkTqu3Xmt1ruYUDLU8uaMoPpsfjqYF4TQMMKtw5KF7cpWrkWpk3 |
跑一跑Gohunt,猜测逻辑是有一个original.jpg,输入flag,之后输出了flag.jpg。
Gohunt分析
发现其中导入了很多github相关函数,那么,直接binaryAI处理一波(没啥卵用),Go_parser报错,显示找不到Moduledata,看来是作者自己删了。
重点是分析main_main函数。对于__encoding_base64_Encoding__DecodeString
函数,得到其转换码表:
1 | decodeMap = [b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'.', b'\xff', b'\xff', b'\xff', b'\xff', b'\x0e', b'\x17', b'\x1b', b'0', b')', b'\x0f', b'$', b'\x13', b'=', b'-', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'!', b'*', b'\x18', b'5', b'2', b'\x1a', b':', b'?', b'\x04', b'%', b'8', b'6', b'\x1e', b' ', b'\x00', b'4', b',', b'\x02', b'\x0b', b'\x1c', b'\n', b'&', b'3', b';', b'\x06', b'9', b'\t', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'+', b'(', b'1', b'\x14', b'\x0c', b'\x1d', b'\x11', b'7', b'\x08', b"'", b'\x01', b'\x15', b'\x07', b'\x10', b'\r', b'\x19', b'"', b'#', b'<', b'>', b'\x12', b'\x16', b'\x1f', b'\x03', b'\x05', b'/', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'\xff', b'=', b'\x00', b'\x00', b'\x00'] |
时间过了好久好久,已经到 2024-02-29,我又开启了 CTF 的刷题旅程。
Step1:发现函数 __github_com_yeqown_go_qrcode_v2_QRCode__Save(*&old[56LL], __PAIR128__(v417.value, v185.len))
,动态调试查看最终保存的数据是哪个参数。最终发现 v.len
保存要输出的数据。
Step2:对 v.len
的写操作进行分析,分析不出来啥。已知题目中使用的项目为 link1,因此打算先做一个 demo,然后对 demo 进行分析。通过对 demo 和 gohunt 的文件进行相似性查看,找到最初的点(也就是把加密后的 flag 使用 QRcode 这个项目进行处理的最开始的点)。
在此补充一个知识点,这是 go 协程的逆向对比。
Step3: 最终,可以分析出程序的大致逻辑如下。
1 | package main |
Step4: 分析 Step3 中 encrypt 函数的逻辑。并可以写脚本,得到 flag:
1 | 1. 使用 xxtea 算法加密输入,v = 输入,k = FMT2ZCEHS6pcfD2R,输出为 out_1。 |
1 | import binascii |
1 |
|
0x03 ios
1 | wmctf{...} |
发现是 IOS 的 IPA 文件,改为 zip 文件解压缩后,发现 IDA 的调试文件:
打开后发现是典型的 OLLVM 混淆:
Step1: 使用 Binary Ninja 的去混淆脚本 llvm-deobfuscator 报错,分析发现是此脚本将程序入口定位到了 0x10000a9a0
,且此函数并没有其他函数的入口。但是可以看到红框部分应该是主程序的入口,其地址是由参数控制的。
根据 link,可以知道:IOS 程序会先执行 main 函数(也就是上图中的 MEMORY[0x10000A9A0]
),main 函数内部会调用 UIApplicationMain 函数。如下所示:
1 | UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])) |
在 UIApplicationMain 中,会创建 UIApplication 对象与 UIApplication 的 delegate 对象(又叫做 AppDelegate,它会开启一个消息循环,并监听对应的系统事件)。当应用程序启动完成后,UIApplicationMain 会调用 application:didFinishLaunchingWithOptions 方法,在这个方法中,会创建窗口、设置视图控制器、配置数据模型。它标志着程序启动完成。
Step2: 查看 application:didFinishLaunchingWithOptions,并未发现什么有用的逻辑。
Step3: 询问 GPT 后发现,IOS 程序的主逻辑大概有以下几个地方:
1 | (1)View Controllers,主要就是 window.rootViewController。 |
找到一个 ViewController handleButtonClick 函数,但是还是 OLLVM,所以打算手动修复一下 llvm-deobfuscator 的脚本,猜测:
(1)脚本不 work 的原因可能是函数一开始是有参数的。但是经过调研,了解插件的运行原理之后,发现是自己插件使用的有问题。
(2)继续调研后发现,即使正常调用脚本,也会出现错误,我真是服了,继续调研。
(3)我用的 binary ninja 3.5 的版本,很多 API 更新了。经过调研,应该就是这个问题。
Step4: 本来想将 x64 的脚本改成 ARM 的,但是发现很多地方都不适配,所以想自己写一个 binary ninja 的脚本。首先,找到此题目中 OLLVM 的规律。
规律 1:看黄框,每一个函数的开头都定义了一些变量,这些变量供下面的 cmp 使用。
规律 2:看红框,之后紧接着好多 cmp 指令,用于 dispatch。
规律 3:看蓝框,紧接着有好多实际的逻辑块,最后是 return 块。
那么,我们可以设计如下去 OLLVM 的算法:
1 | 1. 根据红框,确定要关注的变量,例如这里的 w11。 |
但是我看其他函数好多还不是这个逻辑,头疼,打算用符号执行了。具体可以见 link2。
Step5: 符号执行可以使用 angr 或者 unicorn 工具。使用 angr 去 OLLVM 时,见 useful-scripts。Angr 去除 OLLVM 时,是直接以块为起点,模拟执行来找到下一个块的,如果在这个块之前还产生了某个事件,这个事件对这个块的执行流程产生了影响的,那么本次模拟处理的结果就是不准确的。
相比而言,unicorn 在去除花指令或去除 OLLVM 方面更有优势,这是因为:unicorn 相当于一个虚拟 cpu,所以处理的细微程度高于 angr 的代码块级,修复出来的代码精度更高。
与 angr 类似,unicorn 写去 OLLVM 脚本的思路也是通过代码的CFG图,分析出代码块之间的关系,然后模拟执行每个代码块
。但是 fallwind 的脚本是针对 ELF 文件的,我也懒得该脚本了。直接看 wp。wm 给出的 wp 是使用 iPhone 进行的 frida-hook,与我的解题思路不同。
Step6:wp 的复现,wp1,wp2。在 wp1 中,我一直不能使自己的 ipad 越狱,会出现以下报错,且我是 intel 的处理器(网上说只有 AMD 的处理器才会报这种错误)。
可以采取上题 ezandroid 中划分 block 的方法,对 handleButtonClick 函数进行分析。最终用 unicorn 脚本:
1 | import idaapi |
可以将 handleButtonClick 展平为:
1 | ------------------------- Block 0 | Addr 0x100006d50 ------------------------- |
展平脚本还是不对,猜测是函数返回的结果也会影响执行流。
Step7:硬看 OLLVM。几个函数:
1 | 1. objc_msgSend:当向一个对象发送消息时,objc_msgSend 负责找到对应的方法实现并执行它。 |
可以得到一些信息:
跟踪此函数,发现有 RC4 算法(主要是问了一下 GPT),然后又发现:
这题需要读 __dyld_get_image_header/__dyld_get_image_vmaddr_slide
,可能必须得动调?直接放弃好吧。草!我怎么这么菜。
0x04 RightBack
1 | wmctf{...} |
pyc 文件逆向。使用 pycdc 反编译失败。直接拿到字节码(并且由于 pyc 文件做了某些改动,需要修 dis.py):
1 | import dis |
可以得到字节码,分析后得到:
1 | import struct |
其中,Fun 函数是一个 vm,经过实验,其步骤为 8 轮相同操作:
1 | ECX = 0 |
1 | c = (a + extendKey[0]) & 0xffffffff |
可以得到 extendKey 与 data2:
1 | extendKey: |
之后的脚本不想写了(分析了就感觉有点小麻烦),直接给佬的 wp(一个 RC5 脚本):
1 |
|
但是实际上,自己的反编译还是有点错误的,从而导致输入正确的 flag 之后还是报 i believe you can do it。 PZ 是做了一个很严谨的去花:PoZeep (P.Z) · GitHub,还是得细心一点,匠人精神!
留言
- 文章链接: https://wd-2711.tech/
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明出处!