Yesterday, Matt Green asked me to take a look one of the leaked NSA tools, SECONDDATE, to try to confirm some of its behavior. See Sam Biddle’s Intercept article for details on that.

SECONDDATE is cross-platform and is apparently designed to be loaded into some existing process’s address space. The code relies on the functionality of the host process. In an apparent attempt to simplify the construction of the code, the host functionality is captured by a table of function (and, I’m guessing, data) pointers. Calling a host function requires indexing into the table. If this sounds like calling a function in a dynamic library which could be loaded at any address, you’re right.

Calling a host function

As a simple example of calling a host function, consider the SD_getTime function which does nothing but call some function in this table. Here’s the x86 version of the function.

SD_getTime:
                push    ebx
                sub     esp, 8
                call    __i686_get_pc_thunk_bx
                add     ebx, 3EA3h
                mov     ecx, ds:(OS_VER_ptr - 0E3CCh)[ebx]
                mov     edx, [ecx]
                mov     eax, [edx]
                call    dword ptr [eax+58h]
                add     esp, 8
                pop     ebx
                retn

The equivalent C code is a little clearer, but obfuscates the mechanism.

int SD_getTime()
{
  return (*(int (**)(void))(*(_DWORD *)OS_VER + 0x58))();
}

The __i686_get_pc_thunk_bx function simply sets ebx to be the address of the following instruction which happens to be 0xA529. Now IDA has helpfully identified that this is trying to load from the address named OS_VER_ptr (located at address 0x70). And indeed, we can confirm this is the case by computing (0x70 - 0xE3CC) + 0xA529 + 0x3EA3 = 0x70. This is standard position-independent code. OS_VER_ptr is the address of a pointer to OS_VER which is itself the address of a pointer to the table of functions. Finally, 0x58 bytes into that table is the address of the host function to call.

Now I said that this is cross-platform and indeed, you can see that the PPC version is very similar.

offset_to_got2_end:.long -0xD24          # DATA XREF: SD_getTime+14r

SD_getTime:
.set var_8, -8
.set arg_4,  4

                stwu      sp, -0x18(sp)       # Store Word with Update
                mflr      r7                  # Move from link register
                bcl       20, 4*cr7+so, loc_8E88 # Branch Conditional
loc_8E88:
                stw       r30, 0x18+var_8(sp) # Store Word
                mflr      r30                 # Move from link register
                lwz       r6, -0x10(r30)      # offset_to_got_end # Load Word and Zero
                stw       r7, 0x18+arg_4(sp)  # Store Word
                add       r30, r6, r30        # Add
                lwz       r5, -0x7FFC(r30)    # 0x120
                lwz       r4, 0(r5)           # Load Word and Zero
                lwz       r3, 0(r4)           # Load Word and Zero
                lwz       r0, 0x58(r3)        # Load Word and Zero
                mtctr     r0                  # Move to count register
                bctrl                         # Branch unconditionally
                lwz       r30, 0x18+var_8(sp) # Load Word and Zero
                lwz       r4, 0x18+arg_4(sp)  # Load Word and Zero
                addi      sp, sp, 0x18        # Add Immediate
                mtlr      r4                  # Move to link register
                blr                           # Branch unconditionally

The mechanism is roughly the same: the bcl stores the address of the following instruction into the link register which is moved to r30. The offset_to_got2_end variable holds the offset to the end of the .got2 section which is added to r30. Then -0x7FFC is added to r30 and a word is loaded which happens to be the address of OS_VER.

.got2:00000120                 .long OS_VER

As before, OS_VER is loaded (into r4) which holds the address of the host function table. This is loaded into r3 and finally the word 0x58 bytes into the table is loaded into r0, moved to the count register, and called.

Setting up the host function table

The host function table must be constructed ahead of time for each operating system architecture and version. The table along with the code most be loaded into memory and the OS_VER variable needs to be set to the table’s address. This is handled by code in the .got_loader (which also fixes up the .got/.got2 references to things like PCRE tables).

The mechanism for setting OS_VER appears different for different versions of SECONDDATE. For example, in the PPC (BANANAGLEE/BG2100/Install/LP/Modules/PPC/SecondDate-2123.exe) and MIPS (BANANAGLEE/BG2100/Install/LP/Modules/MIPS/SecondDate-2122.exe) versions, the address of the table is hard coded. In the PPC version, it is set to 0x61FF8.

                bl        _dummy_load      # Branch
_dummy_load:
                mflr      r0               # Move from link register
                mr        r21, r0          # Move Register
                addi      r21, r21, -0x28  # Add Immediate
                addi      r22, r21, _end   # Add Immediate
                lis       r23, 6 # 0x61FF8 # Load Immediate Shifted
                addi      r23, r23, 0x1FF8 # 0x61FF8 # Add Immediate
                stw       r23, 4(r22)      # Store Word

(The final store is storing 0x61FF8 four bytes beyond _end which happens to be OS_VER.) In the MIPS version, it is 0x80000.

                bal     dummy            # Branch Always and Link
                nop
dummy:
                la      $s1, dummy       # Load Address
                beqz    $s1, cleanup     # Branch on Zero
                subu    $s1, $ra, $s1    # Subtract Unsigned
                la      $s3, OS_VER      # Load Address
                addu    $s3, $s1         # Add Unsigned
                lui     $s4, 8           # Load Upper Immediate
                sw      $s4, 0($s3)      # Store Word

The newer x86 version (BANANAGLEE/BG3000/Install/LP/Modules/PIX/SecondDate-3021.exe) is a little more interesting. It expects the address of the table to be the four bytes in memory directly preceding the binary.

_start:
                pusha
                call    SELF
SELF:
                pop     edx
                sub     edx, 6
                mov     eax, edx
                sub     eax, 4
                mov     ecx, eax
                mov     ds:OS_VER[edx], ecx

This code appears at the very beginning of the binary so the sub edx, 6 sets edx to the loaded address of _start. Therefore the final mov is setting OS_VER to be _start - 4.

Overall impression

Although I was only looking at this code for a very specific purpose and didn’t examine its real functionality in depth, the code quality and general design seems much better than the redirector code I was looking at two days ago.