initial commit

This commit is contained in:
laura 2025-11-10 04:53:37 -03:00
commit 4de9088898
Signed by: w
GPG key ID: BCD2117C99E69817
13 changed files with 610 additions and 0 deletions

8
.clang-format Normal file
View file

@ -0,0 +1,8 @@
BasedOnStyle: LLVM
BreakBeforeBraces: Linux
UseTab: Always
IndentWidth: 4
TabWidth: 4
AllowShortIfStatementsOnASingleLine: false
AllowShortFunctionsOnASingleLine: false
IndentCaseLabels: true

2
.clangd Normal file
View file

@ -0,0 +1,2 @@
CompileFlags:
Add: [-std=c23]

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
*.o
ratazana
compile_commands.json
.cache

10
LICENSE Normal file
View file

@ -0,0 +1,10 @@
Copyright (c) 2025 favewa
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

33
Makefile Normal file
View file

@ -0,0 +1,33 @@
TARGET = ratazana
CC = clang
CFLAGS = -Wall -Wextra -pedantic -std=c23 -O2
DEBUGFLAGS = -g -O0 -DDEBUG -fsanitize=address -fsanitize=undefined
LDFLAGS =
LIBS =
SOURCES = hidpp20.c hidraw.c utils.c main.c
HEADERS = ratazana.h hidpp20.h hexdump.h
OBJECTS = $(SOURCES:.c=.o)
all: $(TARGET)
debug: CFLAGS += $(DEBUGFLAGS)
debug: clean $(TARGET)
$(TARGET): $(OBJECTS)
@$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)
%.o: %.c ratazana.h
@$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJECTS) $(TARGET)
fmt:
clang-format -i $(SOURCES) $(HEADERS)
lint:
clang-tidy $(SOURCES) -- $(CFLAGS)
.PHONY: all debug clean fmt lint

30
README.md Normal file
View file

@ -0,0 +1,30 @@
# 🐀 ratazana
**ratazana** (portuguese for «brown rat») is minimal implementation of logitech and razer mouse firmware, repurposing their onboard memory as a covert channel for arbitrary data
## status of this project
### hid++ 2.0 (logitech g series)
<sub><sup>...Assuming that your device is a G403 HERO</sup></sub>
- [x] Basic protocol detection (ping/pong)
- [x] Device enumeration and initialization
- [x] Packet send/receive with error handling
- [ ] Feature discovery (IRoot, IFeatureSet)
- [ ] Onboard memory management
- [ ] Profile manipulation
- [ ] Device-specific transactions
### razer protocol implementation
<sub><sup>...Assuming that your device is a Viper Mini</sup></sub>
- _tbd_
### core features
- [ ] arbitrary data storage algorithm
- [ ] data encoding/compression
- [ ] wireless communication support
- [ ] multi-device coordination

53
hexdump.h Normal file
View file

@ -0,0 +1,53 @@
/**
* Copyright (c) 2025 favewa
* SPDX-License-Identifier: BSD-3-Clause
*/
#pragma once
#include <ctype.h>
#include <stdint.h>
#include <stdio.h>
typedef enum {
HEXDUMP_COMPACT,
HEXDUMP_DETAILED,
} hexdump_style;
static constexpr size_t BYTES_PER_LINE = 16;
static constexpr int ADDR_WIDTH = (int)(sizeof(size_t) * 2);
void hexdump(const char *restrict prefix, const uint8_t data[static 1],
size_t len, hexdump_style style)
{
if (style == HEXDUMP_COMPACT) {
if (prefix)
printf("%s (%zu bytes): ", prefix, len);
for (size_t i = 0; i < len; i++) {
printf("%02X ", data[i]);
}
printf("\n");
return;
}
if (prefix)
printf("%s (%zu bytes):\n", prefix, len);
for (size_t i = 0; i < len; i += BYTES_PER_LINE) {
printf(" %0*zx ", ADDR_WIDTH, i);
for (size_t j = 0; j < BYTES_PER_LINE; j++) {
i + j < len ? printf("%02X ", data[i + j]) : printf(" ");
if (j == 7)
printf(" ");
}
printf(" |");
for (size_t j = 0; j < BYTES_PER_LINE && i + j < len; j++) {
uint8_t c = data[i + j];
printf("%c", (c >= 32 && c < 127) ? c : '.');
}
printf("|\n");
}
}

145
hidpp20.c Normal file
View file

