建议投诉 | 联系我们 | 关注微信 安全虎 - 专注于互联网信息安全的科技新媒体!
你的位置:首页 > 技术前沿 » 正文

2018 腾讯TP游戏安全技术竞赛-决赛进阶版

来源:2018-05-17 | 人围观 | 评论:暂无评论

 收到官方邀请写write-up, 还是蛮开心的,

第一题实在过于简单,一个OpenGL的3D程序, 只要CE修改camera坐标到右上角就能看到flag了
这里分析第二题的进阶版

题目下载

PC游戏安全方向 - 决赛第2题 - 进阶版
可以看到今年的题目是个UE4引擎的程序, 游戏竞赛终于和游戏有关系了, (往年的也没参加过就是了...)

第一关

x32dbg载入, 各种关键点下断, 程序依赖了两个dll, 入口全部下断再说,
果然点击第一关的验证按钮, 断在了tmgs.dll的maln函数
其实这个程序分析的时候大量时间花在了lua函数的识别上,
1.png
function_verify核心如下:
2.png
function_verify里面是调用lua脚本进行一个check, 这里是对照lua源码一个个标记的函数名, 花费了很多时间
同时根据上图可以发现lua脚本在luaOpcode里面,将lua的luaOpcode导出使用luadec反编译可以得出验证逻辑,
标准版到这里基本就结束了,但是进阶版官方做了一些小动作,lua的op_mode被做了一些处理

通过搜索关键词”must be a number” 定位到luaV_execute函数,此处通过和lua源代码对比发现opcode做了大量改动,通过逐一对比47个lua指令switch的大量代码,人肉得出还原表 写出函数 de_op_codes

int de_op_codes(int en) {     int r = 0;     switch (en)     {     case 0:         return 0xfe;     case 6u:     case 7u:     case 0x16u:     case 0x1Bu:         return 0;     case 0x22u:     case 0x28u:     case 0x29u:     case 0x3Cu:         return 1;     case 0x3Eu:         return 2;     case 0x3Bu:         return 3;     case 0x12u:         return 4;     case 8u:     case 0x11u:     case 0x17u:     case 0x36u:         return 5;     case 2u:         return 6;     case 0xDu:         return 7;     case 0x1Au:         return 8;     case 1u:         return 9;     case 0x1Du:         return 0xA;     case 0x1Fu:         return 0xB;     case 0xEu:         return 0xC;     case 0x31u:         return 0xD;     case 0x2Fu:         return 0xE;     case 0x1Eu:         return 0xF;     case 0x15u:         return 0x10;     case 0x3Au:         return 0x11;     case 0x13u:         return 0x12;     case 0x24u:         return 0x13;     case 0x2Bu:         return 0x14;     case 0x1Cu:         return 0x15;     case 0x2Du:         return 0x16;     case 0x19u:         return 0x17;     case 0x3Fu:         return 0x18;     case 0x18u:         return 0x19;     case 0x33u:         return 0x1A;     case 0xFu:         return  0x1B;     case 0x34u:         return 0x1C;     case 0x20u:         return 0x1D;     case 5u:     case 9u:     case 0xAu:     case 0x25u:         return 0x1E;     case 0x30u:         return 0x1F;     case 0x26u:         return 0x20;     case 0x35u:         return 0x21;     case 0x38:         return 0x22;     case 0x2Au:         return 0x23;     case 0x23u:     case 0x37u:     case 0x39u:     case 0x3Du:         return 0x24;     case 0x27u:         return 0x25;     case 4u:         return 0x26;     case 0x2Cu:         return 0x27;     case 0x32u:         return 0x28;     case 0x21u:         return 0x29;     case 0x03:         return 0x2A;     case 0xCu:         return 0x2B;     case 0x2Eu:         return 0x2C;     case 0x14u:         return 0x2D;     case 0xB:         return 0x2E;     case 0x10:         return 46;     default:         return 0;     } } 

