Так сложилось, что меня всегда интересовала тема реверса, дизассембла и вообще того, как выглядит бинарь изнутри, особенно с точки зрения всяких кряков. Многие пользовались разным софтом, в который уже встроены обходы лицензий, а кто-то, вполне возможно, даже вспомнит тот качевый музон, который воспроизводили всякого рода KeyGen.exe.
Но для того, чтобы крякнуть программу, нужно понять, что и где патчить, и какая функция отвечает за валидацию лицензии. Для этого и существуют программы вроде IDA Pro. Помимо дизассемблирования они умеют генерировать псевдокод на C, строить графы вызовов и много чего еще.
И ведь никто не хочет, чтобы крякнули именно его софт? А чтобы этому противостоять, надо понимать как это работает и где можно вставить палки в колеса тем, кто будет анализировать ваш бинарь. Для этого я решил создать что-то типа небольшой лабораторной, в которой посмотрю как строят связи статические анализаторы и что можно сделать, чтобы этому противостоять.
Над чем будем проводить экзекуцию?
Решил написать программку, которую буду анализировать и дорабатывать впоследствии. Отключил оптимизации и CRT, чтобы получилось максимально наглядно.
Пишу небольшой класс, который будет выполнять поиск последовательностей в данных
Создал структуру, которая будет хранить в себе паттерн для поиска:
#define PATTERN_SIZE 128 #define ANY 0xFF00 #define END 0xFFFF struct BytePattern { uint16_t bytes[PATTERN_SIZE]; };
Поиск будет осуществляться побайтово, uint16_t используется, чтобы была возможность отметить произвольные байты в последовательности и завершить последовательность терминатором.
Делаю пошаговый поиск. Имеет смысл добавить возможность выбирать направление поиска:
enum class SearchDirection { NONE, UP_TO_DOWN, DOWN_TO_UP, };
Сами стадии поиска будут храниться в отдельной структуре:
struct SearchStage { SearchDirection type; uint16_t bytes[PATTERN_SIZE]; int byteCount; };
Определяю методы и переменные класса:
#define STAGES_COUNT 4 class PatternSearcher { public: PatternSearcher(); void addStage(SearchDirection type, const BytePattern bytes); void setInfo(void *begin, size_t size); void *find(); private: bool _performStageSearch(const SearchStage &stage); bool _matchAtOffset(const SearchStage &stage, size_t offset); SearchStage stages[STAGES_COUNT]; size_t currentOffset; uint8_t *begin; size_t size; };
Через setInfo() задается информация об области поиска, через addStage() добавляются стадии поиска, а после вызывается find(), который возвращает указатель внутри области поиска в случае успеха, либо nullptr.
Добил это всё удобным constexpr парсером, который будет вызываться через лямбду:
#define PATTERN(str) [](){ auto seq = pattern::parse(str); return seq; }() template <size_t N> constexpr BytePattern parse(const char(&str)[N])
Полный код:
Взглянем на основной код
Из-за того, что не использую CRT, определил entry следующим образом:
extern "C" void _start()
Выполнять поиск буду внутри строки "Hello World!":
#define SEQUENCE "Hello World!" const char* sequence = SEQUENCE; int sequenceSize = sizeof(SEQUENCE);
Искать буду поэтапно:
Поиск от начала к концу строки, последовательность
72 ?? 64 21,"r?d!", где'?'- любой байтПоиск от результатов предыдущего поиска к началу, последовательность
65 6C 6C 6F"ello"На выходе получаю указатель на
sequence[1], который указывает на"ello World!"
PatternSearcher searcher; searcher.setInfo((void*)sequence, sequenceSize); BytePattern pattern1 = PATTERN("72 ?? 64 21"); searcher.addStage(SearchDirection::UP_TO_DOWN, pattern1); BytePattern pattern2 = PATTERN("65 6C 6C 6F"); searcher.addStage(SearchDirection::DOWN_TO_UP, pattern2); void *result = searcher.find();
Вывожу результат как строку в MessageBox, завершаю процесс:
MessageBoxA(NULL, (const char*)result, "Msg", MB_OK); ExitProcess(0);
Написал небольшой cstd.cpp, в котором реализованы memset и memcpy.
Полный код:
Коротко о сборке
Собираю небольшой CMakeLists.txt, собирать буду через clang-cl, линковать lld-link:
cmake_minimum_required(VERSION 3.20) project(hideCall LANGUAGES CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_COMPILER clang-cl) set(CMAKE_LINKER lld-link) add_executable(hideCall main.cpp patternSearcher.cpp cstd.cpp)
Отключил исключения, RTTI, stack-protection, оптимизацию:
target_compile_options(hideCall PRIVATE /EHs- /EHa- /EHsc- /GR- /GS- /Od )
Выключил все стандартные библиотеки, указал свою entry:
target_link_options(hideCall PRIVATE /ENTRY:_start /NODEFAULTLIB /SUBSYSTEM:WINDOWS /MANIFEST:NO )
И в целом -DCMAKE_BUILD_TYPE=Debug
Полный код:
Весь stage1
Опции сборки менять в процессе не буду, только добавлять новые исходные файлы.

