强网杯S8决赛Reverse
2024-12-21 09:59:0 Author: mp.weixin.qq.com(查看原文) 阅读量:1 收藏

复盘一下强网决赛的Reverse题。


S1mpleVM

附件下载:https://xia0ji233.pro/2024/12/11/qwb2024_final_reverse/S1mpLeVM_6d429db3ceeba8f95131c477020ee899.zip

题目名字已经很明显的告诉你了,就是 vm 逆向。

基本分析

入口其实没啥,就是输入 32 长度的 passcode 然后校验,启动方式是./secret_box.exe quest命令行传参。

可以找到最关键的函数sub_140001D30就是 VM 入口。

这个函数里面很明显的 vm_handler

__int64 __fastcall vmrun(char *input, char *vmcode)
{
//some defs for local variable
v2 = 0LL;
v3 = *vmcode - 16;
v5 = v48;
v6 = vmcode + 1;
while ( 2 )
{
switch ( v3 )
{
case 0u:
if ( v2 )
{
v9 = v2;
v2 = (signed int *)*((_QWORD *)v2 + 1);
v7 = *v9;
free(v9);
if ( v2 )
{
v10 = v2;
v2 = (signed int *)*((_QWORD *)v2 + 1);
v8 = *v10;
free(v10);
}
else
{
v8 = 0x80000000;
}
}
else
{
v7 = 0x80000000;
v8 = 0x80000000;
}
v11 = (signed int *)j__malloc_base(0x10uLL);
*v11 = v7 % v8;
goto LABEL_53;
case 1u:
//...
LABEL_53:
*((_QWORD *)v11 + 1) = v2;
v2 = v11;
}
}
}

不难发现,v3 是所谓的 opcode,v6 是 PC 指针,并且 vmcode 是实际的字节码- 0x10,下面来一个个分析这些 vm 的指令。

首先是 0 号指令,做了一个较为复杂的指针操作。这里初看可能啥也看不明白,但是可以发现最下面它分配了 0x10 的空间同时又 v7 和 v8 做模运算了赋值给 v11 指向的地址。

操作完成之后又执行了*((_QWORD *)v11 + 1) = v2;v2 = v11;,如此种种的迹象显然不难让人联想到一种结构:链表。如果将划分的 0x10 字节内存进行划分,也可以看出,前八个字节存储数据,后八个字节存储指针。

shift+F1打开 IDA 的local type窗口,按insert键插入结构体的定义:

struct LinkEntry{
signed val;
LinkEntry * next;
}

将 v11 和 v2 定义修改之后,IDA 将展示如下的伪代码:

case 0u:
if ( v2 )
{
v9 = &v2->val;
v2 = v2->next;
v7 = *v9;
free(v9);
if ( v2 )
{
v10 = &v2->val;
v2 = v2->next;
v8 = *v10;
free(v10);
}
else
{
v8 = 0x80000000;
}
}
else
{
v7 = 0x80000000;
v8 = 0x80000000;
}
v11 = (LinkEntry *)j__malloc_base(0x10uLL);
v11->val = v7 % v8;
goto LABEL_53;

可以说,基本上是一目了然了,并且据此可以联想到一个栈的结果,将两个数 push 进栈中,再弹出来做计算,计算的结果重新入栈。

这里可以将所有 malloc 返回值接收的变量都改成LinkEntry *类型。

此时再来观察整个反编译的代码:

