Runtime code injection/patching using PTrace

Runtime code injection/patching using PTrace


 

Consider you want to change the runtime behavior of a program without stopping it. Linux offers one simple function for playing with processes, and it can do pretty much everything we need to do: it is called ptrace().

In order to use conveniently ptrace the following helper functions may be used:

  • Attach to a process
  • Continue execution
  • Detach from a process
  • Read data from a particular address
  • Write data to a particular address

See ptraceex.c for further info.

Resolving symbols

In order to intercept/modify one or more functions in the binary one convenient way is to use link-map. link_map is dynamic linkers internal structure with which it keeps track of loaded libraries and symbols within libraries.

It is basically a linked list. Therefore like dynamic linker does when it needs to find symbol, we can travel this list back and forth, go through each library on the list to find our symbol. The link-map can be found on the second entry of GOT (global offset table) of each object file.

The structure is the following:


struct link_map
{
    ElfW(Addr) l_addr;  /* Base address shared object is loaded */
    char *l_name;  	    /* Absolute file name object was found in.  */
    ElfW(Dyn) *l_ld;  	/* Dynamic section of the shared object.  */
    struct link_map *l_next, *l_prev; /* Chain of loaded objects.*/
};

Where:

  • l_addr: Base address where shared object is loaded. This value can also be found from /proc/<pid>/maps
  • l_name: pointer to library name in string table
  • l_ld: pointer to dynamic (DT_*) sections of shared lib
  • l_next: pointer to next link_map node
  • l_prev: pointer to previous link_map node

For symbol resolving sake the algorithm used traverse throught link_map list, comparing each l_name item until the library where the symbol is supposed to reside is found. Then move to l_ld struct and traverse through dynamic sections until DT_SYMTAB and DT_STRTAB have been found, and finally seek the symbol from DT_SYMTAB. See linkerex.c for a simple implementation of such algorithm.

Patching solutions

For patching and injecting code one simple way is to use dl-open function and force the process to load the shared library written in pure C that implements the patch.

The function to call is _dl_open() that can be found in glibc/elf/dl-open.c


void * 
    internal_function 
    _dl_open(const char *file, int mode, const void *caller);

Parameters are pretty much the same as in dlopen(), having only one 'extra' parameter *caller, which is pointer to calling routine and its not really important to us and we can safely ignore it. We will not need other dl* functions now either.

Since _dl_open() is defined as an 'internal_function' parameters are passed via registers instead of stack:


EAX = const char *file
ECX = const void *caller (we set it to NULL)
EDX = int mode (RTLD_LAZY)

The tiny .so loader code will be:


_start:	jmp string

begin:  pop eax             ; char *file
        xor ecx, ecx        ; *caller
        mov edx, 0x1        ; int mode

        mov ebx, 0x12345678 ; addr of _dl_open()
		call ebx            ; call _dl_open!
        add esp, 0x4
		
        int3                ; breakpoint

string: call begin
        db "/tmp/libpatch.so",0x00

The code is position indipendent since jmp to string loads the actual address of the file to load. The int3 after 'call' can be used to stop process execution and restore original code if necessary.

A cleaner way would be getting the registers with ptrace(pid, PTRACE_GETREGS,...) and write the parameters to user_regs_struct structure, store libpath string in the stack and inject plain int 0x80 and int3. The best way to proceed is to find the free space by examining the /proc/<pid>/maps file of the traced process. This simple function does this job:


long freespaceaddr(pid_t pid)
{
    FILE *fp;
    char filename[30];
    char line[85];
    long addr;
    char str[20];
    sprintf(filename, "/proc/%d/maps", pid);
    fp = fopen(filename, "r");
    if(fp == NULL)
        exit(1);
    while(fgets(line, 85, fp) != NULL) {
        sscanf(line, "%lx-%*lx %*s %*s %s", &addr,
               str, str, str, str);
        if(strcmp(str, "00:00") == 0)
            break;
    }
    fclose(fp);
    return addr;
}

Each line in /proc/<pid>/maps represents a mapped region of the process. An entry in /proc/<pid>/maps looks like this:


map start-mapend    protection  offset     device
inode      process file
08048000-0804d000   r-xp        00000000   03:08
66111      /opt/kde2/bin/kdeinit

The following program injects code into free space:


...
pid_t traced_process;
struct user_regs_struct oldregs, regs;
...
char insertcode[] = {}; // load/insert your code to inject HERE

ptrace(PTRACE_ATTACH, traced_process,
       NULL, NULL);
wait(NULL);
ptrace(PTRACE_GETREGS, traced_process,
       NULL, &regs);
addr = freespaceaddr(traced_process);
getdata(traced_process, addr, backup, len);
putdata(traced_process, addr, insertcode, len);
memcpy(&oldregs, &regs, sizeof(regs));
regs.eip = addr;
ptrace(PTRACE_SETREGS, traced_process,
       NULL, &regs);
ptrace(PTRACE_CONT, traced_process,
       NULL, NULL);
ptrace(PTRACE_DETACH, traced_process,
      NULL, NULL);

In order to restore the original code an int 3 instruction (breakpoint) can be inserted into the injected code such that once hit triggers the debugger (in this case our patching program).

The code above therefore can be slightly modified to fit this need:


// wait for the debugged process hit a breakpoint
wait(NULL);
printf("The process stopped, Putting back "
       "the original instructions\n");
putdata(traced_process, addr, backup, len);
ptrace(PTRACE_SETREGS, traced_process,
       NULL, &oldregs);
printf("Letting it continue with "
       "original flow\n");
ptrace(PTRACE_DETACH, traced_process,
      NULL, NULL);

 

你可能感兴趣的:(ptrace)