Самое интересное
Гружу бинарь в отрыве от .pdb файла, чтобы IDA сама не тянула структуры, классы, имена функций, но подпишу всё это ручками, как делал бы, если бы анализировал какой-то левый бинарь (пусть и с учетом, что об этом бинаре я всё знаю).
Сразу же то, ради чего это всё затевалось

Вижу все вызовы, которые есть в программе, смотрю ближе
В первую очередь меня интересует функция _start, что в ней происходит
Создаются нужные переменные на стеке:
const CHAR *lpText; // [rsp+28h] [rbp-850h] _BYTE pattern2[256]; // [rsp+30h] [rbp-848h] BYREF _WORD _pattern2[128]; // [rsp+130h] [rbp-748h] BYREF _BYTE pattern1[256]; // [rsp+230h] [rbp-648h] BYREF _WORD _pattern1[128]; // [rsp+330h] [rbp-548h] BYREF _BYTE searcher[1084]; // [rsp+430h] [rbp-448h] BYREF int sequenceSize; // [rsp+86Ch] [rbp-Ch] const char *sequence; // [rsp+870h] [rbp-8h]
Инициализации переменных:
sequence = "Hello World!"; sequenceSize = 13;
Вызов конструктора для searcher:
PatternSearcher::PatternSearcher(searcher);
lea rcx, [rsp+878h+searcher] call PatternSearcher__PatternSearcher
Передача данных в setInfo:
PatternSearcher::setInfo(searcher, sequence, sequenceSize);
movsxd r8, [rsp+878h+sequenceSize] mov rdx, [rsp+878h+sequence] lea rcx, [rsp+878h+searcher] call PatternSearcher__setInfo
Распаковка constexpr паттерна:
memset(_pattern1, 0, 256); _pattern1[0] = 114; _pattern1[1] = -256; _pattern1[2] = 100; _pattern1[3] = 33; _pattern1[4] = -1; memcpy(pattern1, _pattern1, 256);
lea rcx, [rsp+878h+_pattern1] ; void * xor edx, edx ; Val mov r8d, 100h ; Size call memset mov [rsp+878h+_pattern1], 72h ; 'r' mov [rsp+878h+var_546], 0FF00h mov [rsp+878h+var_544], 64h ; 'd' mov [rsp+878h+var_542], 21h ; '!' mov [rsp+878h+var_540], 0FFFFh lea rcx, [rsp+878h+pattern1] ; void * lea rdx, [rsp+878h+_pattern1] ; Src mov r8d, 100h ; Size call memcpy
Создание этапа поиска, через addStage:
PatternSearcher::addStage(searcher, 1, pattern1);
lea rcx, [rsp+878h+searcher] mov edx, 1 lea r8, [rsp+878h+pattern1] call PatternSearcher__addStage
Создание паттерна и этапа повторяется два раза.
Вызывается find, результат сохраняется:
lpText = (const CHAR *)PatternSearcher::find(searcher);
lea rcx, [rsp+878h+searcher] call PatternSearcher__find mov [rsp+878h+lpText], rax
Конец программы:
MessageBoxA(0, lpText, "Msg", 0); ExitProcess(0);
mov rdx, [rsp+878h+lpText] ; lpText xor eax, eax mov ecx, eax ; hWnd lea r8, Caption ; "Msg" xor r9d, r9d ; uType call cs:__imp_MessageBoxA xor ecx, ecx ; uExitCode call cs:__imp_ExitProcess
Ничего необычного, IDA спокойно видит куда ссылаются все инструкции call и без проблем строит от них связи, другого результата тут быть не могло, на это и был расчёт.
Как можно эти связи разорвать?
Я подумал, что не будет ничего проще, чем просто передать указатель на функцию, но в процессе реализации пришло понимание, что не всё так просто:
PatternSearcher(); void addStage(SearchDirection type, const BytePattern bytes); void setInfo(void *begin, size_t size); void *find(); bool _performStageSearch(const SearchStage &stage); bool _matchAtOffset(const SearchStage &stage, size_t offset); int __stdcall MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType); void __stdcall ExitProcess(UINT uExitCode);
У всех функций, которые я использую, разные типы аргументов, их количество, а помимо этого, методы класса PatternSearcher еще и принимают указатель на объект searcher.
PatternSearcher::PatternSearcher(searcher); PatternSearcher::setInfo(searcher, sequence, sequenceSize); PatternSearcher::addStage(searcher, 1, pattern1); PatternSearcher::find(searcher);
Любое значение должно поместиться в uint64_t, будь то указатель, число или bool. Аргументы вызова можно передать структурой, содержащей массив uint64_t значений, каждая функция будет распаковывать её вручную и возвращать будет uint64_t.
#define MAX_CALL_ARGS 6 struct CallArguments { uint64_t args[MAX_CALL_ARGS]; }; using Fn = uint64_t (*)(CallArguments args);
Одна общая функция, которая будет выполнять роль диспетчера вызовов:
uint64_t doCall(Fn func, CallArguments args);
Все функции, которые будут вызываться должны иметь одну сигнатуру Fn. Реализовал их в C-подобном стиле, но от конструктора не отказался и ниже покажу почему.
namespace PatternSearcher { struct Object { Object(); SearchStage stages[STAGES_COUNT]; size_t currentOffset; uint8_t* begin; size_t size; }; uint64_t addStage(dispatch::CallArguments args); uint64_t setInfo(dispatch::CallArguments args); uint64_t find(dispatch::CallArguments args); uint64_t _performStageSearch(dispatch::CallArguments args); uint64_t _matchAtOffset(dispatch::CallArguments args); }
Внутри каждой функции выполняю распаковку аргументов:
namespace PatternSearcher { uint64_t addStage(dispatch::CallArguments args) { Object *self = (Object*)args.args[0]; SearchDirection type = (SearchDirection)args.args[1]; BytePattern* pattern = (BytePattern*)args.args[2]; /* ... */ } }
И, собственно, вызов:
dispatch::doCall( &PatternSearcher::setInfo, dispatch::CallArguments{ (uint64_t)&searcher, (uint64_t)(void*)sequence, (uint64_t)sequenceSize } );
Решил упростить себе жизнь. Пытался сделать инлайн шаблон, чтобы не мудрить с дефайнами, но получалось не так адекватно, как я хотел. Заполнение структур дублировалось, создавалось больше информационного шума в дизасме, который напрямую не влиял на исследуемую тему.
#define CALL_0(func) doCall(&func, dispatch::CallArguments{}) #define CALL_1(func, a1) doCall(&func, dispatch::CallArguments{ (uint64_t)(a1) }) #define CALL_2(func, a1, a2) doCall(&func, dispatch::CallArguments{ (uint64_t)(a1), (uint64_t)(a2) }) //.... #define CALL(func, ...) PP_CAT(CALL_, PP_NARG(__VA_ARGS__)) (func, __VA_ARGS__)
Получаю что-то такое:
CALL(PatternSearcher::setInfo, &searcher, (void*)sequence, sequenceSize);
Отдельно добавил и обертки для MessageBox и ExitProcess:
uint64_t MessageBoxA_wrapper(dispatch::CallArguments args) { HWND hWnd = (HWND)args.args[0]; LPCSTR lpText = (LPCSTR)args.args[1]; LPCSTR lpCaption = (LPCSTR)args.args[2]; UINT uType = (UINT)args.args[3]; return (uint64_t)MessageBoxA(hWnd, lpText, lpCaption, uType); } uint64_t ExitProcess_wrapper(dispatch::CallArguments args) { UINT uExitCode = (UINT)args.args[0]; ExitProcess(uExitCode); return 0; }
Как выглядит главная функция:
const char* sequence = SEQUENCE; int sequenceSize = sizeof(SEQUENCE); PatternSearcher::Object searcher; CALL(PatternSearcher::setInfo, &searcher, (void*)sequence, sequenceSize); BytePattern pattern1 = PATTERN("72 ?? 64 21"); CALL(PatternSearcher::addStage, &searcher, SearchDirection::UP_TO_DOWN, (void*)&pattern1); BytePattern pattern2 = PATTERN("65 6C 6C 6F"); CALL(PatternSearcher::addStage, &searcher, SearchDirection::DOWN_TO_UP, (void*)&pattern2); void *result = (void*)CALL(PatternSearcher::find, &searcher); CALL(MessageBoxA_wrapper, NULL, (const char*)result, "Msg", MB_OK); CALL(ExitProcess_wrapper, 0);
Также все вызовы внутренних реализаций завернул в CALL.
Полный код:
Собираю, смотрю на разницу