__int64 __fastcall vmrun(char *input, char *vmcode)
{
LinkEntry *v2; // rdi
unsigned int opcode; // eax
unsigned int v5; // r14d
char *PC; // rbp
signed int v7; // esi
signed int v8; // ebx
signed int *v9; // rcx
signed int *v10; // rcx
LinkEntry *v11; // rcx
int v12; // ebx
LinkEntry *v13; // rax
unsigned int *v14; // rcx
unsigned int v15; // esi
unsigned int v16; // ebx
unsigned int *v17; // rcx
unsigned int *v18; // rcx
LinkEntry *v19; // rax
unsigned int v20; // esi
unsigned int v21; // ebx
unsigned int *v22; // rcx
unsigned int *v23; // rcx
int v24; // eax
int v25; // ebx
LinkEntry *v26; // rax
int v27; // esi
int v28; // ebx
signed int *v29; // rcx
int *v30; // rcx
LinkEntry *v31; // rax
unsigned int v32; // esi
unsigned int v33; // ebx
unsigned int *v34; // rcx
unsigned int *v35; // rcx
LinkEntry *v36; // rax
unsigned int v37; // ebx
unsigned int v38; // esi
unsigned int *v39; // rcx
unsigned int *v40; // rcx
LinkEntry *v41; // rax
signed int v42; // esi
signed int v43; // ebx
signed int *v44; // rcx
signed int *v45; // rcx
int v46; // eax
unsigned int v48; // [rsp+58h] [rbp+10h]

v2 = 0LL;
opcode = *vmcode - 16;
v5 = v48;
PC = vmcode + 1;
while ( 2 )
{
switch ( opcode )
{
case 0u:
if ( v2 )
{
v9 = &v2->val;
v2 = v2->next;
v7 = *v9;
free(v9);
if ( v2 )
{
v10 = &v2->val;
v2 = v2->next;
v8 = *v10;
free(v10);
}
else
{
v8 = 0x80000000;
}
}
else
{
v7 = 0x80000000;
v8 = 0x80000000;
}
v11 = (LinkEntry *)j__malloc_base(0x10uLL);
v11->val = v7 % v8;
goto LABEL_53;
case 1u:
v12 = *PC;
v13 = (LinkEntry *)j__malloc_base(0x10uLL);
++PC;
v13->next = v2;
v2 = v13;
v13->val = v12;
goto LABEL_54;
case 2u:
if ( v2 )
{
v14 = (unsigned int *)v2;
v2 = v2->next;
v5 = *v14;
free(v14);
}
else
{
v5 = 0x80000000;
}
goto LABEL_54;
case 3u:
if ( v2 )
{
v17 = (unsigned int *)v2;
v2 = v2->next;
v15 = *v17;
free(v17);
if ( v2 )
{
v18 = (unsigned int *)v2;
v2 = v2->next;
v16 = *v18;
free(v18);
}
else
{
v16 = 0x80000000;
}
}
else
{
v15 = 0x80000000;
v16 = 0x80000000;
}
v19 = (LinkEntry *)j__malloc_base(0x10uLL);
v19->next = v2;
v2 = v19;
v19->val = v15 * v16;
goto LABEL_54;
case 4u:
if ( v2 )
{
v22 = (unsigned int *)v2;
v2 = v2->next;
v20 = *v22;
free(v22);
if ( v2 )
{
v23 = (unsigned int *)v2;
v2 = v2->next;
v21 = *v23;
free(v23);
}
else
{
v21 = 0x80000000;
}
}
else
{
v20 = 0x80000000;
v21 = 0x80000000;
}
v11 = (LinkEntry *)j__malloc_base(0x10uLL);
v24 = v21 + v20;
goto LABEL_52;
case 5u:
sub_1400011B0("%c", v5);
goto LABEL_54;
case 6u:
v25 = *input;
v26 = (LinkEntry *)j__malloc_base(0x10uLL);
v26->next = v2;
v2 = v26;
v26->val = v25;
goto LABEL_54;
case 7u:
if ( v2 )
{
v29 = &v2->val;
v2 = v2->next;
v27 = *v29;
free(v29);
if ( v2 )
{
v30 = &v2->val;
v2 = v2->next;
v28 = *v30;
free(v30);
}
else
{
v28 = 0x80000000;
}
}
else
{
LOBYTE(v27) = 0;
v28 = 0x80000000;
}
v31 = (LinkEntry *)j__malloc_base(0x10uLL);
v31->next = v2;
v2 = v31;
v31->val = (v28 >> v27) & 1;
goto LABEL_54;
case 8u:
if ( v2 )
{
v34 = (unsigned int *)v2;
v2 = v2->next;
v32 = *v34;
free(v34);
if ( v2 )
{
v35 = (unsigned int *)v2;
v2 = v2->next;
v33 = *v35;
free(v35);
}
else
{
v33 = 0x80000000;
}
}
else
{
v32 = 0x80000000;
v33 = 0x80000000;
}
v36 = (LinkEntry *)j__malloc_base(0x10uLL);
v36->next = v2;
v2 = v36;
v36->val = v32 ^ v33;
goto LABEL_54;
case 9u:
++input;
goto LABEL_54;
case 0xAu:
return v5;
case 0xBu:
if ( v2 )
{
v39 = (unsigned int *)v2;
v2 = v2->next;
v37 = *v39;
free(v39);
if ( v2 )
{
v40 = (unsigned int *)v2;
v2 = v2->next;
v38 = *v40;
free(v40);
}
else
{
v38 = 0x80000000;
}
}
else
{
v37 = 0x80000000;
v38 = 0x80000000;
}
v41 = (LinkEntry *)j__malloc_base(0x10uLL);
v41->next = v2;
v2 = v41;
v41->val = v37 - v38;
goto LABEL_54;
case 0xCu:
if ( v2 )
{
v44 = &v2->val;
v2 = v2->next;
v42 = *v44;
free(v44);
if ( v2 )
{
v45 = &v2->val;
v2 = v2->next;
v43 = *v45;
free(v45);
}
else
{
v43 = 0x80000000;
}
}
else
{
v42 = 0x80000000;
v43 = 0x80000000;
}
v11 = (LinkEntry *)j__malloc_base(0x10uLL);
v24 = v42 / v43;
LABEL_52:
v11->val = v24;
LABEL_53:
v11->next = v2;
v2 = v11;
LABEL_54:
v46 = *PC++;
opcode = v46 - 16;
if ( opcode <= 0xC )
continue;
goto LABEL_57;
default:
LABEL_57:
sub_1400011B0("WTF are u doinggg...");
exit(1);
}
}
}