@ -0,0 +1,145 @@
/**
* Copyright (c) 2025 favewa
* SPDX-License-Identifier: BSD-3-Clause
*/
#include "hidpp20.h"
#include "hexdump.h"
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static inline bool hidpp_is_valid_report(uint8_t report_type)
{
return report_type == HIDPP_REPORT_SHORT ||
report_type == HIDPP_REPORT_LONG;
}
static inline size_t hidpp_get_report_size(uint8_t report_type)
{
return report_type == HIDPP_REPORT_SHORT ? HIDPP_SHORT_REPORT_SIZE
: HIDPP_LONG_REPORT_SIZE;
}
bool hidpp_match_logitech_mouse(ratazana_hid_device_t *info)
{
return info && info->vendor_id == HIDPP_VENDOR_LOGITECH &&
strstr(info->name, "Mouse") != nullptr;
}
ratazana_result_t hidpp_write(const ratazana_device_t *restrict dev,
const uint8_t *restrict data, size_t len)
{
if (!dev || !data || dev->fd < 0)
return RATAZANA_ERROR_INVALID_ARG;
hexdump("TX", data, len, HEXDUMP_DETAILED);
ssize_t written = write(dev->fd, data, len);
if (written < 0)
return perror("failed to write to device"), RATAZANA_ERROR_IO;
if ((size_t)written != len) {
fprintf(stderr,
"written data length mismatch. expected %zu, got %zd bytes\n",
len, written);
return RATAZANA_ERROR_IO;
}
return RATAZANA_OK;
}
ratazana_result_t hidpp_recv(const ratazana_device_t *restrict dev,
uint8_t *restrict data, size_t len, int timeout_ms)
{
if (!dev || dev->fd < 0)
return RATAZANA_ERROR_INVALID_ARG;
struct pollfd pfd = {
.fd = dev->fd,
.events = POLLIN,
};
int ret = poll(&pfd, 1, timeout_ms);
if (ret < 0)
return perror("polling failed"), RATAZANA_ERROR_IO;
if (ret == 0)
return RATAZANA_ERROR_TIMEOUT;
len = read(dev->fd, data, len);
if (len < 0)
return perror("read failed"), RATAZANA_ERROR_IO;
return hexdump("RX", data, len, HEXDUMP_DETAILED), RATAZANA_OK;
}
ratazana_result_t hidpp_communicate(ratazana_device_t *restrict dev,
const hidpp_payload_t *restrict payload,
hidpp_payload_t *restrict response)
{
if (!dev || !payload || !response) {
return RATAZANA_ERROR_INVALID_ARG;
}
const size_t len = hidpp_get_report_size(payload->packet.report_type);
ratazana_result_t ret = hidpp_write(dev, payload->raw, len);
if (ret != RATAZANA_OK)
return ret;
hidpp_payload_t buffer;
constexpr int MAX_RETRIES = 2;
for (int retry = 0; retry < MAX_RETRIES; retry++) {
ret = hidpp_recv(dev, buffer.raw, HIDPP_LONG_REPORT_SIZE, 1000);
if (ret == RATAZANA_ERROR_TIMEOUT) {
fprintf(stderr, "hid++ timeout (%d/%d)\n", retry + 1, MAX_RETRIES);
continue;
}
if (ret != RATAZANA_OK)
break;
if (!hidpp_is_valid_report(buffer.packet.report_type))
continue;
if (buffer.packet.function_id == HIDPP_FUNCTION_ERROR) {
fprintf(stderr, "hid++ error: ");
hexdump(nullptr, buffer.packet.params, 1, HEXDUMP_DETAILED);
return RATAZANA_ERROR_PROTOCOL;
}
if (buffer.packet.device_idx == payload->packet.device_idx &&
buffer.packet.feature_id == payload->packet.feature_id &&
buffer.packet.function_id == payload->packet.function_id) {
return *response = buffer, RATAZANA_OK;
}
};
return ret;
}
ratazana_result_t hidpp_detect_protocol(ratazana_device_t *restrict dev,
hidpp_version_t *restrict version)
{
if (!dev || !version)
return RATAZANA_ERROR_INVALID_ARG;
hidpp_payload_t payload = {.packet.report_type = HIDPP_REPORT_SHORT,
.packet.device_idx = dev->idx,
.packet.feature_id = HIDPP_FUNCTION_PING,
.packet.function_id = HIDPP_SOFTWARE_ID};
hidpp_payload_t response;
ratazana_result_t result = hidpp_communicate(dev, &payload, &response);
if (result == RATAZANA_OK) {
version->major = response.packet.params[0];
version->minor = response.packet.params[1];
printf("hid++ %u.%u protocol detected\n", version->major,
version->minor);
}
return result;
}

64
hidpp20.h Normal file
View file

