Skip to content

Commit 3b46cf7

Browse files
committed
Accept and patch hex values of any length
1 parent 57708be commit 3b46cf7

3 files changed

Lines changed: 151 additions & 35 deletions

File tree

.vs/winpatch.vcxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@
147147
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
148148
<ExceptionHandling>false</ExceptionHandling>
149149
<CompileAs>CompileAsC</CompileAs>
150+
<UndefinePreprocessorDefinitions>NDEBUG</UndefinePreprocessorDefinitions>
150151
</ClCompile>
151152
<Link>
152153
<SubSystem>Console</SubSystem>
@@ -161,6 +162,7 @@
161162
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
162163
<ExceptionHandling>false</ExceptionHandling>
163164
<CompileAs>CompileAsC</CompileAs>
165+
<UndefinePreprocessorDefinitions>NDEBUG</UndefinePreprocessorDefinitions>
164166
</ClCompile>
165167
<Link>
166168
<SubSystem>Console</SubSystem>
@@ -175,6 +177,7 @@
175177
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
176178
<ExceptionHandling>false</ExceptionHandling>
177179
<CompileAs>CompileAsC</CompileAs>
180+
<UndefinePreprocessorDefinitions>NDEBUG</UndefinePreprocessorDefinitions>
178181
</ClCompile>
179182
<Link>
180183
<SubSystem>Console</SubSystem>
@@ -190,6 +193,7 @@
190193
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
191194
<ExceptionHandling>false</ExceptionHandling>
192195
<CompileAs>CompileAsC</CompileAs>
196+
<UndefinePreprocessorDefinitions>NDEBUG</UndefinePreprocessorDefinitions>
193197
</ClCompile>
194198
<Link>
195199
<SubSystem>Console</SubSystem>
@@ -207,6 +211,7 @@
207211
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
208212
<ExceptionHandling>false</ExceptionHandling>
209213
<CompileAs>CompileAsC</CompileAs>
214+
<UndefinePreprocessorDefinitions>NDEBUG</UndefinePreprocessorDefinitions>
210215
</ClCompile>
211216
<Link>
212217
<SubSystem>Console</SubSystem>
@@ -224,6 +229,7 @@
224229
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
225230
<ExceptionHandling>false</ExceptionHandling>
226231
<CompileAs>CompileAsC</CompileAs>
232+
<UndefinePreprocessorDefinitions>NDEBUG</UndefinePreprocessorDefinitions>
227233
</ClCompile>
228234
<Link>
229235
<SubSystem>Console</SubSystem>

README.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,28 @@ Usage
2929
-----
3030

3131
```
32-
winpatch target original_qword patched_qword [original_qword patched_qword [...]]
32+
winpatch target <SEARCH> <REPLACE> [<SEARCH> <REPLACE> [...]]
3333
```
3434

3535
Where:
36-
* `target` is the path of the system file you want to patch
37-
* `original_qword` is a **64-bit hex value** that matches the original data you want to patch
38-
* `patched_qword` is a **64-bit hex value** with the data you want to replace the orignal with.
36+
* `target` is the path of the system file you want to patch.
37+
* `<SEARCH>` is a hex string that matches the original data you want to patch.
38+
* `<REPLACE>` is a hex string containing the data you want to replace it with.
3939

40-
Note that the qwords are big-endian, which means the hex values should appear in the same byte
41-
order as the one you see from a hex-dump of the file.
40+
The hex strings can be of any length between 2 and 128 bytes, as long as the `<SEARCH>`
41+
and `<REPLACE>` strings that make one pair are of the same length.
4242

43-
No specific alignment is required on the qwords (meaning that `winpatch` will match and patch
44-
qwords that start on a odd byte address for instance).
43+
Subsequent pairs are not required to have the same length as the previous one, meaning
44+
that you can issue something like `winpatch driver.sys ABCD 9090 12345678 00000000`.
4545