恢复结构体之后这个 vm 还是很一目了然的,下面解释一下各个 opcode 的作用。

◆0:取模操作(先弹出的值在运算符左侧)

◆1:push 操作

◆2:pop 操作

◆3:乘法操作

◆4:加法操作

◆5:输出

◆6:取输入指针

◆7:右移位后取最低位(先弹出的值在运算符右侧)

◆8:异或操作

◆9:输入指针+1

◆10:返回

◆11:减法操作(先弹出的值在运算符左侧)

◆12:除法操作(先弹出的值在运算符左侧)

动态分析

同时可以根据操作自己实现虚拟机,这里已经很清楚是栈的数据结构了就直接用std::stack实现即可。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/fcntl.h>
#include<stack>
std::stack<int>s;
char buffer[0x10000];
int getstackval(){
int val=0x80000000;
if(s.size()){
val=s.top();
s.pop();
}
return val;
}
void pushstackval(int val){
printf("push val %d\n",val);
s.push(val);
}
int main(){

int fd=open("./quest",0);
size_t ret=read(fd,buffer,0x10000);
char *PC=buffer;
int reg1,reg2,reg3;
char input[]="flag{aaaaaaaaaaaaaaaaaaaaaaaaaa}";
char *p=input;
while(1){
char opcode=*PC-0x10;
char operand;
PC++;

switch (opcode) {
case 0:
reg2=getstackval();
reg3=getstackval();
pushstackval(reg2%reg3);
printf("calc %d %% %d=%d",reg2,reg3,reg2%reg3);
break;
case 1:
operand=*PC++;
pushstackval(operand);
break;
case 2:
reg1=getstackval();
printf("pop %d to reg1\n",reg1);
break;
case 3:
reg2=getstackval();
reg3=getstackval();
printf("calc %d*%d=%d\n",reg2,reg3,reg2*reg3);
pushstackval(reg2*reg3);
break;
case 4:
reg2=getstackval();
reg3=getstackval();
printf("calc %d+%d=%d\n",reg2,reg3,reg2+reg3);
pushstackval(reg2+reg3);
break;
case 5:
printf("output a char %c\n",reg1);
break;
case 6:
printf("get %d input\n",p-input+1,*p);
pushstackval(*p);
break;
case 7:
reg3=getstackval();
reg2=getstackval();
printf("calc (%d>>%d)&1=%d\n",reg2,reg3,(reg2>>reg3)&1);
pushstackval((reg2>>reg3)&1);

break;
case 8:
reg2=getstackval();
reg3=getstackval();
printf("calc %d^%d=%d\n",reg2,reg3,reg2^reg3);
pushstackval(reg2^reg3);
break;
case 9:
p++;
break;
case 10:
printf("retval=%d\n",reg1);
return reg1;
case 11:
reg2=getstackval();
reg3=getstackval();
printf("calc %d-%d=%d\n",reg2,reg3,reg2-reg3);
pushstackval(reg2-reg3);
break;
case 12:
reg2=getstackval();
reg3=getstackval();
printf("calc %d/%d=%f\n",reg2,reg3,reg2/reg3);
pushstackval(reg2/reg3);
break;
default:
printf("invalid op");
exit(0);
break;
}
}
}

