PE with no imports (aka Windows version by ret addr) (last update: 2012-10-20, created: 2012-10-19) back to the list ↑
A long time ago (in 2006) I experimented with an idea to create a working program (PE exe) with no imports whatsoever. The idea based on the fact that at least two DLL modules are always in memory - kernel32.dll and ntdll.dll (on Windows 7 there is also kernelbase.dll afair, and I think there is no ntdll.dll on Windows 98).



The question was - how to find the DLLs in memory and how to find the functions themselves. Well, if you've done some low level coding you probably know that there is a commonly used method to walk the loaded module list pointed to by TEB→PEB→list. However, I decided to use a different approach (that is actually way worse then the aforementioned method, but well, there is no shame in experimenting with random ideas, even poor ones).

The idea was to correlate return address found on the stack at main image (exe) entry point (the one that points to the return of the process loading/initializing procedure) with the address of LoadLibrary and the address of GetProdAddress. Please note, that these were times before ASLR on Windows, so it was a little easier.

So I created a small app that gathered this data and send it to my friends to get the addresses from the versions of Windows that they had. Here's the results I've got (most of the columns are easy to deduce; COUNT is the number of samples I've got with this result):

RET      LOADLIB  GETPROC  COUNT VERSION
77E4F38C 77E4850D 77E42DFB 1     5.2.3790
77E7EB69 77E805D8 77E7A5FD 4     5.1.2600
77E8141A 77E7D8B4 77E7B285 1     5.1.2600 Service Pack 1
77E814C7 77E7D961 77E7B332 4     5.1.2600 Dodatek Service Pack. 1
77E87903 77E98023 77E9564B 1     5.0. ??? bug?
77E962B6 77E8DF64 77E8D2D3 1     5.1.2600
77E97D08 77E8A254 77E89AC1 1     5.0.2195 Dodatek Service Pack. 2
793487F5 793505CF 7934E6A9 1     5.0.2195 Service Pack 4
7C816D4F 7C801D77 7C80AC28 8     5.1.2600 Dodatek Service Pack 2
BFF8B537 BFF776D4 BFF76DAC 1     4.10.1998
BFF8B560 BFF776D0 BFF76DA8 5     4.10.2222 A

So a couple of things can be spotted right away:

1. Even the same exact version (as in the "VERSION" column above) of the OS can have different results, e.g.:

77E7EB69 77E805D8 77E7A5FD 4     5.1.2600
77E962B6 77E8DF64 77E8D2D3 1     5.1.2600

This is because of individual patches that don't change the OS version.

2. The language version of the OS also has an impact here. E.g. there are to 5.1.2600 SP1 entries in the table, one for English version of the OS, and one for Polish:

77E8141A 77E7D8B4 77E7B285 1     5.1.2600 Service Pack 1
77E814C7 77E7D961 77E7B332 4     5.1.2600 Dodatek Service Pack. 1

So, to sum up the results:
- It's doable for a limited amount of OSes.
- If one would want the table to cover even one major version of Windows, he would have a hard time gathering the data (languages * patches * service packs * perhaps types of Windows, like "Ultimate" and "Home").

UPDATE: Ange Albertini has derived a similar method, but based on DLL timestamp instead of the return address. Pretty clever. Take a look here if you're interested.

Appendix: Executable with "concealed" imports

Download (source + exe): conceal_pe.zip (don't expect this to run on your OS; the table was gathered in 2006 and is very limited)

Source:


#define NULL 0
typedef unsigned int addr;
typedef unsigned int uint32_t;
typedef unsigned int uint16_t;

int load_imports(addr hash);

int
WinMainCRTStartup(void)
{
  addr kernel_ret = NULL;
  
  // get kernel_ret
  asm volatile(
      "movl 4(%%ebp), %%eax\n\t"
      "movl %%eax, %0"
      : "=g"(kernel_ret));

  if( load_imports(kernel_ret) != 0 )
    return 1;

  return 0;  
}

void my_strcpy(char *where, const char *what)
{
  while((*where = *what) != '\0') what++, where++;
}

void my_strcat(char *where, const char *what)
{
  while(*where != '\0') where++;
  while((*where = *what) != '\0') what++, where++;
}

int load_imports(addr hash)
{
  addr kernel32_instance = NULL;
  addr user32_instance = NULL;  
  __stdcall addr (*GetProcAddress)(addr,const char*);
  __stdcall addr (*LoadLibraryA)(const char*);  
  __stdcall addr (*MessageBoxA)(addr, const char*, const char*, uint32_t);
  char info[256];
  

  static struct {
    addr hash;
    addr loadlib;
    addr getproc;         
    const char *version;
  } *p, addr_table[] = {
    { 0x77E4F38C, 0x77E4850D, 0x77E42DFB, "5.2.3790" },
    { 0x77E7EB69, 0x77E805D8, 0x77E7A5FD, "5.1.2600" },
    { 0x77E8141A, 0x77E7D8B4, 0x77E7B285, "5.1.2600 Service Pack 1" },
    { 0x77E814C7, 0x77E7D961, 0x77E7B332, "5.1.2600 Dodatek Service Pack. 1" },
    { 0x77E87903, 0x77E98023, 0x77E9564B, "5.0.????" },
    { 0x77E962B6, 0x77E8DF64, 0x77E8D2D3, "5.1.2600" },
    { 0x77E97D08, 0x77E8A254, 0x77E89AC1, "5.0.2195 Dodatek Service Pack. 2" },
    { 0x793487F5, 0x793505CF, 0x7934E6A9, "5.0.2195 Service Pack 4" },   
    { 0x7C816D4F, 0x7C801D77, 0x7C80AC28, "5.1.2600 Dodatek Service Pack 2" },
    { 0xBFF8B537, 0xBFF776D4, 0xBFF76DAC, "4.10.1998" },
    { 0xBFF8B560, 0xBFF776D0, 0xBFF76DA8, "4.10.2222 A" },
    { NULL,       NULL,       NULL,       NULL }
  };

  /* get addresses */
  for(p = addr_table; p->hash; p++)
  {
    if(p->hash == hash)
    {
      LoadLibraryA = (void*)p->loadlib;
      GetProcAddress = (void*)p->getproc;
      break;
    }
  }

  if(p->hash == NULL)
    return -1;

  /* load lib */
  //LoadLibraryA = (void*)GetProcAddress(kernel32_instance, "LoadLibraryA");
  user32_instance = LoadLibraryA("user32.dll");
  MessageBoxA = (void*)GetProcAddress(user32_instance, "MessageBoxA");

  my_strcpy(info, "Import concealment r&d by Gynvael Coldwind of Vexillium.\n"
                  "Detected windows version ");
  my_strcat(info, p->version);

  MessageBoxA(0, info, "Import concealment", 0);  

  return 0;
}

【 design & art by Xa / Gynvael Coldwind 】 【 logo font (birdman regular) by utopiafonts / Dale Harris 】