吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 4668|回复: 2
收起左侧

[原创] 【Reversing.kr】WindowsKernel

[复制链接]
whklhh 发表于 2017-10-1 23:25
Reversing.kr是韩国的一个逆向题目网站
有分国度的排行榜,还是挺有意思的
本题即来自于第十一关WindowsKernel
http://reversing.kr/challenge.php可以下载到文件

EasyElf太简单了没有写的价值,自己糊弄一篇博客算了,就不往吾爱上献丑了


解压缩出来readme、WindowsKernel.exe和WinKer.sys吸取之前的教训,先仔细读懂ReadMe再做题:
Please authenticate to lowercase.


看不懂(:з」∠)认证,小写?
读懂ReadMe是不可能的,这辈子都读不懂ReadMe的

在物理机下运行exe,提示[Error]OpenSCManager,查了一下是系统服务的API
给了个系统管理员权限运行后卡了一阵子,然后[Error]StartService

啥玩意儿啊……拖x86-xp里运行试试,果然正常
x64没人权啊QAQ

驱动安装完毕以后点击Enable,输入框可见,输入一些内容后再点击Check按钮弹窗Wrong!

大概流程清楚了,EXE先安装驱动,WinerKer就是驱动程序了
依次逆向吧

GUI程序逆起来已经轻车熟路了,找到DiglogFunc回调函数,发现调用了关键函数sub_401110

[C++] 纯文本查看 复制代码
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; // [sp+8h] [bp-204h]@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显示的函数不多,每个函数大概扫一眼,还真发现了有用的东西:

[C++] 纯文本查看 复制代码
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的交叉引用,正好只有另一个函数中:

[C++] 纯文本查看 复制代码
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
发现连续三个类似结构的函数,再往上就是重点了:

[C++] 纯文本查看 复制代码
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为例

[C++] 纯文本查看 复制代码
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’的断码标注错了,要自己修正下
复制下来,通过脚本清洗一下得到字典
清洗脚本:

[Python] 纯文本查看 复制代码
s1 = s.split('\n')
s2 = {}
for i in range(35):
    s2[s1[3*i+2]] = s1[3*i]


再逆向处理下就能得到输入了:



[Python] 纯文本查看 复制代码
flag = [0xa5, 0x92, 0x95, 0xb0, 0xb2, 0x85, 0xa3, 0x86 , 0xb4, 0x8f, 0x8f, 0xb2 ]
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[hex(flag[i*4+j]^k)[2:]], end='')


keybdinthook


这下知道ReadMe啥意思了,由于端口接收的是按键的扫描码,因此无论大小写都是可以令程序Correct的(显示大小写则是通过Caps Lock的标志位来判断,在系统内核阶段处理,端口处于更底层)
提交给网站的flag应该用小写,OK

还好这次的程序结构不算太复杂,静态分析足矣
Poner大大那个程序静态找不到突破口,很为难

免费评分

参与人数 5吾爱币 +9 热心值 +5 收起 理由
Three_fish + 1 + 1 谢谢@Thanks!
2864095098 + 1 + 1 热心回复!
ming188 + 1 + 1 谢谢@Thanks!
Sound + 5 + 1 已经处理,感谢您对吾爱破解论坛的支持!
lies2014 + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

yysniper 发表于 2017-10-2 09:04
来学习一下
2864095098 发表于 2017-10-2 23:42
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-3-29 15:16

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表