首页
社区
课程
招聘
[原创]Gtuner软件分析(一)
发表于: 2025-2-15 00:04 3134

[原创]Gtuner软件分析(一)

2025-2-15 00:04
3134

1.前言

最近空闲时间简单分析了一款游戏转换器的上位机程序。本文主要是介绍硬件与软件通讯的发包函数和硬件更新固件的流程。(如有侵权联系作者删除)

2.软件介绍

程序是美国consoletuner公司开发的一款针对鼠标键盘和游戏手柄转换成特定手柄型号的硬件(PS\XBOX\NS),配套软件名为Gtuner软件。
软件界面:
图片描述
硬件外观:
图片描述
官网:
图片描述

  • 使用此硬件的玩家主要是利用鼠标键盘转换成手柄来。同时拥有便捷的操作并且可以享受手柄玩家的辅助瞄准达到增强游戏效果(作者也不喜欢这种行为,我认为应该属于作弊)。
  • 这是许多游戏厂商不希望看到的( 包括企鹅在PS5等设备上开始进行检测),希望此文章对于这类型硬件检测有帮助。

3.分析思路

通过官网查询能发现它是支持HID协议的。查看硬件接口是Micro-USB接口。先使用USBlyzer进行简单分析。
图片描述
可以看到bMaxPacketSize0是40H,这个信息可以利用。
当软件打开的时候Gtuner就会循环打开和关闭设备,同通过hidusb能判断程序内部应该使用了hidsdi和setupapi相关的api。
图片描述
通过微软官方资料可以得知在交互hid硬件的时候,CreateFile创建设备,发送数据是WriteFile,读取数据是ReadFile。
使用IDA验证我的猜想:
图片描述
图片描述
导入表中确实存在WriteFile和ReadFile。
图片描述
通过对函数调用查询发现了两处使用WriteFile。0x14011AD30和0x14011A8B0。

0x14011AD30函数分析

首先计算一下0x14011AD30函数的RVA是0x0011AD30,通过PE格式可以知道0x140000000,RVA = 0x14011AD30 - 0x140000000 = 0x0011AD30
图片描述
使用X64动态调试对这个函数下普通断点
图片描述
根据上面USBlyzer分析可以知道,正常情况下最大传输值是64BYTE,所以这个函数肯定不是发包函数。
图片描述
通过对汇编的分析这里最大支持0x40F显然不是64BYTE。
通过对软件进行功能测试,发现在硬件进行更新的时候断点会断下,暂时猜测应该是固件更新时候用的发包函数。
图片描述
通过栈回溯方式我们找到上层函数
图片描述
我们直接跳转到00007FF7EA6E64CB,并且计算RVA = 00007FF7EA6E64CB - 00007FF7EA650000 = 0x964CB,方便我们在IDAPro中定位这个函数
图片描述
IDAPro用的是模拟地址,所以这个地址定位应该是0x140000000 + 0x964CB = 0x1400964CB
图片描述
对这个函数进行简单分析,有更新固件字样
图片描述
可以看到,发包函数传递的时候,主要的固件数据是通过QByteArray这个对象进行存储并且传递的,并且每次传递的大小是0x240。为了印证我的猜测所以我对这个发包函数进行还原:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#define MAX_BUFFER_SIZE 0x40F
DWORD64 __fastcall write_device(void* Src, size_t Size, DWORD dwMilliseconds)
{
    // 参数验证
    if (!HidDeviceObject) {
        return WRITE_ERROR;
    }
     
    if (Size > MAX_BUFFER_SIZE) {
        return WRITE_ERROR;
    }
 
    // 创建事件
    HANDLE hEvent = CreateEventW(NULL, TRUE, TRUE, NULL);
    if (!hEvent) {
        return WRITE_ERROR;
    }
 
    // 初始化异步结构
    OVERLAPPED Overlapped = {0};
    ResetEvent(hEvent);
    Overlapped.hEvent = hEvent;
 
    // 准备数据缓冲区
    char WriteBuffer[1040] = {0};  // 报告ID + Data
    memcpy(WriteBuffer + 1, Src, Size);  // 拷贝数据
 
    // 写入数据
    DWORD NumberOfBytesTransferred;
    if (WriteFile(HidDeviceObject, WriteBuffer, Size + 1, NULL, &Overlapped)) {
        goto GET_RESULT;
    }
 
    // 处理异步写入
    if (GetLastError() == ERROR_IO_PENDING) {
        DWORD waitResult = WaitForSingleObject(hEvent, dwMilliseconds);
         
        if (waitResult == 258) {  // WAIT_TIMEOUT
            CancelIo(HidDeviceObject);
            return WRITE_ERROR;
        }
         
        if (waitResult) {
            return WRITE_ERROR;
        }
 
GET_RESULT://正确处理
        if (GetOverlappedResult(HidDeviceObject, &Overlapped, &NumberOfBytesTransferred, FALSE)) {
            return NumberOfBytesTransferred;
        }
    }
 
    return WRITE_ERROR;
}

