Author Topic: Modifying/patching existing Windows executables with NASM  (Read 14545 times)

Offline snobel

  • New Member
  • Posts: 1
Modifying/patching existing Windows executables with NASM
« on: February 13, 2012, 01:47:50 PM »
I'm doing binary modifications to an old game to make it run in widescreen modes, among other things. At first I did the patching using OllyDbg, but when the changes became complex enough to make that infeasible I looked for a better tool and found a way to do it with NASM.

Since it has worked quite well for me I'll describe it here. (Of course, if you know of a better solution, please post.) The code provided is in a 'works for me' state, and the usual disclaimers apply.

This is how it works:

- Use e.g. OllyDbg to identify the absolute addresses (in virtual address space) where modifications must be made
- Use a tool (provided below) to generate an exe-specific include file
- In the top level asm file, use a couple of macros to move in virtual address space
- Write code and insert data as usual
- Run NASM on the top level asm file. The output will be a modified but valid binary

Here's the source for a small command line tool, which can be run on an exe file to produce an include file containing the following:

- Information from the exe's PE header about the various sections it contains
- Section definitions with the vstart attribute properly set
- Macros to advance the assembly position in the virtual address space

Code: [Select]
// geninc: generate a nasm include file containing information about a win32 exe pe header,
//         and macros to facilitate binary patching of that exe using nasm.
//
//         takes the exe filename as the only argument. the include file is placed in the
//         same directory, and its name is derived from the exe name
//
//         compiled using vs2008. this code is in the public domain

#include "stdafx.h"
#include <windows.h>
#include <iostream>
#include <iomanip>
#include <fstream>
#include <string>
#include <sstream>
#include <vector>

using namespace std;

// bump micro for bugfixes etc
// bump minor if generated incs are backwards compatible with existing including files,
// bump major if not
const string app_version = "0.0.6";

const int lbl_col_width = 16;
const int inst_col_width = 12;
const int op_col_width = 32;

const string inc_ext = "_pe.inc";

struct Section_hdr {
  int number;
  string name;
  DWORD virt_size;
  DWORD virt_addr;
  DWORD raw_size;
  DWORD raw_offs;
};
 
void return_err(const string& msg, int r)
{
    cerr << "error: " << msg << endl;
    exit(r);
}

void remove_ext(string& s)
{
    int last = s.find_last_of('.');
    if (last != s.npos) {
        s = s.substr(0, last);
    }
}

void remove_path(string& s)
{
    int last = s.find_last_of("/\\");
    if (last != s.npos) {
        s = s.substr(last + 1);
    }
}

void get_cur_dir(string& dir)
{
    TCHAR cur_dir[MAX_PATH];
    if (GetCurrentDirectory(MAX_PATH, cur_dir) == 0) return_err("couldn't get current directory", 1);
    // this needs unicode disabled...
    dir = cur_dir;
}

void tolower(string& s)
{
    for (int pos = 0; pos < s.length(); ++pos) {
        s[pos] = tolower(s[pos]);
    }
}

void remove_special_chars(string& s)
{
    // replace non-alphanum chars with underscores.
    for (int i = 0; i < s.length(); ++i) {
        if (! isalnum(s[i])) {
            s[i] = '_';
        }
    }
    // remove leading and trailing underscores
    int first = s.find_first_not_of('_');
    if (first == s.npos) {
        s = "";
    }
    else {
        int last = s.find_last_not_of('_');
        s = s.substr(first, last - first + 1);
    }
    // remove consecutive underscores
    for (int i = s.length() - 2; i > 0; --i) {
        if (s[i] == '_' && s[i + 1] == '_') {
            s.erase(i, 1);
        }
    }
}

void make_inc_nm(const string& exe, string& inc)
{
    get_cur_dir(inc);
    string name = exe;
    remove_path(name);
    remove_ext(name);
    tolower(name);
    remove_special_chars(name);
    if (name == "") return_err("illegal filename", 1);
    inc.append('\\' + name + inc_ext);
}

