DAS 5月

隔天就要考试,单纯摸了摸,看能否摸到工作量少的,于是选择了luajit,网上的方法几乎尝试了一遍也没有解决方案,版本信息没有,luajit自带的反汇编尝试无果,体验极差。

wer

求解

bcf虚假控制流,真正的逻辑不在main,而是在某个函数内对输入进行了转存,之后又进行了相关处理。

image-20220527091239631

直接定位到输入部分,要求输入为38位并且将输入存入两个xmmword下。

image-20220527091852751

跟进sub_1400DB50,a2中存放的我们的输入,可见对输入进行了转移,之后对xmmword_140035C20交叉引用,定位到一个新的处理函数。

image-20220527092129863

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'}')
#flag{ce3cba82ca6de596565c3f231ecd4675}

下次时间紧还是老老实实摸windows了,不摸不熟悉的领域。😭😭😭

溯源

查阅Windows 错误报告 - Win32 应用|微软文档 (microsoft.com)官方文档,了解WER和ARR(程序恢复和重启)的相关知识。

错误报告功能使用户能够通知微软有关应用程序故障、内核故障、无反应的应用程序和其他应用程序的具体问题。并且可以使用应用程序恢复和重启(ARR),以确保客户在其应用程序崩溃时不会丢失数据,并允许用户快速返回到他们的任务。

对getflag的主要函数进行交叉引用,观察函数调用的关系。

image-20220527124229347

首先atexit注册了终止函数,可以在任何地方注册终止函数,并且他在程序结束时调用。

并且注册是在main函数运行前完成的。

initterm函数负责遍历函数指针表并初始化它们的内部方法。

image-20220527125654996

这个特性为其能隐藏控制流,偷天换日,图穷而匕不见提供了支撑。

其次是注册了恢复回调函数

如果应用程序遇到未处理的异常或变得无响应,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, //恢复 ping 间隔,以毫秒为单位,必须要在这个间隔内调用ApplicationRecoveryInProgress函数来说明正在主动恢复,否则会被wer终止。
[in] DWORD dwFlags //目前无意义
);

ApplicationRecoveryInProgress(&pbCancelled);// 指示调用应用程序正在继续恢复数据。

HRESULT ApplicationRecoveryInProgress(
[out] PBOOL pbCancelled //用户是否取消恢复,由wer设置 ;取消恢复/恢复完成时 pbCancelled为1
);

void ApplicationRecoveryFinished(
[in] BOOL bSuccess //恢复成功输入为1 不成功为0
);

注册函数

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; // rax

ForegroundWindow = GetForegroundWindow(); //检索前台窗口的句柄(用户当前正在使用的窗口)
WerReportHang(ForegroundWindow, 0i64); // wer函数,在指定的窗口上启动“无响应”报告。
sub_14000F490(); //没有执行到 进程就结束了
}

.text:000000014000F490 sub_14000F490 proc near ; CODE XREF: sub_14000F4A0+19↓j
.text:000000014000F490 ; DATA XREF: .pdata:0000000140036894↓o
.text:000000014000F490 ; __unwind { // sub_140010E50
.text:000000014000F490 xor ecx, ecx
.text:000000014000F492 mov eax, 61h ; 'a'
.text:000000014000F497 mov [rcx], ax //内存地址0是未分配的区域 会引发内存访问异常
.text:000000014000F49A retn
.text:000000014000F49A ; } // starts at 14000F490
.text:000000014000F49A sub_14000F490 endp

因为Windows会自动报告未处理的异常,应用程序不应该处理致命的异常。

如果出现故障或不响应的进程是交互式的,WER会显示一个用户界面,告知用户这个问题。

如果在用户试图与应用程序进行交互时,应用程序在五秒钟内不响应Windows消息,则被认为是无响应的。

因为我们WerReportHang启动了"无响应"报告,WER会有程序未响应的异常,在异常退出之前,调用恢复回调。

  1. 使用恢复回调尝试在应用程序终止之前保存数据和状态信息。 然后,可以在重启应用程序时使用保存的数据和状态信息。
  2. 无响应报告完成后,它将终止创建窗口的进程。这也是调试到这一步会挂掉的原因。

整体流程: 注册终止函数/恢复函数 -> main(虚假控制流结束) -> 终止函数 -> 当前窗口启动"无响应"报告 -> 触发异常 -> 恢复函数。

CEF

求解

程序的依赖有点多,当时存下文件就没细看,后来看群#REtard师傅发了一张图,看着有点熟悉的sm4,终于还是对他下手了。

运行程序,会有一段报错,不过可以根据这个明文信息进行搜索。

image-20220527201157512

查看strings窗口,交叉引用please input your flag得到的代码逻辑非check逻辑,应该是在注册窗口。

image-20220527201315343

可以在数据段对其他可疑的数据进行交叉引用,尤其是长度为32位的(经典flag len),或者是加密逻辑中需要用的数据表。

可以通过交叉引用定位到sub_44F360函数,其中一些显示明文有异或解密得到。

image-20220527202145352

解密可得document.write(‘%c’); 并且之后其他字符可得alert(‘Correct’)。

😀😀😀,通过解密出的一些明文字符和加密逻辑走向确定该处是关键check点。

image-20220527202548836

这步逻辑没有涉及输入,如果有了解过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++) { //初始秘钥异或FK
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] #其实这是SM4标准的CK

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进行加密。

image-20220527204027178

sub_4520E0函数加密主体如下

image-20220527204401527

过程与秘钥扩展类似。

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='')
#flag{3b2365b04700b5eac3a5fd0ba21b687f}

显示的无法打开url,实际输入解密的字符却有回显。

image-20220527210629834

溯源