doCallУже выглядит интересно, прямые связи IDA рисует только к memcpy, memset и конструктору, которые я не обернул в dispatch. В остальном же видно, что doCall вызывается из трёх разных функций, а большая часть функций вообще висит в подвешенном состоянии, словно они есть, но на них никто не ссылается.
В IDA можно включить отображение связей по упоминаниям адресов функций, и тогда картина сильно меняется

Зашел сравнить дизасм
Конструктор не изменился, что и ожидалось:
PatternSearcher::Object(searcher);
lea rcx, [rsp+7D8h+searcher] call PatternSearcher__Object
Вызов setInfo:
v16[0] = searcher; v16[1] = sequence; v16[2] = sequenceSize; v4 = &v17; do *v4++ = 0; while ( v4 != (__int64 *)searcher ); doCall(PatternSearcher::setInfo, v16);
lea rcx, [rsp+7D8h+var_478] lea rax, [rsp+7D8h+searcher] mov [rsp+7D8h+var_478], rax mov rax, [rsp+7D8h+sequence] mov [rsp+7D8h+var_470], rax movsxd rax, [rsp+7D8h+sequenceSize] mov [rsp+7D8h+var_468], rax mov rax, rcx add rax, 18h add rcx, 30h ; '0' mov [rsp+7D8h+var_780], rcx mov [rsp+7D8h+var_778], rax loc_1400010EB: mov rax, [rsp+7D8h+var_778] mov rcx, [rsp+7D8h+var_780] mov qword ptr [rax], 0 add rax, 8 cmp rax, rcx mov [rsp+7D8h+var_778], rax jnz short loc_1400010EB lea rcx, PatternSearcher__setInfo lea rdx, [rsp+7D8h+var_478] call doCall
Инструкция call всегда ссылается на функцию doCall, из-за чего прямых связей IDA и не строила, пока я не включил отображение упоминаний. Именно их и видно:
lea rcx, PatternSearcher__setInfo
Но ведь и это можно исправить?
Нужно сделать так, чтобы упоминания функций не появлялись в тех функциях, откуда они будут вызваны. Я решил реализовать это через хеш-таблицу
Буду считать хеш имени функций во время компиляции:
consteval uint64_t hash(const char *str);
Заполнять таблицу где-нибудь в начале выполнения:
#define DISPATCH_REGISTER(name) { constexpr uint64_t _hash = hash(#name); dispatch::registerFunction(_hash, &name); } void registerFunction(const uint64_t hash, Fn func);
Вызывать функции соответственно по хешу:
uint64_t doCall(uint64_t hash, CallArguments args); #define CALL_0(func) doCall(func, dispatch::CallArguments{}) //.... #define CALL(func, ...) PP_CAT(CALL_, PP_NARG(__VA_ARGS__)) (hash(#func), __VA_ARGS__)
Почему же я оставил конструктор?
Чтобы вызвать функцию, нужно предварительно её зарегистрировать в хеш-таблице и я решил, что нет места лучше, чем конструктор. Он вызывается, когда создается объект и может регистрировать свои методы:
namespace PatternSearcher { Object::Object() { DISPATCH_REGISTER(PatternSearcher::addStage); DISPATCH_REGISTER(PatternSearcher::setInfo); DISPATCH_REGISTER(PatternSearcher::find); DISPATCH_REGISTER(PatternSearcher::_performStageSearch); DISPATCH_REGISTER(PatternSearcher::_matchAtOffset); for (size_t i = 0; i < STAGES_COUNT; i++) { this->stages[i].type = SearchDirection::NONE; } } }
Но врапперы, которые я писал для WinAPI функций, придется всё-таки регистрировать в startup:
void start(); extern "C" void _start() { dispatch::DispatchEntry dispatchTable[DISPATCH_TABLE_SIZE] = {}; dispatch::setupDispatchTable(dispatchTable); DISPATCH_REGISTER(MessageBoxA_wrapper); DISPATCH_REGISTER(ExitProcess_wrapper); start(); }
Основной код переехал в start(), а _start() выполняет роль startup
Сделал, чтобы хеш-таблица хранилась на стеке, dispatch::setupDispatchTable() инлайнится и передает указатель на таблицу в глобальную переменную. Так как завершение программы происходит внутри start, переменная всегда будет на стеке до завершения, проблем быть не должно.
Основной код при этом не изменился, но в коде везде будут хеши, которые просчитываются во время компиляции, а ссылки на функции только в startup и конструкторе
Полный код:
В этот-то раз получилось?