更新固件分析

可以看到第二个参数就是包的长度。
继续分析sub_1400960B0,通过IDA Pro查看调用链可以知道
图片描述
可以看到这里只有一个函数调用了sub_1400960B0,继续往外分析sub_140095C90,通过F5对sub_1400960B0进行大概查看,应该类似于一个状态机的函数
图片描述
转到CASE1看到一个URL
图片描述
在这里看到,主要就是为了拼接Url和处理格式问题,对着块位置进行详细分析。
动态调试分析后,处理完URL和前端一些字符信息后就跳转进入QT内部了
图片描述
由于时间有限,且对QT框架不够熟练暂时跳过QT分析,但是我通过上下文字符串文本和之前URL猜测这里面应该是有访问外部请求的,所以我通过抓包软件进行分析。
可以看到跳转回来后,确实访问了一个请求。
图片描述
使用Python我们对这个请求也模拟一下,从请求的信息可以得知,这里应该是固件的版本做校验。
图片描述
并且通过URL可以反推出,之前的拼接函数,应该是一个URl+客户端版本+用户平台+设备型号+设备UUID
图片描述
我大胆猜测校验固件版本后面肯定是下载固件
图片描述
可以看到这里有修改前端按钮字样,结合前端界面分析,应该是对下载按钮添加了文本。
图片描述
QT按钮有槽函数机制,在这个函数内没有看到和信号槽绑定等相关的API。但是通过文字说明到这应该完成所有的函数初始化。
最后这个函数会跳转到这个jmp中然后
图片描述
进入QT内部,并且最后进入消息循环,前端界面解锁。
图片描述
通过对发包函数的分析可以知道,要更新固件必须要重新调用这个函数,因为他是外层函数,所以继续对Switch开始地方下断点。
图片描述
根据Switch表可以计算出他访问的case,这里是rax是4所以索引就是4
图片描述
访问的是这个地址
图片描述
图片描述
在对这块位置进行分析
图片描述
这块位置主要是显示一些前端的debug信息
最后跳转到这里进行槽函数绑定,并且查询资料这应该是一个QT4版本的主流函数,能猜测软件开发时间应该不超过2016。
图片描述
绑定完槽函数后最后跳转进入这段,最后跳转进入QT内部绑定信号槽,解锁前端,并且发送请求。
图片描述
通过抓包拿到请求URl 'http:/XXXX.com.br/硬件型号/FW/?固件版本&设备平台&设备UUID'
图片描述
固件下载完后会继续回到这个函数中,并且状态是case5
图片描述
一路调试跟进最后跳转到这里,应该也是对控制进行输出debug信息
图片描述

图片描述
跳出循环最后,解锁GUI
图片描述
最后再次回到状态机并且状态为case6
图片描述
,而case6就是固件更新的外层更新函数
图片描述
使用Python进行模拟软件下载固件,发现每次下发的固件都不一样,猜测应该是在服务器端进行加密,最后下发到硬件内部进行解密。这里贴出模拟下载代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
 
url = '0f0K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4S2^5P5q4)9J5c8Y4S2^5i4K6u0r3c8W2N6Q4x3V1k6Q4x3@1j5I4i4K6u0W2x3e0q4Q4x3X3f1K6i4K6u0W2x3g2)9J5y4X3q4E0M7q4)9K6b7W2N6u0e0W2)9J5y4X3q4E0M7q4)9K6b7U0m8Q4x3U0k6S2L8i4m8Q4x3@1t1J5y4e0l9^5x3o6l9K6x3W2)9J5y4X3q4E0M7q4)9K6b7U0l9K6x3e0m8Q4x3U0k6S2L8i4m8Q4x3@1u0^5P5s2S2Q4x3U0M7`.
 
headers = {
    'Host': 'isystem.com.br',
    'User-Agent': 'Gtuner/IV',
    'Connection': 'Keep-Alive',
    'Accept-Encoding': 'gzip, deflate',
    'Accept-Language': 'zh-CN,en,*',
}
 
response = requests.get(url, headers=headers)
 
if response.status_code == 200:
    with open('firmware2.bin', 'wb') as file:
        file.write(response.content)
    print("固件下载成功!")
else:
    print(f"请求失败,状态码: {response.status_code}")

010比较固件
图片描述
最后更新完就会跳转到这里设置deubg提示更新成功,解锁前段。
图片描述

固件更新流程图

图片描述


[注意]看雪招聘,专注安全领域的专业人才平台!

最后于 2025-3-12 16:52 被BitWarden编辑 ,原因:
收藏
免费 6
支持
分享
最新回复 (1)
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
强的啊 
2025-2-15 13:30
0
游客
登录 | 注册 方可回帖
返回