起因
模电作业马上就要截止了,然而手中却没有一本趁手的答案以供订正(
然后就收到了一份分享,起初以为是个网盘链接啥的,结果点进去打眼一看,原来是要下 APP??这岂能忍得了,不过为了拿到答案,我还是下了一份应用。熟悉我的朋友们都知道,按照我的做事风格,事情肯定不会到这里就结束了。每次想查个答案还要打开它的 APP,简直不要太憋屈,然而我早已是有着多年逆向工作经验的老油条 (臭不要脸 ^^),这事必须得给它办了
开整
要是放到高中,估计我只会用个小黄鸟抓抓包啥的,不行就开始上 Auto.js 自动化,把每一页都截图。但今时不同往日,在与各种花里胡哨的应用斗智斗勇的过程中我也掌握了不少新手段,今天就拿这「大学搜题酱」来练练手
APP 里答案的每一页都是一张图片,那么先从最简单的办法开始,用 mitmproxy 进行一个包的抓,发现 URL 毫无章法,但每一张图片都可以在浏览器直接粘贴 URL 看到,并没有做身份验证:
那么应该会首先有一个 /api/list
之类的请求用于获取每一页图片的 URL 列表,然而这个 APP 有证书固定,mitmproxy 的根证书没法解密它的 https,尝试抓取会产生一个网络错误:
于是开始尝试到 /data/data
底下翻找有没有什么缓存,可能是做了数据加密或者根本就没有缓存,除了一堆乱七八糟的文件以外什么都没找到。不得已只能上魔法了,使用 Activity 或 Intent 记录工具得知查看答案资源的活动名为 AnswerBrowseActivity
,查看安装包的 dex 文件,发现有一层腾讯御安全的壳😰,时间成本再次++
用 Frida-DexDump 脱好壳,拉到 jadx 里看一眼,很快就能发现类里有一个 ArrayList<String>
类型的属性,以及对它做了修改的方法,很明显是从启动这个 Activity 的 Intent 中取出了一个叫 INPUT_IMG_URL_LIST
的列表,而这显然就是我们想要的东西:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class AnswerBrowseActivity extends TitleActivity { public ArrayList<String> f = new ArrayList<>(); private void f () { Intent intent = getIntent(); if (intent != null ) { this .t = getIntent().getIntExtra("INPUT_BOOK_TYPE" , 0 ); this .h = getIntent().getIntExtra("INPUT_PHOTO_POSITION" , 0 ); ArrayList<String> stringArrayListExtra = intent.getStringArrayListExtra("INPUT_IMG_URL_LIST" ); if (stringArrayListExtra != null ) { this .f.clear(); this .f.addAll(stringArrayListExtra); } g(); } } }
接下来只需要把它拿出来就好了,快速搓一个 Xposed 模块,在 onResume
方法开始前查找类型为 ArrayList
的属性,并把它的内容打到 logcat:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class HookEntry : HookHelper ("AnswerDump" ) { override fun onApplicationAttach (context: Context ) { setDefaultClassLoader(context.classLoader) findMethod("com.zmzx.college.search.activity.booksearch.result.activity.AnswerBrowseActivity" ) { name == "onResume" }!! before { param -> catch { param.thisObject.javaClass.declaredFields.forEach { field -> if (ArrayList::class .java == field.type) { @Suppress("Unchecked_Cast" ) (field.get (param.thisObject) as ArrayList<String>).forEach { Log.i(it) } return @forEach } } } } } }
啊好像 hook Activity#onCreate(Bundle)
就行了,这是个 @CallSuper
标记的方法,每个 Activity 实例都会调到这里:
1 2 3 4 5 6 7 8 9 10 11 12 13 XposedHelpers.findAndHookMethod( Activity::class .java, "onCreate" , Bundle::class .java, object : XC_MethodHook() { override fun beforeHookedMethod (param: MethodHookParam ) { with (param.thisObject as Activity) { intent.getStringArrayListExtra("INPUT_IMG_URL_LIST" )?.forEach { Log.i(TAG, it) } } } } )
其实这一步用 Frida 应该会更快,只是我实在懒得修复之前折腾坏的环境(
后面的事情就非常简单了,用 Python 将所有图片一张张下载下来即可,这里直接贴上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from time import sleepimport httpxwith open ('image_urls.txt' , 'r' ) as fp: lines = fp.readlines() for i, line in enumerate (lines): print (f'[{i+1 :0 >3d} /{len (lines):0 >3d} ]: {line.strip()} ' ) resp = httpx.get(line.strip()) with open (f'images/{i:0 >3d} .jpg' , 'wb' ) as fp: fp.write(resp.content) sleep(0.5 )
最后再整合成 PDF:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import reimport osfrom fpdf import FPDFpdf_file = FPDF(unit='pt' , format =(1389 , 2043 )) pdf_file.set_auto_page_break(False ) pdf_file.set_left_margin(0 ) pdf_file.set_top_margin(0 ) images = os.listdir('images/' ) images.sort(key=lambda s: int (re.search(r'^(\d+)' , s).group(1 ))) for file in images: pdf_file.add_page() print (os.path.join('images/' , file)) pdf_file.image(os.path.join('images/' , file)) pdf_file.output('output.pdf' )
整套流程整合起来其实是可以搓出一个支持应用内直接下载 PDF 的 Xposed 模块的,想了想这应用比较小众,而我又没啥需求,还是算了。