mirror of
https://github.com/NohamR/RMHook.git
synced 2026-01-11 06:58:11 +00:00
First release
This commit is contained in:
51
src/core/tinyhook.h
Normal file
51
src/core/tinyhook.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#ifndef tinyhook_h
|
||||
#define tinyhook_h
|
||||
|
||||
#include <objc/objc-runtime.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define CLASS_METHOD 0
|
||||
#define INSTANCE_METHOD 1
|
||||
|
||||
/* inline hook */
|
||||
int tiny_hook(void *function, void *destination, void **origin);
|
||||
|
||||
int tiny_insert(void *address, void *destination, bool link);
|
||||
|
||||
int tiny_insert_far(void *address, void *destination, bool link);
|
||||
|
||||
/* objective-c runtime */
|
||||
int ocrt_hook(const char *cls, const char *sel, void *destination, void **origin);
|
||||
|
||||
int ocrt_swap(const char *cls1, const char *sel1, const char *cls2, const char *sel2);
|
||||
|
||||
void *ocrt_impl(const char *cls, const char *sel, bool type);
|
||||
|
||||
Method ocrt_method(const char *cls, const char *sel, bool type);
|
||||
|
||||
/* memory access */
|
||||
int read_mem(void *destination, const void *source, size_t len);
|
||||
|
||||
int write_mem(void *destination, const void *source, size_t len);
|
||||
|
||||
/* solve symbol */
|
||||
void *symtbl_solve(uint32_t image_index, const char *symbol_name);
|
||||
|
||||
void *symexp_solve(uint32_t image_index, const char *symbol_name);
|
||||
|
||||
/* find in memory */
|
||||
// int find_code(uint32_t image_index, const unsigned char *code, size_t len, int count, void **out);
|
||||
|
||||
int find_data(void *start, void *end, const unsigned char *data, size_t len, int count, void **out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
39
src/core/tinyhook/memory.c
Normal file
39
src/core/tinyhook/memory.c
Normal file
@@ -0,0 +1,39 @@
|
||||
#include <mach/mach_init.h> // mach_task_self()
|
||||
#include <mach/mach_vm.h> // mach_vm_*
|
||||
#include <string.h> // memcpy()
|
||||
|
||||
#ifndef COMPACT
|
||||
#include <mach/mach_error.h> // mach_error_string()
|
||||
#include <printf.h> // fprintf()
|
||||
#endif
|
||||
|
||||
#include "../include/tinyhook.h"
|
||||
|
||||
int read_mem(void *destination, const void *source, size_t len) {
|
||||
int kr = 0;
|
||||
vm_offset_t data;
|
||||
mach_msg_type_number_t dataCnt;
|
||||
kr |= mach_vm_read(mach_task_self(), (mach_vm_address_t)source, len, &data, &dataCnt);
|
||||
memcpy((unsigned char *)destination, (unsigned char *)data, dataCnt);
|
||||
kr |= mach_vm_deallocate(mach_task_self(), data, dataCnt);
|
||||
#ifndef COMPACT
|
||||
if (kr != 0) {
|
||||
fprintf(stderr, "read_mem: %s\n", mach_error_string(kr));
|
||||
}
|
||||
#endif
|
||||
return kr;
|
||||
}
|
||||
|
||||
int write_mem(void *destination, const void *source, size_t len) {
|
||||
int kr = 0;
|
||||
kr |= mach_vm_protect(mach_task_self(), (mach_vm_address_t)destination, len, FALSE,
|
||||
VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY);
|
||||
kr |= mach_vm_write(mach_task_self(), (mach_vm_address_t)destination, (vm_offset_t)source, len);
|
||||
kr |= mach_vm_protect(mach_task_self(), (mach_vm_address_t)destination, len, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
|
||||
#ifndef COMPACT
|
||||
if (kr != 0) {
|
||||
fprintf(stderr, "write_mem: %s\n", mach_error_string(kr));
|
||||
}
|
||||
#endif
|
||||
return kr;
|
||||
}
|
||||
66
src/core/tinyhook/objcrt.c
Normal file
66
src/core/tinyhook/objcrt.c
Normal file
@@ -0,0 +1,66 @@
|
||||
#include <objc/runtime.h> // objc_*, ...
|
||||
|
||||
#ifndef COMPACT
|
||||
#include <mach/mach_error.h> // mach_error_string()
|
||||
#include <printf.h> // fprintf()
|
||||
#endif
|
||||
|
||||
#include "../include/tinyhook.h"
|
||||
|
||||
Method ocrt_method(const char *cls, const char *sel, bool type) {
|
||||
Method oc_method = NULL;
|
||||
Class oc_class = objc_getClass(cls);
|
||||
SEL oc_selector = sel_registerName(sel);
|
||||
if (type == CLASS_METHOD) {
|
||||
oc_method = class_getClassMethod(oc_class, oc_selector);
|
||||
} else if (type == INSTANCE_METHOD) {
|
||||
oc_method = class_getInstanceMethod(oc_class, oc_selector);
|
||||
}
|
||||
#ifndef COMPACT
|
||||
else {
|
||||
fprintf(stderr, "ocrt_method: invalid method type: %d\n", type);
|
||||
}
|
||||
#endif
|
||||
return oc_method;
|
||||
}
|
||||
|
||||
void *ocrt_impl(const char *cls, const char *sel, bool type) {
|
||||
return method_getImplementation(ocrt_method(cls, sel, type));
|
||||
}
|
||||
|
||||
static Method ensure_method(const char *cls, const char *sel);
|
||||
|
||||
int ocrt_swap(const char *cls1, const char *sel1, const char *cls2, const char *sel2) {
|
||||
Method oc_method1 = ensure_method(cls1, sel1);
|
||||
Method oc_method2 = ensure_method(cls2, sel2);
|
||||
if (oc_method1 == NULL || oc_method2 == NULL) {
|
||||
return 1;
|
||||
}
|
||||
method_exchangeImplementations(oc_method1, oc_method2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ocrt_hook(const char *cls, const char *sel, void *destination, void **origin) {
|
||||
Method oc_method = ensure_method(cls, sel);
|
||||
if (oc_method == NULL) {
|
||||
return 1;
|
||||
}
|
||||
void *origin_imp = method_setImplementation(oc_method, destination);
|
||||
if (origin != NULL) {
|
||||
*origin = origin_imp;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Method ensure_method(const char *cls, const char *sel) {
|
||||
Method oc_method = ocrt_method(cls, sel, CLASS_METHOD);
|
||||
if (oc_method == NULL) {
|
||||
oc_method = ocrt_method(cls, sel, INSTANCE_METHOD);
|
||||
}
|
||||
#ifndef COMPACT
|
||||
if (oc_method == NULL) {
|
||||
fprintf(stderr, "ensure_method: method not found!\n");
|
||||
}
|
||||
#endif
|
||||
return oc_method;
|
||||
}
|
||||
23
src/core/tinyhook/search.c
Normal file
23
src/core/tinyhook/search.c
Normal file
@@ -0,0 +1,23 @@
|
||||
#ifndef COMPACT
|
||||
#include <printf.h> // fprintf()
|
||||
#endif
|
||||
|
||||
#include "skip/skip.h"
|
||||
|
||||
#include "../include/tinyhook.h"
|
||||
|
||||
int find_data(void *start, void *end, const unsigned char *data, size_t len, int count, void **out) {
|
||||
int matched;
|
||||
skipidx_t idx;
|
||||
skip_init(&idx, len, data);
|
||||
matched = skip_match(&idx, start, end, count, (offset_t *)out);
|
||||
skip_release(&idx);
|
||||
#ifndef COMPACT
|
||||
if (matched == 0) {
|
||||
fprintf(stderr, "find_data: data not found!\n");
|
||||
}
|
||||
#endif
|
||||
return matched;
|
||||
}
|
||||
|
||||
// int find_code(uint32_t image_index, const unsigned char *code, size_t len, int count, void **out);
|
||||
121
src/core/tinyhook/symsolve/symexport.c
Normal file
121
src/core/tinyhook/symsolve/symexport.c
Normal file
@@ -0,0 +1,121 @@
|
||||
#include <mach-o/dyld.h> // _dyld_*
|
||||
#include <mach-o/loader.h> // mach_header_64, load_command...
|
||||
#include <mach-o/nlist.h> // nlist_64
|
||||
#include <string.h> // strcmp()
|
||||
|
||||
#ifndef COMPACT
|
||||
#include <printf.h> // fprintf()
|
||||
#endif
|
||||
|
||||
#include "../../include/tinyhook.h"
|
||||
|
||||
static void *trie_query(const uint8_t *export, const char *name);
|
||||
|
||||
void *symexp_solve(uint32_t image_index, const char *symbol_name) {
|
||||
void *symbol_address = NULL;
|
||||
intptr_t image_slide = _dyld_get_image_vmaddr_slide(image_index);
|
||||
struct mach_header_64 *mh_header = (struct mach_header_64 *)_dyld_get_image_header(image_index);
|
||||
struct load_command *ld_command = (void *)mh_header + sizeof(struct mach_header_64);
|
||||
#ifndef COMPACT
|
||||
if (mh_header == NULL) {
|
||||
fprintf(stderr, "symexp_solve: image_index out of range!\n");
|
||||
}
|
||||
#endif
|
||||
struct dyld_info_command *dyldinfo_cmd = NULL;
|
||||
struct segment_command_64 *linkedit_cmd = NULL;
|
||||
for (int i = 0; i < mh_header->ncmds; i++) {
|
||||
if (ld_command->cmd == LC_SEGMENT_64) {
|
||||
const struct segment_command_64 *segment = (struct segment_command_64 *)ld_command;
|
||||
if (strcmp(segment->segname, "__LINKEDIT") == 0) {
|
||||
linkedit_cmd = (struct segment_command_64 *)ld_command;
|
||||
}
|
||||
} else if (ld_command->cmd == LC_DYLD_INFO_ONLY || ld_command->cmd == LC_DYLD_INFO) {
|
||||
dyldinfo_cmd = (struct dyld_info_command *)ld_command;
|
||||
if (linkedit_cmd != NULL) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ld_command = (void *)ld_command + ld_command->cmdsize;
|
||||
}
|
||||
if (dyldinfo_cmd == NULL) {
|
||||
#ifndef COMPACT
|
||||
fprintf(stderr, "symexp_solve: LC_DYLD_INFO_ONLY segment not found!\n");
|
||||
#endif
|
||||
return NULL;
|
||||
}
|
||||
// stroff and strtbl are in the __LINKEDIT segment
|
||||
// Its offset will change when loaded into the memory, so we need to add this slide
|
||||
intptr_t linkedit_slide = linkedit_cmd->vmaddr - linkedit_cmd->fileoff;
|
||||
uint8_t *export_offset = (uint8_t *)image_slide + linkedit_slide + dyldinfo_cmd->export_off;
|
||||
symbol_address = trie_query(export_offset, symbol_name);
|
||||
|
||||
if (symbol_address != NULL) {
|
||||
symbol_address += image_slide;
|
||||
}
|
||||
#ifndef COMPACT
|
||||
else {
|
||||
fprintf(stderr, "symexp_solve: symbol not found!\n");
|
||||
}
|
||||
#endif
|
||||
return symbol_address;
|
||||
}
|
||||
|
||||
inline uint64_t read_uleb128(const uint8_t **p) {
|
||||
int bit = 0;
|
||||
uint64_t result = 0;
|
||||
do {
|
||||
uint64_t slice = **p & 0x7f;
|
||||
result |= (slice << bit);
|
||||
bit += 7;
|
||||
} while (*(*p)++ & 0x80);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void *trie_query(const uint8_t *export, const char *name) {
|
||||
// most comments below are copied from <mach-o/loader.h>, not AI generated :P
|
||||
// a trie node starts with a uleb128 stored the lenth of the exported symbol information
|
||||
uint64_t node_off = 0;
|
||||
const char *rest_name = name;
|
||||
void *symbol_address = NULL;
|
||||
bool go_child = true;
|
||||
while (go_child) {
|
||||
const uint8_t *cur_pos = export + node_off;
|
||||
uint64_t info_len = read_uleb128(&cur_pos);
|
||||
// the exported symbol information is followed by the child edges
|
||||
const uint8_t *child_off = cur_pos + info_len;
|
||||
|
||||
if (rest_name[0] == '\0') {
|
||||
if (info_len != 0) {
|
||||
// first is a uleb128 containing flags
|
||||
uint64_t flag = read_uleb128(&cur_pos);
|
||||
if (flag == EXPORT_SYMBOL_FLAGS_KIND_REGULAR) {
|
||||
// normally, it is followed by a uleb128 encoded function offset
|
||||
uint64_t symbol_off = read_uleb128(&cur_pos);
|
||||
symbol_address = (void *)symbol_off;
|
||||
}
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
go_child = false;
|
||||
cur_pos = child_off;
|
||||
// child edges start with a byte of how many edges (0-255) this node has
|
||||
uint8_t child_count = *(uint8_t *)cur_pos++;
|
||||
// then followed by each edge.
|
||||
for (int i = 0; i < child_count; i++) {
|
||||
// each edge is a zero terminated UTF8 of the addition chars
|
||||
char *cur_str = (char *)cur_pos;
|
||||
size_t cur_len = strlen(cur_str);
|
||||
cur_pos += cur_len + 1;
|
||||
// then followed by a uleb128 offset for the node that edge points to
|
||||
uint64_t next_off = read_uleb128(&cur_pos);
|
||||
if (strncmp(rest_name, cur_str, cur_len) == 0) {
|
||||
go_child = true;
|
||||
rest_name += cur_len;
|
||||
node_off = next_off;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return symbol_address;
|
||||
}
|
||||
60
src/core/tinyhook/symsolve/symtable.c
Normal file
60
src/core/tinyhook/symsolve/symtable.c
Normal file
@@ -0,0 +1,60 @@
|
||||
#include <mach-o/dyld.h> // _dyld_*
|
||||
#include <mach-o/loader.h> // mach_header_64, load_command...
|
||||
#include <mach-o/nlist.h> // nlist_64
|
||||
#include <string.h> // strcmp()
|
||||
|
||||
#ifndef COMPACT
|
||||
#include <printf.h> // fprintf()
|
||||
#endif
|
||||
|
||||
#include "../../include/tinyhook.h"
|
||||
|
||||
void *symtbl_solve(uint32_t image_index, const char *symbol_name) {
|
||||
void *symbol_address = NULL;
|
||||
intptr_t image_slide = _dyld_get_image_vmaddr_slide(image_index);
|
||||
struct mach_header_64 *mh_header = (struct mach_header_64 *)_dyld_get_image_header(image_index);
|
||||
struct load_command *ld_command = (void *)mh_header + sizeof(struct mach_header_64);
|
||||
#ifndef COMPACT
|
||||
if (mh_header == NULL) {
|
||||
fprintf(stderr, "symtbl_solve: image_index out of range!\n");
|
||||
}
|
||||
#endif
|
||||
struct symtab_command *symtab_cmd = NULL;
|
||||
struct segment_command_64 *linkedit_cmd = NULL;
|
||||
for (int i = 0; i < mh_header->ncmds; i++) {
|
||||
if (ld_command->cmd == LC_SEGMENT_64) {
|
||||
const struct segment_command_64 *segment = (struct segment_command_64 *)ld_command;
|
||||
if (strcmp(segment->segname, "__LINKEDIT") == 0) {
|
||||
linkedit_cmd = (struct segment_command_64 *)ld_command;
|
||||
}
|
||||
} else if (ld_command->cmd == LC_SYMTAB) {
|
||||
symtab_cmd = (struct symtab_command *)ld_command;
|
||||
if (linkedit_cmd != NULL) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ld_command = (void *)ld_command + ld_command->cmdsize;
|
||||
}
|
||||
// stroff and strtbl are in the __LINKEDIT segment
|
||||
// Its offset will change when loaded into the memory, so we need to add this slide
|
||||
intptr_t linkedit_slide = linkedit_cmd->vmaddr - linkedit_cmd->fileoff;
|
||||
struct nlist_64 *nl_tbl = (void *)image_slide + linkedit_slide + symtab_cmd->symoff;
|
||||
char *str_tbl = (void *)image_slide + linkedit_slide + symtab_cmd->stroff;
|
||||
for (int j = 0; j < symtab_cmd->nsyms; j++) {
|
||||
if ((nl_tbl[j].n_type & N_TYPE) == N_SECT) {
|
||||
if (strcmp(symbol_name, str_tbl + nl_tbl[j].n_un.n_strx) == 0) {
|
||||
symbol_address = (void *)nl_tbl[j].n_value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (symbol_address != NULL) {
|
||||
symbol_address += image_slide;
|
||||
}
|
||||
#ifndef COMPACT
|
||||
else {
|
||||
fprintf(stderr, "symtbl_solve: symbol not found!\n");
|
||||
}
|
||||
#endif
|
||||
return symbol_address;
|
||||
}
|
||||
183
src/core/tinyhook/tinyhook.c
Normal file
183
src/core/tinyhook/tinyhook.c
Normal file
@@ -0,0 +1,183 @@
|
||||
#include <mach/mach_init.h> // mach_task_self()
|
||||
#include <mach/mach_vm.h> // mach_vm_*
|
||||
#include <stdlib.h> // atexit()
|
||||
#include <string.h> // memcpy()
|
||||
|
||||
#ifndef COMPACT
|
||||
#include <mach/mach_error.h> // mach_error_string()
|
||||
#include <printf.h> // fprintf()
|
||||
#endif
|
||||
|
||||
#ifdef __x86_64__
|
||||
#include "fde64/fde64.h"
|
||||
#endif
|
||||
|
||||
#include "../include/tinyhook.h"
|
||||
|
||||
#define MB (1ll << 20)
|
||||
#define GB (1ll << 30)
|
||||
|
||||
#ifdef __aarch64__
|
||||
#define AARCH64_B 0x14000000 // b +0
|
||||
#define AARCH64_BL 0x94000000 // bl +0
|
||||
#define AARCH64_ADRP 0x90000011 // adrp x17, 0
|
||||
#define AARCH64_BR 0xd61f0220 // br x17
|
||||
#define AARCH64_BLR 0xd63f0220 // blr x17
|
||||
#define AARCH64_ADD 0x91000231 // add x17, x17, 0
|
||||
#define AARCH64_SUB 0xd1000231 // sub x17, x17, 0
|
||||
|
||||
#define MAX_JUMP_SIZE 12
|
||||
|
||||
#elif __x86_64__
|
||||
#define X86_64_CALL 0xe8 // call
|
||||
#define X86_64_JMP 0xe9 // jmp
|
||||
#define X86_64_JMP_RIP 0x000025ff // jmp [rip]
|
||||
#define X86_64_CALL_RIP 0x000015ff // call [rip]
|
||||
#define X86_64_MOV_RI64 0xb848 // mov r64, m64
|
||||
#define X86_64_MOV_RM64 0x8b48 // mov r64, [r64]
|
||||
|
||||
#define MAX_JUMP_SIZE 14
|
||||
#endif
|
||||
|
||||
int tiny_insert(void *address, void *destination, bool link) {
|
||||
size_t jump_size;
|
||||
int assembly;
|
||||
unsigned char bytes[MAX_JUMP_SIZE];
|
||||
#ifdef __aarch64__
|
||||
// b/bl imm ; go to destination
|
||||
jump_size = 4;
|
||||
assembly = (destination - address) >> 2 & 0x3ffffff;
|
||||
assembly |= link ? AARCH64_BL : AARCH64_B;
|
||||
*(int *)bytes = assembly;
|
||||
#elif __x86_64__
|
||||
// jmp/call imm ; go to destination
|
||||
jump_size = 5;
|
||||
*bytes = link ? X86_64_CALL : X86_64_JMP;
|
||||
assembly = (long)destination - (long)address - 5;
|
||||
*(int *)(bytes + 1) = assembly;
|
||||
#endif
|
||||
write_mem(address, bytes, jump_size);
|
||||
return jump_size;
|
||||
}
|
||||
|
||||
int tiny_insert_far(void *address, void *destination, bool link) {
|
||||
size_t jump_size;
|
||||
unsigned char bytes[MAX_JUMP_SIZE];
|
||||
#ifdef __aarch64__
|
||||
// adrp x17, imm
|
||||
// add x17, x17, imm ; x17 -> destination
|
||||
// br/blr x17
|
||||
jump_size = 12;
|
||||
int assembly;
|
||||
assembly = (((long)destination >> 12) - ((long)address >> 12)) & 0x1fffff;
|
||||
assembly = ((assembly & 0x3) << 29) | (assembly >> 2 << 5) | AARCH64_ADRP;
|
||||
*(int *)bytes = assembly;
|
||||
assembly = ((long)destination & 0xfff) << 10 | AARCH64_ADD;
|
||||
*(int *)(bytes + 4) = assembly;
|
||||
*(int *)(bytes + 8) = link ? AARCH64_BLR : AARCH64_BR;
|
||||
#elif __x86_64__
|
||||
jump_size = 14;
|
||||
// jmp [rip] ; rip stored destination
|
||||
*(int *)bytes = link ? X86_64_CALL_RIP : X86_64_JMP_RIP;
|
||||
bytes[5] = bytes[6] = 0;
|
||||
*(long long *)(bytes + 6) = (long long)destination;
|
||||
#endif
|
||||
write_mem(address, bytes, jump_size);
|
||||
return jump_size;
|
||||
}
|
||||
|
||||
int position = 0;
|
||||
mach_vm_address_t vm;
|
||||
|
||||
static int get_jump_size(void *address, void *destination);
|
||||
static int insert_jump(void *address, void *destination);
|
||||
static int save_header(void *address, void *destination, int *skip_len);
|
||||
|
||||
int tiny_hook(void *function, void *destination, void **origin) {
|
||||
int kr = 0;
|
||||
if (origin == NULL)
|
||||
insert_jump(function, destination);
|
||||
else {
|
||||
if (!position) {
|
||||
// alloc a vm to store headers and jumps
|
||||
kr = mach_vm_allocate(mach_task_self(), &vm, PAGE_SIZE, VM_FLAGS_ANYWHERE);
|
||||
#ifndef COMPACT
|
||||
if (kr != 0) {
|
||||
fprintf(stderr, "mach_vm_allocate: %s\n", mach_error_string(kr));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
int skip_len;
|
||||
*origin = (void *)(vm + position);
|
||||
position += save_header(function, (void *)(vm + position), &skip_len);
|
||||
position += insert_jump((void *)(vm + position), function + skip_len);
|
||||
insert_jump(function, destination);
|
||||
}
|
||||
return kr;
|
||||
}
|
||||
|
||||
static int get_jump_size(void *address, void *destination) {
|
||||
long long distance = destination > address ? destination - address : address - destination;
|
||||
#ifdef __aarch64__
|
||||
return distance < 128 * MB ? 4 : 12;
|
||||
#elif __x86_64__
|
||||
return distance < 2 * GB ? 5 : 14;
|
||||
#endif
|
||||
}
|
||||
|
||||
static int insert_jump(void *address, void *destination) {
|
||||
if (get_jump_size(address, destination) <= 5)
|
||||
return tiny_insert(address, destination, false);
|
||||
else
|
||||
return tiny_insert_far(address, destination, false);
|
||||
}
|
||||
|
||||
static int save_header(void *address, void *destination, int *skip_len) {
|
||||
int header_len = 0;
|
||||
#ifdef __aarch64__
|
||||
header_len = *skip_len = get_jump_size(address, destination);
|
||||
unsigned char bytes_out[MAX_JUMP_SIZE];
|
||||
read_mem(bytes_out, address, MAX_JUMP_SIZE);
|
||||
for (int i = 0; i < header_len; i += 4) {
|
||||
int cur_asm = *(int *)(bytes_out + i);
|
||||
long cur_addr = (long)address + i, cur_dst = (long)destination + i;
|
||||
if (((cur_asm ^ 0x90000000) & 0x9f000000) == 0) {
|
||||
// adrp
|
||||
// modify the immediate
|
||||
int len = (cur_asm >> 29 & 0x3) | ((cur_asm >> 3) & 0x1ffffc);
|
||||
len += (cur_addr >> 12) - (cur_dst >> 12);
|
||||
cur_asm &= 0x9f00001f;
|
||||
cur_asm = ((len & 0x3) << 29) | (len >> 2 << 5) | cur_asm;
|
||||
*(int *)(bytes_out + i) = cur_asm;
|
||||
}
|
||||
}
|
||||
#elif __x86_64__
|
||||
int min_len;
|
||||
struct fde64s assembly;
|
||||
unsigned char bytes_in[MAX_JUMP_SIZE * 2], bytes_out[MAX_JUMP_SIZE * 4];
|
||||
read_mem(bytes_in, address, MAX_JUMP_SIZE * 2);
|
||||
min_len = get_jump_size(address, destination);
|
||||
for (*skip_len = 0; *skip_len < min_len; *skip_len += assembly.len) {
|
||||
long long cur_addr = (long long)address + *skip_len;
|
||||
decode(bytes_in + *skip_len, &assembly);
|
||||
if (assembly.opcode == 0x8B && assembly.modrm_rm == 0b101) {
|
||||
// mov r64, [rip+]
|
||||
// split it into 2 instructions
|
||||
// mov r64 $rip+(immediate)
|
||||
// mov r64 [r64]
|
||||
*(short *)(bytes_out + header_len) = X86_64_MOV_RI64;
|
||||
bytes_out[header_len + 1] += assembly.modrm_reg;
|
||||
*(long long *)(bytes_out + header_len + 2) = assembly.disp32 + cur_addr + assembly.len;
|
||||
header_len += 10;
|
||||
*(short *)(bytes_out + header_len) = X86_64_MOV_RM64;
|
||||
bytes_out[header_len + 2] = assembly.modrm_reg << 3 | assembly.modrm_reg;
|
||||
header_len += 3;
|
||||
} else {
|
||||
memcpy(bytes_out + header_len, bytes_in + *skip_len, assembly.len);
|
||||
header_len += assembly.len;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
write_mem(destination, bytes_out, header_len);
|
||||
return header_len;
|
||||
}
|
||||
5
src/reMarkable/reMarkable.h
Normal file
5
src/reMarkable/reMarkable.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface reMarkable : NSObject
|
||||
|
||||
@end
|
||||
390
src/reMarkable/reMarkable.m
Normal file
390
src/reMarkable/reMarkable.m
Normal file
@@ -0,0 +1,390 @@
|
||||
#import "reMarkable.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "Constant.h"
|
||||
#import "MemoryUtils.h"
|
||||
#import "Logger.h"
|
||||
#import "ResourceUtils.h"
|
||||
#import <objc/runtime.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#include <stdint.h>
|
||||
#include <limits.h>
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#include <QtNetwork/QNetworkAccessManager>
|
||||
#include <QtNetwork/QNetworkRequest>
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/QIODevice>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/Qt>
|
||||
#include <QtWebSockets/QWebSocket>
|
||||
#include <QtCore/QSettings>
|
||||
#include <QtCore/QVariant>
|
||||
#include <QtCore/QAnyStringView>
|
||||
|
||||
static NSString *const kReMarkableConfigFileName = @"rmfakecloud.config";
|
||||
static NSString *const kReMarkableConfigHostKey = @"host";
|
||||
static NSString *const kReMarkableConfigPortKey = @"port";
|
||||
static NSString *const kReMarkableDefaultHost = @"example.com";
|
||||
static NSNumber *const kReMarkableDefaultPort = @(443);
|
||||
|
||||
static NSString *gConfiguredHost = @"example.com";
|
||||
static NSNumber *gConfiguredPort = @(443);
|
||||
static pthread_mutex_t gResourceMutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static NSString *ReMarkablePreferencesDirectory(void);
|
||||
|
||||
static NSString *ReMarkablePreferencesDirectory(void) {
|
||||
NSArray<NSString *> *libraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
|
||||
NSString *libraryDir = [libraryPaths firstObject];
|
||||
if (![libraryDir length]) {
|
||||
libraryDir = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"];
|
||||
}
|
||||
return [libraryDir stringByAppendingPathComponent:@"Preferences"];
|
||||
}
|
||||
|
||||
static NSString *ReMarkableConfigFilePath(void) {
|
||||
return [ReMarkablePreferencesDirectory() stringByAppendingPathComponent:kReMarkableConfigFileName];
|
||||
}
|
||||
|
||||
static BOOL ReMarkableWriteConfig(NSString *path, NSDictionary<NSString *, id> *config) {
|
||||
NSError *error = nil;
|
||||
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:config options:NSJSONWritingPrettyPrinted error:&error];
|
||||
if (!jsonData || error) {
|
||||
NSLogger(@"[reMarkable] Failed to serialize config: %@", error);
|
||||
return NO;
|
||||
}
|
||||
if (![jsonData writeToFile:path atomically:YES]) {
|
||||
NSLogger(@"[reMarkable] Failed to write config file at %@", path);
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
static void ReMarkableLoadOrCreateConfig(void) {
|
||||
NSString *configPath = ReMarkableConfigFilePath();
|
||||
NSString *directory = [configPath stringByDeletingLastPathComponent];
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
BOOL isDirectory = NO;
|
||||
NSError *error = nil;
|
||||
|
||||
if (![fileManager fileExistsAtPath:directory isDirectory:&isDirectory] || !isDirectory) {
|
||||
if (![fileManager createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error]) {
|
||||
NSLogger(@"[reMarkable] Failed to create config directory %@: %@", directory, error);
|
||||
}
|
||||
}
|
||||
|
||||
NSDictionary<NSString *, id> *defaults = @{kReMarkableConfigHostKey : kReMarkableDefaultHost,
|
||||
kReMarkableConfigPortKey : kReMarkableDefaultPort};
|
||||
|
||||
if ([fileManager fileExistsAtPath:configPath isDirectory:&isDirectory] && !isDirectory) {
|
||||
NSData *data = [NSData dataWithContentsOfFile:configPath];
|
||||
if ([data length] > 0) {
|
||||
NSError *jsonError = nil;
|
||||
id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
|
||||
if (!jsonError && [jsonObject isKindOfClass:[NSDictionary class]]) {
|
||||
NSDictionary *configDict = (NSDictionary *)jsonObject;
|
||||
NSString *hostValue = configDict[kReMarkableConfigHostKey];
|
||||
NSNumber *portValue = configDict[kReMarkableConfigPortKey];
|
||||
|
||||
NSString *resolvedHost = ([hostValue isKindOfClass:[NSString class]] && [hostValue length]) ? hostValue : kReMarkableDefaultHost;
|
||||
NSInteger portCandidate = kReMarkableDefaultPort.integerValue;
|
||||
if ([portValue respondsToSelector:@selector(integerValue)]) {
|
||||
NSInteger candidate = [portValue integerValue];
|
||||
if (candidate > 0 && candidate <= 65535) {
|
||||
portCandidate = candidate;
|
||||
} else {
|
||||
NSLogger(@"[reMarkable] Ignoring invalid port value %@, falling back to default.", portValue);
|
||||
}
|
||||
}
|
||||
|
||||
gConfiguredHost = [resolvedHost copy];
|
||||
gConfiguredPort = @(portCandidate);
|
||||
NSLogger(@"[reMarkable] Loaded config from %@ with host %@ and port %@", configPath, gConfiguredHost, gConfiguredPort);
|
||||
return;
|
||||
} else {
|
||||
NSLogger(@"[reMarkable] Failed to parse config file %@: %@", configPath, jsonError);
|
||||
}
|
||||
} else {
|
||||
NSLogger(@"[reMarkable] Config file %@ was empty, rewriting with defaults.", configPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (ReMarkableWriteConfig(configPath, defaults)) {
|
||||
NSLogger(@"[reMarkable] Created default config at %@", configPath);
|
||||
}
|
||||
gConfiguredHost = [kReMarkableDefaultHost copy];
|
||||
gConfiguredPort = kReMarkableDefaultPort;
|
||||
}
|
||||
|
||||
static inline QString QStringFromNSStringSafe(NSString *string) {
|
||||
if (!string) {
|
||||
return QString();
|
||||
}
|
||||
return QString::fromUtf8([string UTF8String]);
|
||||
}
|
||||
|
||||
@interface MenuActionController : NSObject
|
||||
@property (strong, nonatomic) NSURL *targetURL;
|
||||
- (void)openURLAction:(id)sender;
|
||||
+ (void)addCustomHelpMenuEntry:(NSString *)title withURL:(NSString *)url;
|
||||
+ (void)addCustomHelpMenuEntry:(NSString *)title withURL:(NSString *)url withDelay:(NSTimeInterval)delay;
|
||||
@end
|
||||
|
||||
@implementation MenuActionController
|
||||
|
||||
- (void)openURLAction:(id)sender {
|
||||
if (self.targetURL) {
|
||||
[[NSWorkspace sharedWorkspace] openURL:self.targetURL];
|
||||
NSLogger(@"[+] URL opened successfully: %@", self.targetURL);
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)addCustomHelpMenuEntry:(NSString *)title withURL:(NSString *)url {
|
||||
[self addCustomHelpMenuEntry:title withURL:url withDelay:1.0];
|
||||
}
|
||||
|
||||
+ (void)addCustomHelpMenuEntry:(NSString *)title withURL:(NSString *)url withDelay:(NSTimeInterval)delay {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
MenuActionController *controller = [[MenuActionController alloc] init];
|
||||
controller.targetURL = [NSURL URLWithString:url];
|
||||
|
||||
NSMenu *mainMenu = [NSApp mainMenu];
|
||||
if (!mainMenu) {
|
||||
NSLogger(@"[-] Main menu not found");
|
||||
return;
|
||||
}
|
||||
|
||||
NSMenuItem *helpMenuItem = nil;
|
||||
for (NSMenuItem *item in [mainMenu itemArray]) {
|
||||
if ([[item title] isEqualToString:@"Help"]) {
|
||||
helpMenuItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!helpMenuItem) {
|
||||
NSLogger(@"[-] Help menu item not found");
|
||||
return;
|
||||
}
|
||||
|
||||
NSMenu *helpMenu = [helpMenuItem submenu];
|
||||
if (!helpMenu) {
|
||||
NSLogger(@"[-] Help submenu not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if ([helpMenu numberOfItems] > 0) {
|
||||
[helpMenu addItem:[NSMenuItem separatorItem]];
|
||||
}
|
||||
|
||||
NSMenuItem *customMenuItem = [[NSMenuItem alloc] initWithTitle:title
|
||||
action:@selector(openURLAction:)
|
||||
keyEquivalent:@""];
|
||||
[customMenuItem setTarget:controller];
|
||||
[helpMenu addItem:customMenuItem];
|
||||
|
||||
objc_setAssociatedObject(helpMenu,
|
||||
[title UTF8String],
|
||||
controller,
|
||||
OBJC_ASSOCIATION_RETAIN);
|
||||
|
||||
NSLogger(@"[+] Custom menu item '%@' added successfully", title);
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface reMarkableDylib : NSObject
|
||||
|
||||
- (BOOL)hook;
|
||||
|
||||
@end
|
||||
|
||||
@implementation reMarkable
|
||||
|
||||
+ (void)load {
|
||||
NSLogger(@"reMarkable dylib loaded successfully");
|
||||
|
||||
// Initialize the hook
|
||||
reMarkableDylib *dylib = [[reMarkableDylib alloc] init];
|
||||
[dylib hook];
|
||||
|
||||
// Add custom Help menu entry to open config file
|
||||
NSString *configPath = ReMarkableConfigFilePath();
|
||||
NSString *fileURL = [NSString stringWithFormat:@"file://%@", configPath];
|
||||
[MenuActionController addCustomHelpMenuEntry:@"Open rmfakecloud config"
|
||||
withURL:fileURL
|
||||
withDelay:2.0];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation reMarkableDylib
|
||||
|
||||
static QNetworkReply *(*original_qNetworkAccessManager_createRequest)(
|
||||
QNetworkAccessManager *self,
|
||||
QNetworkAccessManager::Operation op,
|
||||
const QNetworkRequest &request,
|
||||
QIODevice *outgoingData) = NULL;
|
||||
|
||||
static void (*original_qWebSocket_open)(
|
||||
QWebSocket *self,
|
||||
const QNetworkRequest &request) = NULL;
|
||||
|
||||
static int (*original_qRegisterResourceData)(
|
||||
int,
|
||||
const unsigned char *,
|
||||
const unsigned char *,
|
||||
const unsigned char *) = NULL;
|
||||
|
||||
static inline bool shouldPatchURL(const QString &host) {
|
||||
if (host.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return QString(R"""(
|
||||
hwr-production-dot-remarkable-production.appspot.com
|
||||
service-manager-production-dot-remarkable-production.appspot.com
|
||||
local.appspot.com
|
||||
my.remarkable.com
|
||||
ping.remarkable.com
|
||||
internal.cloud.remarkable.com
|
||||
eu.tectonic.remarkable.com
|
||||
backtrace-proxy.cloud.remarkable.engineering
|
||||
dev.ping.remarkable.com
|
||||
dev.tectonic.remarkable.com
|
||||
dev.internal.cloud.remarkable.com
|
||||
eu.internal.tctn.cloud.remarkable.com
|
||||
webapp-prod.cloud.remarkable.engineering
|
||||
)""")
|
||||
.contains(host, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
- (BOOL)hook {
|
||||
NSLogger(@"[reMarkable] Starting hooks...");
|
||||
|
||||
ReMarkableLoadOrCreateConfig();
|
||||
NSLogger(@"[reMarkable] Using override host %@ and port %@", gConfiguredHost, gConfiguredPort);
|
||||
|
||||
[MemoryUtils hookSymbol:@"QtNetwork"
|
||||
symbolName:@"__ZN21QNetworkAccessManager13createRequestENS_9OperationERK15QNetworkRequestP9QIODevice"
|
||||
hookFunction:(void *)hooked_qNetworkAccessManager_createRequest
|
||||
originalFunction:(void **)&original_qNetworkAccessManager_createRequest
|
||||
logPrefix:@"[reMarkable]"];
|
||||
|
||||
[MemoryUtils hookSymbol:@"QtWebSockets"
|
||||
symbolName:@"__ZN10QWebSocket4openERK15QNetworkRequest"
|
||||
hookFunction:(void *)hooked_qWebSocket_open
|
||||
originalFunction:(void **)&original_qWebSocket_open
|
||||
logPrefix:@"[reMarkable]"];
|
||||
|
||||
// WIP: Implement resource data registration hooking
|
||||
// [MemoryUtils hookSymbol:@"QtCore"
|
||||
// symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_"
|
||||
// hookFunction:(void *)hooked_qRegisterResourceData
|
||||
// originalFunction:(void **)&original_qRegisterResourceData
|
||||
// logPrefix:@"[reMarkable]"];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
extern "C" QNetworkReply* hooked_qNetworkAccessManager_createRequest(
|
||||
QNetworkAccessManager* self,
|
||||
QNetworkAccessManager::Operation op,
|
||||
const QNetworkRequest& req,
|
||||
QIODevice* outgoingData
|
||||
) {
|
||||
const QString host = req.url().host();
|
||||
if (shouldPatchURL(host)) {
|
||||
// Clone request to keep original immutable
|
||||
QNetworkRequest newReq(req);
|
||||
QUrl newUrl = req.url();
|
||||
const QString overrideHost = QStringFromNSStringSafe(gConfiguredHost);
|
||||
newUrl.setHost(overrideHost);
|
||||
newUrl.setPort([gConfiguredPort intValue]);
|
||||
newReq.setUrl(newUrl);
|
||||
|
||||
if (original_qNetworkAccessManager_createRequest) {
|
||||
return original_qNetworkAccessManager_createRequest(self, op, newReq, outgoingData);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (original_qNetworkAccessManager_createRequest) {
|
||||
return original_qNetworkAccessManager_createRequest(self, op, req, outgoingData);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
extern "C" void hooked_qWebSocket_open(
|
||||
QWebSocket* self,
|
||||
const QNetworkRequest& req
|
||||
) {
|
||||
if (!original_qWebSocket_open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString host = req.url().host();
|
||||
if (shouldPatchURL(host)) {
|
||||
QUrl newUrl = req.url();
|
||||
const QString overrideHost = QStringFromNSStringSafe(gConfiguredHost);
|
||||
newUrl.setHost(overrideHost);
|
||||
newUrl.setPort([gConfiguredPort intValue]);
|
||||
|
||||
QNetworkRequest newReq(req);
|
||||
newReq.setUrl(newUrl);
|
||||
|
||||
original_qWebSocket_open(self, newReq);
|
||||
return;
|
||||
}
|
||||
|
||||
original_qWebSocket_open(self, req);
|
||||
}
|
||||
|
||||
extern "C" int hooked_qRegisterResourceData(
|
||||
int version,
|
||||
const unsigned char *tree,
|
||||
const unsigned char *name,
|
||||
const unsigned char *data
|
||||
) {
|
||||
if (!original_qRegisterResourceData) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&gResourceMutex);
|
||||
|
||||
struct ResourceRoot resource = {
|
||||
.data = (uint8_t *)data,
|
||||
.name = (uint8_t *)name,
|
||||
.tree = (uint8_t *)tree,
|
||||
.treeSize = 0,
|
||||
.dataSize = 0,
|
||||
.originalDataSize = 0,
|
||||
.nameSize = 0,
|
||||
.entriesAffected = 0,
|
||||
};
|
||||
|
||||
statArchive(&resource, 0);
|
||||
processNode(&resource, 0, "");
|
||||
resource.tree = (uint8_t *)malloc(resource.treeSize);
|
||||
if (resource.tree) {
|
||||
memcpy(resource.tree, tree, resource.treeSize);
|
||||
}
|
||||
|
||||
NSLogger(@"[reMarkable] Registering Qt resource version %d tree:%p (size:%zu) name:%p (size:%zu) data:%p (size:%zu)",
|
||||
version, tree, resource.treeSize, name, resource.nameSize, data, resource.dataSize);
|
||||
|
||||
int status = original_qRegisterResourceData(version, tree, name, data);
|
||||
pthread_mutex_unlock(&gResourceMutex);
|
||||
if (resource.tree) {
|
||||
free(resource.tree);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
@end
|
||||
7
src/utils/Constant.h
Normal file
7
src/utils/Constant.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface Constant : NSObject
|
||||
|
||||
+ (NSString *)getCurrentAppPath;
|
||||
|
||||
@end
|
||||
25
src/utils/Constant.m
Normal file
25
src/utils/Constant.m
Normal file
@@ -0,0 +1,25 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "Constant.h"
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import "Logger.h"
|
||||
|
||||
@implementation Constant
|
||||
|
||||
static NSString *_currentAppPath;
|
||||
|
||||
+ (void)initialize {
|
||||
if (self == [Constant class]) {
|
||||
NSLogger(@"[Constant] Initializing...");
|
||||
|
||||
NSBundle *app = [NSBundle mainBundle];
|
||||
_currentAppPath = [[app bundlePath] copy];
|
||||
|
||||
NSLogger(@"[Constant] App path: %@", _currentAppPath);
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSString *)getCurrentAppPath {
|
||||
return _currentAppPath;
|
||||
}
|
||||
|
||||
@end
|
||||
6
src/utils/Logger.h
Normal file
6
src/utils/Logger.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#ifndef Logger_h
|
||||
#define Logger_h
|
||||
|
||||
#define NSLogger(fmt, ...) NSLog((fmt), ##__VA_ARGS__)
|
||||
|
||||
#endif /* Logger_h */
|
||||
33
src/utils/MemoryUtils.h
Normal file
33
src/utils/MemoryUtils.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface MemoryUtils : NSObject
|
||||
|
||||
/**
|
||||
* Hooks a function by symbol name with automatic fallback from symtbl_solve to symexp_solve.
|
||||
*
|
||||
* @param imageName The name of the image/library to search in (e.g., "QtNetwork").
|
||||
* @param symbolName The mangled symbol name to hook.
|
||||
* @param hookFunction The function to replace the original with.
|
||||
* @param originalFunction Pointer to store the original function address.
|
||||
* @param logPrefix Prefix for log messages (optional, can be nil).
|
||||
* @return YES if the hook was successfully installed, NO otherwise.
|
||||
*/
|
||||
+ (BOOL)hookSymbol:(NSString *)imageName
|
||||
symbolName:(NSString *)symbolName
|
||||
hookFunction:(void *)hookFunction
|
||||
originalFunction:(void **)originalFunction
|
||||
logPrefix:(NSString *)logPrefix;
|
||||
|
||||
/**
|
||||
* Hooks a function by symbol name with delay support.
|
||||
*
|
||||
* @param delayInSeconds The delay in seconds before installing the hook (use 0 for immediate hooking).
|
||||
*/
|
||||
+ (BOOL)hookSymbol:(NSString *)imageName
|
||||
symbolName:(NSString *)symbolName
|
||||
hookFunction:(void *)hookFunction
|
||||
originalFunction:(void **)originalFunction
|
||||
logPrefix:(NSString *)logPrefix
|
||||
delayInSeconds:(NSTimeInterval)delayInSeconds;
|
||||
|
||||
@end
|
||||
106
src/utils/MemoryUtils.m
Normal file
106
src/utils/MemoryUtils.m
Normal file
@@ -0,0 +1,106 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "MemoryUtils.h"
|
||||
#import <mach-o/dyld.h>
|
||||
#import "Logger.h"
|
||||
#import "tinyhook.h"
|
||||
|
||||
@implementation MemoryUtils
|
||||
|
||||
+ (int)indexForImageWithName:(NSString *)imageName {
|
||||
uint32_t imageCount = _dyld_image_count();
|
||||
for (uint32_t i = 0; i < imageCount; i++) {
|
||||
const char* currentImageName = _dyld_get_image_name(i);
|
||||
NSString *currentImageNameString = [NSString stringWithUTF8String:currentImageName];
|
||||
|
||||
if ([currentImageNameString.lastPathComponent isEqualToString:imageName]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
+ (BOOL)hookSymbol:(NSString *)imageName
|
||||
symbolName:(NSString *)symbolName
|
||||
hookFunction:(void *)hookFunction
|
||||
originalFunction:(void **)originalFunction
|
||||
logPrefix:(NSString *)logPrefix {
|
||||
return [self hookSymbol:imageName
|
||||
symbolName:symbolName
|
||||
hookFunction:hookFunction
|
||||
originalFunction:originalFunction
|
||||
logPrefix:logPrefix
|
||||
delayInSeconds:0];
|
||||
}
|
||||
|
||||
+ (BOOL)hookSymbol:(NSString *)imageName
|
||||
symbolName:(NSString *)symbolName
|
||||
hookFunction:(void *)hookFunction
|
||||
originalFunction:(void **)originalFunction
|
||||
logPrefix:(NSString *)logPrefix
|
||||
delayInSeconds:(NSTimeInterval)delayInSeconds {
|
||||
|
||||
if (delayInSeconds > 0) {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self performHookSymbol:imageName
|
||||
symbolName:symbolName
|
||||
hookFunction:hookFunction
|
||||
originalFunction:originalFunction
|
||||
logPrefix:logPrefix];
|
||||
});
|
||||
return YES;
|
||||
} else {
|
||||
return [self performHookSymbol:imageName
|
||||
symbolName:symbolName
|
||||
hookFunction:hookFunction
|
||||
originalFunction:originalFunction
|
||||
logPrefix:logPrefix];
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)performHookSymbol:(NSString *)imageName
|
||||
symbolName:(NSString *)symbolName
|
||||
hookFunction:(void *)hookFunction
|
||||
originalFunction:(void **)originalFunction
|
||||
logPrefix:(NSString *)logPrefix {
|
||||
|
||||
NSLogger(@"%@ Starting hook installation for %@", logPrefix, symbolName);
|
||||
|
||||
int imageIndex = [self indexForImageWithName:imageName];
|
||||
if (imageIndex < 0) {
|
||||
NSLogger(@"%@ ERROR: Image %@ not found", logPrefix, imageName);
|
||||
return NO;
|
||||
}
|
||||
|
||||
void* symbolAddress = NULL;
|
||||
|
||||
// Try to find the symbol address using symtbl_solve first
|
||||
symbolAddress = symtbl_solve(imageIndex, [symbolName UTF8String]);
|
||||
|
||||
if (symbolAddress) {
|
||||
NSLogger(@"%@ %@ found with symtbl_solve at address: %p", logPrefix, symbolName, symbolAddress);
|
||||
} else {
|
||||
NSLogger(@"%@ %@ not found with symtbl_solve, trying symexp_solve...", logPrefix, symbolName);
|
||||
symbolAddress = symexp_solve(imageIndex, [symbolName UTF8String]);
|
||||
|
||||
if (symbolAddress) {
|
||||
NSLogger(@"%@ %@ found with symexp_solve at address: %p", logPrefix, symbolName, symbolAddress);
|
||||
} else {
|
||||
NSLogger(@"%@ ERROR: Unable to find symbol %@", logPrefix, symbolName);
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
// Install the hook using tiny_hook and get the original function trampoline if requested
|
||||
int hookResult = tiny_hook(symbolAddress, hookFunction, originalFunction);
|
||||
|
||||
if (hookResult == 0) {
|
||||
NSLogger(@"%@ Hook successfully installed for %@", logPrefix, symbolName);
|
||||
return YES;
|
||||
} else {
|
||||
NSLogger(@"%@ ERROR: Failed to install hook for %@ (code: %d)", logPrefix, symbolName, hookResult);
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
42
src/utils/ResourceUtils.h
Normal file
42
src/utils/ResourceUtils.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#ifndef ResourceUtils_h
|
||||
#define ResourceUtils_h
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct ResourceRoot {
|
||||
uint8_t *data;
|
||||
uint8_t *name;
|
||||
uint8_t *tree;
|
||||
size_t treeSize;
|
||||
size_t dataSize;
|
||||
size_t originalDataSize;
|
||||
size_t nameSize;
|
||||
int entriesAffected;
|
||||
};
|
||||
|
||||
#define TREE_ENTRY_SIZE 22
|
||||
#define DIRECTORY 0x02
|
||||
|
||||
// Read/Write utilities
|
||||
uint32_t readUInt32(uint8_t *addr, int offset);
|
||||
uint16_t readUInt16(uint8_t *addr, int offset);
|
||||
void writeUint32(uint8_t *addr, int offset, uint32_t value);
|
||||
void writeUint16(uint8_t *addr, int offset, uint16_t value);
|
||||
|
||||
// Resource tree utilities
|
||||
int findOffset(int node);
|
||||
void nameOfChild(struct ResourceRoot *root, int node, int *size, char *buffer, int max);
|
||||
void statArchive(struct ResourceRoot *root, int node);
|
||||
void processNode(struct ResourceRoot *root, int node, const char *rootName);
|
||||
void ReMarkableDumpResourceFile(struct ResourceRoot *root, int node, const char *rootName, const char *fileName, uint16_t flags);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ResourceUtils_h */
|
||||
381
src/utils/ResourceUtils.m
Normal file
381
src/utils/ResourceUtils.m
Normal file
@@ -0,0 +1,381 @@
|
||||
#import "ResourceUtils.h"
|
||||
#import "Logger.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <string.h>
|
||||
#import <dispatch/dispatch.h>
|
||||
#import <zstd.h>
|
||||
#import <zlib.h>
|
||||
|
||||
static NSString *ReMarkableDumpRootDirectory(void);
|
||||
static NSString *ReMarkablePreferencesDirectory(void);
|
||||
|
||||
static NSString *ReMarkablePreferencesDirectory(void) {
|
||||
NSArray<NSString *> *libraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
|
||||
NSString *libraryDir = [libraryPaths firstObject];
|
||||
if (![libraryDir length]) {
|
||||
libraryDir = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"];
|
||||
}
|
||||
return [libraryDir stringByAppendingPathComponent:@"Preferences"];
|
||||
}
|
||||
|
||||
static NSString *ReMarkableDumpRootDirectory(void) {
|
||||
static NSString *dumpDirectory = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSString *preferencesDir = ReMarkablePreferencesDirectory();
|
||||
NSString *candidate = [preferencesDir stringByAppendingPathComponent:@"dump"];
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSError *error = nil;
|
||||
if (![fileManager fileExistsAtPath:candidate]) {
|
||||
if (![fileManager createDirectoryAtPath:candidate withIntermediateDirectories:YES attributes:nil error:&error]) {
|
||||
NSLogger(@"[reMarkable] Failed to create dump directory %@: %@", candidate, error);
|
||||
}
|
||||
}
|
||||
dumpDirectory = [candidate copy];
|
||||
});
|
||||
return dumpDirectory;
|
||||
}
|
||||
|
||||
uint32_t readUInt32(uint8_t *addr, int offset) {
|
||||
return (uint32_t)(addr[offset + 0] << 24) |
|
||||
(uint32_t)(addr[offset + 1] << 16) |
|
||||
(uint32_t)(addr[offset + 2] << 8) |
|
||||
(uint32_t)(addr[offset + 3] << 0);
|
||||
}
|
||||
|
||||
void writeUint32(uint8_t *addr, int offset, uint32_t value) {
|
||||
addr[offset + 0] = (uint8_t)(value >> 24);
|
||||
addr[offset + 1] = (uint8_t)(value >> 16);
|
||||
addr[offset + 2] = (uint8_t)(value >> 8);
|
||||
addr[offset + 3] = (uint8_t)(value >> 0);
|
||||
}
|
||||
|
||||
void writeUint16(uint8_t *addr, int offset, uint16_t value) {
|
||||
addr[offset + 0] = (uint8_t)(value >> 8);
|
||||
addr[offset + 1] = (uint8_t)(value >> 0);
|
||||
}
|
||||
|
||||
uint16_t readUInt16(uint8_t *addr, int offset) {
|
||||
return (uint16_t)((addr[offset + 0] << 8) |
|
||||
(addr[offset + 1] << 0));
|
||||
}
|
||||
|
||||
int findOffset(int node) {
|
||||
return node * TREE_ENTRY_SIZE;
|
||||
}
|
||||
|
||||
void statArchive(struct ResourceRoot *root, int node) {
|
||||
int offset = findOffset(node);
|
||||
int thisMaxLength = offset + TREE_ENTRY_SIZE;
|
||||
if (thisMaxLength > (int)root->treeSize) root->treeSize = (size_t)thisMaxLength;
|
||||
uint32_t nameOffset = readUInt32(root->tree, offset);
|
||||
uint32_t thisMaxNameLength = nameOffset + readUInt16(root->name, (int)nameOffset);
|
||||
if (thisMaxNameLength > root->nameSize) root->nameSize = thisMaxNameLength;
|
||||
int flags = readUInt16(root->tree, offset + 4);
|
||||
if (!(flags & DIRECTORY)) {
|
||||
uint32_t dataOffset = readUInt32(root->tree, offset + 4 + 2 + 4);
|
||||
uint32_t dataSize = readUInt32(root->data, (int)dataOffset);
|
||||
uint32_t thisMaxDataLength = dataOffset + dataSize + 4;
|
||||
if (thisMaxDataLength > root->dataSize) root->dataSize = thisMaxDataLength;
|
||||
} else {
|
||||
uint32_t childCount = readUInt32(root->tree, offset + 4 + 2);
|
||||
offset += 4 + 4 + 2;
|
||||
uint32_t childOffset = readUInt32(root->tree, offset);
|
||||
for (int child = (int)childOffset; child < (int)(childOffset + childCount); child++){
|
||||
statArchive(root, child);
|
||||
}
|
||||
}
|
||||
root->originalDataSize = root->dataSize;
|
||||
}
|
||||
|
||||
void nameOfChild(struct ResourceRoot *root, int node, int *size, char *buffer, int max) {
|
||||
if (!buffer || max <= 0) {
|
||||
if (size) {
|
||||
*size = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!root || !root->tree || !root->name) {
|
||||
if (size) {
|
||||
*size = 0;
|
||||
}
|
||||
buffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
if (size) {
|
||||
*size = 0;
|
||||
}
|
||||
buffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
const int offset = findOffset(node);
|
||||
uint32_t nameOffset = readUInt32(root->tree, offset);
|
||||
uint16_t nameLength = readUInt16(root->name, (int)nameOffset);
|
||||
|
||||
if (nameLength > (uint16_t)(max - 1)) {
|
||||
nameLength = (uint16_t)(max - 1);
|
||||
}
|
||||
|
||||
nameOffset += 2; // skip length prefix
|
||||
nameOffset += 4; // skip hash
|
||||
|
||||
if (size) {
|
||||
*size = (int)nameLength;
|
||||
}
|
||||
|
||||
for (int i = 1; i < (int)nameLength * 2; i += 2) {
|
||||
buffer[i / 2] = ((const char *)root->name)[nameOffset + i];
|
||||
}
|
||||
buffer[nameLength] = '\0';
|
||||
}
|
||||
|
||||
void ReMarkableDumpResourceFile(struct ResourceRoot *root, int node, const char *rootName, const char *fileName, uint16_t flags) {
|
||||
if (!root || !root->tree || !root->data || !fileName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int baseOffset = findOffset(node);
|
||||
const uint32_t dataOffset = readUInt32(root->tree, baseOffset + 4 + 2 + 4);
|
||||
const uint32_t dataSize = readUInt32(root->data, (int)dataOffset);
|
||||
if (dataSize == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t payloadStart = dataOffset + 4;
|
||||
if (root->dataSize && (payloadStart + dataSize) > root->dataSize) {
|
||||
NSLogger(@"[reMarkable] Skipping dump for node %d due to size mismatch (%u bytes beyond bounds)", (int)node, dataSize);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t *payload = root->data + payloadStart;
|
||||
uint8_t *ownedBuffer = NULL;
|
||||
size_t bytesToWrite = dataSize;
|
||||
|
||||
if (flags == 4) {
|
||||
size_t expectedSize = ZSTD_getFrameContentSize(payload, dataSize);
|
||||
if (expectedSize == ZSTD_CONTENTSIZE_ERROR) {
|
||||
NSLogger(@"[reMarkable] ZSTD frame content size error for node %d", (int)node);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t bufferSize;
|
||||
if (expectedSize == ZSTD_CONTENTSIZE_UNKNOWN) {
|
||||
if ((size_t)dataSize > SIZE_MAX / 4) {
|
||||
bufferSize = (size_t)dataSize;
|
||||
} else {
|
||||
bufferSize = (size_t)dataSize * 4;
|
||||
}
|
||||
} else {
|
||||
bufferSize = expectedSize;
|
||||
}
|
||||
if (bufferSize < (size_t)dataSize) {
|
||||
bufferSize = (size_t)dataSize;
|
||||
}
|
||||
|
||||
if (bufferSize > SIZE_MAX - 1) {
|
||||
NSLogger(@"[reMarkable] ZSTD decompression size too large for node %d", (int)node);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < 6; ++attempt) {
|
||||
ownedBuffer = (uint8_t *)malloc(bufferSize + 1);
|
||||
if (!ownedBuffer) {
|
||||
NSLogger(@"[reMarkable] Failed to allocate %zu bytes for ZSTD decompression", bufferSize + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t decompressedSize = ZSTD_decompress(ownedBuffer, bufferSize, payload, dataSize);
|
||||
if (!ZSTD_isError(decompressedSize)) {
|
||||
bytesToWrite = decompressedSize;
|
||||
ownedBuffer[bytesToWrite] = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
ZSTD_ErrorCode errorCode = ZSTD_getErrorCode(decompressedSize);
|
||||
free(ownedBuffer);
|
||||
ownedBuffer = NULL;
|
||||
|
||||
if (errorCode == ZSTD_error_dstSize_tooSmall) {
|
||||
if (bufferSize > SIZE_MAX / 2) {
|
||||
NSLogger(@"[reMarkable] ZSTD decompression buffer would overflow for node %d", (int)node);
|
||||
return;
|
||||
}
|
||||
bufferSize *= 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
NSLogger(@"[reMarkable] ZSTD decompression failed for node %d: %s", (int)node, ZSTD_getErrorName(decompressedSize));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ownedBuffer) {
|
||||
NSLogger(@"[reMarkable] ZSTD decompression exhausted retries for node %d", (int)node);
|
||||
return;
|
||||
}
|
||||
} else if (flags == 0) {
|
||||
if ((size_t)dataSize > SIZE_MAX - 1) {
|
||||
NSLogger(@"[reMarkable] Raw resource size too large for node %d", (int)node);
|
||||
return;
|
||||
}
|
||||
ownedBuffer = (uint8_t *)malloc((size_t)dataSize + 1);
|
||||
if (!ownedBuffer) {
|
||||
NSLogger(@"[reMarkable] Failed to allocate %u bytes for raw copy", (unsigned)(dataSize + 1u));
|
||||
return;
|
||||
}
|
||||
memcpy(ownedBuffer, payload, dataSize);
|
||||
ownedBuffer[dataSize] = 0;
|
||||
bytesToWrite = dataSize;
|
||||
|
||||
} else if (flags == 1) {
|
||||
if (dataSize <= 4) {
|
||||
NSLogger(@"[reMarkable] Zlib compressed resource too small for node %d", (int)node);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t expectedSize =
|
||||
((uint32_t)payload[0] << 24) |
|
||||
((uint32_t)payload[1] << 16) |
|
||||
((uint32_t)payload[2] << 8) |
|
||||
((uint32_t)payload[3] << 0);
|
||||
|
||||
if (!expectedSize) {
|
||||
NSLogger(@"[reMarkable] Zlib resource reported zero size for node %d", (int)node);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t *compressedPayload = payload + 4;
|
||||
const size_t compressedSize = (size_t)dataSize - 4;
|
||||
if (compressedSize > UINT_MAX) {
|
||||
NSLogger(@"[reMarkable] Zlib compressed payload too large for node %d", (int)node);
|
||||
return;
|
||||
}
|
||||
|
||||
z_stream stream;
|
||||
memset(&stream, 0, sizeof(stream));
|
||||
stream.next_in = (Bytef *)compressedPayload;
|
||||
stream.avail_in = (uInt)compressedSize;
|
||||
|
||||
int status = inflateInit(&stream);
|
||||
if (status != Z_OK) {
|
||||
NSLogger(@"[reMarkable] Failed to initialize zlib for node %d: %d", (int)node, status);
|
||||
return;
|
||||
}
|
||||
|
||||
ownedBuffer = (uint8_t *)malloc((size_t)expectedSize + 1);
|
||||
if (!ownedBuffer) {
|
||||
NSLogger(@"[reMarkable] Failed to allocate %u bytes for zlib decompression", (unsigned)expectedSize + 1u);
|
||||
inflateEnd(&stream);
|
||||
return;
|
||||
}
|
||||
|
||||
stream.next_out = ownedBuffer;
|
||||
stream.avail_out = (uInt)expectedSize;
|
||||
|
||||
status = inflate(&stream, Z_FINISH);
|
||||
if (status != Z_STREAM_END) {
|
||||
NSLogger(@"[reMarkable] Zlib decompression failed for node %d with status %d", (int)node, status);
|
||||
free(ownedBuffer);
|
||||
ownedBuffer = NULL;
|
||||
inflateEnd(&stream);
|
||||
return;
|
||||
}
|
||||
|
||||
bytesToWrite = (size_t)stream.total_out;
|
||||
inflateEnd(&stream);
|
||||
ownedBuffer[bytesToWrite] = 0;
|
||||
} else {
|
||||
NSLogger(@"[reMarkable] Unknown compression flag %u for node %d; skipping dump", flags, (int)node);
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *dumpRoot = ReMarkableDumpRootDirectory();
|
||||
if (![dumpRoot length]) {
|
||||
if (ownedBuffer) {
|
||||
free(ownedBuffer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *rootComponent = [NSString stringWithUTF8String:rootName ? rootName : ""];
|
||||
NSString *fileComponent = [NSString stringWithUTF8String:fileName];
|
||||
if (!rootComponent) {
|
||||
rootComponent = @"";
|
||||
}
|
||||
if (!fileComponent) {
|
||||
fileComponent = @"";
|
||||
}
|
||||
|
||||
NSString *relativePath = [rootComponent stringByAppendingString:fileComponent];
|
||||
if ([relativePath hasPrefix:@"/"]) {
|
||||
relativePath = [relativePath substringFromIndex:1];
|
||||
}
|
||||
if (![relativePath length]) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *fullPath = [dumpRoot stringByAppendingPathComponent:relativePath];
|
||||
NSString *directoryPath = [fullPath stringByDeletingLastPathComponent];
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSError *directoryError = nil;
|
||||
if ([directoryPath length] && ![fileManager createDirectoryAtPath:directoryPath withIntermediateDirectories:YES attributes:nil error:&directoryError]) {
|
||||
NSLogger(@"[reMarkable] Failed to create directory for dump %@: %@", directoryPath, directoryError);
|
||||
if (ownedBuffer) {
|
||||
free(ownedBuffer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const void *dataSource = ownedBuffer ? (const void *)ownedBuffer : (const void *)payload;
|
||||
NSData *dataObject = [NSData dataWithBytes:dataSource length:bytesToWrite];
|
||||
NSError *writeError = nil;
|
||||
if (![dataObject writeToFile:fullPath options:NSDataWritingAtomic error:&writeError]) {
|
||||
NSLogger(@"[reMarkable] Failed to write dump file %@: %@", fullPath, writeError);
|
||||
} else {
|
||||
NSLogger(@"[reMarkable] Dumped resource to %@ (%zu bytes)", fullPath, bytesToWrite);
|
||||
}
|
||||
|
||||
if (ownedBuffer) {
|
||||
free(ownedBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
void processNode(struct ResourceRoot *root, int node, const char *rootName) {
|
||||
int offset = findOffset(node) + 4;
|
||||
uint16_t flags = readUInt16(root->tree, offset);
|
||||
offset += 2;
|
||||
int stringLength = 0;
|
||||
char nameBuffer[256];
|
||||
nameOfChild(root, node, &stringLength, nameBuffer, (int)sizeof(nameBuffer));
|
||||
|
||||
if (flags & DIRECTORY) {
|
||||
uint32_t childCount = readUInt32(root->tree, offset);
|
||||
offset += 4;
|
||||
uint32_t childOffset = readUInt32(root->tree, offset);
|
||||
const size_t rootLength = rootName ? strlen(rootName) : 0;
|
||||
char *tempRoot = (char *)malloc(rootLength + (size_t)stringLength + 2);
|
||||
if (!tempRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rootLength > 0) {
|
||||
memcpy(tempRoot, rootName, rootLength);
|
||||
}
|
||||
memcpy(tempRoot + rootLength, nameBuffer, (size_t)stringLength);
|
||||
tempRoot[rootLength + stringLength] = '/';
|
||||
tempRoot[rootLength + stringLength + 1] = '\0';
|
||||
|
||||
for (uint32_t child = childOffset; child < childOffset + childCount; ++child) {
|
||||
processNode(root, (int)child, tempRoot);
|
||||
}
|
||||
|
||||
free(tempRoot);
|
||||
} else {
|
||||
NSLogger(@"[reMarkable] Processing node %d: %s%s", (int)node, rootName ? rootName : "", nameBuffer);
|
||||
uint16_t fileFlags = readUInt16(root->tree, offset - 2);
|
||||
ReMarkableDumpResourceFile(root, node, rootName ? rootName : "", nameBuffer, fileFlags);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user