下面节选一段log:

get 1 input
push val 102
push val 0
calc (102>>0)&1=0
push val 0
push val 2
calc 2*0=0
push val 0
get 1 input
push val 102
push val 1
calc (102>>1)&1=1
push val 1
push val 3
calc 3*1=3
push val 3
get 1 input
push val 102
push val 2
calc (102>>2)&1=1
push val 1
push val 67
calc 67*1=67
push val 67
get 1 input
push val 102
push val 3
calc (102>>3)&1=0
push val 0
push val 37
calc 37*0=0
push val 0
get 1 input
push val 102
push val 4
calc (102>>4)&1=0
push val 0
push val 41
calc 41*0=0
push val 0
get 1 input
push val 102
push val 5
calc (102>>5)&1=1
push val 1
push val 11
calc 11*1=11
push val 11
get 1 input
push val 102
push val 6
calc (102>>6)&1=1
push val 1
push val 13
calc 13*1=13
push val 13
get 1 input
push val 102
push val 7
calc (102>>7)&1=0
push val 0
push val 89
calc 89*0=0
push val 0
calc 0+13=13
push val 13
calc 13+11=24
push val 24
calc 24+0=24
push val 24
calc 24+0=24
push val 24
calc 24+67=91
push val 91
calc 91+3=94
push val 94
calc 94+0=94
push val 94
push val 70
calc 70^94=24
push val 24
get 2 input
push val 108
push val 0
calc (108>>0)&1=0

最前面事实上就是输出一句话Thank for providing passcode, my ultimate secret box is checking...用的,跳过之后就能看到。其中最明显的应该能看到它频繁的取输入字符并做(x>>y)&1的运算,y 从0~7,不难想到,这是在一个一个取出输入字节的每一位,每一位都对应了一个权值。

第一个字节可以看出来,从低位到高位权值分别为:

2 3 67 37 41 11 13 89

而最后,它将所有权值相加,得到的结果和 70 做异或运算得到 24,将该值存入栈底。

而把log拉到最后发现:

calc 137+71=208
push val 208
calc 208+39=247
push val 247
calc 247+120=367
push val 367
calc 367+22=389
push val 389
calc 389+119=508
push val 508
calc 508+89=597
push val 597
calc 597+22=619
push val 619
calc 619+218=837
push val 837
calc 837+203=1040
push val 1040
calc 1040+125=1165
push val 1165
calc 1165+125=1290
push val 1290
calc 1290+5=1295
push val 1295
calc 1295+118=1413
push val 1413
calc 1413+30=1443
push val 1443
calc 1443+59=1502
push val 1502
calc 1502+89=1591
push val 1591
calc 1591+213=1804
push val 1804
calc 1804+114=1918
push val 1918
calc 1918+35=1953
push val 1953
calc 1953+18=1971
push val 1971
calc 1971+18=1989
push val 1989
calc 1989+121=2110
push val 2110
calc 2110+65=2175
push val 2175
calc 2175+32=2207
push val 2207
calc 2207+221=2428
push val 2428
calc 2428+253=2681
push val 2681
calc 2681+348=3029
push val 3029
calc 3029+130=3159
push val 3159
calc 3159+92=3251
push val 3251
calc 3251+140=3391
push val 3391
calc 3391+24=3415
push val 3415
pop 3415 to reg1
retval=3415