@ -0,0 +1,64 @@
/**
* Copyright (c) 2025 favewa
* SPDX-License-Identifier: BSD-3-Clause
*/
#pragma once
#include "ratazana.h"
#include <stddef.h>
#include <stdint.h>
constexpr uint16_t HIDPP_VENDOR_LOGITECH = 0x046d;
constexpr uint8_t HIDPP_DEVICE_WIRED = 0xFF;
constexpr uint8_t HIDPP_DEVICE_WIRELESS_1 = 0x01;
constexpr uint8_t HIDPP_REPORT_SHORT = 0x10;
constexpr uint8_t HIDPP_REPORT_LONG = 0x11;
constexpr size_t HIDPP_SHORT_REPORT_SIZE = 7;
constexpr size_t HIDPP_LONG_REPORT_SIZE = 20;
constexpr uint8_t HIDPP_FUNCTION_PING = 0x00;
constexpr uint8_t HIDPP_FUNCTION_ERROR = 0x8F;
constexpr uint8_t HIDPP_SOFTWARE_ID = 0x10;
typedef struct __attribute__((packed)) {
uint8_t report_type;
uint8_t device_idx;
uint8_t feature_id;
uint8_t function_id;
uint8_t params[HIDPP_LONG_REPORT_SIZE - 4];
} hidpp_packet_t;
typedef union {
hidpp_packet_t packet;
uint8_t raw[HIDPP_LONG_REPORT_SIZE];
} hidpp_payload_t;
_Static_assert(sizeof(hidpp_packet_t) == HIDPP_LONG_REPORT_SIZE,
"hid++20 packet size mismatch");
typedef struct {
uint8_t major;
uint8_t minor;
} hidpp_version_t;
bool hidpp_match_logitech_mouse(ratazana_hid_device_t *info);
ratazana_result_t hidpp_detect_protocol(ratazana_device_t *restrict dev,
hidpp_version_t *restrict version);
ratazana_result_t hidpp_write(const ratazana_device_t *restrict dev,
const uint8_t *restrict data, size_t len);
ratazana_result_t hidpp_recv(const ratazana_device_t *restrict dev,
uint8_t *restrict data, size_t len,
int timeout_ms);
ratazana_result_t hidpp_communicate(ratazana_device_t *restrict dev,
const hidpp_payload_t *restrict request,
hidpp_payload_t *restrict response);

113
hidraw.c Normal file
View file

@ -0,0 +1,113 @@
/**
* Copyright (c) 2025 favewa
* SPDX-License-Identifier: BSD-3-Clause
*/
#include "ratazana.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stddef.h>
#include <linux/hidraw.h>
#include <sys/ioctl.h>
static constexpr int HID_OPEN_FLAGS = O_RDONLY | O_NONBLOCK;
static ratazana_result_t query_hidraw_info(int fd, const char *path,
ratazana_hid_device_t *info)
{
struct hidraw_devinfo devinfo = {0};
if (ioctl(fd, HIDIOCGRAWINFO, &devinfo) < 0)
return close(fd), RATAZANA_ERROR_IO;
char name[256] = {0};
if (ioctl(fd, HIDIOCGRAWNAME(sizeof(name)), name) < 0)
strncpy(name, "Unknown Device", sizeof(name) - 1);
*info = (ratazana_hid_device_t){
.vendor_id = devinfo.vendor,
.product_id = devinfo.product,
.bus_type = devinfo.bustype,
};
strncpy(info->path, path, sizeof(info->path) - 1);
strncpy(info->name, name, sizeof(info->name) - 1);
return close(fd), RATAZANA_OK;
}
ratazana_result_t ratazana_hid_enumerate(ratazana_hid_device_t *out,
ratazana_device_matcher_t match)
{
if (!out)
return RATAZANA_ERROR_INVALID_ARG;
DIR *dir = opendir("/dev");
if (dir == nullptr)
return RATAZANA_ERROR_IO;
struct dirent *entry;
ratazana_result_t result = RATAZANA_ERROR_NOT_FOUND;
while ((entry = readdir(dir)) != nullptr) {
if (strncmp(entry->d_name, "hidraw", 6) != 0)
continue;
char path[256];
snprintf(path, sizeof(path), "/dev/%s", entry->d_name);
int fd = open(path, HID_OPEN_FLAGS);
if (fd < 0)
continue;
ratazana_hid_device_t info = {0};
ratazana_result_t query_result = query_hidraw_info(fd, path, &info);
close(fd);
if (query_result)
continue;
if (match == nullptr || match(&info))
return (*out = info), closedir(dir), RATAZANA_OK;
}
return closedir(dir), result;
}
ratazana_result_t ratazana_device_open(ratazana_device_t *restrict dev,
const char *restrict path,
uint8_t device_index)
{
if (!dev || !path)
return RATAZANA_ERROR_INVALID_ARG;
dev->fd = open(path, O_RDWR);
if (dev->fd < 0) {
perror("Failed to open device");
if (errno == EACCES) {
fprintf(stderr, "Hint: You may need root privileges or udev rules "
"to access this device\n");
return RATAZANA_ERROR_ACCESS;
}
return RATAZANA_ERROR_IO;
}
strncpy(dev->path, path, sizeof(dev->path) - 1);
dev->idx = device_index;
printf("Device opened: %s [idx=0x%02X]\n", path, device_index);
return RATAZANA_OK;
}
void ratazana_device_close(ratazana_device_t *dev)
{
if (dev != nullptr && dev->fd >= 0) {
close(dev->fd);
dev->fd = -1;
}
}