46-
The exit code of winpatch is the number of qwords that were successfully patched (`0` if none
47-
were) or a negative value on error.
46+
The hex values should be provided in the same byte order as the one you see from a
47+
hex-dump of the file, i.e. big-endian.
48+
49+
No specific alignment is required on the data, meaning that `winpatch` will match and
50+
patch data that start on a odd byte address for instance.
51+
52+
The exit code of winpatch is the number of qwords that were successfully patched (`0`
53+
if none) or a negative value on error.
4854

4955
Example
5056
-------

src/winpatch.c

Lines changed: 129 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include <accctrl.h>
3333
#include <aclapi.h>
3434
#include <imagehlp.h>
35+
#include <assert.h>
3536

3637
#include "msapi_utf8.h"
3738

@@ -41,6 +42,8 @@
4142

4243
#define _STRINGIFY(x) #x
4344
#define STRINGIFY(x) _STRINGIFY(x)
45+
#define MIN_CHUNK_SIZE 2
46+
#define MAX_CHUNK_SIZE 128
4447

4548
#define safe_free(p) do {free((void*)p); p = NULL;} while(0)
4649
#define safe_min(a, b) min((size_t)(a), (size_t)(b))
@@ -466,25 +469,77 @@ static BOOL UpdateChecksum(const char* filename, DWORD* dwCheckSum)
466469
return bRet;
467470
}
468471

