2022-10月DASCTF X GFCTF

2022-10月DASCTF X GFCTF

考点: 字节码、ollvm混淆、duilib程序逆向、sys驱动、异常处理

算法浓度不是很高,🐹🐹比较喜欢,babysys个人感觉题目不错,本文也主要介绍了该题的做题思路。

PYCODE

version3.9,dis拿字节码,直接翻译即可。

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
from Crypto.Util.number import *
cc='8b2e4e858126bc8478d6a6a485215f03'
def extract_number(x):
x = x ^ (x >> 11)
x=x&0xffffffff
x = x ^ ((x << 7) & 2022072721)
x = x & 0xffffffff
x = x ^ ((x << 15) & 2323163360)
x = x & 0xffffffff
x = x ^ (x >> 18)
x = x & 0xffffffff
return x

def transform(m):
new_message = ''
l = len(m)
for i in range(l//4):
enc = m[4 * i:4 * i + 4]
enc = bytes_to_long(enc)
enc = extract_number(enc)
new_message += long_to_bytes(enc, 4)
return new_message
cc=bytes.fromhex(cc)
c=[]
for i in range(len(cc)//4):
c.append(bytes_to_long(cc[4*i:4*i+4]))

def re(x):
x=x^(x>>18)
t=(x^((x<<15)&2323163360))&0xffffffff
x=x^((t<<15)&2323163360)
x = x & 0xffffffff
t1=(x ^ ((x << 7) & 2022072721))&0xffffffff #14
t2=(x ^ ((t1 << 7) & 2022072721))&0xffffffff#21
t3=(x ^ ((t2 << 7) & 2022072721))&0xffffffff #28
x=x ^ ((t3 << 7) & 2022072721)
x = x & 0xffffffff
t = x ^ (x >> 11) #22
x=x ^ (t >> 11)
return x

for i in range(len(c)):
print(long_to_bytes(re(c[i])).hex(),end='')
#DASCTF{89196e63ab5556e7389d2bb44f8e6e06}

贪玩CTF

主要逻辑在sub_7FF6A72619C0,TLS函数又反调试,nop掉即可,可以通过设置条件断点便于调试。

image-20221023180526090

对账号加密单纯异或最后一位,对密码加密则是AES,首先会动态恢复sbox。

image-20221023180723914

1
2
3
4
5
6
7
8
9
10
11
s=[ 0x04, 0x1F, 0x1F, 0x1E, 0x43, 0x4B, 0x43, 0x45, 0x44, 0x00, 0x16, 0x10, 0x55, 0x17, 0x12, 0x73]
for i in range(15):
s[i]^=s[15]
print(bytes(s))
k=bytes(s)
#wllm08067sec&das
from Crypto.Cipher.AES import *
aes=new(k,MODE_ECB)
c=bytes([0x3C, 0x97, 0x72, 0x96, 0x5A, 0x33, 0x63, 0x9C, 0x97, 0x30, 0x4D, 0x90, 0x84, 0xE8, 0x5F, 0x56])
print(aes.decrypt(c))
#DASCTF{wllm08067sec&dase4deb7a6510a10f7}

cutere

通过一场处理修改了rc4的秘钥和base64表,加密主体部分采用ollvm混淆过了,不过加密逻辑没变,调试出关键值直接解密即可。

image-20221023181006551

直接cyberchef一把梭

rc4

image-20221023181117914

换表base64

image-20221023181223173

最后交替拼接即可

1
2
3
4
5
6
7
print('a'*32)
rc4='szv~'
buf0='DST{Wo7Xj5Ad8Nx8'
buf1='ACFg0Gw1Jo5Ix9C}'
nbase='ghijklmnopqrstuvwxyz0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef'
for i in range(len(buf1)):
print(buf0[i]+buf1[i],end='')

babysys

无双击调试经验,只能硬动态分析。本题内核成分不多,主要是异常处理,感谢Retard爷的点播,🐹🐹我啊~ ,才理清程序逻辑。

异常补充

关于x64SEH相关补充,修复控制流的相关结构体

异常注册

每个非叶函数只要对应一个RUNTIME_FUNCTION结构体,如果叶函数使用了SEH,也会有对应的结构体。

不调用函数、又没有修改栈指针,也没有使用 SEH 的函数就叫做“叶函数”,树的叶子节点。

x64 windows中异常注册信息发生了巨大的改变,异常注册信息在编译过程中生成,链接时写入 PE+ 头中的 ExceptionDirectory ,其中几乎包含所有函数的栈操作、异常处理等信息。

程序出现异常->查找函数的runtime_function结构体->找到unwind_info,判断flags,如果没有异常处理程序,那么根据异常的位置参照unwind_code进行回滚,返回到上层函数,继续查runtime_function结构体,查看有无处理函数,如果有则进行异常处理,如果无则继续回滚重复查runtime_function

至于findally块的执行,满足全局展开,会先找一个异常过滤程序执行,如果返回1

ExceptionDir

其中包含多条字段RUNTIME_FUNCTION,一共12字节。

1
2
3
4
5
6
7
8
typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY {
DWORD BeginAddress; //函数首地址
DWORD EndAddress; //函数结束地址
union { //函数unwind相关信息
DWORD UnwindInfoAddress;
DWORD UnwindData;
} DUMMYUNIONNAME;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION, _IMAGE_RUNTIME_FUNCTION_ENTRY, *_PIMAGE_RUNTIME_FUNCTION_ENTRY;

unwind结构体记录有关异常处理和函数操作的信息,用于回滚。

易失寄存器rcx、rdx、r8、r9、r10、r11

1
2
3
4
5
6
7
8
9
10
11
12
13
struct UNWIND_INFO_HDR
{
char Ver3_Flags; //低3位是version 高5位是flags
char PrologSize; //函数头大小,函数头一般是对栈进行密集操作的一部分
char CntUnwindCodes;//UWIND_CODE的数量
char FrReg_FrRegOff;// FrReg用作fp的寄存器编号 fp中存放 rsp+16*FrRegOff
};
/*flags值 - 关键
UNW_FLAG_NHANDLER 0x0 不对异常进行处理
UNW_FLAG_EHANDLER 0x01 使用Except函数进行处理。
UNW_FLAG_UHANDLER 0x02 使用finally函数处理。
UNW_FLAG_CHAININFO 0x04 使用调用链,某些函数被其他函数调用,在异常会滚时也需要做调用他函数的回滚工作,可以直接用其上层函数的runtime_func即可。
*/

当unwind_info—中的flag字段为后三种情况时,后续不定长数组unwind_code的结构如下。

image-20221024202124917

UWIND_CODE用于记录函数头中有关非易失性寄存器和RSP的操作。

1
2
3
4
5
6
7
struct UNWIND_CODE
{
char PrologOff; // 执行此操作的指令末尾的偏移量
char OpCode_OpInfo;
//Unwind operation code low 4bit
//Operation info high 4bit
};

PrologOff距离头部偏移的偏移量,例如值为x,那么在main函数x字节之外发生异常,则需要此次回滚,否则不进行回滚,info则是根据op的类型所需的相关参数。

image-20221024185740395

例如47a0函数的一条unwind_code如下,第一个值是4,结合函数处的汇编为sub rsp,0x38表示的该4字节的栈操作,而0x62,低4位为2即UWOP_ALLOC_SMALL,对于info的值为6,查看官方文档。

image-20221024190207532

在栈上开辟了一个8*info+8大小的空间,即6*8+8 = 0x38, 也就该条unwind_code记录着该函数前4字节做的操作在栈上开辟了0x38的空间,如果在之后出现异常,则回滚时则需要进行相应的栈处理。

unwind_code的处理顺序和函数中对栈操作的指令相反。

对于flags为1或2时,即函数存在异常处理/终止处理的程序。

unwind_info 中会塞入__C_specific_handler函数,他根据SCOPE_TABLE进行异常处理或终止处理,该函数后跟SCOPE_TABLE的数量,之后便是,相关的scope结构体数组。

C_SCOPE_TABLE结构体记录了异常处理的结构,大小16字节,4个RVA

1
2
3
4
5
6
7
8
9
10
11
struct C_SCOPE_TABLE
{
void *__ptr32 Begin; //try块开始的首地址
void *__ptr32 End; // try块结束的地址
void *__ptr32 Handler;
void *__ptr32 Target;
};
/*对于handler和target
如果UNWIND_INFO_HDR中Flags为UNW_FLAG_EHANDLER(1),则以此为filter程序和except程序。
如果Flags为UNW_FLAG_UHANDLER(2),则为finally程序和0。
*/

综上,RUNTIME_FUNCTION最后会指向一系列的unwind_code用于回滚。

结构体汇总

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
typedef struct _RUNTIME_FUNCTION {
ULONG BeginAddress;
ULONG EndAddress;
ULONG UnwindData;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;

typedef enum _UNWIND_OP_CODES {
UWOP_PUSH_NONVOL = 0,
UWOP_ALLOC_LARGE, // 1
UWOP_ALLOC_SMALL, // 2
UWOP_SET_FPREG, // 3
UWOP_SAVE_NONVOL, // 4
UWOP_SAVE_NONVOL_FAR, // 5
UWOP_SPARE_CODE1, // 6
UWOP_SPARE_CODE2, // 7
UWOP_SAVE_XMM128, // 8
UWOP_SAVE_XMM128_FAR, // 9
UWOP_PUSH_MACHFRAME // 10
} UNWIND_OP_CODES, *PUNWIND_OP_CODES;

typedef union _UNWIND_CODE {
struct {
UCHAR CodeOffset;
UCHAR UnwindOp : 4;
UCHAR OpInfo : 4;
};

USHORT FrameOffset;
} UNWIND_CODE, *PUNWIND_CODE;

#define UNW_FLAG_NHANDLER 0x0 //no
#define UNW_FLAG_EHANDLER 0x1 //exception
#define UNW_FLAG_UHANDLER 0x2 //unwind(finally)
#define UNW_FLAG_CHAININFO 0x4 //chain

typedef struct _UNWIND_INFO {
UCHAR Version : 3; //前部分一共4字节,IDA中定义为UNWIND_INFO_HDR
UCHAR Flags : 5;
UCHAR SizeOfProlog; //函数头部大小
UCHAR CountOfCodes; //不定长结构体大小,不过更准确应该是包含的dw数据的个数
UCHAR FrameRegister : 4;
UCHAR FrameOffset : 4;
UNWIND_CODE UnwindCode[1]; //不定长数组

//
// The unwind codes are followed by an optional DWORD aligned field that
// contains the exception handler address or a function table entry if
// chained unwind information is specified. If an exception handler address
// is specified, then it is followed by the language specified exception
// handler data.
//
// union {
// struct {
// ULONG ExceptionHandler;
// ULONG ExceptionData[];
// };
//
// RUNTIME_FUNCTION FunctionEntry;
// };
//

} UNWIND_INFO, *PUNWIND_INFO;

typedef struct _SCOPE_TABLE {
ULONG Count;
struct
{
ULONG BeginAddress;
ULONG EndAddress;
ULONG HandlerAddress;
ULONG JumpTarget;
} ScopeRecord[1];
} SCOPE_TABLE, *PSCOPE_TABLE;

详见官方: https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64?view=msvc-170

异常信息

__finally代码块为终止程序,__except(filter())为异常处理程序,filter为异常过滤程序,返回值有三种情况。

1
2
3
EXCEPTION_CONTINUE_EXECUTION(-1)  //在发生异常的地方继续执行
EXCEPTION_EXECUTE_HANDLER(1) //执行异常处理程序
EXCEPTION_CONTINUE_SEARCH(0) //继续向上寻找异常过滤程序,直到能处理

image-20221024221213623

糊~

异常处理中与异常信息相关结构体,可由GetExceptionInformation函数获取,返回EXCEPTION_POINTERS类型的指针。

EXCEPTION_POINTERS

1
2
3
4
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord; //与CPU无关的信息
PCONTEXT ContextRecord; //CPU相关的异常信息,寄存器等。
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

PEXCEPTION_RECORD

1
2
3
4
5
6
7
8
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

题解

用户程序访问驱动,通过CreateFile访问设备。

image-20221024222100867

同时使用DeviceIoControl函数向驱动发送信息,控制设备执行相关操作,操作控制代码为0x222000。

1
2
3
4
5
6
7
8
9
BOOL DeviceIoControl(
(HANDLE) hDevice, // handle to device
NULL, // lpInBuffer
0, // nInBufferSize
(LPVOID) lpOutBuffer, // output buffer
(DWORD) nOutBufferSize, // size of output buffer
(LPDWORD) lpBytesReturned, // number of bytes returned
(LPOVERLAPPED) lpOverlapped // OVERLAPPED structure
);

驱动程序通过IoCreateDevice创建设备名称,并使用IoCreateSymbolicLink创建符号链接。

image-20221024222622619

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _DRIVER_OBJECT {
CSHORT Type;
CSHORT Size;
PDEVICE_OBJECT DeviceObject;
ULONG Flags;
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
UNICODE_STRING DriverName;
PUNICODE_STRING HardwareDatabase;
PFAST_IO_DISPATCH FastIoDispatch;
PDRIVER_INITIALIZE DriverInit;
PDRIVER_STARTIO DriverStartIo;
PDRIVER_UNLOAD DriverUnload;
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT, *PDRIVER_OBJECT;

image-20221023233226231

并且驱动程序注册了一个DriverObject->MajorFunction[14]的函数,0xe对应着IRP_MJ_DEVICE_CONTROL,即应用程序调用 DeviceIoControl,会指向注册的sub_14000175C函数。

image-20221024230024387

该函数会匹配操作控制码,并且获取应用程序传入的输入计算其长度,并使用sub_1400018d8函数进行处理。

image-20221024230632623

后续函数多使用异或来做SMC,并且在调用后恢复。

get_func_len函数是通过访问runtime_function结构体获取函数开始和结束位置做差获取。

image-20221024231130224

如果对PE文件熟悉的话,0x3c是dos头最后四字节,指向NT头,而0xA0则对应ExceptionDir的首地址,相关结构体上文已给出。

使用ipy解密代码,但是发现后文并没有直接调用,而是在调用hook_2_func_encrypt后就将解密后的代码还原了。

1
2
3
4
5
6
7
8
9
import idautils
adr=0x140001288
end=0x1400014C1
size=end-adr
code=bytearray(get_bytes(adr,size))
for i in range(len(code)):
code[i]^=0xab
patch_byte(adr+i,code[i])
print('win')

查看hook_2_func_encrypt地址的函数,其通过触发int3异常来执行异常过滤程序,这样便能调用刚刚解密的 sub_140001288函数。

image-20221024231929089

sub_140001288

image-20221024232450647

image-20221025083058978

通过v3(cnt)从0开始计数,并比对v3的值完成不同操作。如果v3的值小于7,则执行另外两组操作。并且在最后通过EXCEPTION_POINTERS获取异常ip,如果异常处指令为0xcc并且v3不等于1就将ip加1后返回,返回值为-1即继续在异常处执行。

如果v3为1或3时,会先对输入进行+i的处理,之后调用sub_140001AB4函数。

image-20221025085825841

sub_140001AB4函数加密逻辑如下

image-20221025090206334

通过计算两个固定函数的md5相异或,将得到的32个字符分为16个字节一组,第一组用做tea加密,第二组用作tea解密,类似3DES处理。

当v3等于2/4时,会首先调用 sub_140001C60,并且异或0x10。

image-20221025090522937

一共有6个int3,并且第一次不改ip故cnt一共加了7次,第7次进入if块处理。

首先会调用sub_140001868函数,修改触发异常函数的runtime_function结构体,修改其异常处理程序为0x140001009处的func_encrypt函数。

image-20221025094325672

之后会将sub_1400014C4函数异或0x66,并且返回-1,下次触发异常对应的cnt为8则返回1,那么异常由except块的异常处理程序进行处理,即调用刚刚修改的0x140001009 地址处的函数。

sub_1400014C4函数还会异或0xcc,调用结束后会异或0xaa,因为0x66^0xcc == 0xaa,这里套了一层。

函数的smc不在赘述,处理流程为将密文再异或0x99并使用sub_140001C60进行tea加密,结果与v11比对。

image-20221026172745157

sub_140001c60函数

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
void __fastcall sub_140001C60(unsigned int *a1)
{
__int64 v2; // r10
unsigned int v0; // r8d
int sum; // r11d
unsigned int v1; // r9d
__int64 round; // rbx

v2 = 5i64;
do
{
v0 = *a1;
sum = 0;
v1 = a1[1];
round = 32i64;
do
{
sum -= 0x61C88647;
v0 += (16 * v1 + 17) ^ ((v1 >> 5) + 34) ^ (sum + v1);
v1 += (16 * v0 + 51) ^ ((v0 >> 5) + 68) ^ (sum + v0);
--round;
}
while ( round );
*a1 = v0;
a1[1] = v1;
a1 += 2;
--v2;
}
while ( v2 );
}

整体加密流程简化后如下

1
2
3
4
5
6
7
8
9
//循环两轮
ms[i]+=i
sub_140001AB4(v2); //两个tea 加密 解密
sub_140001C60(v2); //tea
ms[i]^=0x10

//sub_140001638异常处理函数
ms[i]^=0x99
sub_140001C60(v2) //tea

算法无魔改,只要取到key值即可,解密脚本如下。

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
85
86
87
88
89
90
91
92
93
94
#include<iostream>
#define ut32 unsigned int
#define delta 0x9e3779b9
void __fastcall sub_140001C60(unsigned int* a1)
{
__int64 v2; // r10
unsigned int v0; // r8d
int sum; // r11d
unsigned int v1; // r9d
__int64 round; // rbx

v2 = 1i64;
do
{
v0 = *a1;
sum = 0;
v1 = a1[1];
round = 32i64;
do
{
sum -= 0x61C88647;
v0 += (16 * v1 + 17) ^ ((v1 >> 5) + 34) ^ (sum + v1);
v1 += (16 * v0 + 51) ^ ((v0 >> 5) + 68) ^ (sum + v0);
--round;
} while (round);
*a1 = v0;
a1[1] = v1;
a1 += 2;
--v2;
} while (v2);
printf("sum: %08x\n", sum);
}
void Tea_Encrypt(ut32* src, ut32* k) {
ut32 sum = 0;
ut32 v0 = src[0];
ut32 v1 = src[1];
for (int i = 0; i < 0x20; i++) {
sum += delta;
v0 += ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);
v1 += ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);
}
src[0] = v0;
src[1] = v1;
}
void Tea_Decrypt(ut32* enc, ut32* k) {
int sum = delta * 0x20;
ut32 v0 = enc[0];
ut32 v1 = enc[1];
for (int i = 0; i < 0x20; i++) {
v1 -= ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);
v0 -= ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);
sum -= delta;
}
enc[0] = v0;
enc[1] = v1;
}

void output(ut32* m, ut32 n) {
for (int i = 0; i < n; i++)
printf("0x%08x ", m[i]);
printf("\n");
}

int main() {
ut32 m[10] = { 0xbb929b51,0x25fed57f,0x988aa03d,0xbdc35d79,0xe60aca2f,0xf2755515,0x67aa0fc2,0xeb1992a8,0x62759e50,0x461e460c };
ut32 k1[4] = { 0x656565e,0x57575207,0x5201040a,0xf045605 };

ut32 k2[4] = { 0x7595701,0x5601560c,0x5651565a,0x5525056 };

ut32 k[4] = { 17,34,51,68 };

for(int i=0;i<10;i+=2)
Tea_Decrypt(m+i, k);

for (int i = 0; i < 10; i++) {
m[i] ^= 0x99999999;
}
for (int j = 0; j < 2; j++) {
for (int i = 0; i < 10; i++)
m[i] ^= 0x10101010;
for (int i = 0; i < 10; i += 2) {
Tea_Decrypt(m + i, k);
Tea_Encrypt(m + i, k2);
Tea_Decrypt(m + i, k1);
}
for (int i = 0; i < 40; i++) {
*((unsigned char*)m + i) -= i;
}
}
for (int i = 0; i < 40; i++) {
putchar(*((char*)m + i));
}
return 0;
}

参考

(向量化异111常处理)VEH hook

基于异常处理的Hook函数(VEH Hook)

Windows 驱动程序逆向工程方法

SEH分析笔记(X64篇)


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!