windows礼包

在翻hctf的大佬以前的博客时看见了几道不错的Windows逆向题目,正好到了带新生的时候,详细写写wp也算是带新生入门吧

网盘链接:链接:https://pan.baidu.com/s/1F0bg1rS2gtDNZuO7falRjg 密码:nhru

Broken Windows

拿peid查一下壳,这里就不再放截图(懒的打开虚拟机了。。。)了,发现有upx壳,可以手脱也可以工具直接脱,也不再演示了

ida载入发现停在了WinMain这个地方。可能大家对这个函数还不是很熟悉,简单提及一下

1
2
3
4
5
int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
DialogBoxParamA(hInstance, 0x65, 0, DialogFunc, 0);
return 0;
}

WinMain是一个函数,该函数的功能是被系统调用,作为一个32位应用程序的入口点。WinMain函数应初始化应用程序,显示主窗口,进入一个消息接收一发送循环,这个循环是应用程序执行的其余部分的顶级控制结构。

可以看到这个函数有两个关键的词了,第一个就是入口点,你可以简单的理解为windows窗口中的main函数(没学过c语言的新生就当是程序从此处开始执行代码吧),接着再来看函数的内容。可以看到WinMain又去调用DialogBoxParam函数,然后就return了,似乎什么也没有干…吗?这其实就是第二个关键词了,循环,可以想象一下我们常用的windows应用窗口界面(比如你打个游戏什么的),他们可不会说是执行完你的操作就跟完事了,他们依然在循环等待着你的操作。

对这个结构有了一些了解之后我们接着看DialogBoxParam这个函数,简单说就是创建一个对话框,然后用你的第四个参数(也就是某个函数)去处理这个对话框的具体过程,所以我们下一步的分析重点就应该是它,接下来我们就点进去看一下

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
BOOL __stdcall DialogFunc(HWND hWnd, UINT a2, WPARAM a3, LPARAM a4)
{
HWND v5; // eax

if ( a2 > 0x111 )
{
if ( a2 == 274 )
{
CreateThread(0, 0, StartAddress, 0, 0, 0);
if ( a3 == 61536 )
EndDialog(hWnd, 61536);
}
}
else
{
switch ( a2 )
{
case 0x111u:
CreateThread(0, 0, StartAddress, 0, 0, 0);
if ( a3 == 1 )
{
v5 = GetDlgItem(hWnd, 1001);
SendMessageA(v5, 0xDu, 0x21u, dword_403428);
qmemcpy(Caption, dword_403428, 0x23u);
CreateThread(0, 0, sub_401020, 0, 0, 0);
return 0;
}
break;
case 0xFu:
CreateThread(0, 0, StartAddress, 0, 0, 0);
hdc = BeginPaint(hWnd, &Paint);
hdcSrc = CreateCompatibleDC(hdc);
SelectObject(hdcSrc, h);
BitBlt(hdc, 0, 0, dword_403414, cy, hdcSrc, 0, 0, 0xCC0020u);
DeleteDC(hdcSrc);
EndPaint(hWnd, &Paint);
return 0;
case 0x110u:
hDlg = hWnd;
CreateThread(0, 0, StartAddress, 0, 0, 0);
CreateThread(0, 0, sub_401160, 0, 0, 0);
dword_40339C = GetWindowLongA(hWnd, -6);
h = LoadBitmapA(dword_40339C, 0x66);
GetObjectA(h, 24, &unk_403410);
SetWindowPos(hWnd, HWND_MESSAGE|0x2, 0, 0, dword_403414, cy, 6u);
return 0;
}
}
return 0;
}

可以看到有大量的函数调用,而且全部都是系统API,如果是比赛场上第一次做这种题,一个一个去学习函数似乎难度系数就有点大了,那我们该怎么去找关键函数呢?这里有三种办法,一是我们从ida给我们的Function窗口中去人工找key function,因为这道题目函数不是很多所以很容易定位到关键的加密函数,之后我们在函数名处按x键就可以找到该函数在哪些地方被调用过,跟过去就可以大体了解到DialogFunc的逻辑了;第二种方法就是去找CreateThread一类的函数,这是windows编程中常用的一种操作,这类函数有什么作用呢?其实他会创建一个线程(可以想象成一个子程序),然后根据你传入的第四个参数(也是个函数)来执行;第三就是提前了解一下这些的作用喽,其实他们都对应的不同的消息响应,在下边一个题目中会具体提到。

