whklhh 发表于 2017-10-1 23:25

【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大大那个程序静态找不到突破口,很为难

yysniper 发表于 2017-10-2 09:04

来学习一下

2864095098 发表于 2017-10-2 23:42

感觉算法好难学
页: [1]
查看完整版本: 【Reversing.kr】WindowsKernel