The delayed import-table phantomDLL opportunities

Many native OS PE files still rely on delayed imports. When APIs imported this way are called for the first time, a so-called delay load helper function is executed first – it loads the actual delayed library, resolves the address of its APIs and finally substitutes the APIs’ addresses (that point to a delay load helper function at first) with the actual API functions’ addresses, then the actual API is called. It’s a really clever mechanism that… is kinda obsolete today. Still, from this moment on, any calls to any of the delayed APIs will be redirected to the already resolved APIs addresses.

As expected, some of the DLLs listed as delayed imports by OS EXEs and DLLs are actually not present on the system. This is an opportunity. I originally wanted to research a bit more before publishing anything, but then sixtyvividtails killed it with this comment:

I love this comment, because it demonstrates not only this individual’s in-depth knowledge of the OS but also a good nose for ‘the research opportunities’…

So, since the cat is out of the bag, let’s quickly assess what that means…

There is a small number of EXEs and DLLs (tested on windows 11 23H2) that reference delayed imports pointing to DLLs that do not exist.

For instance, UPPrinterInstaller.exe includes delay-imported APi set (FindLivePdmPrinterById, SavePdmPrinter, RemovePdmPrinterById) from a non-existing PdmUtilities.dll.

Analyzing the logic of the program, we can see that:

  • it requires exactly 10 arguments (unlike 8 arguments that are presented as an example on Microsoft pages); in essence, the win11 versions of UPPrinterInstaller.exe require additional (on top of 8 already described) argument of usersid/sid f.ex:
"C:\Windows\System32\UPPrinterInstaller.exe" -i -psi 12345678-89ab-cdef-0123-456789abcdef -sid 1234 -oai 12345678-89ab-cdef-0123-456789abcdef -cri 12345678-89ab-cdef-0123-456789abcdef
  • it requires some Registry settings to be present for the given printer’s shared ID (fake here, just for the test):

Yet it still won’t load that precious delay-imported code, because on your typical desktop or server OS instance a call to RtlGetDeviceFamilyInfoEnum that this program does (after checking all the Registry business I faked above) does not return 16 (DEVICEFAMILYINFOENUM_WINDOWS_CORE), but 3 (DEVICEFAMILYINFOENUM_DESKTOP) or 9 (DEVICEFAMILYINFOENUM_SERVER).

Executable files that delay-load phantom DLLs are at least approachable, because we can run them in the best possible conditions that we can create/optimize by creating necessary files, registry entries, etc.

What about DLLs importing delayed PhantomDLLs?

After looking at a few, I am pessimistic. The delay-loaded code usually requires a lot of conditions to be met before it can be reached via typical code paths exposed by the importing DLLs. As such, these are not very promising avenues. And after looking at it for far too long, I got a bit bored and am officially throwing a towel 🙂

Subfrida v0.1

As many of you know, I am a big fan of Frida framework and I love its intuitiveness and flexibility, especially when it comes to auto-generating handlers for hooked functions, even if they are randomly chosen.

In my older Frida Delphi project I focused on functions that I could define. Today, I will focus on functions that are unknown.

How?

We are going to write an IdaPython script that will generate simple logging/tracing function stubs for all the subroutines that IDA ‘sees’ inside the executable.

When you load any executable into IDA it parses the analyzed program’s segments, recognizes the code, and… in it – many functions. We don’t really know or care what they do, other than being aware that they exist. FLIRT signatures help in recognizing some, but it is non-trivial, as well.

So, the value-proposition here is that we will try to use Frida to run the program and log calls to every subroutine ‘discovered’ or ‘recognized’ by IDA, and print out the strings that subroutine arguments may point to when the function is executed — for this exercise we will try to log ANSI and WIDE strings potentially passed to these functions, and strings delivered in their output.

Why?

This may help us to quickly understand the inner-workings of the program, and in some lucky cases extract IOCs, and overall, help in reverse engineering efforts. Especially for samples that are written in modern languages like Rust, Go, Nim.

The idea sounds great, but there is a problem. One that I don’t know how to solve, but by publishing my partial research, I hope someone more knowledgeable will help me to address… The problem is that any error in your OnEnter or OnLeave Frida handler function forces the script to bail out.

It’s a pity.

My ‘original’ code for this exercise looked like this:

import os
import shutil
import idautils
import idaapi
import idc
import re

idf = idc.get_idb_path()

print ("Original IDA File: %s" % idf)

m = re.match(r"\.idb", idf)

arch = 0
if m:
   arch = 32
   print ("- 32-bit")
else:
   arch = 64
   print ("- 64-bit")

if arch == 32:
   idf = idf.replace('.idb','.frida')
else:
	 idf = idf.replace('.i64','.frida')

print ("Output idf: %s" % idf)

filename=re.sub(r"\.frida", "", re.sub(r"^.+[\\/]", "", idf))
handlers=re.sub(r"[^\\/]+$", "", idf) + "__handlers__" + "/" + filename + "/"

if os.path.isdir(handlers):
	 print ("Deleting old handlers directory: %s" % handlers)
	 shutil.rmtree(handlers)

os.mkdir(handlers)