通过这几种方法我们找到了这两个关键的函数

1
2
3
4
5
DWORD __stdcall sub_401160(LPVOID lpThreadParameter)
{
sub_401080(byte_403018);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DWORD __stdcall sub_401020(LPVOID lpThreadParameter)
{
_BYTE *v1; // eax
_BYTE *v2; // eax
unsigned int v3; // eax
int v4; // ecx

v1 = sub_401080(dword_403428);
v2 = sub_401080(v1);
sub_401080(v2);
v3 = 32;
v4 = 0;
while ( dword_403428[v4] == byte_403018[v4] )
{
v3 -= 4;
++v4;
if ( v3 < 4 )
{
MessageBoxA(0, Caption, Caption, 0);
return 0;
}
}
return 0;
}

第一个函数很简单,就是将数据进行了处理,我们可以在函数名处按n键对函数进行重命名,方便我们之后的观看,这里我们就命名为encode,而数据我们直接dump出来,也就是

1
list = [0x68, 0x23, 0x51, 0x8D, 0xC8, 0xC9, 0x1F, 0x93, 0xF3, 0xFA, 0xFF, 0x9E, 0x37, 0x77, 0x1B, 0x83, 0x81, 0x69, 0x6D, 0x46, 0x64, 0xCF, 0x4B, 0xAD, 0x6A, 0xA8, 0xAA, 0xEA, 0x41, 0x45, 0x7Bh, 0xAB]

第二个函数稍显复杂,首先是对我们输入的数据进行encode,然后返回值在encode,然后返回值再encode。然后是一段循环处理,可以看到v4作为数组的下标是在不断++的,而一旦encode后的数据和我们输入的某一位不一样,那就直接return了,v3则是作为计数器,他的初始值等于我们的数据长度,而在每次都会-4,最后如果减到了<4的情况,就会弹出一个MessageBox

接下来的任务就是分析encode了,ida跳过去(这里我已经把一些关键变量改好名字了)

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
_BYTE *__usercall sub_401080@<eax>(_BYTE *a1@<edi>)
{
int v1; // ebx
signed int count; // esi
_BYTE *array; // eax
char v4; // cl
char v5; // cl
char v6; // dl
char v7; // cl
char v8; // dl
char v9; // cl
char v10; // dl

v1 = 1 - a1;
count = 0;
array = a1;
while ( 1 )
{
v4 = __ROL1__(*array, 3); //ROL是移位(就是c语言的位操作)的意思
*array = v4;
if ( count >= 1 )
*array = v4 ^ a1[count - 1];
v5 = __ROL1__(*array, 4) + 3;
v6 = __ROL1__(array[1], 3);
*array = v5; //数据被四个分为了一组,这是每组的第一位
array[1] = v6;
if ( &array[v1] >= 1 )
array[1] = v6 ^ v5;
v7 = __ROL1__(array[1], 4) + 3;
v8 = __ROL1__(array[2], 3);
array[1] = v7; //每组数据的第二位
array[2] = v8;
if ( &array[2 - a1] >= 1 )
array[2] = v8 ^ v7;
v9 = __ROL1__(array[2], 4) + 3;
v10 = __ROL1__(array[3], 3);
array[2] = v9; //第三位
array[3] = v10;
if ( &array[3 - a1] >= 1 )
array[3] = v10 ^ v9;
array[3] = __ROL1__(array[3], 4) + 3; //第四位
count += 4; //起到了计数器的作用
array += 4; //注意这里不是真的地址+4,而是由第1个数据变为第1+4个数据,可以替换为数组下标
if ( count >= 32 )
break;
v1 = 1 - a1;
}
return a1 + 1; //注意这里返回的是a1加1,也就是传入数据的下一位
}

可以看到该函数将传入的数据四个分为一组进行处理移位,且处理有规律可言,我们对上述的函数进行简化

1
2
3
4
5
6
7
8
unsigned char * encrypt(unsigned char * pt) {
pt[0] = rol(pt[0], 7) + 3;
for (int i = 1; i < 32; i++) {
pt[i] = rol(pt[i], 3) ^ pt[i-1];
pt[i] = rol(pt[i], 4) + 3;
}
return pt+1;
}

由于移位操作和减法都是简单可逆(减的加上,左移的右移回去)的,所以我们很轻松就可以写出解密函数

1
2
3
4
5
def decode(list, fuck):
for i in range(31-fuck+1,1,-1):
list[i + fuck] = ror(list[i + fuck]-3,4) & 0xff
list[i + fuck] = ror((list[i + fuck] & 0xff) ^ (list[i + fuck - 1] & 0xff),3) & 0xff
list[fuck] = ror(list[fuck]-3,7) & 0xff

这里要注意一个细节问题,在ida中我们可以清楚的看到我们传入的数据是unsigned char(也就是一个字节)类型的,而我们在使用python去进行解密的时候由于用的是int的list,所以会产生数据大小不对称的问题,我们这里用&0xff的方式来限制我们的数据大小。

最后的脚本要注意,程序先将数据encode,接着将输入encode,输入的返回值encode,返回值再encode,所以我们在逆向解密时,应该先将数据encode,再将数据decode,再将数据从1的位置decode,再将数据从2的位置decode

1
2
3
4
encode(list)
decode(list,2)
decode(list,1)
decode(list,0)

具体的脚本就不放了,最终解得的flag是

image-20181225160804820

这道题目就到这里了,难度不是很高,主要还是要耐心去逆向算法,

Open Windows

还是按照上面的方法找到DialogFunc的位置,这里要提到一些新知识了

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
BOOL __stdcall DialogFunc(HWND a1, UINT vm_command, WPARAM a3, LPARAM a4)
{
WPARAM v4; // ebx
HWND v6; // esi
HWND v7; // esi
const CHAR *v8; // ebx

if ( vm_command > 0x110 )
{
if ( vm_command == 274 && a3 == 61536 )
{
HIBYTE(a3) = 0;
sub_4012E0((_BYTE *)&a3 + 3);
v8 = Src;
if ( dword_403848 - (_DWORD)Src == 11 && x == 771 && y == 0x63A421C737F6FFE0i64 )
{
if ( sub_401040(Src) )
MessageBoxA(0, v8, "success", 0);
}
EndDialog(a1, 61536);
}
}
else
{
switch ( vm_command )
{
case 0x110u: // 初始化
v7 = a1;
y = 1i64;
x = 0i64;
dword_4037CC = GetWindowLongA(a1, -6);
h = LoadBitmapA((HINSTANCE)dword_4037CC, (LPCSTR)0x68);
GetObjectA(h, 24, &unk_403820);
SetWindowPos(v7, HWND_MESSAGE|0x2, 0, 0, dword_403824, cy, 6u);
return 0;
case 0xFu: // 画图
v6 = a1;
hdc = BeginPaint(a1, &Paint);
hdcSrc = CreateCompatibleDC(hdc);
SelectObject(hdcSrc, h);
BitBlt(hdc, 0, 0, dword_403824, cy, hdcSrc, 0, 0, 0xCC0020u);
DeleteDC(hdcSrc);
EndPaint(v6, &Paint);
return 0;
case 0x101u: // 实际操作的keyup
v4 = a3;
HIBYTE(a1) = a3;
sub_4012E0((_BYTE *)&a1 + 3);
y *= v4;
x += v4;
return 0;
}
}
return 0;
}

可以看到进入函数后会对vm_command(我这里进行了重命名,ida原本应该是a2)进行比较,对不同的vm_command进行不同的操作。这其实就是windows的消息机制,vvm_command就是个消息,而我们的DialogFunc会对不同消息进行不同的响应,举个例子,你的鼠标点击、键盘敲击等等都属于一种消息,当我们点击“关闭”按钮的时候,就发送了一个关闭窗口的消息,他有一个对应的值,这时windows检测到了这个消息发生了,就会进行关闭窗口的操作并且执行用户写在此处的相关代码

image-20181226103224409

当我们分析这个程序时,首先从vm_command来将程序划为不同的区域,如:case 0x110处就是用来初始化的函数,而0xF则涉及到图形绘制的一些功能,很显然这都不是我们要关心的,而case 0x101我们则要特别注意,因为0x101代表的事件是用户在使用键盘按键(准确的说是按键“弹起来”)了。

在这里可以看到函数调用了两个函数,然后将我们的键盘对应的按键的值加给了x,乘给了y,也就是说x是累加器,y是累乘器。HIBYTE是从给定的16位中提取其中的高位部分,因为我们键盘上的按键是拿ASCII来标示的,所以其实只占用了一个字节,也就是8位,所以也就是让a1的高位部分等于键盘输入的按键值

接着进入第二个函数来分析

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
_BYTE *__usercall sub_4012E0@<eax>(_BYTE *a1@<eax>)
{
_BYTE *v1; // esi
_BYTE *v2; // eax
LPCSTR v3; // ecx
int v4; // esi
_BYTE *result; // eax

v1 = a1;
v2 = konw_what;
if ( v1 >= konw_what || (v3 = Src, Src > v1) )
{
if ( konw_what == dword_40384C )
{
sub_401350();
v2 = konw_what;
}
if ( v2 )
{
*v2 = *v1;
v2 = konw_what;
}
}
else
{
v4 = v1 - Src;
if ( konw_what == dword_40384C )
{
sub_401350();
v2 = konw_what;
v3 = Src;
}
if ( v2 )
{
*v2 = v3[v4];
return (konw_what++ + 1);c
}
}
result = v2 + 1;
konw_what = result;
return result;
}

可以看到这个函数稍显复杂,我们当然可以一步一步慢慢分析,但我们可以从第15行的函数中发现一些线索,我们点进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned int sub_401350()
{
LPCSTR v0; // ecx
int v1; // eax
unsigned int result; // eax

v0 = Src;
v1 = konw_what - Src;
if ( (konw_what - Src) > 0xFFFFFFFE )
std::_Xlength_error("vector<T> too long");
result = v1 + 1;
if ( result > dword_40384C - v0 )
result = sub_4013A0();
return result;
}

很显然这里实现了类似异常检测的功能,当我们的know_what(命名鬼才)-src的值过大的时候就会产生too long的错误,我们就可以推测,know_what - src的值就应该是用户输入的长度。

最后一部分是关闭窗口的代码

1
2
3
4
5
if ( konw_what - Src == 11 && x == 771 && y == 0x63A421C737F6FFE0i64 )
{
if ( sub_401040(Src) )
MessageBoxA(0, v8, "success", 0);
}

可以看到他要求我们输入10个字符,和为771,乘积为0x63A421C737F6FFE0,,接着还会调用一个函数,在点进去看看

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
signed int __usercall sub_401040@<eax>(const char *input@<ebx>)
{
int i; // ecx
char data; // al
_DWORD *judge_table; // esi
signed int j; // edi
signed int count; // eax
_DWORD *v7; // [esp+0h] [ebp-4h]

if ( strlen(input) == 10 ) //检测输入长度是否为10
{
i = 0; //循环变量我用i、j、count进行了标示
while ( 1 )
{
data = input[i];
if ( data > 90 || data < 65 ) // 循环每一个值检测是否为大写字母
break;
if ( ++i >= 10 )
{
judge_table = &table;
j = 0;
v7 = &table;
while ( 1 )
{
count = j;
if ( j < 10 )
break;
LABEL_12:
judge_table += 0x11; //judge_table每0x11个是一组
++j;
v7 = judge_table;
if ( judge_table >= &border ) //检查judge_table是否到头了
return 1;
}
while ( input[j] > input[count] == *judge_table ) //检测input的每个值和所有的值比较的结果是不是符合judge_table的结果
{
++count;
++judge_table;
if ( count >= 10 )
{
judge_table = v7;
goto LABEL_12; //这里实现了嵌套循环的功能
}
}
return 0;
}
}
}
return 0;
}

逻辑也不难,首先检测每个值是不是都是大写字母,然后就是依次拿出input的每个值,把值和另外的其他的值进行比较,且比较的结果要符合judge_table的里保存的真值。而由于judge_table是16个一循环的,我们可以把它当作一个矩阵结构,然后使用它的前十行前十列即可。

到这里我们就有了如下的信息

  • flag长度为10
  • flag的每个值和另外的值比较的结果满足judge_table
  • 所有的值都是大写字母
  • 和为771,乘积为0x63A421C737F6FFE0

我们可以通过分解乘积,将分解出的值固定在大写字母范围以内的方式来确定输入的字母,然后提取出来judge_table来搞定字母的顺序

最终flag为LKSOQWEDSF