我们所计算的第一个异或值,在最后一刻被加起来返回了。

而外面判断我们的输入是否正确,依赖于返回值是否为 0,因此我们要尽可能让每次异或值都相等,这里用个小技巧,将要输出的值打印到 stderr 中,再用重定向2>out.txt就可以快速拿到一些值。

首先我们拿异或的目标值,在异或的 opcode 中加入fprintf(stderr,"%d,",reg2);,得到值。

然后拿每一个字节的每一位的权值,在*运算中加入fprintf(stderr,"%d,",reg2);,得到值。

最终根据逻辑,写出还原 passcode 的逻辑:

#include<stdio.h>
char v[]={
2,3,67,37,41,11,13,89,2,3,67,5,7,47,61,29,2,67,37,7,43,11,13,31,97,3,41,73,11,13,53,29,97,67,3,11,43,13,47,83,67,5,37,71,7,11,89,29,2,3,5,11,13,83,53,61,2,3,7,71,43,83,29,31,7,73,11,13,53,89,29,31,2,3,5,37,7,43,13,61,2,5,7,43,11,13,53,89,5,7,73,43,11,13,59,31,3,5,73,41,43,13,83,89,2,7,71,11,43,13,29,61,2,5,7,11,13,79,47,83,3,67,37,5,73,11,13,61,2,67,5,7,71,11,13,61,67,3,5,37,43,11,13,61,2,3,37,7,71,41,11,29,3,5,41,11,43,47,53,29,2,3,7,71,43,13,47,79,2,3,5,37,11,43,13,79,97,67,5,37,7,41,11,61,3,71,7,43,11,79,53,61,2,3,71,73,11,13,61,31,97,2,3,67,5,11,13,83,2,3,5,37,7,41,11,53,2,3,73,43,11,13,53,61,2,67,3,37,7,11,47,59,2,37,5,73,13,47,53,59,2,67,71,73,41,11,13,89,2,3,67,37,73,11,43,59,};
char target[]={70,56,70,77,74,90,87,82,60,67,86,95,64,94,85,66,33,69,64,98,67,71,94,93,90,32,65,82,68,65,93,96,};
int checkval(int i,int pos){
int sum=0;
for(int j=0;j<8;j++){
sum+=((i>>j)&1)*v[j+pos*8];
}
return sum;
}
int main(){
for(size_t i=0;i<sizeof(target);i++){
for(int j=0x20;j<127;j++){
if(target[i]==checkval(j,i)){
putchar(j);
break;
}
}
}
}
//s1mpl3_VM_us3s_link3d_l1st_st4ck

输入后得到flag

最后注意一下,这个反调试有点隐蔽,只要中了反调试就会修改 quest 这个文件,如果你不注意文件修改日期的话那么永远也算不出正确答案了。

反调试手段分析

如何发现反调试?通常情况下,我们不会刻意地去注意反调试,只有当程序提示,报错,运行结果附加调试器与不附加调试器运行结果有较大差异时,才会去注意反调试。

这里程序初始化的时候会拉起反调试,不过需要非常仔细,能够敏锐地观察到 vm 的代码文件被修改了。

首先确定反调试的位置,在main函数下断点,检查文件是否被修改。

发现main函数之前反调试就运行完了,那么就要讲到 main 函数之前调用的代码了,在 Linux 中,会保存一个 init_array,它会保存一系列的函数指针,这些函数先于 main 被调用;同样的,在 windows 中也有类似的。

通过 initterm 函数交叉引用找到 First 指针,获取函数指针的起始地址。

在指向的函数中,sub_140001190是反调试的关键函数:

其中对 FileName 变量似乎在做一个异或解密的操作,异或的key是0x69,dump下来解密之后发现果然是在对目标文件进行操作。

下面一系列就是写这个文件了,不过在这之前有一个关键判断qword_14002ED10,这个函数指针保存了哪个函数,大概率是 IsDebuggerPresent 了,但是还是去验证一遍。

交叉找到赋值的位置,是在这之前执行的函数内容。