472+
typedef struct {
473+
size_t size;
474+
uint8_t* org;
475+
uint8_t* new;
476+
bool patched;
477+
} chunk;
478+
479+
static void FreeChunkList(chunk* list, size_t size)
480+
{
481+
size_t i;
482+
for (i = 0; i < size; i++) {
483+
free(list[i].org);
484+
free(list[i].new);
485+
}
486+
free(list);
487+
}
488+
489+
static uint8_t* HexStringToBin(const char* str)
490+
{
491+
size_t i, len = safe_strlen(str);
492+
uint8_t val = 0, * ret = NULL;
493+
char c;
494+
495+
if ((len < 2) || (len % 2))
496+
return NULL;
497+
ret = malloc(len / 2);
498+
if (ret == NULL)
499+
return NULL;
500+
501+
for (i = 0; i < len; i++) {
502+
val <<= 4;
503+
c = (char)tolower(str[i]);
504+
if (c < '0' || (c > '9' && c < 'a') || c > 'f') {
505+
fprintf(stderr, "Invalid hex character '%c' in string '%s'\n", c, str);
506+
return NULL;
507+
}
508+
val |= ((c - '0') < 0xa) ? (c - '0') : (c - 'a' + 0xa);
509+
if (i % 2)
510+
ret[i / 2] = val;
511+
}
512+
513+
return ret;
514+
}
515+
469516
static int main_utf8(int argc, char** argv)
470517
{
471518
FILE* file = NULL;
472519
DWORD r, dwCheckSum[2];
473520
int i, patched = 0;
474-
uint64_t* patch, val, pos;
521+
uint64_t pos;
522+
uint8_t val[MAX_CHUNK_SIZE];
523+
chunk* chunk_list;
475524
char system_dir[128];
525+
size_t chunk_list_size, min_chunk_size = MAX_CHUNK_SIZE, max_chunk_size = 0;
526+
size_t val_size;
527+
bool force = false;
476528

477529
if (!IsCurrentProcessElevated()) {
478530
fprintf(stderr, "This command must be run from an elevated prompt.\n");
479531
return -1;
480532
}
481533

482-
if (argc < 2) {
483-
fprintf(stderr, "Usage: %s filename [QWORD QWORD [QWORD QWORD]...].\n", appname(argv[0]));
534+
fprintf(stderr, "%s %s © 2020 Pete Batard <pete@akeo.ie>\n\n", appname(argv[0]), APP_VERSION_STR);
535+
536+
if ((argc < 2) || (((argv[1][0] == '/') || (argv[1][0] == '-')) && ((argv[1][1] == '?') || (argv[1][1] == 'h')))) {
537+
fprintf(stderr, "Usage: %s filename <SEARCH> <REPLACE> [<SEARCH> <REPLACE> [...]].\n", appname(argv[0]));
538+
fprintf(stderr, "Where <SEARCH> and <REPLACE> are string of the same size, containing\n");
539+
fprintf(stderr, "the hex data that should be searched and replaced.\n");
484540
return -2;
485541
}
486542

487-
fprintf(stderr, "%s %s © 2020 Pete Batard <pete@akeo.ie>\n\n", appname(argv[0]), APP_VERSION_STR);
488543
fprintf(stderr, "This program is free software; you can redistribute it and/or modify it under \n");
489544
fprintf(stderr, "the terms of the GNU General Public License as published by the Free Software \n");
490545
fprintf(stderr, "Foundation; either version 3 of the License or any later version.\n\n");
@@ -517,61 +572,110 @@ static int main_utf8(int argc, char** argv)
517572
return -1;
518573
}
519574

575+
if (((argv[2][0] == '/') || (argv[2][0] == '-')) && (argv[2][1] == 'f'))
576+
goto skip_patch;
577+
520578
if (argc % 2) {
521-
fprintf(stderr, "Values must be provided in [ORIGINAL PATCHED] pairs\n");
579+
fprintf(stderr, "Values must be provided in [<SEARCH> <REPLACE>] pairs\n");
522580
return -1;
523581
}
524582

525-
// We're not going to win prizes for speed, but who cares...
526583
file = fopenU(argv[1], "rb+");
527584
if (file == NULL) {
528585
fprintf(stderr, "Could not open '%s'\n", argv[1]);
529586
return -1;
530587
}
531588

532-
patch = calloc((size_t)argc - 2, sizeof(uint64_t));
533-
if (patch == NULL) {
589+
chunk_list_size = ((size_t)argc - 2) / 2;
590+
chunk_list = calloc(chunk_list_size, sizeof(chunk));
591+
if (chunk_list == NULL) {
534592
fprintf(stderr, "calloc error\n");
535593
return -1;
536594
}
537595

538-
for (i = 0; i < argc - 2; i++)
539-
patch[i] = strtoull(argv[i + 2], NULL, 16);
596+
for (i = 1; i < argc / 2; i++) {
597+
chunk_list[i - 1].patched = false;
598+
chunk_list[i - 1].size = strlen(argv[2 * i]);
599+
if (chunk_list[i - 1].size % 2) {
600+
fprintf(stderr, "The number of hex digits for %s must be a multiple of 2\n",
601+
argv[(2 * i) - 1]);
602+
FreeChunkList(chunk_list, chunk_list_size);
603+
return -1;
604+
}
605+
if (chunk_list[i - 1].size != strlen(argv[2 * i + 1])) {
606+
fprintf(stderr, "<SEARCH> and <REPLACE> length differ for arguments %s and %s\n",
607+
argv[(2 * i) - 1], argv[2 * i]);
608+
FreeChunkList(chunk_list, chunk_list_size);
609+
return -1;
610+
}
611+
chunk_list[i - 1].size /= 2;
612+
if ((chunk_list[i - 1].size < MIN_CHUNK_SIZE) || (chunk_list[i - 1].size > MAX_CHUNK_SIZE)) {
613+
fprintf(stderr, "A value can not be smaller than %d bytes or larger than %d bytes\n",
614+
MIN_CHUNK_SIZE, MAX_CHUNK_SIZE);
615+
FreeChunkList(chunk_list, chunk_list_size);
616+
return -1;
617+
}
618+
min_chunk_size = min(min_chunk_size, chunk_list[i - 1].size);
619+
max_chunk_size = max(max_chunk_size, chunk_list[i - 1].size);
620+
chunk_list[i - 1].org = HexStringToBin(argv[2 * i]);
621+
chunk_list[i - 1].new = HexStringToBin(argv[2 * i + 1]);
622+
if (chunk_list[i - 1].org == NULL || chunk_list[i - 1].new == NULL) {
623+
fprintf(stderr, "Could not convert hex string\n");
624+
FreeChunkList(chunk_list, chunk_list_size);
625+
return -1;
626+
}
627+
}
540628

541-
if (fread(&val, 1, 7, file) != 7) {
629+
val_size = min_chunk_size - 1;
630+
if (fread(val, 1, val_size, file) != val_size) {
542631
fprintf(stderr, "Could not read file\n");
632+
FreeChunkList(chunk_list, chunk_list_size);
543633
return -1;
544634
}
545-
val = _byteswap_uint64(val);
546-
for (pos = 0; fread(&val, 1, 1, file) == 1; pos++) {
547-
for (i = 0; i < argc - 2; i += 2) {
548-
if (val == patch[i]) {
549-
fprintf(stdout, "%08llX: %016llX -> %016llX... ", pos, patch[i], patch[i + 1]);
550-
fseek(file, -1 * (long)sizeof(uint64_t), SEEK_CUR);
551-
val = _byteswap_uint64(patch[i + 1]);
552-
if (fwrite(&val, sizeof(uint64_t), 1, file) != 1) {
635+
636+
for (pos = 0; ; pos++) {
637+
assert(val_size < max_chunk_size);
638+
if (fread(&val[val_size++], 1, 1, file) != 1)
639+
// Note: If we have a patch smaller than max_chunk_size at the very end,
640+
// it won't be applied because we break too soon. But we don't care about
641+
// this in this patcher because the end should be the digital signature...
642+
break;
643+
for (i = 0; i < (int)chunk_list_size; i++) {
644+
if ((chunk_list[i].size <= val_size) && (memcmp(chunk_list[i].org, val, chunk_list[i].size) == 0)) {
645+
if (chunk_list[i].patched) {
646+
fprintf(stderr, "WARNING: Multiple sections with data %s are being patched!\n", argv[2 * i + 2]);
647+
}
648+
fprintf(stdout, "%08llX: %s\n========> %s... ", pos, argv[2 * i + 2], argv[2 * i + 3]);
649+
memcpy(val, chunk_list[i].new, chunk_list[i].size);
650+
fseek(file, (long)pos, SEEK_SET);
651+
if (fwrite(&val, 1, chunk_list[i].size, file) != chunk_list[i].size) {
553652
fprintf(stdout, "ERROR!\n");
554653
} else {
555654
fprintf(stdout, "SUCCESS\n");
655+
chunk_list[i].patched = true;
556656
patched++;
557-
fflush(file);
558-
// We don't allow patch overlap so move 7 bytes ahead
559-
pos += fread(&val, 1, 7, file);
560-
val = _byteswap_uint64(val) >> 8;
561657
}
562658
fflush(file);
659+
// Now reposition ourselves to the next byte to read...
660+
fseek(file, (long)pos + val_size, SEEK_SET);
661+
// ...and prevent patch overlap by removing our data
662+
val_size -= chunk_list[i].size;
663+
memmove(val, &val[chunk_list[i].size], val_size);
563664
break;
564665
}
565666
}
566-
val <<= 8;
667+
if (val_size == max_chunk_size)
668+
memmove(val, &val[1], --val_size);
567669
}
568-
free(patch);
670+
671+
FreeChunkList(chunk_list, chunk_list_size);
569672
fclose(file);
570673
if (patched == 0) {
571674
fprintf(stdout, "No elements were patched - aborting\n");
572675
return 0;
573676
}
574677

678+
skip_patch:
575679
// Since the whole point is to alter the file, remove the digital signature
576680
r = RemoveDigitalSignature(argv[1]);
577681
switch (r) {

0 commit comments

Comments
 (0)