void make_inc_guard(const string& exe, string& gd)
{
    gd = exe;
    remove_ext(gd);
    remove_path(gd);
    gd.append(inc_ext);
    remove_special_chars(gd);
    tolower(gd);
}

void write_top(ofstream& ofs, const string& nm, DWORD base)
{
    string guard;
    make_inc_guard(nm, guard);
    ofs << "; generated using version " << app_version << endl;
    ofs << endl;
    ofs << "%ifndef " << guard << endl;
    ofs << "%define " << guard << endl;
    ofs << endl;
    ofs << "%ifndef ptarget" << endl;
    ofs << "    %define ptarget " << '"' << nm << '"' << endl;
    ofs << "%endif" << endl;
    ofs << endl;
    ofs << setw(lbl_col_width) << "imagebase" << setw(inst_col_width) << "equ" << '0' << base << 'h' << endl;
    ofs << endl;
}

void write_section_info(ofstream& ofs, Section_hdr& sect)
{
    string prefix = sect.name;
    remove_special_chars(prefix);
    tolower(prefix);
    // in case a section name was all non-alphanum chars
    if (prefix == "") {
        stringstream ss;
        ss << "sect" << sect.number;
        prefix = ss.str();
    }
   
    ofs << "; " << sect.name << endl;
    ofs << setw(lbl_col_width) << "virt." + prefix << setw(inst_col_width) << "equ" << "imagebase";
    if (sect.virt_addr > 0) {
        ofs << " + 0" << sect.virt_addr << 'h';
    }
    ofs << endl;
    ofs << setw(lbl_col_width) << "raw." + prefix << setw(inst_col_width) << "equ" << '0' << sect.raw_offs << 'h' << endl;
    ofs << setw(lbl_col_width) << "rsize." + prefix << setw(inst_col_width) << "equ" << '0' << sect.raw_size << 'h' << endl;
    ofs << setw(lbl_col_width) << prefix + "_vsize" << setw(inst_col_width) << "equ" << '0' << sect.virt_size << 'h' << endl;
    ofs << setw(lbl_col_width) << prefix + "_end" << setw(inst_col_width) << "equ" << "virt." + prefix << " + " << prefix + "_vsize" << endl;
    ofs << endl;
}

void write_section_decl(ofstream& ofs, const string& sect_nm, const string& prev_nm)
{
    ofs << setw(lbl_col_width) << "" << setw(inst_col_width) << "section" << sect_nm << " vstart=virt" << sect_nm;
    if (prev_nm != "") {
      ofs << " follows=" << prev_nm;
    }
    ofs << endl;
}

