起因 #
买了个这个画画用,虽然看起来很不正经,没有商业级的工业设计,不过实际使用体验还是挺好的,感觉比起tourbox那种一堆奇怪控制器而且abs材质的要好多了。就是店家写的驱动感觉有点狂野,所以打算逆向一下协议自己写个简洁点的。
初步分析 #
根据驱动的行为和功能来看,应该不是模拟鼠标键盘,而是跟驱动程序通信,让驱动去执行功能。
总之先拆开看看是什么单片机。拆开背面之后发现按键跟板子的结合方式不太好拆,强行拆了感觉很难装回去。不过核心直接露出来了所以起码能先确认下型号。看了下是ESP32 WRoom。
然后就开始逆向驱动。
初步反编译 #
因为是第一次搞Python逆向,对Python也不是很了解,不知道pyd是什么。根据对脚本语言的刻板印象,想当然地以为是某种字节码格式。
不管如何,总之先找找现成的工具。
首先找到的是pyinstxtractor 。用了之后就直接解开了,出来一堆pyc文件。此时我还没有意识到问题的严重性,以为pyc跟pyd是同一类东西。
又找了下pyc的反编译工具,比较流行的就只有rocky
写的一堆工具。总之用用试试。然后发现有几个文件报错了,而且卡在了PYZ-00.pyz_extracted/jiemain2.pyc,等待了很久之后stack overflow了,算是遇到第一个挑战了。
不过这时候还没到必须自己动手修复的问题,先找找别的工具试试。
又试了几个工具也都失败了,不过最后还是找到了pycdc 这个能正常反编译的。
看了一下反编译的结果发现是一万行出头的PyQt的界面初始化代码,main打成mian见多了,反过来的还是第一次遇到。
看了一下里面并没有逻辑相关的,估计是生成的,没什么太大的价值,只能看看按钮关联着什么函数。
#...
self.pushButton.clicked.connect(MainWindow.all_save)
self.pushButton_2.clicked.connect(MainWindow.chose_layer)
self.pushButton_7.clicked['bool'].connect(MainWindow.rgb_switch)
self.horizontalSlider.valueChanged['int'].connect(MainWindow.change_brightness)
self.comboBox.currentIndexChanged['int'].connect(MainWindow.change_light_mode)
#...
寻找业务逻辑 #
把所有反编译的pyc都简单看了下也没找到业务逻辑,这时候总算意识到事情有点不妙。
然后想起来最开始看到pyd文件,于是查了下发现原来是native的dll。于是打开ida看看什么情况。
上来随便扒了下发现一个巨大的无法反编译的函数,看了下应该是某种注册函数。
找到了跟连接设备相关的函数之后发现这种全是CPython API调用的伪c可读性非常差,而且没有sig文件连API的签名也是错的,于是决定先不静态分析硬啃,先用运行时手段旁敲侧击一下。
Monkey Patch #
本来打算注入的,但是看了一下目录发现cv2这个库没有编译,还是py源文件。
这就好办了,直接在__init__.py里开始用monkey patch的方式hook。
先hook一下serial的read和write,看看互相都发了些什么。
orig_write = serial.Serial.write
def hooked_write(self, data):
with open("log", "a") as f:
f.write("write: " + data.hex() + "\n")
orig_write(self, data)
orig_read = serial.Serial.read
def hooked_read(self, size=1):
result = orig_read(self, size)
with open("log", "a") as f:
f.write("read: " + result.hex() + "\n")
return result
serial.Serial.write = hooked_write
serial.Serial.read = hooked_read
每个按键按了几遍后然后得到了这样的log:
write: 0191b3561e
read: 0b79183c90
read: 1e492501e7
write: 2d19644dfc
write: 0001620200
read: 0001000000
read: 0000000000
read: 0001000000
...
read: 0001000100
read: 0000000100
read: 0001000100
...
read: 0001000200
read: 0000000200
read: 0001000200
...
很明显能看出来后面那一串全是按键的msg,而且是明文没加密。
又重复试了几次发现,开头的四条消息每次都会变化,而0001620200是不变的,这条应该就是握手成功激活设备的指令。
第一条消息的后四字节每次都会变化,那要么是用了随机,要么是编码了时间,总之不管怎么样,固定第一条消息应该是没有任何影响的。
于是通过hook然后第一条消息固定发送0100000000,发现固定收到设备回复39e5b4830d和一条每次都变化的消息。
看来两边做的事是对称的,都是随机生成一条消息,要求对面生成正确的回复来握手。那需要做的事就只剩下找出这个生成算法了。
通过检查反编译出来的入口文件的引入,发现跟加密相关的引用就只有hashlib和random了。
# Source Generated with Decompyle++
# File: YYR4.pyc (Python 3.7)
import re
import sys
import os
import random
import hashlib
import _thread
import serial
import time
import win32api
import win32con
import configparser
import serial.tools.list_ports as serial
from key_map import key_map
from pynput_map import pynput_map
from jiemain2 import Ui_MainWindow
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.Qt import QPropertyAnimation, QRect, QSequentialAnimationGroup
from PyQt5.QtWidgets import QLabel, QDesktopWidget, QApplication, QDialog, QMessageBox
from PyQt5.QtNetwork import QLocalSocket, QLocalServer
from Autorun import AutoRun
import win32process
import win32gui
import wmi
from time import sleep
import pythoncom
from system_hotkey import SystemHotkey
from pynput.keyboard import Controller as Con_keyboard, Key
from pynput.mouse import Controller as Con_mouse, Button
from update_config import update_config
from language import change_to_chinese, change_to_english
from pyautogui import locateCenterOnScreen
import main
hash的可能性明显更大,所以直接hook里面所有hash算法看看用的是哪个。
然后发现是sha256,于是接着hook。
然后由于对Python不熟导致hook的方式错了,log出来的sha256全是固定的,而且以为自己没弄错,于是就开始怀疑人生了。
静态分析 #
没办法,只能回到静态分析上硬啃了。
然后半读半猜大概理解了调用函数的流程,但是还是很疑惑为什么还是完全看不出来任何像加密的逻辑。不过好在大概整理出了它是这么个逻辑:创建sha256 -> utf8 -> update -> digest。
然后这时候也明白了sha256的hook有问题,于是修复了之后得到了这样的log:
sha256: 0 -> e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
sha256 update: b'10000'
digest: 39e5b4830d4d9c14db7368a95b65d5463ea3d09520373723430c03a5a453b5df
write: 0100000000
read: 39e5b4830d
read: ddfe4f316b
sha256: 0 -> e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
sha256 update: b'24831503573'
digest: 490b83d63653cad0c3f631f3b31588870bbe263d857433c8ddd1ebf457ab7e64
write: 490b83d636
然后这种简单到极致的逻辑就一眼能看出来了,就是把设备发过来的随机消息的所有字节转成十进制字符串然后拼接在一起,再塞进sha256,结果截取前5位。
随便写了个串口通信验证了,果然成功连接了。
结尾 #
虽然回忆起来感觉蠢爆了,这么简单的东西花了这么久才搞明白,但是当时协议跑通的一瞬间还是爽到高潮一般,逆向真是世界上最棒的开放式解谜游戏。
不过坏处是太容易上头了,因为是晚上突然想搞的所以没忍住直接通宵了,没搞定睡了一会儿又直接爬起来搞终于搞定,然后巨大的兴奋感又直接消除了睡意,然后就基本上两天没怎么睡。下场就是胃特别难受,歇了两三天什么都没干才缓过来。唉,逆向害人啊。