CEFChromium 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
// 加载本地文件“c:\example\example.html” 
...
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"

// When generating projects with CMake the CEF_USE_SANDBOX value will be defined
// automatically if using the required compiler version. Pass -DUSE_SANDBOX=OFF
// to the CMake command-line to disable use of the sandbox.
// Uncomment this line to manually enable sandbox support.
// #define CEF_USE_SANDBOX 1

#if defined(CEF_USE_SANDBOX)
// The cef_sandbox.lib static library may not link successfully with all VS
// versions.
#pragma comment(lib, "cef_sandbox.lib")
#endif

// Entry point function for all processes.
int APIENTRY wWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow) {
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);

// Enable High-DPI support on Windows 7 or newer.
CefEnableHighDPISupport();

void* sandbox_info = nullptr;

#if defined(CEF_USE_SANDBOX)
// Manage the life span of the sandbox information object. This is necessary
// for sandbox support on Windows. See cef_sandbox_win.h for complete details.
CefScopedSandboxInfo scoped_sandbox;
sandbox_info = scoped_sandbox.sandbox_info();
#endif

// Provide CEF with command-line arguments.
CefMainArgs main_args(hInstance);

/*
CEF应用程序有多个子进程(渲染、GPU等),共享同一个可执行文件。这个函数检查命令行,如果这是个 一个子进程,则执行适当的逻辑。
*/
int exit_code = CefExecuteProcess(main_args, nullptr, sandbox_info);
if (exit_code >= 0) {
// The sub-process has completed so return here.
return exit_code;
}

// Parse command-line arguments for use in this method.
CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
command_line->InitFromString(::GetCommandLineW());

// Specify CEF global settings here.
CefSettings settings;

if (command_line->HasSwitch("enable-chrome-runtime")) {
// Enable experimental Chrome runtime. See issue #2969 for details.
settings.chrome_runtime = true;
}

#if !defined(CEF_USE_SANDBOX)
settings.no_sandbox = true;
#endif

// SimpleApp implements application-level callbacks for the browser process.
/*
它将在CEF初始化后的OnContextInitialized()中创建第一个浏览器实例。
*/
CefRefPtr<SimpleApp> app(new SimpleApp);

// 初始化CEF
CefInitialize(main_args, settings, app.get(), sandbox_info);

// Run the CEF message loop. This will block until CefQuitMessageLoop() is
// called.
CefRunMessageLoop();

// Shut down CEF.
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
///
// Returns the current (top) context object in the V8 context stack.
///
CEF_EXPORT cef_v8context_t* cef_v8context_get_current_context(); //返回V8引擎的上下文

介于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(); //获取V8js引擎上下文
Print("my_cef_v8context_get_current_context js_context: %X!!\n", js_context);
browser = js_context->get_browser(js_context); //得到浏览器对象!!!
//Print("[!!!] browser = %X\n", browser);
_cef_frame_t* frame = browser->get_main_frame(browser);
//Print("[!!!] frame = %X\n", frame);
CefString script = payload;
CefString url = xorstr("app/wd").crypt_get();
frame->execute_java_script(frame, script.GetStruct(), url.GetStruct(), 0);
return js_context;
}
// write by renbohan https://bbs.pediy.com/thread-268570.htm

至此目的已经达成,即我们成功拿到了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的模板。

image-20220528115928721

并且也直接进行了反汇编,当然阅读luajit反汇编之后的字节码也是一种折磨,于是在gayhub上四处搜寻反编译的轮子,不过结果都不尽人意。

总结: 前辈们的工具大多一脉相承,根据版本更新添加新的opcode;同时只能反编译luajit编译出32位的文件,对64位不适用。

而且本题没有给出luajit的具体版本,想采用luajit原生的反汇编工具进行反汇编都要自行摸索版本,主流应该在2.1或2.0😭。

LJD最近的一个项目**luajit-decompiler**,知道题目对应版本才可知是否需要改opcode,或者是工具的有关报错信息。

又在吾爱上看到一篇博客luajit反编译、解密,其中提到了另一种工具** luajit-decomp**,此工具使用起来只需下载对应版本的luajit并编译,拿到其中的luajit.exelua51.dlljit文件夹放到decomp文件夹下,运行jitdecomp即可。

  1. luajit-decomp\data\luajit下存放待反编译的jit文件。
  2. 运行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"

image-20220528200018823

在MSVC 32位编译环境下 msvcbuild.bat 编译32位

msvcbuild.bat gc64编译64位,编译64位后面一定要跟gc64, 即便是开启的x64的编译命令行。

之后成功编译出luajit.exelua51.dll这两个程序,这也是luajit-decomp工具实现反编译的支撑。

同目录下用过luajit.exe -bl task.jit 可以显示反编译后的字节码。

经典luajit.exe -h 罗列指令菜单

image-20220528201038811

反编译不得就撸字节码,不过官网给出的主要是2.0版本的bytecode , 详细可见src/lj_bc.h

可以自己写一些lua代码,之后luajit -b test.lua xxx 进行编译之后查看字节码对照。

或者用luajit-decomp反编译一下,可读性是有点差的。先暂时告一段落,之后填坑吧,即使我的知识星球已经被挖的坑坑洼洼咯!

image-20220528213003579

相关连接:

LuaJit官网 : http://luajit.org/ 一些参考文档和编译方法

LuaJIT 2.0 Bytecode Instructions

总结

知识面和广度还是差了亿点,之前太多题目都只是局限在解密表面,没去溯源。同时单纯解密的话思路要打开,能在最短时间内定位关键处理函数的能力十分重要,同时赛后的复现和深入学习题目涉及的考点也是必不可少的部分。😃😃😃