void write_bottom(ofstream& ofs)
{
    ofs << endl;
    ofs << "; start in the .hdrs pseudo section" << endl;
    ofs << "                section     .hdrs" << endl;
    ofs << "%assign cur_raw raw.hdrs" << endl;
    ofs << "%assign cur_virt virt.hdrs" << endl;
    ofs << "%assign cur_rsize rsize.hdrs" << endl;
    ofs << endl;
    ofs << "; move assembly position to the start of a new section" << endl;
    ofs << "%macro va_section 1" << endl;
    ofs << "                incbin      ptarget, cur_raw + ($-$$), raw%1 - (cur_raw + ($-$$))" << endl;
    ofs << "                section     %1" << endl;
    ofs << "    %assign cur_raw  raw%1" << endl;
    ofs << "    %assign cur_virt virt%1" << endl;
    ofs << "    %assign cur_rsize rsize%1" << endl;
    ofs << "%endmacro" << endl;
    ofs << endl;
    ofs << "; move assembly position forward within the current section." << endl;
    ofs << "; use 'va_org end' at the end of the code to append the remainder of the original data" << endl;
    ofs << "%macro va_org 1" << endl;
    ofs << "    %ifidn %1, end" << endl;
    ofs << "                incbin      ptarget, cur_raw + ($-$$)" << endl;
    ofs << "    %elif %1 >= cur_virt && %1 < cur_virt + cur_rsize" << endl;
    ofs << "                incbin      ptarget, cur_raw + ($-$$), %1 - (cur_virt + ($-$$))" << endl;
    ofs << "    %else" << endl;
    ofs << "        %error address %1 out of section range" << endl;
    ofs << "    %endif" << endl;
    ofs << "%endmacro" << endl;
    ofs << endl;
    ofs << "%endif" << endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
    // this needs unicode disabled...
    string exe_nm = argv[1];
    if (exe_nm == "") return_err("error in args", 1);

    ifstream exe_file(exe_nm.c_str(), ios::in | ios::binary);
    if (! exe_file) return_err("file not found", 1);
 
    IMAGE_DOS_HEADER dos_hdr;
    exe_file.read(reinterpret_cast<char*>(&dos_hdr), sizeof(IMAGE_DOS_HEADER));
    if (dos_hdr.e_magic != IMAGE_DOS_SIGNATURE) return_err("dos signature not found", 1);

    IMAGE_NT_HEADERS nt_hdrs;
    exe_file.seekg(dos_hdr.e_lfanew);
    exe_file.read(reinterpret_cast<char*>(&nt_hdrs), sizeof(IMAGE_NT_HEADERS));
    if (nt_hdrs.Signature != IMAGE_NT_SIGNATURE) return_err("nt signature not found", 1);

    DWORD img_base = nt_hdrs.OptionalHeader.ImageBase;
    IMAGE_SECTION_HEADER cur_section;
    WORD sect_cnt = nt_hdrs.FileHeader.NumberOfSections + 1;
    vector<Section_hdr> sections(sect_cnt);

    for (int i = 1; i < sect_cnt; ++i) {
        exe_file.read(reinterpret_cast<char*>(&cur_section), sizeof(IMAGE_SECTION_HEADER));
        string name(reinterpret_cast<char*>(&cur_section.Name[0]), IMAGE_SIZEOF_SHORT_NAME);
        sections[i].number = i;
        sections[i].name = name.c_str();  // get rid of extra nul bytes in name
        sections[i].raw_offs = cur_section.PointerToRawData;
        sections[i].raw_size = cur_section.SizeOfRawData;
        sections[i].virt_addr = cur_section.VirtualAddress;
        sections[i].virt_size = cur_section.Misc.VirtualSize;
    }
    // section[0] is a pseudo section for the pe-headers
    sections[0].number = 0;
    sections[0].name = ".hdrs";  // maybe choose a 9-char name to avoid clashes?
    sections[0].raw_offs = 0;
    sections[0].raw_size = sections[1].raw_offs; // size includes alignment!?
    sections[0].virt_addr = 0;
    sections[0].virt_size = exe_file.tellg();
    if (! exe_file) return_err("failed to read file", 1);

    string inc_nm;
    make_inc_nm(exe_nm, inc_nm);
   
    ofstream inc_file(inc_nm.c_str());
    inc_file << left << hex;
    write_top(inc_file, exe_nm, img_base);
    if (! inc_file) return_err("failed to write inc file (top)", 1);
   
    for (int i = 0; i < sect_cnt; ++i) {
        write_section_info(inc_file, sections[i]);
        if (! inc_file) return_err("failed to write inc file (section info)", 1);
    }

    inc_file << "; pre-define all sections" << endl;
    for (int i = 0; i < sect_cnt; ++i) {
        string prev_name;
        if (i > 0) {
            prev_name = sections[i - 1].name;
        }
        write_section_decl(inc_file, sections[i].name, prev_name);
        if (! inc_file) return_err("failed to write inc file (section declarations)", 1);
    }

    write_bottom(inc_file);
    if (! inc_file) return_err("failed to write inc file (bottom)", 1);
   
    return 0;
}

For convenience the macros (which are always the same) are defined in the same include as the exe specific defines. The va_section macro moves to another section by using incbin to insert a chunk of the original executable. You can skip sections, but you can't move backwards, of course. Similarly the va_org macro moves forward within the current section, with "va_org end" as a special case to append the remainder of the original data after the last modification.

This example include file is generated from one of the binaries I'm working with:

Code: [Select]
; generated using version 0.0.5

%ifndef ilauncher_pe_inc
%define ilauncher_pe_inc