下断点,看看能否断在这里,发现果真如我们所料:

那么这个反调试的过程就是在main函数之前先执行了两个函数,一个函数获取 IsDebuggerPresent 函数的地址保存在全局变量中,第二个函数调用这个 API 判断,如果的确被调试那么修改 quest 的文件内容。

对于这个绕过直接上 xdbg 的反调试插件 or 修改文件名,队爹就是直接上 xdbg 甚至感受不到反调试的存在,而我狂踩坑...


UnsafeFile

题目描述

附件下载,解压密码2024qwbfinal

作者在此申明:本题目为类勒索病毒分析的题目,若要分析请一定一定不要在个人电脑或者公共电脑上运行此程序,请在虚拟机中调试分析,若因此造成任何的损失与作者无关。

以下是原题目描述:

小Y玩游戏很菜,于是他找了个神秘人要了一个修改器文件,在开启功能后,发现他的一个重要文件居然被加密了,你能想办法帮他恢复吗?

请不要在物理机上运行题目中的任何文件,主办方对由此造成的任何损失不承担任何责任,如有需要请在虚拟机内进行运行和调试,解压密码:2024qwbfinal

基本分析

压缩包给了两个文件,一个是 CT 脚本,一个是 .pdf.yr,看起来 .yr 是一个勒索了 pdf 类型文件的后缀。

先看看 CT 脚本,运行之后会拉起 DBK 驱动,运行计算器,并且看标题似乎是一个植物大战僵尸的修改器。

其中比较主要的就是运行了一个decodeFunction去解密一段函数运行,这里可以直接用网上的脚本还原这段。

运行后得到一个 luac 文件,luac 文件需要用另一个工具去还原为 lua 脚本,这里我是用的是unluac_2023_12_24.jar,同样附上下载地址:https://sourceforge.net/projects/unluac/files/Unstable/

前面都是一些赋值函数,拉到最后发现几个有意思的字符串,其中C:\\system.dll引起了注意,于是去对应目录下,能找到一个 dll 文件,那么毫无疑问,剩下的就是对 system.dll 进行分析了,lua 脚本应该就是做注入用的。

dllmain 一个很标准的起线程的动作:

这个 StartAddress 就比较有意思,一直执着于判断自身某个内存的标志位,循环,而循环体内就是一直在 Sleep。

中间用 FindCrypt 发现 AES 的模式:

那么毫无疑问,勒索的文件应该是使用 AES 加密的,交叉找到对应的函数,其中一个是10001840,另一个是10001790

静态分析比较难了,下面开始动态分析。

动态调试

因为是个 dll,还不像 exe 那样好调试,这里我写了一个简单的 demo:

#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
int main(){
while(1){
Sleep(1000);
}
}

然后直接远线程注入这个模块,为了能断下,选择 patch dll 的 dllmain 函数开头为int 3。

然后,虚拟机里。

x32 起被调试程序,用注入器注入这个 dll。

程序成功被断下来:

这里可以去恢复 int 3 指令然后重新执行一遍。如果不改那个标志位,发现while ( !byte_1000C6DC )将会是一个死循环,这里估计是要等某个合适的时机,因此找到这个标志位将它赋值为1。

跳出循环之后,执行了一个函数:

看字符串,获取了用户名,又有 Documents 字符串,猜测是对我们用户目录下的文档文件感兴趣,这里可以随意写几个pdf文件让它加密看看它的规律。

往后跟进几步,发现关键字符串:

往后跟进之后发现,抛异常了,不过很幸运的是,栈中有数据提示我们。

文件大小需要时 16 的倍数,那就换一个 16 个 a 的 pdf 文件再来一次。

随后调试到调用 AES 函数之前,这里需要分析一下这个参数的作用,调用约定属于 thiscall,this 指针存 ECX,其余参数从右往左压入栈中。

这里记录一下第三个参数指向的一片内存,这个内存每次运行都不一样,先记录一下。

C3 67 B7 93 5E 0D AB 9A 48 3D BA EB 65 5F B5 92

而第二个参数就是指向了一片0xBAADF00D的内存。运行放过去,同时火绒剑也查获了一下这个 dll 的行为。

