像所有程序员一样,您喜欢代码。 你和他是最好的朋友。 但是,一生中迟早会有这样的时刻,那就是您没有代码。 是的,很难相信,但是你们之间会有巨大的鸿沟:你们在外面,而他在里面很深。 由于无望,您和所有人一样,将不得不走到另一边。 在逆向工程方面。
使用
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:
对从转储接收的数据运行我们的脚本:
在解密的流量中,我们找到了最理想的线路!
NQ2019DABE17518674F97DBA393415E9727982FC52C202549E6C1740BC0933C694B3DE不久将有分析剩余任务的文章,不要错过!
PS并且我们提醒您,每个在NeoQUEST-2019上至少完成一项任务的人都有权获得奖励! 检查您的邮件是否有信件,如果没有收到,请写信至
support@neoquest.ru !