将此函数写入 luadec的GET_OPCODE宏指令中,重新编译运行luadec即可还原出lua, 关键部分如下
3.png

可以看到程序里面通过rc4做了验证,由于rc4是对称加密算法,因此只需要对dst的密文加密一次就可以得到通关密码
4.png

第二关

通过在UE引擎关键地方下断点,发现第二问处理逻辑主要在UE引擎内部,
UE4引擎事件分发如下, 下文函数都会标记详细的偏移

#define RESULT_DECL = void*const Z_Param__Result  // UObject::ProcessInternal_62E860 void UObject::ProcessInternal( UObject* Context, FFrame& Stack, RESULT_DECL) call eax  // UObject::execLet_632310 void UObject::execLet( UObject* Context, FFrame& Stack, RESULT_DECL) call eax  // UObject::execContext_6319D0 void UObject::execContext( UObject* Context, FFrame& Stack, RESULT_DECL ) P_THIS->ProcessContextOpcode(Stack, RESULT_PARAM, /*bCanFailSilently=*/ false);  // UObject::ProcessContextOpcode_62E0B0   void UObject::ProcessContextOpcode( FFrame& Stack, RESULT_DECL, bool bCanFailSilently ) call eax  // UObject::execFinalFunction_631D50 void UObject::execFinalFunction( UObject* Context, FFrame& Stack, RESULT_DECL ) P_THIS->CallFunction( Stack, RESULT_PARAM, (UFunction*)Stack.ReadObject() );  // UObject::CallFunction_628860 void UObject::CallFunction( FFrame& Stack, RESULT_DECL, UFunction* Function ) Function->Invoke(this, Stack, RESULT_PARAM);  // Function_Invoke_641570 void UFunction::Invoke(UObject* Obj, FFrame& Stack, RESULT_DECL) return (*Func)(Obj, Stack, RESULT_PARAM); 

一个完整流程:

-> Function_Invoke_641570  -> UObject::ProcessInternal_62E860  -> UObject::CallFunction_628860  -> UObject::ProcessInternal_62E860  -> UObject::execLet_632310 -> UObject::execContext_6319D0  -> UObject::ProcessContextOpcode_62E0B0  -> sub_631DF0   -> UObject::execFinalFunction_631D50  -> UObject::CallFunction_628860  -> Function_Invoke_641570 

核心函数在 Function_Invoke_641570 里面, 通过对此函数进行下断点可以记录到调用了以下几个函数, 下面给出函数地址和具体功能

Function: 16D6860 check2_str_to_FString_846860 Function: 16CAF10 check2_GetUnicodeStringLength_83AF10 Function: 16D6860 check2_str_to_FString_846860 Function: 139B530 check2_md5_50B530 Function: 16CAF10 check2_GetUnicodeStringLength_83AF10 ... 此处省略N个无关函数 Function: 139B340 check2_get_a_md5_50B340  # 内置了一个字符串, 获取内置字符串的md5 Function: 16D6860 check2_str_to_FString_846860 Function: 139B530 check2_md5_50B530 Function: 16C5C60 check2_main_835C60 Function: 50B420 fun_showmsg_50B420(&第二关:验证失败,请重试) ... 此处继续省略N个无关函数 

在调用显示函数显示出通过失败的上一步, 就是核心判断函数check2_main_835C60
再次向上,是check2_md5_50B530,这一步里面会将一个字符串计算md5。
check2_md5_50B530内部又调用check2_md5_calc_50A800进行真正的计算。
check2_md5_calc_50A800下断点可以发现程序依次对

E0EA72E0E1C1BFFBC26E8B47AD9D809C tencent_mobile_game+-999893888 输入的PASSWORD 

这三个内容计算md5,
继续单步跟踪发现程序在check2_main_835C60里面对 第二次 和 第三次 字符串md5做对比, 那么key显而易见了, 就是第二次的字符串
第二次的字符串tencent_mobile_game+是写死在程序里面的
-999893888 是这里算出来的
2_1.png
2_2.png

