Так сложилось, что меня всегда интересовала тема реверса, дизассембла и вообще того, как выглядит бинарь изнутри, особенно с точки зрения всяких кряков. Многие пользовались разным софтом, в который уже встроены обходы лицензий, а кто-то, вполне возможно, даже вспомнит тот качевый музон, который воспроизводили всякого рода 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_wrappermov 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++, брейнфаку и другим не менее полезным яп.
