DAS 5月
隔天就要考试,单纯摸了摸,看能否摸到工作量少的,于是选择了luajit,网上的方法几乎尝试了一遍也没有解决方案,版本信息没有,luajit自带的反汇编尝试无果,体验极差。
wer
求解
bcf虚假控制流,真正的逻辑不在main,而是在某个函数内对输入进行了转存,之后又进行了相关处理。
直接定位到输入部分,要求输入为38位并且将输入存入两个xmmword下。
跟进sub_1400DB50
,a2中存放的我们的输入,可见对输入进行了转移,之后对xmmword_140035C20
交叉引用,定位到一个新的处理函数。
check逻辑为 input[i]^v3[i]==102 ,循环32次如果成功就输出correct。
exp
1 2 3 4 5 enc=[0x05 , 0x03 , 0x55 , 0x05 , 0x04 , 0x07 , 0x5E , 0x54 , 0x05 , 0x07 , 0x50 , 0x02 , 0x03 , 0x53 , 0x5F , 0x50 , 0x53 , 0x50 , 0x53 , 0x05 , 0x55 , 0x00 , 0x54 , 0x55 , 0x57 , 0x03 , 0x05 , 0x02 , 0x52 , 0x50 , 0x51 , 0x53 ] for i in range (len (enc)): enc[i]^=102 print (b'flag{' +bytes (enc)+b'}' )
下次时间紧还是老老实实摸windows了,不摸不熟悉的领域。😭😭😭
溯源
查阅Windows 错误报告 - Win32 应用|微软文档 (microsoft.com) 官方文档,了解WER和ARR(程序恢复和重启)的相关知识。
错误报告功能使用户能够通知微软有关应用程序故障、内核故障、无反应的应用程序和其他应用程序的具体问题。并且可以使用应用程序恢复和重启(ARR),以确保客户在其应用程序崩溃时不会丢失数据,并允许用户快速返回到他们的任务。
对getflag的主要函数进行交叉引用,观察函数调用的关系。
首先atexit注册了终止函数,可以在任何地方注册终止函数,并且他在程序结束时调用。
并且注册是在main函数运行前完成的。
initterm函数负责遍历函数指针表并初始化它们的内部方法。
这个特性为其能隐藏控制流,偷天换日,图穷而匕不见提供了支撑。
其次是注册了恢复回调函数
如果应用程序遇到未处理的异常或变得无响应,Windows 错误报告 (WER) 将调用指定的恢复回调。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 HRESULT RegisterApplicationRecoveryCallback ( [in] APPLICATION_RECOVERY_CALLBACK pRecoveyCallback, [in, optional] PVOID pvParameter, [in] DWORD dwPingInterval, [in] DWORD dwFlags ) ;ApplicationRecoveryInProgress(&pbCancelled); HRESULT ApplicationRecoveryInProgress ( [out] PBOOL pbCancelled ) ;void ApplicationRecoveryFinished ( [in] BOOL bSuccess ) ;
注册函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void __fastcall sub_14000F4A0 () { HWND ForegroundWindow; ForegroundWindow = GetForegroundWindow(); WerReportHang(ForegroundWindow, 0 i64); sub_14000F490(); } .text:000000014000F 490 sub_14000F490 proc near ; CODE XREF: sub_14000F4A0+19 ↓j .text:000000014000F 490 ; DATA XREF: .pdata:0000000140036894 ↓o .text:000000014000F 490 ; __unwind { .text:000000014000F 490 xor ecx, ecx .text:000000014000F 492 mov eax, 61 h ; 'a' .text:000000014000F 497 mov [rcx], ax .text:000000014000F 49A retn .text:000000014000F 49A ; } .text:000000014000F 49A sub_14000F490 endp
因为Windows会自动报告未处理的异常,应用程序不应该处理致命的异常。
如果出现故障或不响应的进程是交互式的,WER会显示一个用户界面,告知用户这个问题。
如果在用户试图与应用程序进行交互时,应用程序在五秒钟内不响应Windows消息,则被认为是无响应的。
因为我们WerReportHang
启动了"无响应"报告,WER会有程序未响应的异常,在异常退出之前,调用恢复回调。
使用恢复回调尝试在应用程序终止之前保存数据和状态信息。 然后,可以在重启应用程序时使用保存的数据和状态信息。
无响应报告完成后,它将终止创建窗口的进程。这也是调试到这一步会挂掉的原因。
整体流程: 注册终止函数/恢复函数 -> main(虚假控制流结束) -> 终止函数 -> 当前窗口启动"无响应"报告 -> 触发异常 -> 恢复函数。
CEF
求解
程序的依赖有点多,当时存下文件就没细看,后来看群#REtard师傅发了一张图,看着有点熟悉的sm4,终于还是对他下手了。
运行程序,会有一段报错,不过可以根据这个明文信息进行搜索。
查看strings窗口,交叉引用please input your flag
得到的代码逻辑非check逻辑,应该是在注册窗口。
可以在数据段对其他可疑的数据进行交叉引用,尤其是长度为32位的(经典flag len),或者是加密逻辑中需要用的数据表。
可以通过交叉引用定位到sub_44F360
函数,其中一些显示明文有异或解密得到。
解密可得document.write(‘%c’); 并且之后其他字符可得alert(‘Correct’)。
😀😀😀,通过解密出的一些明文字符和加密逻辑走向确定该处是关键check点。
这步逻辑没有涉及输入,如果有了解过SM4 - mygithub 加密的话,可以看出他大致为秘钥扩展的过程,由TK生成CK(轮秘钥),不过进行了简化没有sbox的替换过程。
并且出题人对细节处理比较好,对空间进行了优化,通过模运算来使轮秘钥的生成更节省空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void sm4_exkey (uint32_t *mkey) { int i; for (i = 0 ; i < 4 ; i++) { mkey[i] ^= FK[i]; } rK = (uint32_t *)(mkey + 4 ); for (i = 0 ; i < 32 ; i++) { uint32_t tmp = mkey[i + 1 ] ^ mkey[i + 2 ] ^ mkey[i + 3 ] ^ CK[i]; sbox_replace(&tmp); rK[i]=mkey[i]^L2(tmp); } }
python模拟实现如下
命名可能有些差异,主要是加密流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def rol (n,x ): return (n<<x | n>>(32 -x))&0xffffffff def ror (n,x ): return (n>>x | n<<(32 -x))&0xffffffff x=[0xA3B1BAC6 , 0x56AA3350 , 0x677D9197 , 0xB27022DC ] s=[0x12345678 , 0x90ABCDEF , 0xFEDCBA09 , 0x87654321 ] block=[x[i]^s[i] for i in range (4 )] tb=[0x00070E15 , 0x1C232A31 , 0x383F464D , 0x545B6269 , 0x70777E85 , 0x8C939AA1 , 0xA8AFB6BD , 0xC4CBD2D9 , 0xE0E7EEF5 , 0xFC030A11 , 0x181F262D , 0x343B4249 , 0x50575E65 , 0x6C737A81 , 0x888F969D , 0xA4ABB2B9 , 0xC0C7CED5 , 0xDCE3EAF1 , 0xF8FF060D , 0x141B2229 , 0x30373E45 , 0x4C535A61 , 0x686F767D , 0x848B9299 , 0xA0A7AEB5 , 0xBCC3CAD1 , 0xD8DFE6ED , 0xF4FB0209 , 0x10171E25 , 0x2C333A41 , 0x484F565D , 0x646B7279 ] ck=[] for i in range (32 ): v11=tb[i]^block[(i+1 )%4 ]^block[(i+2 )%4 ]^block[(i+3 )%4 ] t=v11^ror(v11,9 )^rol(v11,13 ) block[i%4 ]=block[i%4 ]^t ck.append(block[i%4 ])
之后可以观察到调用了CK_table,并且将输入4个一组转为DWORD类型,之后调用sub_4520E0
进行加密。
sub_4520E0
函数加密主体如下
过程与秘钥扩展类似。
1 2 3 4 5 mstr=[0 ]*32 for i in range (32 ): v3=ck[i]^mstr[(i+1 )%4 ]^mstr[(i+2 )%4 ]^mstr[(i+3 )%4 ] t=v3^rol(v3,2 )^ror(v3,8 )^rol(v3,10 )^ror(v3,14 ) mstr[i%4 ]=mstr[i%4 ]^t
之后再赋值时,Block[0-3] 分别对应 v42、v41、v40、v39,之后存到CK[5]开始的地址,所以在二轮解密时ck的4个数据被替换。
综上,解密流程如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 def rol (n,x ): return (n<<x | n>>(32 -x))&0xffffffff def ror (n,x ): return (n>>x | n<<(32 -x))&0xffffffff x=[0xA3B1BAC6 , 0x56AA3350 , 0x677D9197 , 0xB27022DC ] s=[0x12345678 , 0x90ABCDEF , 0xFEDCBA09 , 0x87654321 ] block=[x[i]^s[i] for i in range (4 )] tb=[0x00070E15 , 0x1C232A31 , 0x383F464D , 0x545B6269 , 0x70777E85 , 0x8C939AA1 , 0xA8AFB6BD , 0xC4CBD2D9 , 0xE0E7EEF5 , 0xFC030A11 , 0x181F262D , 0x343B4249 , 0x50575E65 , 0x6C737A81 , 0x888F969D , 0xA4ABB2B9 , 0xC0C7CED5 , 0xDCE3EAF1 , 0xF8FF060D , 0x141B2229 , 0x30373E45 , 0x4C535A61 , 0x686F767D , 0x848B9299 , 0xA0A7AEB5 , 0xBCC3CAD1 , 0xD8DFE6ED , 0xF4FB0209 , 0x10171E25 , 0x2C333A41 , 0x484F565D , 0x646B7279 ] ck=[] for i in range (32 ): v11=tb[i]^block[(i+1 )%4 ]^block[(i+2 )%4 ]^block[(i+3 )%4 ] t=v11^ror(v11,9 )^rol(v11,13 ) block[i%4 ]=block[i%4 ]^t ck.append(block[i%4 ]) """ encry mstr=[0]*32 for i in range(32): v3=ck[i]^mstr[(i+1)%4]^mstr[(i+2)%4]^mstr[(i+3)%4] t=v3^rol(v3,2)^ror(v3,8)^rol(v3,10)^ror(v3,14) mstr[i%4]=mstr[i%4]^t """ def decry (enc ): for i in range (31 , -1 , -1 ): v3 = ck[i] ^ enc[(i + 1 ) % 4 ] ^ enc[(i + 2 ) % 4 ] ^ enc[(i + 3 ) % 4 ] t = v3 ^ rol(v3, 2 ) ^ ror(v3, 8 ) ^ rol(v3, 10 ) ^ ror(v3, 14 ) enc[i % 4 ] = enc[i % 4 ] ^ t enc=[0xC0CB547D , 0xD7F5DB74 , 0x1B92D96F , 0x204628EB , 0x60D3D5E5 , 0x2F366D80 , 0x612F63B0 , 0x30A90F20 ] c1=enc[:4 ][::-1 ] decry(c1) for i in range (4 ): print (int .to_bytes(c1[i], 4 , 'little' ).decode(),end='' ) for i in range (4 ): ck[5 +i]=enc[i] c2=enc[4 :][::-1 ] decry(c2) for i in range (4 ): print (int .to_bytes(c2[i], 4 , 'little' ).decode(),end='' )
显示的无法打开url,实际输入解密的字符却有回显。
溯源
CEF
是Chromium Embedded Framework
的缩写,即“Chromium嵌入式框架”,采用c++编写,地位类似于Electron,是web开发应用程序的重要框架。
目录特征
1 2 3 4 cef.pak cef_ 100_ percent.pak cef_ 200_ percent.pak libcef.dll
cef使用多进程,主进程为"浏览器进程",并且他实现的功能就是将Chromium 的浏览器嵌入到应用程序中。
cef应用程序demo默认加载google.com
,当然这个url可以切换为本地文件。
1 2 3 4 5 ... if (url.empty()) url = "file://c:/example/example.html" ; …
本题url设置的please input your flag 当然是错误的无法加载页面。
入口点函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 #include <windows.h> #include "include/cef_command_line.h" #include "include/cef_sandbox_win.h" #include "tests/cefsimple/simple_app.h" #if defined(CEF_USE_SANDBOX) #pragma comment(lib, "cef_sandbox.lib" ) #endif int APIENTRY wWinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); CefEnableHighDPISupport(); void * sandbox_info = nullptr ; #if defined(CEF_USE_SANDBOX) CefScopedSandboxInfo scoped_sandbox; sandbox_info = scoped_sandbox.sandbox_info(); #endif CefMainArgs main_args (hInstance) ; int exit_code = CefExecuteProcess(main_args, nullptr , sandbox_info); if (exit_code >= 0 ) { return exit_code; } CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine(); command_line->InitFromString(::GetCommandLineW()); CefSettings settings; if (command_line->HasSwitch("enable-chrome-runtime" )) { settings.chrome_runtime = true ; } #if !defined(CEF_USE_SANDBOX) settings.no_sandbox = true ; #endif CefRefPtr<SimpleApp> app (new SimpleApp) ; CefInitialize(main_args, settings, app.get(), sandbox_info); CefRunMessageLoop(); CefShutdown(); return 0 ; }
至于一些回调函数和更详细的实例可以结合CEF-WIKI 提供的demo源码进行调试和学习。
大致了解流程,这个框架还是比较有趣的,之后可以深入学习一下:)。
小插曲
对解密出的js api是否感到异或,搜索CEF逛看雪时捉到了出题人的身影。
不过本题来说是cef直接调用js的,并非采用的以下技术手段。
将js代码注入到第三方CEF应用程序的一点浅见-编程技术-看雪论坛-安全社区|安全招聘|bbs.pediy.com
该文作者抓住CEF程序创建浏览器的特点,试图找到生成的浏览器对象,以此来任意执行js代码。
cef 的核心全部在libcef.dll里,所以从其中创建浏览器的API入手,为了拿到创建出的浏览器对象。
尝试
找到创建浏览器的API,进行hook拿到浏览器对象。
1 2 3 4 5 6 int cef_browser_host_create_browser ( const cef_window_info_t * windowInfo, struct _cef_client_t * client, const cef_string_t * url, struct _cef_browser_settings_t * settings, struct _cef_request_context_t * request_context) ;
而这个函数是异步,所以cef_browser_t*
存在于创建成功的回调中,作者之后又通过_cef_client_t* client
参数入手去hook回调,但最终以失败告终。
同步和异步复习同步(Synchronous)和异步(Asynchronous) - myCpC - 博客园 (cnblogs.com)
柳暗花明
师傅又通过观察libcef的其他API,找到了突破口。
1 2 3 4 CEF_EXPORT cef_v8context_t * cef_v8context_get_current_context () ;
介于V8js与browser是绑定的,所以借助v8js引擎的上下文来拿到browser对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 cef_browser_t * browser = NULL ;cef_v8context_t * my_cef_v8context_get_current_context () { cef_v8context_t * js_context = ori_cef_v8context_get_current_context(); Print("my_cef_v8context_get_current_context js_context: %X!!\n" , js_context); browser = js_context->get_browser(js_context); _cef_frame_t * frame = browser->get_main_frame(browser); CefString script = payload; CefString url = xorstr("app/wd" ).crypt_get(); frame->execute_java_script(frame, script.GetStruct(), url.GetStruct(), 0 ); return js_context; }
至此目的已经达成,即我们成功拿到了cef创建出的浏览器对象。所谓过河拆桥,拿到该对象后就可以非常方便的执行js代码,从而cef的打工生涯结束,js才是check代码的主场。
LuaJit
哦,多么痛的领悟~😱😱😱
拿到文件后可以通过010打开,发现是非PE文件格式,根据文件头1B 4C 4A 02
搜索得知为luajit编译的文件。
LuaJIT 使用了一种全新的方式来编译和执行 Lua 脚本。经过处理后的 LuaJIT 程序,字节码的编码实现更加简单,执行效率相比 Lua 和 Luac 更加高效。
直接百度luajit逆向,之前没接触过此类逆向,格局小了🙃。有幸之前在🐟哥 - Mas0n 博客上看到过几种lua的保护方式和解决方案。
可以通过非虫大牛的Lua程序逆向之Luajit字节码与反汇编 了解luajit的opcode结构和文件格式,并且提供了010的luajit.bt
的模板。
并且也直接进行了反汇编,当然阅读luajit反汇编之后的字节码也是一种折磨,于是在gayhub
上四处搜寻反编译的轮子,不过结果都不尽人意。
总结: 前辈们的工具大多一脉相承,根据版本更新添加新的opcode;同时只能反编译luajit编译出32位的文件,对64位不适用。
而且本题没有给出luajit的具体版本,想采用luajit原生的反汇编工具进行反汇编都要自行摸索版本,主流应该在2.1或2.0😭。
LJD最近的一个项目**luajit-decompiler **,知道题目对应版本才可知是否需要改opcode,或者是工具的有关报错信息。
又在吾爱上看到一篇博客luajit反编译、解密 ,其中提到了另一种工具** luajit-decomp **,此工具使用起来只需下载对应版本的luajit并编译,拿到其中的luajit.exe
、lua51.dll
、jit文件夹
放到decomp文件夹下,运行jitdecomp即可。
luajit-decomp\data\luajit
下存放待反编译的jit文件。
运行jitdecomp,结果会在luajit-decomp\data\out
路径下。
虽然能对32/64位编译文件成功进行反编译,不过反编译后的代码也比较难读,相比读字节码并没有减少太多工作量,不过也不失为一种不错方法。
后来和群友讨论,#REtard师傅说版本为LuaJIT-2.1.0-beta3,并且t0hka师傅arch拉的最新的包,用其自带的luajit -bl 即完成了反汇编。
去官网下载2.1.0-beta3的源码,使用VS工具包中的命令行编译工具vcvars64.bat
来完成编译,在Luajit根目录下打开cmd输入vcvars64.bat
文件路径。
1 "F:\visual studio2019\VC\Auxiliary\Build\vcvars64.bat"
在MSVC 32位编译环境下 msvcbuild.bat 编译32位
msvcbuild.bat gc64编译64位,编译64位后面一定要跟gc64, 即便是开启的x64的编译命令行。
之后成功编译出luajit.exe
和 lua51.dll
这两个程序,这也是luajit-decomp工具实现反编译的支撑。
同目录下用过luajit.exe -bl task.jit 可以显示反编译后的字节码。
经典luajit.exe -h 罗列指令菜单
反编译不得就撸字节码,不过官网给出的主要是2.0版本的bytecode , 详细可见src/lj_bc.h
。
可以自己写一些lua代码,之后luajit -b test.lua xxx 进行编译之后查看字节码对照。
或者用luajit-decomp反编译一下,可读性是有点差的。先暂时告一段落,之后填坑吧,即使我的知识星球已经被挖的坑坑洼洼咯!
相关连接:
LuaJit官网 : http://luajit.org/ 一些参考文档和编译方法
LuaJIT 2.0 Bytecode Instructions
总结
知识面和广度还是差了亿点,之前太多题目都只是局限在解密表面,没去溯源。同时单纯解密的话思路要打开,能在最短时间内定位关键处理函数的能力十分重要,同时赛后的复现和深入学习题目涉及的考点也是必不可少的部分。😃😃😃