print ("Saving frida input file to '%s'" % idf)
print ("Saving '%s' handlers to '%s'" % (filename, handlers) )
g = open(idf, 'w')
base = idaapi.get_imagebase()
for f in idautils.Functions():
    dism_addr = list(idautils.FuncItems(f))
    ofs = "%X"%(dism_addr[0]-base)
    g.write ("-a %s!0x%s\n" % (filename, ofs))
    h = open(handlers + "/" + "sub_"+ofs+".js", 'w')
    h.write("""
{

  onEnter(log, args, state) {
    out = 'onenter: """+ofs+"""\\n'

    log(out)

    for (i = 0; i < 4; i++)
    {
       if (args[i]>0)
       {
          console.log(args[i].readUtf8String());
          console.log(args[i].readUtf16String());
          a = args[i].readUtf8String(256)
          if (a > 0)
          {
             out = out + ' [' + i + ']a ' + JSON.stringify(a) + '\\n'
          }
          w = args[i].readUtf16String(256)
          if (w > 0)
          {
             out = out + ' [' + i + ']w ' + JSON.stringify(w) + '\\n'
          }
       }
       this.args [i] = args [i]
    }

    if (typeof state ['log_file'] === 'undefined' || state ['log_file'] === null)
    {
        state ['log_file']=new File('logfile.bin', 'wb');
    }

    if (! (typeof state ['log_file'] === 'undefined' || state ['log_file'] === null) )
    {
        state ['log_file'].write(out);
        state ['log_file'].flush();
    }

  },

  onLeave(log, retval, state) {
    out = 'onenter: """+ofs+"""\\n'

    log(out)

    for (i = 0; i < 4; i++)
    {
       if (this.args[i]>0)
       {
          console.log(this.args[i].readUtf8String());
          console.log(this.args[i].readUtf16String());
          a = this.args[i].readUtf8String(256)
          if (a > 0)
          {
             out = out + ' [' + i + ']a ' + JSON.stringify(a) + '\\n'
          }
          w = this.args[i].readUtf16String(256)
          if (w > 0)
          {
             out = out + ' [' + i + ']w ' + JSON.stringify(w) + '\\n'
          }
       }
    }

    if (typeof state ['log_file'] === 'undefined' || state ['log_file'] === null)
    {
        state ['log_file']=new File('logfile.bin', 'wb');
    }

    if (! (typeof state ['log_file'] === 'undefined' || state ['log_file'] === null) )
    {
        state ['log_file'].write(out);
        state ['log_file'].flush();
    }
  }
}
    """)
    h.close()


g.close()

When executed in a Windows IDA the code generates:

  • a .frida file with a list of RVA addresses for frida-trace to intercept
  • a list of generic handlers and their code for all these subroutines that simply try to log 4 first arguments passed to these functions – both at the entry point, and the function return.

Unfortunately, Frida is very sensitive and any error during processing of these handlers forces a bail out :(.

So, after toying around with different variations of this, and similar code, I came up with this dumb script:

import os
import shutil
import idautils
import idaapi
import idc
import re

idf = idc.get_idb_path()

print ("Original IDA File: %s" % idf)

m = re.match(r"\.idb", idf)

arch = 0
if m:
   arch = 32
   print ("- 32-bit")
else:
   arch = 64
   print ("- 64-bit")

if arch == 32:
   idf = idf.replace('.idb','.frida')
else:
	 idf = idf.replace('.i64','.frida')

print ("Output idf: %s" % idf)

filename=re.sub(r"\.frida", "", re.sub(r"^.+[\\/]", "", idf))
handlers=re.sub(r"[^\\/]+$", "", idf) + "__handlers__" + "/" + filename + "/"

if os.path.isdir(handlers):
	 print ("Deleting old handlers directory: %s" % handlers)
	 shutil.rmtree(handlers)

os.mkdir(handlers)

print ("Saving frida input file to '%s'" % idf)
print ("Saving '%s' handlers to '%s'" % (filename, handlers) )
g = open(idf, 'w')
base = idaapi.get_imagebase()
for f in idautils.Functions():
    dism_addr = list(idautils.FuncItems(f))
    ofs = "%X"%(dism_addr[0]-base)
    g.write ("-a %s!0x%s\n" % (filename, ofs))
    h = open(handlers + "/" + "sub_"+ofs+".js", 'w')
    h.write("""
{

  onEnter(log, args, state) {
    out = 'onenter: """+ofs+"""\\n'
    log(out)

    for (i = 0; i < 4; i++)
    {
       console.log(' - '+ args[i] + 'a->' + args[i].readUtf8String()+'\\n');
       console.log(' - '+ args[i] + 'w->' + args[i].readUtf16String()+'\\n');
       this.args [i] = args [i]
    }
  },

  onLeave(log, retval, state) {
    out = 'onenter: """+ofs+"""\\n'
    log(out)
    for (i = 0; i < 4; i++)
    {
       console.log(' - '+ this.args[i] + 'a->' + this.args[i].readUtf8String()+'\\n');
       console.log(' - '+ this.args[i] + 'w->' + this.args[i].readUtf16String()+'\\n');
    }

  }
}
    """)
    h.close()


g.close()

It at least populates the console.log file with anything that may be of interest and we can grep, rg it to our liking…