Table of Contents
We’re going to see how a program can parse the PEB to recover Kernel32.dll
address, and then load any other library. Not a single import is needed !
Introduction
Writing my PE packer (see tutorial HERE), I managed to drastically reduce the number of imported functions a packed binary needs. I ended up with 5 :
LoadLibrary
GetProcAddress
GetModuleHandle
VirtualAlloc
VirtualProtect
Now the last 3 can be easily removed from the import table, by retrieving them using LoadLibrary
and GetProcAddress
. Those 2 functions can be used together to import any single other function from the system. My question is: is it possible to remove even them ? Can we import external functions without using any already imported ones ?
We would need to determine LoadLibrary
and GetProcAdress
addresses at some point. To do so we would need to know where Kernel32.dll
is loaded, but with ASLR, we have no way of knowing any library location.
General solution
The solution lies in an OS structure in memory. When loading the process, the OS maintains 2 structures :
- The PEB, for “Process Environment Block”, one by process as the names suggests.
- The TEB, for “Thread Environment Block”, one for each thread.
The PEB contains a list of loaded modules, and their addresses. So we can go through this list, and find the address of Kernel32.dll
, which is always loaded by the system, even if not explicitely imported by the binary.
Once we have this address, we will just find LoadLibrary
and GetProcAddress
addresses, and then we can use them to load anything else we want.
So basically, here is what we need to achieve:
- Get the PEB.
- Get the address of
Kernel32.dll
, from the module list in the PEB. - Find
GetProcAddress
andLoadLibrary
inKernel32.dll
. - Use those 2 functions to load anything else.
The code below will be targeting an x86 architecture and compiled with Mingw32
. We’ll compile with this command:
gcc.exe noimport.c -o noimport -nostartfiles -nostdlib "-Wl,--entry=__start" -masm=intel
It removes any standard libraries (and so every default imports) and defines the assembly syntax to intel (instead of AT&T).
Parsing the PEB
To begin with, we want to retrive a loaded module address from its name : that would be the GetModuleHandle
function. As names are stored in widechar strings in memory, we’ll make a wide string version:
void* myGetModuleHandleW(WCHAR* module_name) {
PEB structures
First thing we are going to need is to get the PEB address. We can’t have it directly, but it is stored in the TEB, which is very easy to get, it’s pointed to by the fs
segment register. Here is the TEB structure (source) :
typedef struct _TEB {
PVOID Reserved1[12];
PPEB ProcessEnvironmentBlock;
PVOID Reserved2[399];
BYTE Reserved3[1952];
PVOID TlsSlots[64];
BYTE Reserved4[8];
PVOID Reserved5[26];
PVOID ReservedForOle;
PVOID Reserved6[4];
PVOID TlsExpansionSlots;
} TEB, *PTEB;
We want the ProcessEnvironmentBlock
field, at offset 0x30
. Reading this field is a matter of a single inline assembly line (in intel syntax, thanks to the -masm=intel
compilation option) :
PEB* PEB_ptr = NULL;
__asm__(
"mov %[PEB_ptr], fs:[0x30];"
: [PEB_ptr] "=r" (PEB_ptr)
: :
);
Now the PEB structure is the following one (source):
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
//we won't be using anything below here
} PEB, *PPEB;
We are going to use the field Ldr
, pointing to a PEB_LDR_DATA
struct, as follow (source):
typedef struct _PEB_LDR_DATA {
BYTE Reserved1[8];
PVOID Reserved2[3];
LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
There are many more fields to this structure, the MSDN documentation is far from being complete. You can find a more complete description here, but the default Windows headers in MingW are enough for now.
Now this InMemoryOrderModuleList
is the field we are looking for, it’s a bi-directional linked list (source):
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, PRLIST_ENTRY;
Each list element points on a LDR_DATA_TABLE_ENTRY
, which contains all the data we’re going to need concerning a loaded module. Now this structure is incomplete in both the documentation on MSDN, and the headers included with most compiler (like Mingw
, which I’m using). Here is a more complete one (source):
typedef struct _LDR_DATA_TABLE_ENTRY_COMPLETED
{
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
// won't be using anything below this point
} LDR_DATA_TABLE_ENTRY_COMPLETED, *PLDR_DATA_TABLE_ENTRY_COMPLETED;
We can rename it with _COMPLETED
to avoid conflicts with the default headers. Now the important thing to note is that the InMemoryOrderLinks
(the linked list elements) points towards one another, we’ll see that again below.
Going through the PEB
So here is the code to go through every LDR_DATA_TABLE_ENTRY
structures. We’ll use some include for the types and structures:
#include <windows.h>
#include <winnt.h>
#include <winternl.h>
We’ll start by getting the first linked list item, and define some objects we’ll be using:
PEB_LDR_DATA* peb_ldr_data = PEB_ptr->Ldr;
LIST_ENTRY* list_head = &(peb_ldr_data->InMemoryOrderModuleList);
LIST_ENTRY* list_entry;
LDR_DATA_TABLE_ENTRY_COMPLETED* ldr_entry;
Then, we can simply move from item to item in the list. But be aware that it’s circular, so don’t expect a NULL pointer at the “end” : we’ll stop once we come back to the first item.
for(list_entry = list_head->Flink; list_entry != list_head; list_entry = list_entry->Flink){
We actually start by the “second” item (list_head->Flink
), as the head is not a LDR_DATA_TABLE_ENTRY
, but it points to one.
Now for each element, we’re going to retrieve the corresponding LDR_DATA_TABLE_ENTRY
. But we need to account for the fact that the LIST_ENTRY
we are using all points toward the InMemoryOrderLinks
fields, not the begining of the structure:
ldr_entry = (LDR_DATA_TABLE_ENTRY_COMPLETED*) ((char*)list_entry - sizeof(LIST_ENTRY));
After that, we just have to compare the name of the current module with the name we are looking for and we’re done :
WCHAR* name = ldr_entry->BaseDllName.Buffer;
if(wstring_cmp_i(module_name, name)) {
return ldr_entry->DllBase;
}
The function wstring_cmp_i
should compare 2 wide strings, and be case insensitive. Here is my implementation for reference:
int wstring_cmp_i(WCHAR* cmp, WCHAR* other) {
/* Case insensitive Wstring compare, cmp must be lowercase */
WORD* w_cmp = (WORD*) cmp;
WORD* w_other = (WORD*) other;
while(*w_other != 0) {
WORD lowercase_other = ( (*w_other>='A' && *w_other<='Z')
? *w_other - 'A' + 'a'
: *w_other);
if(*w_cmp != lowercase_other) {
return 0;
}
w_cmp ++;
w_other ++;
}
return (*w_cmp == 0);
}
First step is over, we can call our myGetProcAddress(L"kernel32.dll")
to get the address where Kernel32.dll
has been loaded.
Parsing the exports
Now we want, given a module address, to find the address of an exported function. That would be GetProcAddress
:
void* myGetProcAddress(char* module, char* search_name){
Note that module
is a char*
here, to simplify pointers arithmetic.
We’re going to parse the module PE header, to get to its export table:
IMAGE_DOS_HEADER* p_DOS_HDR = (IMAGE_DOS_HEADER*) module;
IMAGE_NT_HEADERS* p_NT_HDR = (IMAGE_NT_HEADERS*) (((char*) p_DOS_HDR) + p_DOS_HDR->e_lfanew);
IMAGE_EXPORT_DIRECTORY* export_directory = (IMAGE_EXPORT_DIRECTORY*) (module +
p_NT_HDR->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
Now the structure of the export table in a PE file is a complex one. There are 3 arrays to consider together:
AddressOfNames
: contains strings with the function names.AddressOfFunctions
: contains the RVA of the exported function.AddressOfNameOrdinals
: links the arrays of names with the array of functions RVA by index.
It would be very hard to explain clearly how it’s done, but the code is actually quite simple:
// Get the arrays based on their RVA in the IMAGE_EXPORT_DIRECTORY struct
DWORD* names_RVA_array = (DWORD*) (module + export_directory->AddressOfNames);
DWORD* function_RVA_array = (DWORD*) (module + export_directory->AddressOfFunctions);
WORD* name_ordinals_array = (WORD*) (module + export_directory->AddressOfNameOrdinals);
//Then for each function
for(int i=0; i< export_directory->NumberOfFunctions; ++i) {
// Get the functions ordinal, name and code RVA
//DWORD exported_ordinal = name_ordinals_array[i] + export_directory->Base;
char* funct_name = module + names_RVA_array[i];
DWORD exported_RVA = function_RVA_array[name_ordinals_array[i]];
if(string_cmp(search_name, funct_name)){
return (void*) (module + exported_RVA);
}
}
You’ll need to write your own version of strcmp
, which I called string_cmp
in my code.
Now we are done, given a module address, we can retrieve a function address based on its name. Let’s put everything together.
Final program
We’re going to compile with no standard library, which means no runtime environment, so we’ll call our entrypoint _start
. We start by getting Kernel32.dll
address, using our own version of GetModuleHandle
:
int _start(){
void* kernel32_dll = myGetModuleHandleW(L"kernel32.dll");
Now we could use our own GetProcAddress
all along, but I prefer finding and using the real one (which handles more cases that our version, like import by ordinal for example). We retrieve it using our own version:
void* GetProcAddress_addr = myGetProcAddress(kernel32_dll, "GetProcAddress");
void* (__stdcall *GetProcAddress)(void*,char*) = myGetProcAddress(kernel32_dll, "GetProcAddress");
We need to explicite the calling convention __stdcall
, as MingW
defaults to __cdecl
.
We only miss LoadLibrary
, which we’ll get using the real GetProcAddress
:
void* (__stdcall *LoadLibraryA)(char*) = GetProcAddress(kernel32_dll, "LoadLibraryA");
And that’s it, we have everything we need. We can load any library and get any function inside. Let’s open a message box for example:
void* User32_dll = LoadLibraryA("user32.dll");
int (__stdcall *MessageBoxA)(void*, char*, char*, int) = GetProcAddress(User32_dll, "MessageBoxA");
MessageBoxA(NULL, "Hello, with no imports", "Hello world", 0);
Conclusion
The final code can be found here: https://github.com/jeremybeaume/experiments/blob/master/no_imports/noimport.c.
So we have a message box here, nothing fancy. But let us have a look at our binary headers, with CFF explorer:
MingW
still produced and import directory entry, and an .idata
section, but if we look at the content:
This section is completely empty! We could remote it, and set the import directory to zero, it would be the same.
Our goal is achieved, we have a binary with no imports, and it still manages to use external libraries. We could put anything in the import table to appear legitimate. From the outside: nothing dangerous would be loaded, and nothing more could be imported (as LoadLibrary
would not be imported). So that would seem pretty safe … But in fact, we can load anything ! This is a simple trick to hide functionalities in a binary.
Александра
02 ноября 2022
Держитесь подальше от брокера esperio, аналитиков и финансовых специалистов, как они себя называют, работающих от имени этого брокера! Находят людей на различных сайтах по работе, предлагают заполнять для начала отчеты в экселе, якобы тестировать рынок! А потом втираются в доверие и предлагают открыть брокерский счет! Еще разводят на обучение за 50 тыс. Заманивают большими бонусами, не говоря при этом что их придется сделками отрабатывать! А потом под чутким их руководством предлагают открывать сделки и вот незадача одна сделка резко уходит в минус и сделать уже ничего нельзя и депозит схлопывается в ноль! А они говорят это же рынок, просадка это нормально, просто ваш депозит не выдержал! Меня развела молодая девушка и зовут ее Алчинова Татьяна Сергеевна !!! Ни в коем случае с ней не связывайтесь! Живет она сейчас в Тюмени, а до этого жила в Новосибирске! Будьте осторожны!
Ответить
5 Полезно
Тоха
23 июня 2022
Что я вам могу сказать. Это самый настоящий скам, который скоро закроется и прихватит с собой деньги пользователей. У Esperio нет никаких оснований называться брокером. Перед сотрудничеством с любым посредником нужно проверять доки, возраст, проверять достоверность информации. А здесь сплошнее вранье!!!! И ведутся на подобную херабору новички, которые верят на слово аферистам и доверяют им свои деньги. Поэтому сколько бы вы сюда не вложили, заработать и вывести деньги не удастся. Так что держитесь подальше от этой лохвозки!!!
Ответить
0 Полезно
Karina
18 мая 2022
Торговая платформа здесь полное дерьмо. Незнаю, на чем они там реально предлагают торговать, но заработать здесь невозможно. Одна выкачка денег и не более. Я несколько раз пополняла счет в надежде отыграться, но все бестолку. Хорошо хоть вовремя остановилась и не залезла в кредиты. Терминал постоянно подвисает, сделки закрываются в минус. Мало того что деньги списываются на фоне убыточных сделок, так с депозита постоянно списываются непонятные суммы и они нигде не отображаются. Внятного ответа в техподдержке мне никто не дал. Такое впечатление что ее вообще нет. В общей сложности я здесь потеряла около 3 тысяч((((
Ответить
0 Полезно
Давид
21 апреля 2022
Я конечно слышал, что сейчас в интернете полно аферистов, но никогда не думал, что я лично стану жертвой мошеннической схемы. Вот что значит отсутствие опыта. Представители Esperio сами вышли со мной на связь, только ума не приложу где они взяли мой номер телефона. Правда я всегда лазил по различным форумам по инвестициям, может быть оттуда. Они мне предложили заработок на онлайн-торговле, говорили что будут помогать. Я изучил условия и согласился. Как только я пополнил счет, мой ЛК тут же заблокировали. На этом наше общение прекратилось… Если бы вы знали, как я виню себя за свою спешку и доверчивость((((
Ответить
1 Полезно
Егор
16 марта 2022
Не будьте наивными и доверчивыми и держитесь подальше от этой лохотронской платформы Эсперио. То что мне здесь обещали и гарантировали, оказалось полной ерундой. На самом деле контора работает без юридических документов и не имеет доступа к поставщиком ликвидности. Я сам думал что торговля здесь реальная и все возможно, а оказалось меня кормили обычной имитацией трейдинга на подконтрольной платформе. Выводить сделки на межбанк здесь никто не собирается. А все деньги со старта уходят в лапы этих тварей. Чтобы вы подавились моей тыщей!!!!!!
Ответить
0 Полезно
Павел
15 февраля 2022
Я работал с несколькими брокерами и задержки есть везде. Ни разу не сталкивался, чтобы был мгновенный вывод. Главное чтобы он вообще был, здесь есть и это радует. По условиям мне пока нормально, хочу по инструментам всем пройтись. Но на фоне кухонь бесполезных, то тут можно еще работать я вижу. Более менее.
Ответить
0 Полезно
Karlitos
07 февраля 2022
О брокере много не слышал, но все таки решился. Что сказать, условия как везде, ничего я нового здесь не увидел. Разве, что очень удивила меня аналитика, брокер вроде не сильно популярный а аналитика на уровне. Не у всех есть что почитать. неплохо. Вывод протестировал, задержали но вывели. Ставлю три звезды пока, время покажет.
Ответить
0 Полезно
Арсений Vivat
14 декабря 2021
Не понимаю хваленых отзывов об Эсперио, типичные же мошенники с обещаниями золотых гор. Хотя похвалю их, сайт сделали добротный, очень даже правдоподобно выглядит))?
Ответить
0 Полезно
Макс
11 декабря 2021
Я начал работать с Esperio месяц назад и вот уже 2 недели воюю с ними за мой депозит, мне не хотят возвращаться мои же деньги под какими-то дурацкими предлогами. То не так, это не так, одним словом сплошная проблема! Сумма не критическая, всего сотня баксов, но знал бы чем закончится эта история, вложил бы куда нибудь в другое место.
Ответить
0 Полезно
Кирилл Мовин
09 декабря 2021
Даже не вздумайте ввязываться в эту систему. Эсперио только делает вид, что это супер компания с кучей положительных отзывов, а на самом деле это приманка.. Никаких положительных отзывов этот брокер иметь не может, они просто чистят историю негативных комментариев пострадавших людей!
Ответить
0 Полезно
Леся
06 декабря 2021
Из плюсов компании Esperio могу найти только один, это отсутствие минимального депозита, какой бы пакет ты не выбрал, тебе не придется подстраиваться под ее условия и закидывать столько, сколько можешь. А вот минусов гораздо больше, я хоть и пополнила счет на 50 долларов, но вывести их почему-то не могу, постоянно требуется дополнительная верификация, очень все сложно и проблемно. Пока не понятно, удасться ли мне вернуть депозит, но впечатление полностью испорчено
Great blog.
I think you have a typo after the implementation of “wstring_cmp_i”.
I think you wanted to say:
First step is over, we can call our myGetModuleHandleW(L”kernel32.dll”) to get the address where Kernel32.dll has been loaded.