Видно, что start и конструктор ссылаются на registerFunction и на другие функции, но заглянем сразу внутрь main
Конструктор я не скрывал, он вызывается напрямую:
PatternSearcher::Object(searcher);
lea rcx, [rsp+7D8h+searcher] call PatternSearcher__Object
Остальные вызовы идут через doCall, в который передается хеш:
doCall(0xBA7C49E66C136A52uLL, v16); // setInfo doCall(0x602156B538A2FB49LL, v13); // addStage result = doCall(0x8FCCE0054447B64DuLL, v8); // find doCall(0x6C5DF279D37AF158LL, v6); // MessageBoxA_wrapper doCall(0x8279D977F515DD86uLL, v5); // ExitProcess_wrapper
mov rcx, 0BA7C49E66C136A52h lea rdx, [rsp+7D8h+var_478] call doCall mov rcx, 602156B538A2FB49h lea rdx, [rsp+7D8h+var_5A8] call doCall mov rcx, 8FCCE0054447B64Dh lea rdx, [rsp+7D8h+var_710] call doCall mov [rsp+7D8h+result], rax mov rcx, 6C5DF279D37AF158h lea rdx, [rsp+7D8h+var_740] call doCall mov rcx, 8279D977F515DD86h lea rdx, [rsp+7D8h+var_770] call doCall
В отрыве от контекста эти хеши ничего не значат. Вместо адреса функции передается непонятное uint64_t значение. Поэтому IDA не строит полноценные связи.
Смотрю start
dispatchTable создается на стеке, передается в глобальный указатель:
_BYTE dispatchTable[4096]; // [rsp+30h] [rbp-1008h] BYREF _BYTE *v2; // [rsp+1030h] [rbp-8h] memset(dispatchTable, 0, sizeof(dispatchTable)); v2 = dispatchTable; g_dispatchTablePtr = dispatchTable;
lea rcx, [rsp+arg_28] ; void * xor edx, edx ; Val mov r8d, 1000h ; Size call memset lea rax, [rsp+arg_28] mov [rsp+arg_1028], rax mov rax, [rsp+arg_1028] mov cs:g_dispatchTablePtr, rax
Регистрация функций в хеш-таблице, отсюда IDA и рисует связи:
/*hash "MessageBoxA_wrapper"*/ dispatch::registerFunction(0x6C5DF279D37AF158LL, MessageBoxA_wrapper); /*hash "ExitProcess_wrapper"*/ dispatch::registerFunction(0x8279D977F515DD86uLL, ExitProcess_wrapper);
mov rax, 6C5DF279D37AF158h ; hash "MessageBoxA_wrapper" mov [rsp+arg_20], rax mov rcx, 6C5DF279D37AF158h ; hash "MessageBoxA_wrapper" lea rdx, MessageBoxA_wrapper call dispatch__registerFunction mov rax, 8279D977F515DD86h ; hash "ExitProcess_wrapper" mov [rsp+arg_18], rax mov rcx, 8279D977F515DD86h ; hash "ExitProcess_wrapper" lea rdx, ExitProcess_wrapper call dispatch__registerFunction
То же самое и в конструкторе:
dispatch::registerFunction(0x602156B538A2FB49LL, PatternSearcher::addStage); dispatch::registerFunction(0xBA7C49E66C136A52uLL, PatternSearcher::setInfo); dispatch::registerFunction(0x8FCCE0054447B64DuLL, PatternSearcher::find); dispatch::registerFunction(0x734AD296B2D306D6LL, PatternSearcher::_performStageSearch); dispatch::registerFunction(0xD6D8F1824447D932uLL, PatternSearcher::_matchAtOffset);
mov rax, 602156B538A2FB49h mov [rsp+68h+var_18], rax mov rcx, 602156B538A2FB49h lea rdx, PatternSearcher__addStage call dispatch__registerFunction mov rax, 0BA7C49E66C136A52h mov [rsp+68h+var_20], rax mov rcx, 0BA7C49E66C136A52h lea rdx, PatternSearcher__setInfo call dispatch__registerFunction mov rax, 8FCCE0054447B64Dh mov [rsp+68h+var_28], rax mov rcx, 8FCCE0054447B64Dh lea rdx, PatternSearcher__find call dispatch__registerFunction mov rax, 734AD296B2D306D6h mov [rsp+68h+var_30], rax mov rcx, 734AD296B2D306D6h lea rdx, PatternSearcher___performStageSearch call dispatch__registerFunction mov rax, 0D6D8F1824447D932h mov [rsp+68h+var_38], rax mov rcx, 0D6D8F1824447D932h lea rdx, PatternSearcher___matchAtOffset call dispatch__registerFunction
Если ручками поковыряться, то можно найти какой хеш к какой функции относится, напрямую перейти к ней, кликнув дважды на вызов не получится, придется как-то для себя это всё помечать, а от рабочего кода IDA связи построить уже не сможет, кроме вызовов регистрации
Но это все ещё палево
Так что сформирую хеш-таблицу на этапе компиляции и буду хранить в статических данных:
#define DISPATCH_REGISTER(name) { hash(#name), &name } struct DispatchTable { DispatchEntry entries[DISPATCH_TABLE_SIZE]; }; consteval DispatchTable createDispatchTable(DispatchTable table);
#include "dispatch.h" #include "patternSearcher.h" extern uint64_t MessageBoxA_wrapper(dispatch::CallArguments args); extern uint64_t ExitProcess_wrapper(dispatch::CallArguments args); namespace dispatch { dispatch::DispatchTable dispatchTable = createDispatchTable((dispatch::DispatchTable){ .entries = { DISPATCH_REGISTER(PatternSearcher::addStage), DISPATCH_REGISTER(PatternSearcher::setInfo), DISPATCH_REGISTER(PatternSearcher::find), DISPATCH_REGISTER(PatternSearcher::_performStageSearch), DISPATCH_REGISTER(PatternSearcher::_matchAtOffset), DISPATCH_REGISTER(MessageBoxA_wrapper), DISPATCH_REGISTER(ExitProcess_wrapper), } }); }
Заполнение хеш-таблицы происходит на этапе компиляции и хранится в инициализированных данных внутри бинаря.
Из-за такого подхода я решил, что и конструктор мне в принципе уже не нужен, код из него перенес в setInfo()
Код внутри start просто передает управление дальше, startup отсутствует:
void start(); extern "C" void _start() { start(); }
Остальной код не изменился
Полный код:
И вот результат


