【Reversing.kr】WindowsKernel
Reversing.kr是韩国的一个逆向题目网站有分国度的排行榜,还是挺有意思的
本题即来自于第十一关WindowsKernel
http://reversing.kr/challenge.php可以下载到文件
EasyElf太简单了没有写的价值,自己糊弄一篇博客算了,就不往吾爱上献丑了
解压缩出来readme、WindowsKernel.exe和WinKer.sys吸取之前的教训,先仔细读懂ReadMe再做题:
Please authenticate to lowercase.
看不懂(:з」∠)认证,小写?
读懂ReadMe是不可能的,这辈子都读不懂ReadMe的
在物理机下运行exe,提示OpenSCManager,查了一下是系统服务的API
给了个系统管理员权限运行后卡了一阵子,然后StartService
啥玩意儿啊……拖x86-xp里运行试试,果然正常
x64没人权啊QAQ
驱动安装完毕以后点击Enable,输入框可见,输入一些内容后再点击Check按钮弹窗Wrong!
大概流程清楚了,EXE先安装驱动,WinerKer就是驱动程序了
依次逆向吧
GUI程序逆起来已经轻车熟路了,找到DiglogFunc回调函数,发现调用了关键函数sub_401110
HWND __thiscall sub_401110(HWND hDlg)
{
HWND v1; // edi@1
HWND result; // eax@3
HWND v3; // eax@4
HWND v4; // eax@4
HWND v5; // eax@9
WCHAR String; // @1
v1 = hDlg;
GetDlgItemTextW(hDlg, 1003, &String, 512);
if ( lstrcmpW(&String, L"Enable") )
{
result = (HWND)lstrcmpW(&String, L"Check");
if ( !result )
{
if ( Click(v1, 0x2000u) == 1 )
MessageBoxW(v1, L"Correct!", L"Reversing.Kr", 0x40u);
else
MessageBoxW(v1, L"Wrong", L"Reversing.Kr", 0x10u);
SetDlgItemTextW(v1, 1002, &word_4021F0);
v5 = GetDlgItem(v1, 1002);
EnableWindow(v5, 0);
result = (HWND)SetDlgItemTextW(v1, 1003, L"Enable");
}
}
else if ( Click(v1, 0x1000u) )
{
v3 = GetDlgItem(v1, 1002);
EnableWindow(v3, 1);
SetDlgItemTextW(v1, 1003, L"Check");
SetDlgItemTextW(v1, 1002, &word_4021F0);
v4 = GetDlgItem(v1, 1002);
result = SetFocus(v4);
}
else
{
result = (HWND)MessageBoxW(v1, L"Device Error", L"Reversing.Kr", 0x10u);
}
return result;
}
首先cmp按钮字符是否为Enable,代表首次运行
则调用Click(v1, 0x1000),并将文本框可用、改变按钮字符为Check
第二次再点击时字符为Check,代表第二次运行
则调用Click(v1, 0x2000),并根据返回值进行提示正误
Click内部通过DeviceIoControl函数与驱动交互
查询可知该函数的第二个参数为dwIoControlCode,即Click的第二个参数
CTL_CODE:用于创建一个唯一的32位系统I/O控制代码,这个控制代码包括4部分组成:DeviceType(设备类型,高16位(16-31位)),Access(访问限制,14-15位),Function(功能2-13位),Method(I/O访问内存使用方式)。
很容易猜出,0x1000令驱动开始记录,0x2000开始Check
接下来就该对驱动进行逆向来找到验证逻辑了
驱动的入口函数为DriverEntry,但是太复杂了没看到什么有用的信息
字符串也没有好用的
查了一些信息试图进行动态调试来跟踪字符串的路径
中间找到了@Poner版主大大前几天写的CM:https://www.52pojie.cn/thread-645359-1-1.html
用DbgViewer看到了DbgPrint显示的调试信息
只逆出了调用了时间结构体,下面的变量交互太复杂看不懂……以后会动态调试了再来吧(:з」∠)
内核调试由于被调试的对象是OS,系统内核,因此在暂停时会导致整个系统暂停。所以通常使用双机调试(两台物理机进行连接)或虚拟机调试
SoftICE是支持本机内核调试的工具,然而早就停止维护更新了
动态的路子断了,于是只好硬着头皮静态分析
IDA显示的函数不多,每个函数大概扫一眼,还真发现了有用的东西:
int __stdcall accept(int a1, PIRP Irp)
{
int v2; // edx@1
_IRP *v3; // eax@1
v2 = *(Irp->Tail.Overlay.PacketType + 12);
v3 = Irp->AssociatedIrp.MasterIrp;
if ( v2 == 0x1000 ) // 初始化
{
*&v3->Type = 1;
flag = 1;
x = 0;
return_value = 0;
fail = 0;
}
else if ( v2 == 0x2000 ) // Check,返回
{
flag = 0;
*&v3->Type = return_value;
}
Irp->IoStatus.Status = 0;
Irp->IoStatus.Information = 4;
IofCompleteRequest(Irp, 0);
return 0;
}
这个函数中看到了熟悉的0x1000和0x2000,说明它就是处理交互信息的地方了
大概分析一下可以看出来IofCompleteRequest是发送返回信息的地方,IRP结构体就是返回内容
那么v3->Type这里很可能就是返回值,因为它在初始化时被赋值为0,结束时赋值为一个全局变量
查看对return_value的交叉引用,正好只有另一个函数中:
int __stdcall check_0(char a1)
{
int result; // eax@1
char v2; // cl@1
bool v3; // zf@3
result = x - 200;
v2 = a1 ^ 5;
switch ( x )
{
case 200:
case 202:
case 204:
case 206:
goto LABEL_2;
case 201:
v3 = v2 == 0xB4u;
goto LABEL_4;
case 203:
case 205:
v3 = v2 == 0x8Fu;
LABEL_4:
if ( v3 )
goto LABEL_2;
goto LABEL_10;
case 207:
if ( v2 != 0xB2u )
goto LABEL_10;
return_value = 1; // success
LABEL_2:
++x;
break;
case 208:
return_value = 0; // wrong
LABEL_10:
fail = 1;
break;
default:
return result;
}
return result;
}
这个赋值结构很明显就是通过case进行的判断
中间流程又出现了v2==0x8f等的比较,很熟悉的结构
那么继续查看该函数的交叉引用,向上溯源找a1
发现连续三个类似结构的函数,再往上就是重点了:
void __stdcall input(struct _KDPC *Dpc, PVOID DeferredContext, PVOID SystemArgument1, PVOID SystemArgument2)
{
char v4; // al@1
v4 = READ_PORT_UCHAR(0x60);
check_2(v4);
}
READ_PORT_UCHAR这个API是从端口中获取字节信息,0x60是PS2键盘的数据端口
那么整个流程就清晰了:
驱动接受到0x1000指令后,开始记录键盘输入并比对,直到接受0x2000指令返回值
那么check_0-check_2的逻辑只需要慢慢分析一下就行了,以check_2为例
int __stdcall check_2(char a1)
{
int result; // eax@1
bool v2; // zf@5
result = 1; // */xa5*/x92*/x95*/xb0
if ( fail != 1 )
{
switch ( x )
{
case 0:
case 2:
case 4:
case 6:
goto LABEL_3;
case 1:
v2 = a1 == 0xA5u;
goto LABEL_6;
case 3:
v2 = a1 == 0x92u;
goto LABEL_6;
case 5:
v2 = a1 == 0x95u;
LABEL_6:
if ( !v2 )
goto fail;
LABEL_3:
++x;
break;
case 7:
if ( a1 == 0xB0u )
x = 100;
else
fail:
fail = 1;
break;
default:
result = check_1(a1);
break;
}
}
return result;
}
第一个a1输入时x=0,直接x++后break,意味着第一个a1可以为任意值
第二个a1输入时x=1,进入case 1判断,要求a1==0xA5否则v2=0会使得全局变量fail=1,从而再也无法进行switch判断
以此类推,得到*/xa5*/x92*/x95*/xb0的输入,接着进入check_1函数
唯一的区别就是check_1对a1进行了^0x12的变换,其他完全一致
三个函数逆下来就能得到输入指令:
*/xa5*/x92*/x95*/xb0
*/xb2*/x85*/xa3*/x86^12
*/xb4*/xbf*/xbf*/xb2^5
很明显它们超过了ASCII的范围,并且奇数输入都为任意值
猜想端口输入应该包括了按键按下和弹起的命令,并且有另外一套对应码表
由于不能动态调试所以无法验证,查询过程中找到了WinIO这个可以直接对端口操作的程序,然而x64的dll需要交叉注册,xp又运行不了
于是直接搜索键盘扫描码,得到对应码表
0x60端口的数据中
低7位代表扫描码,高1位代表状态
0表示按下,称为通码
1表示弹起,称为断码
从http://blog.csdn.net/firas/article/details/26267573可以得到,不过’e’的断码标注错了,要自己修正下
复制下来,通过脚本清洗一下得到字典
清洗脚本:
s1 = s.split('\n')
s2 = {}
for i in range(35):
s2] = s1
再逆向处理下就能得到输入了:
flag =
for i in range(3):
if (i == 0):
k = 0
elif (i == 1):
k = 0x12
elif (i == 2):
k = 5 ^ 0x12
for j in range(4):
print(s2^k)], end='')
keybdinthook
这下知道ReadMe啥意思了,由于端口接收的是按键的扫描码,因此无论大小写都是可以令程序Correct的(显示大小写则是通过Caps Lock的标志位来判断,在系统内核阶段处理,端口处于更底层)
提交给网站的flag应该用小写,OK
还好这次的程序结构不算太复杂,静态分析足矣
Poner大大那个程序静态找不到突破口,很为难 来学习一下 感觉算法好难学
页:
[1]