加密文件,删除文件,典型的勒索病毒,同时也注意到多次运行的加密结果是不一样的,而且原本16字节的结果变成了48字节,AES就算是 padding 模式也不会多 32 字节,于是断定它必然是随机加密,而密钥肯定也保存到了文件里,这里 AES 的参数很有可能就是密钥。

其实原本这里有点山穷水尽了,后面的结论要得出来对我就比较看运气(高手肉眼就看出来)了。

偶然的情况下,上面的第二个参数生成了 00 字节,而对应的第二行位置上生成了 0x5A,又联想到之前 lua 脚本写过一段异或 0x5a 的脚本。

相对应地,去源码中找到这一段system.dll+25C6,在此下断点调试。

发现它在异或 0068D14D 的内存,异或了 0x10 个字节,难道说这就是所看到的密钥,验证一遍。

发现果然就是它会将真实密钥每个字节异或 0x5A 之后保存到倒数第二行,那么最后一行必然不可能是 padding,猜测应该是初始向量,这是一个 CBC 模式的 AES。

通过研究 lua 脚本还发现,它似乎还对 DLL 进行了 hook,并且使用了 WriteByte 将system.dll+C6DC写为了 1,好样的,就是前面死循环的条件while(!byte_1000C6DC),在这一刻完成了闭环。

这个 dll 不仅用 lua 进行注入,还进行了一定的 hook,直接运行分析样本可能真分析不太出来,其实这里猜也能猜个大概了,但是作者这里觉得还是力求分析完整这个样本。

首先看看 initterm 函数的函数指针,发现了一些有意思的函数:

使用std::_Random_device获取随机数,随后使用梅森旋转算法计算后续的随机数,这里0x6C078965是该算法的一个常数,搜也是能搜出来的。

而随后将计算出的这么多随机数,使用一定的算法将某 0x10 个字节赋值到了 this 指针,一共调用了两次这个函数,一个在sub_10001000,另一个在sub_10001020。赋值的全局变量分别在1000C6E01000C6EC,这个是一开始就生成好的。

自己再去调试一遍也可以验证得到1000C6E0指针所指向的值,就是被异或加密前的密钥,或者说就是 pdf 倒数第二行异或 0x5A 的结果,而最后一行的结果与1000C6EC指针所指向的内容是一致的。

因此可以验证一遍:

确定没问题之后就可以开始恢复 pdf 文件了,但是因为我的 system.dll 没有做 hook,而 lua 脚本运行的时候做了 hook,因此在题目加密的 pdf 文件中,要先交换高低半个字节,再异或,才是原始密钥。

先写一下解密 key 的脚本:

#include<stdio.h>

int main(){
unsigned char key[]="\xcd\x8b\x95\xe3\x1f\x16\xd9\x21\x6b\x3c\x3c\x24\xb2\x6e\x98\xe7";
for(int i=0;i<16;i++){
unsigned char t=key[i]&0xf;
key[i]>>=4;
key[i]|=t<<4;
key[i]^=0x5A;
printf("%02x ",key[i]);
}
}

拿得到的结果去解密:

可以发现已经是一个 pdf 文件头了,下载,打开查看,flag 到手。


还有一道 bvp47 也是一道恶意样本分析的题,但是目前精力有限,可能要咕很久才能做出来了233。

看雪ID:xi@0ji233

https://bbs.kanxue.com/user-home-919002.htm

*本文为看雪论坛精华文章,由 xi@0ji233 原创,转载请注明来自看雪社区

# 往期推荐

1、Frida 逆向一个 APP

2、强网杯S8 Rust Pwn chat-with-me出题思路分享

3、浅析libc2.38版本及以前tcache安全机制演进过程与绕过手法

4、购物APP设备风控SDK-mtop简单分析

5、PWN入门:偷吃特权-SetUID

球分享

球点赞

球在看

点击阅读原文查看更多


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458587593&idx=2&sn=d75b7bc1f33210cf17b4f9d3c8bfa298&chksm=b18c214386fba855317cd3ddece1d0add96f22753a325b45ea7df0604c8018e3a2d0d7bde360&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh