沉浸在驱动程序中:使用NeoQUEST-2019任务示例进行反向的一般原理


像所有程序员一样,您喜欢代码。 你和他是最好的朋友。 但是,一生中迟早会有这样的时刻,那就是您没有代码。 是的,很难相信,但是你们之间会有巨大的鸿沟:你们在外面,而他在里面很深。 由于无望,您和所有人一样,将不得不走到另一边。 在逆向工程方面。

使用NeoQUEST-2019在线阶段任务2的示例我们将分析反向驱动程序Windows的一般原理。 当然,该示例已相当简化,但是过程的本质并未因此改变-唯一的问题是需要查看的代码量。 有了经验和运气,让我们开始吧!

给定


根据传说,我们得到了两个文件:流量转储和生成相同流量的二进制文件。 首先,使用Wireshark查看转储:


转储包含UDP数据包流,每个数据包包含6个字节的数据。 乍一看,这些数据是一些随机的字节集-无法从流量中获取任何信息。 因此,我们将注意力转向二进制,它将告诉您如何解密所有内容。
在IDA中打开它:


看来我们正在面对某种驱动程序。 具有WSK前缀的功能是指Windows内核模式网络编程接口Winsock内核。 在MSDN上,您可以看到 WSK中使用的结构和功能描述。

为了方便起见,您可以将Windows Driver Kit 8(内核模式)-wdk8_km(或任何更新的)库加载到IDA中,以使用在那里定义的类型:


注意,反向!


与往常一样,从入口点开始:


让我们去吧。 首先,初始化Wsk,创建并绑定套接字-我们将不详细描述这些功能,它们不携带任何对我们有用的信息。

sub_140001608函数设置4个全局变量。 我们称它为InitVars。 在其中之一中,将值写入地址0xFFFFF78000000320。 稍微搜索一下该地址,我们可以假设它记录了系统启动后系统计时器的滴答数。 现在,让我们将变量命名为TickCount。


然后,EntryPoint设置用于处理IRP数据包(I / O请求数据包)的功能。 您可以在MSDN上阅读有关它们的更多信息。 对于所有类型的请求,都定义了一个函数,该函数将数据包简单地传递到堆栈中的下一个驱动程序。


但是对于类型IRP_MJ_READ(3),定义了一个单独的函数; 我们称之为IrpRead。



依次安装了CompletionRoutine。


CompletionRoutine使用从IRP接收的数据填充未知结构,并将其放在列表中。 到目前为止,我们还不知道包中的内容-我们稍后将返回此函数。
我们在EntryPoint中进一步看。 定义IRP处理程序后,将调用sub_1400012F8函数。 让我们看一下内部,立即注意到在其中创建了一个设备(IoCreateDevice)。


调用函数AddDevice。 如果类型正确,那么我们将看到设备名称为“ \\ Device \\ KeyboardClass0”。 因此,我们的驱动程序与键盘进行交互。 在键盘上下文中查询IRP_MJ_READ,您会发现 KEYBOARD_INPUT_DATA结构是以数据包形式传输的。 让我们回到CompletionRoutine,看看它传递什么样的数据。


这里的IDA不能很好地解析该结构,但是通过偏移量和进一步的调用,您可以理解它由ListEntry,KeyData(密钥的扫描代码存储在此处)和KeyFlags组成。
在AddDevice之后,在EntryPoint中调用函数sub_140001274。 她创建了一个新的流。


让我们看看ThreadFunc中发生了什么。


她从列表中获取值并进行处理。 立即注意功能sub_140001A18。


它将处理后的数据与指向WskSocket的指针以及数字0x89E0FEA928230002一起传递到sub_140001A68函数的输入。 通过字节分析参数编号(0x89 = 137、0xE0 = 224、0xFE = 243、0xA9 = 169、0x2328 = 9000),我们从流量转储中获得了完全相同的地址和端口:169.243.224.137:9000。 逻辑上假设此功能将网络数据包发送到指定的地址和端口-我们将不对其进行详细介绍。
让我们看看在发送之前如何处理数据。

对于前两个元素,将对生成的值进行等效处理。 由于滴答数是用于计算的,因此可以假定我们面临着伪随机数的产生。



生成数字后,它将覆盖我们之前称为TickCount的变量的值。 公式变量在InitVars中设置。 如果返回此函数的调用,我们将找出这些变量的值,结果将得到以下公式:

(54773 + 7141 * prev_value)%259200

这是线性一致伪随机数生成器 。 它是使用TickCount在InitVars中初始化的。 对于每个后续数字,前一个用作初始值(生成器返回一个双字节值,并且该值用于后续生成)。


在等效于从键盘传输的两个值的随机数之后,将调用一个函数,该函数形成消息的剩余两个字节。 它仅产生两个已加密参数和某个常数值的异或 。 这不太可能以某种方式解密数据,因此对我们而言,消息的最后两个字节没有携带任何有用的信息,因此无法考虑。 但是,如何处理加密数据?
让我们仔细看看到底是什么加密。 KeyData是一种扫描代码,可以采用相当宽范围的值;猜测并不容易。 但是KeyFlags是一个位字段:


如果查看扫描代码 ,您会发现大多数情况下标志是0(键按下)或1(键升起)。 KEY_E0很少被公开,但是可能会碰到,但是满足KEY_E1的机会很小。 因此,您可以尝试执行以下操作:我们检查转储中的数据,选择一个加密的KeyFlags值,使其等于0,生成两个连续的PSC。 首先,KeyData是一个字节,我们可以通过高字节检查生成的MSS的正确性。 其次,当使用正确的PSC执行等效操作时,下一个加密的KeyFlags将采用相同的位值。 如果发现这是错误的,则我们假设我们最初查看的KeyFlags为1,依此类推。
让我们尝试实现我们的算法。 我们将为此使用python:

算法实现
#  -   keymap = […] # ,   Wireshark traffic_dump = […] #  def bxnor(a, b): return ((~a & 0xffff) | b) & (a | (~b & 0xffff)) #   def brgen(a): return ((7141 * a + 54773) % 259200) & 0xffff def decode(): #     for i in range(0, len(traffic_dump) - 1): #   KeyFlags probe = traffic_dump[i][1] #   - scancode = traffic_dump[i+1][0] #    KeyFlags tester = traffic_dump[i+1][1] fail = True #     (  KEY_E1) for flag in range(4): rnd_flag = bxnor(flag, probe) rnd_sc = brgen(rnd_flag) next_flag = bxnor(tester, brgen(rnd_sc)) #   KeyFlags if next_flag in range(4): sc = bxnor(rnd_sc, scancode) if sc < len(keymap): sym = keymap[sc] if next_flag % 2 == 0: print(sym, end='') fail = False break #   -      KeyFlags   if fail: print('Something went wrong on {} pair'.format(i)) return print() if __name__ == "__main__": decode() 


对从转储接收的数据运行我们的脚本:


在解密的流量中,我们找到了最理想的线路!

NQ2019DABE17518674F97DBA393415E9727982FC52C202549E6C1740BC0933C694B3DE


不久将有分析剩余任务的文章,不要错过!

PS并且我们提醒您,每个在NeoQUEST-2019上至少完成一项任务的人都有权获得奖励! 检查您的邮件是否有信件,如果没有收到,请写信至support@neoquest.ru

Source: https://habr.com/ru/post/zh-CN446462/


All Articles