本题主要考察UE4事件分发流程了, 当然如果什么都不懂的话直接ida findcrypt在md5的地方下断也能做出来就是了...

第三关

算法导出

到了这一关, 点击按钮又顺利断下来了, 还是上次的dll, 进入了ths函数
3_1.png
可以看到是在ph2.dll里面进行的处理(基础版直接是sub_100363A0,和第一关类似的过程)
3_2.png
进入ph2.dll,依然是lua脚本,直接dump出bin变量的脚本,
可以看到脚本是头部有jt,是使用luajit生成的字节码,直接使用luajit-decomp进行反编译, 编译后代码:
3_3.png

check函数内生成了一个虚拟机, 执行虚拟机代码对输入的数据(plainBs)进行加密处理, 加密后和内置的(dstRes)进行比对
通过对虚拟机代码进行导出,导出工具以及导出的z3t_table.lua都已经放在附录,
虚拟机代码有18个指令, 分别为加减乘除判断跳转压栈出栈等,且采用了大数运算库
这里需要对虚拟机代码进行分析, 附录有我自己通过js自己重新实现的,
其实这里标准版和进阶版差不多了, 附录的代码里面附带了标准版的实现, 可以说进阶版除了数大一点(BigNumber), 其余的全是一样的

分析后如下:

算法分析

首先初始化一个b64字母表
3_4.png

然后对输入的数据进行分组,8个一组,前两个做以下运算, 生成8字节数据
3_5.png

对后6个的运算如下

InfInt x = (x2 * 256 + x1) + (x3 * 256 * 256) + 256 * 256 * 256 * (x5 * 256 + x4 + x6 * 256 * 256); (实际上是 x1-x6分别为二进制8位排开) a = savebyte + (x % 61454 * 256)  b = (x % 54732) + ((x % 5136) % 256 * 256 * 256)  c = (x % 25548) * 256 + ((x % 5136) >> 8)  

随后对a/b/c/(res2)生成4个字节目标数据, 一共3*4=12字节
3_6.png

逆向思路:

dstRes分组,每组8+12=20位,前8为计算出原有前2位,后12位计算出后6位

详细逆向过程:

前2位可以直接约束求解
3_7.png

后6位算法较为复杂, 且数据较大, 可以先化简分析
由于计算过程是

x = (x2 * 256 + x1) + (x3 * 256 * 256) + 256 * 256 * 256 * (x5 * 256 + x4 + x6 * 256 * 256) a = savebyte + (x % 61454 * 256)  b = (x % 54732) + ((x % 5136) % 256 * 256 * 256)  c = (x % 25548) * 256 + ((x % 5136) >> 8)  

因此逆向过程为 abc已知, 求x(x1-x6可以通过x算出)
令R=savebyte
公式写为

R+(x%61454*256)=a, (x%54732)+((x%5136)%256*256*256)=b, (x%25548)*256+((x%5136)>>8)=c 

化简:

x%61454 = (a-R) >> 8 x%54732 = b & 0xFFFF x%5136  = ((b & 0xFF0000) >> 16)  +  ((c & 0xFF) << 8) x%25548 = c >> 8 

到这一步可以看出右边均是已知,转化为同余方程组,
由于m不互质,因此不可以使用孙子定理, 这就和这一题一样了
project euler problem 531

对于一个同余方程: 

3_8.png

设g=gcd(n1,n2),可以得到若方程有解,则g|(a1−a2) 必成立;其逆否命题成立。 

3_9.png

复杂度O(n2logn) 。因此此题中四组可以两两分组 x1 and x2 得到 x',x' and x3 得到 x'',x'' and x4 得到 answer 

算出x之后,x1-x6可以这样算出
3_10.png

至此,三道题求解完毕
3_11.png

标签:

相关内容推荐:

Top