IDA совсем не видит связей, но вызовы также производятся по хешам.
В стат��ческих данных можно увидеть записи в таблице, но не так явно, как было до этого.
; .... ; .... ; .... .data:00000001400034D0 db 4Dh ; M .data:00000001400034D1 db 0B6h .data:00000001400034D2 db 47h ; G .data:00000001400034D3 db 44h ; D .data:00000001400034D4 db 5 .data:00000001400034D5 db 0E0h .data:00000001400034D6 db 0CCh .data:00000001400034D7 db 8Fh .data:00000001400034D8 dq offset PatternSearcher__find ; .... ; .... ; .... .data:0000000140003520 db 52h ; R .data:0000000140003521 db 6Ah ; j .data:0000000140003522 db 13h .data:0000000140003523 db 6Ch ; l .data:0000000140003524 db 0E6h .data:0000000140003525 db 49h ; I .data:0000000140003526 db 7Ch ; | .data:0000000140003527 db 0BAh .data:0000000140003528 dq offset PatternSearcher__setInfo .data:0000000140003530 align 80h .data:0000000140003580 db 58h ; X .data:0000000140003581 db 0F1h .data:0000000140003582 db 7Ah ; z .data:0000000140003583 db 0D3h .data:0000000140003584 db 79h ; y .data:0000000140003585 db 0F2h .data:0000000140003586 db 5Dh ; ] .data:0000000140003587 db 6Ch ; l .data:0000000140003588 dq offset MessageBoxA_wrapper ; .... ; .... ; ....

Proximity View показывает на что ссылаются те или иные участки программы, включая функции, переменные, ресурсы. Как видно, связей тут тоже немного.
Конечно, это не панацея

В рантайме эти вызовы все равно можно отследить дебаггером, проставить везде комментарии. После этого вернуться к статическому анализу, если нужно. От этого уже не защитит никакая магия с кодом.
Не сказать, что это удобный подход для написания программ, скорее просто исследование поведения статического анализатора. Правильнее было бы написать отдельный упаковщик для бинарей, который не интегрировался бы в код, а выполнял перепаковку уже после сборки, но это вполне себе тянет уже на отдельную статью про виртуализацию выполняемого кода. Впрочем, это уже совсем другая история.
Репоз с кодом: GitHub
За вредные советы спасибо комьюнити SenJun - opensource курсов по C++, брейнфаку и другим не менее полезным яп.