diff --git a/src/stub.c b/src/stub.c index c162a736..09a23247 100644 --- a/src/stub.c +++ b/src/stub.c @@ -23,6 +23,16 @@ const BYTE Signature[] = { 0x41, 0xb6, 0xba, 0x4e }; #define OP_CREATE_INST_DIRECTORY 8 #define OP_MAX 9 +/** Manages digital signatures **/ + +/* see https://en.wikipedia.org/wiki/Portable_Executable for explanation of these header fields */ +#define SECURITY_ENTRY(header) ((header)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY]) + +static LPVOID ocraSignatureLocation(LPVOID, DWORD); +static PIMAGE_NT_HEADERS retrieveNTHeader(LPVOID); +static char isDigitallySigned(LPVOID); +/******************************/ + BOOL ProcessImage(LPVOID p, DWORD size); BOOL ProcessOpcodes(LPVOID* p); void CreateAndWaitForProcess(LPTSTR ApplicationName, LPTSTR CommandLine); @@ -366,13 +376,56 @@ int CALLBACK _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCm return 0; } +/* The NTHeader is another name for the PE header. It is the 'modern' executable header + as opposed to the DOS_HEADER which exists for legacy reasons */ +static PIMAGE_NT_HEADERS retrieveNTHeader(LPVOID ptr) { + PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)ptr; + + /* e_lfanew is an RVA (relative virtual address, i.e offset) to the NTHeader + to get a usable pointer we add the RVA to the base address */ + return (PIMAGE_NT_HEADERS)((DWORD)dosHeader + (DWORD)dosHeader->e_lfanew); +} + +/* Check whether there's an embedded digital signature */ +static char isDigitallySigned(LPVOID ptr) { + PIMAGE_NT_HEADERS ntHeader = retrieveNTHeader(ptr); + return SECURITY_ENTRY(ntHeader).Size != 0; +} + +/* Find the location of ocra's signature + NOTE: *not* the same as the digital signature from code signing +*/ +static LPVOID ocraSignatureLocation(LPVOID ptr, DWORD size) { + if (!isDigitallySigned(ptr)) { + return ptr + size - 4; + } + else { + PIMAGE_NT_HEADERS ntHeader = retrieveNTHeader(ptr); + DWORD offset = SECURITY_ENTRY(ntHeader).VirtualAddress - 1; + char* searchPtr = (char *)ptr; + + /* There is unfortunately a 'buffer' of null bytes between the + ocraSignature and the digital signature. This buffer appears to be random + in size, so the only way we can account for it is to search backwards + for the first non-null byte. + NOTE: this means that the hard-coded Ocra signature cannot end with a null byte. + */ + while(!searchPtr[offset]) + offset--; + + /* -3 cos we're already at the first byte and we need to go back 4 bytes */ + return (LPVOID)&searchPtr[offset - 3]; + } +} + /** Process the image by checking the signature and locating the first opcode. */ BOOL ProcessImage(LPVOID ptr, DWORD size) { - LPVOID pSig = ptr + size - 4; + LPVOID pSig = ocraSignatureLocation(ptr, size); + if (memcmp(pSig, Signature, 4) == 0) { DEBUG("Good signature found."); diff --git a/test/fake_code_signer.rb b/test/fake_code_signer.rb new file mode 100644 index 00000000..f94775ee --- /dev/null +++ b/test/fake_code_signer.rb @@ -0,0 +1,58 @@ +require File.join File.dirname(__FILE__), "fake_code_signer/pe_wrapper" + +# This class digitally signs an executable. +# In reality, it doesn't create a "valid" digital signature - but it sets up the executable +# headers correctly with the appropriate size and offsets. It also appends the "signature" to +# the end of the executable, complete with 'padding' between the end of the file and sig. +# Although this signature is not 'valid' in the sense that it meets the authenticode standard +# it still meets the expectations of Ocra which only needs the appended "signature" to match +# the size and location as specificed in the header. +# +# The purpose of this class is to create a "signed" executable so that we can test Ocra's +# ability to build executables that are resilient to authenticode code signing, i.e that +# Ocra executables still work after they've been digitally signed. +# +# For more information see: +# The PE Format deep dive: https://msdn.microsoft.com/en-us/library/ms809762.aspx +# Reverse engineering the authenticode format: https://www.cs.auckland.ac.nz/~pgut001/pubs/authenticode.txt +class FakeCodeSigner + FAKE_SIG = "fake signature" + + def initialize(input_file:, output_file:, padding: 4) + @output_file = output_file + @padding = padding + @image = File.binread(input_file) + end + + def sign + if pe_wrapper.security_size != 0 + raise "Binary already signed, nothing to do!" + end + + # Below we access an instance of the IMAGE_DATA_DIRECTORY struct. + # This instance is called IMAGE_DIRECTORY_ENTRY_SECURITY and it contains information about the digital signature + # see: https://msdn.microsoft.com/en-us/library/windows/desktop/ms680305(v=vs.85).aspx + + # write the offset (address) of the digital signature to the security header (VirtualAddress field) + pe_wrapper.security_address = @image.size + @padding + + # write the size of the digital signature to the security header (Size field) + pe_wrapper.security_size = FAKE_SIG.size + + # append the "digital signature" to the end of the executable, complete with padding + pe_wrapper.append_data(padding_string + FAKE_SIG) + + # Write out the "signed" image + File.binwrite(@output_file, pe_wrapper.to_s) + end + + private + + def padding_string + "\x0" * @padding + end + + def pe_wrapper + @pe_wrapper ||= PEWrapper.new(@image) + end +end diff --git a/test/fake_code_signer/pe_wrapper.rb b/test/fake_code_signer/pe_wrapper.rb new file mode 100644 index 00000000..b2964422 --- /dev/null +++ b/test/fake_code_signer/pe_wrapper.rb @@ -0,0 +1,126 @@ +class FakeCodeSigner + # This class exists to navigate and manipulate the data of the Windows + # executable format, PE, see: https://en.wikipedia.org/wiki/Portable_Executable + # also https://msdn.microsoft.com/en-us/library/ms809762.aspx + class PEWrapper + # size of a DWORD is 2 words (4 bytes) + DWORD_SIZE = 4 + + # struct IMAGE_DOS_HEADER + # https://www.nirsoft.net/kernel_struct/vista/IMAGE_DOS_HEADER.html + DOS_HEADER_SIZE = 64 + + # first field from IMAGE_NT_HEADERS + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms680336(v=vs.85).aspx + PE_SIGNATURE_SIZE = 4 + + # struct IMAGE_FILE_HEADER (second field from IMAGE_NT_HEADERS) + # see: https://msdn.microsoft.com/en-us/library/windows/desktop/ms680313(v=vs.85).aspx + IMAGE_FILE_HEADER_SIZE = 20 + + # struct IMAGE_OPTIONAL_HEADER + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms680339(v=vs.85).aspx + IMAGE_OPTIONAL_HEADER_SIZE = 224 + + # http://bytepointer.com/resources/pietrek_in_depth_look_into_pe_format_pt1_figures.htm + NUMBER_OF_DATA_DIRECTORY_ENTRIES = 16 + + # struct IMAGE_DATA_DIRECTORY + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms680305(v=vs.85).aspx + DATA_DIRECTORY_ENTRY_SIZE = 8 + + def initialize(image) + @image = image + end + + # set digital signature address in security header + def security_address=(offset) + @image[security_address_offset, DWORD_SIZE] = raw_bytes(offset) + end + + # set digital signature size in security header + def security_size=(size) + @image[security_size_offset, DWORD_SIZE] = raw_bytes(size) + end + + # location of the digital signature + def security_address + deref(security_address_offset) + end + + # size of the digital signature + def security_size + deref(security_size_offset) + end + + # append data to end of executable + def append_data(byte_string) + @image << byte_string + end + + # string representation of this object + def to_s + @image + end + + private + + # convert an integer to a raw byte string + def raw_bytes(int) + [int].pack("L") + end + + # security is the 4th element in the data directory array + # see http://bytepointer.com/resources/pietrek_in_depth_look_into_pe_format_pt1_figures.htm + def security_offset + image_data_directory_offset + DATA_DIRECTORY_ENTRY_SIZE * 4 + end + + alias security_address_offset security_offset + + def security_size_offset + security_offset + DWORD_SIZE + end + + # dereferences a pointer + # the only pointer type we support is an unsigned long (DWORD) + def deref(ptr) + @image[ptr, DWORD_SIZE].unpack("L").first + end + + # offset of e_lfanew (which stores the offset of the actual PE header) + # see: https://msdn.microsoft.com/en-us/library/ms809762.aspx for more info + def e_lfanew_offset + DOS_HEADER_SIZE - DWORD_SIZE + end + + # We dereference e_lfanew_offset to get the actual pe_header_offset + # the pe_header is represented by the IMAGE_NT_HEADERS struct + def pe_header_offset + deref(e_lfanew_offset) + end + + # offset of the IMAGE_OPTIONAL_HEADER struct + # IMAGE_OPTIONAL_HEADER is the third field in IMAGE_NT_HEADERS, so we add + # the size of the previous two fields to locate it. + # see: https://msdn.microsoft.com/en-us/library/windows/desktop/ms680313(v=vs.85).aspx + def image_optional_header_offset + pe_header_offset + PE_SIGNATURE_SIZE + IMAGE_FILE_HEADER_SIZE + end + + # DataDirectory is an array + # and is the last element of the IMAGE_OPTIONAL_HEADER struct + # see: https://msdn.microsoft.com/en-us/library/windows/desktop/ms680339(v=vs.85).aspx + def data_directories_array_size + DATA_DIRECTORY_ENTRY_SIZE * NUMBER_OF_DATA_DIRECTORY_ENTRIES + end + + # Since the DataDirectory is the last item in the optional header + # struct we find the DD offset by navigating to the end of the struct + # and then going back `data_directories_array_size` bytes + def image_data_directory_offset + image_optional_header_offset + IMAGE_OPTIONAL_HEADER_SIZE - + data_directories_array_size + end + end +end diff --git a/test/test_ocra.rb b/test/test_ocra.rb index c75b7dbe..1d7efa03 100644 --- a/test/test_ocra.rb +++ b/test/test_ocra.rb @@ -4,6 +4,7 @@ require "fileutils" require "rbconfig" require "pathname" +require File.join(File.dirname(__FILE__), "fake_code_signer") begin require "rubygems" @@ -749,4 +750,20 @@ def test_nonexistent_temp end end + # Test that code-signed executables still work + def test_codesigning_support + with_fixture 'helloworld' do + each_path_combo "helloworld.rb" do |script| + assert system("ruby", ocra, script, *DefaultArgs) + FakeCodeSigner.new(input_file: "helloworld.exe", + output_file: "helloworld-signed.exe", + padding: rand(20)).sign + + pristine_env "helloworld.exe", "helloworld-signed.exe" do + assert system("helloworld.exe") + assert system("helloworld-signed.exe") + end + end + end + end end