%ifndef ptarget
    %define ptarget "ilauncher.exe"
%endif

imagebase       equ         0400000h

; .hdrs
virt.hdrs       equ         imagebase
raw.hdrs        equ         00h
rsize.hdrs      equ         01000h
hdrs_vsize      equ         0290h
hdrs_end        equ         virt.hdrs + hdrs_vsize

; .text
virt.text       equ         imagebase + 01000h
raw.text        equ         01000h
rsize.text      equ         07000h
text_vsize      equ         06d16h
text_end        equ         virt.text + text_vsize

; .rdata
virt.rdata      equ         imagebase + 08000h
raw.rdata       equ         08000h
rsize.rdata     equ         02000h
rdata_vsize     equ         01f8ah
rdata_end       equ         virt.rdata + rdata_vsize

; .data
virt.data       equ         imagebase + 0a000h
raw.data        equ         0a000h
rsize.data      equ         01000h
data_vsize      equ         04604h
data_end        equ         virt.data + data_vsize

; .rsrc
virt.rsrc       equ         imagebase + 0f000h
raw.rsrc        equ         0b000h
rsize.rsrc      equ         07000h
rsrc_vsize      equ         06830h
rsrc_end        equ         virt.rsrc + rsrc_vsize

; pre-define all sections
                section     .hdrs vstart=virt.hdrs
                section     .text vstart=virt.text follows=.hdrs
                section     .rdata vstart=virt.rdata follows=.text
                section     .data vstart=virt.data follows=.rdata
                section     .rsrc vstart=virt.rsrc follows=.data

; start in the .hdrs pseudo section
                section     .hdrs
%assign cur_raw raw.hdrs
%assign cur_virt virt.hdrs
%assign cur_rsize rsize.hdrs

; move assembly position to the start of a new section
%macro va_section 1
                incbin      ptarget, cur_raw + ($-$$), raw%1 - (cur_raw + ($-$$))
                section     %1
    %assign cur_raw  raw%1
    %assign cur_virt virt%1
    %assign cur_rsize rsize%1
%endmacro

; move assembly position forward within the current section.
; use 'va_org end' at the end of the code to append the remainder of the original data
%macro va_org 1
    %ifidn %1, end
                incbin      ptarget, cur_raw + ($-$$)
    %elif %1 >= cur_virt && %1 < cur_virt + cur_rsize
                incbin      ptarget, cur_raw + ($-$$), %1 - (cur_virt + ($-$$))
    %else
        %error address %1 out of section range
    %endif
%endmacro

%endif

And here is the asm file, in this case the patch is minimal. (Actually it's been made more complex to serve as an example.) It modifies some existing code to change a logfile path and adds the path data in the unused space at the end of the .rdata section. Because of the added data, the .rdata section header is updated at the beginning of the file, before the assembly position is moved into the .text section:

Code: [Select]
%include        "ilauncher_pe.inc"

                bits        32

; update VirtualSize in the .rdata section header
;
                va_org      400220h
                dd          new_size_rdata

                va_section  .text

; patch: store the path string in the buffer. the patch replaces part of the
; code which would set up the path as a folder in the user's Documents folder
;
                va_org      401249h
                mov         eax, [relpath]
                mov         [edi], eax
                jmp         skip_buf        ; continue, skipping normal path setup
                nop
                nop

                va_org      40128bh
skip_buf:

                va_section  .rdata

; dword sized relative path for "launcher.log", no trailing '\'
;
                va_org      rdata_end
relpath         db          '..', 0, 0

new_size_rdata  equ         $-$$

                va_org      end

Running "nasm -o ilauncher-patched.exe ilauncher.asm" will then produce the patched executable.

The patch for the main executable contains 12 functionally separate subpatches with about 30 modifications to existing code and data, and about 5K of new code and data. So I have an include for each subpatch and use one-off macros for the various modifications, because the order in which they must be inserted is given by their absolute addresses, and modifications from different subpatches will often need to be interleaved. (The need to keep the absolute addresses manually sorted is the main caveat here.)