46
main.c Normal file
View file

@ -0,0 +1,46 @@
/**
* Copyright (c) 2025 favewa
* SPDX-License-Identifier: BSD-3-Clause
*/
#include "hidpp20.h"
#include "ratazana.h"
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
ratazana_hid_device_t hid_device;
ratazana_result_t result;
result = ratazana_hid_enumerate(&hid_device, hidpp_match_logitech_mouse);
if (result != RATAZANA_OK) {
ratazana_fatal("failed to locate a logitech mouse: %s",
ratazana_strerror(result));
}
printf("Found device: %s [%04X:%04X]\n", hid_device.name,
hid_device.vendor_id, hid_device.product_id);
ratazana_device_t device = {.fd = -1};
result = ratazana_device_open(&device, hid_device.path, HIDPP_DEVICE_WIRED);
if (result != RATAZANA_OK)
ratazana_fatal("failed to open device: %s", ratazana_strerror(result));
hidpp_version_t version;
result = hidpp_detect_protocol(&device, &version);
if (result != RATAZANA_OK) {
ratazana_device_close(&device);
ratazana_fatal("Failed to detect HID++ protocol: %s",
ratazana_strerror(result));
}
printf("Successfully initialized HID++ %u.%u protocol\n", version.major,
version.minor);
ratazana_device_close(&device);
printf("Device closed successfully\n");
return EXIT_SUCCESS;
}

55
ratazana.h Normal file
View file

@ -0,0 +1,55 @@
/**
* Copyright (c) 2025 favewa
* SPDX-License-Identifier: BSD-3-Clause
*/
#pragma once
#include <stdbool.h>
#include <stdint.h>
typedef struct {
int fd;
uint8_t idx;
char path[256];
} ratazana_device_t;
typedef enum {
RATAZANA_OK = 0,
RATAZANA_ERROR_INVALID_ARG = -1,
RATAZANA_ERROR_IO = -2,
RATAZANA_ERROR_TIMEOUT = -3,
RATAZANA_ERROR_NOT_FOUND = -4,
RATAZANA_ERROR_PROTOCOL = -5,
RATAZANA_ERROR_ACCESS = -6,
} ratazana_result_t;
typedef enum {
HIDPP20, // mainly Logitech G-series gaming mice
} protocol_t;
typedef struct {
unsigned int vendor_id;
unsigned int product_id;
unsigned int bus_type;
char path[256]; // /dev/hidrawX path
char name[256]; // human-readable name
} ratazana_hid_device_t;
typedef bool (*ratazana_device_matcher_t)(ratazana_hid_device_t *info);
ratazana_result_t ratazana_hid_enumerate(ratazana_hid_device_t *out,
ratazana_device_matcher_t matcher);
ratazana_result_t ratazana_device_open(ratazana_device_t *restrict dev,
const char *restrict path,
uint8_t device_index);
void ratazana_device_close(ratazana_device_t *dev);
void ratazana_cleanup(ratazana_device_t *dev);
const char *ratazana_strerror(ratazana_result_t result);
[[gnu::format(printf, 1, 2)]]
_Noreturn void ratazana_fatal(const char *fmt, ...);

47
utils.c Normal file
View file

@ -0,0 +1,47 @@
/**
* Copyright (c) 2025 favewa
* SPDX-License-Identifier: BSD-3-Clause
*/
#include "ratazana.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
[[gnu::format(printf, 1, 2)]]
_Noreturn void ratazana_fatal(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
fprintf(stderr, "Fatal error: ");
vfprintf(stderr, fmt, args);
fprintf(stderr, "\n");
fflush(stderr);
va_end(args);
exit(EXIT_FAILURE);
}
const char *ratazana_strerror(ratazana_result_t result)
{
switch (result) {
case RATAZANA_OK:
return "Success";
case RATAZANA_ERROR_INVALID_ARG:
return "Invalid argument";
case RATAZANA_ERROR_IO:
return "I/O error";
case RATAZANA_ERROR_TIMEOUT:
return "Timeout";
case RATAZANA_ERROR_NOT_FOUND:
return "Device not found";
case RATAZANA_ERROR_PROTOCOL:
return "Protocol error";
case RATAZANA_ERROR_ACCESS:
return "Access denied";
default:
return "Unknown error";
}
}