Morten's Dev

C++, Cybersecurity, Python, Rust, Emacs, Clang, and other technobabble..

Patching Binaries: Jumping The Fence


Series:
  1. Patching Binaries: Strings
  2. Patching Binaries: Jumping The Fence
  3. Patching Binaries: Live Modification

Patching Binaries is a series of articles about how to extract information and modify program behavior. It focuses on the Mac Mach-O executable format for the x86-64 architecture, but the techniques are similar for other formats.

Before proceeding it is recommended reading the first part about the fundamentals of finding and changing constant strings in binaries.

The files used in this article can be found here.

An approach to change the behavior of a program is to jump or bypass instructions. Or in other words: jumping the fence!

Compile and strip example program “verify.cc” as “verify” and run it:

% ./verify
Serial number: 83
Incorrect!

% ./verify
Serial number: 90
Correct!

If we disregard knowing from the source code how the numbers are verified, we would like to somehow bypass the check so that all inputs are deemed correct. There are numerous ways of doing this but the easiest is putting NOP (No Operation - opcode 0x90) instructions at tactical places in the program. They are a means of masking out functionality because they simply do nothing.

First we need to find out where to place the NOPs. And as a preliminary deviation we will look at how to determine a program’s entry point address; where the first program instruction resides. When dealing with stripped executables we can’t break at main anymore.

In Mach-O binaries the program instructions are located in section __text of segment __TEXT:

% otool -s __TEXT __text verify | head -n 3
verify:
Contents of (__TEXT,__text) section
00000001000010c0    55 48 89 e5 48 83 ec 30 48 8b 3d 39 0f 00 00 48

To easily retrieve the entry point and start LLDB with a break point on that address, take a look at “lldb_entrypoint.sh”:

#!/bin/sh
ADDR=`otool -s __TEXT __text $1 | head -n 3 | tail -n 1 | awk '{print $1;}'`
echo "b 0x${ADDR}" > /tmp/.lldbcmds
lldb -s /tmp/.lldbcmds $1

With that let’s start up the debugger with our program and reach the break point:

% ./lldb_entrypoint.sh verify
(lldb) target create "verify"
Current executable set to 'verify' (x86_64).
(lldb) command source -s 0 '/tmp/.lldbcmds'
Executing commands in '/tmp/.lldbcmds'.
(lldb) b 0x00000001000010c0
Breakpoint 1: address = 0x00000001000010c0
(lldb) r
Process 88913 launched: '/Users/netrom/git/patching/jumps/verify' (x86_64)
Process 88913 stopped
* thread #1: tid = 0x233953, 0x00000001000010c0 verify`___lldb_unnamed_symbol1$$verify, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x00000001000010c0 verify`___lldb_unnamed_symbol1$$verify
verify`___lldb_unnamed_symbol1$$verify:
->  0x1000010c0 <+0>: pushq  %rbp
    0x1000010c1 <+1>: movq   %rsp, %rbp
    0x1000010c4 <+4>: subq   $0x30, %rsp
    0x1000010c8 <+8>: movq   0xf39(%rip), %rdi         ; (void *)0x00007fff78ba32f8: std::__1::cout
(lldb)

The next step is to disassemble to get an overview:

(lldb) dis
verify`___lldb_unnamed_symbol1$$verify:
->  0x1000010c0 <+0>:   pushq  %rbp
    0x1000010c1 <+1>:   movq   %rsp, %rbp
    0x1000010c4 <+4>:   subq   $0x30, %rsp
    0x1000010c8 <+8>:   movq   0xf39(%rip), %rdi         ; (void *)0x00007fff78ba32f8: std::__1::cout
    0x1000010cf <+15>:  leaq   0xe86(%rip), %rsi         ; "Serial number: "
    0x1000010d6 <+22>:  movl   $0x0, -0x4(%rbp)
    0x1000010dd <+29>:  callq  0x100001150               ; ___lldb_unnamed_symbol2$$verify
    0x1000010e2 <+34>:  movq   0xf17(%rip), %rdi         ; (void *)0x00007fff78ba3250: std::__1::cin
    0x1000010e9 <+41>:  leaq   -0x8(%rbp), %rsi
    0x1000010ed <+45>:  movq   %rax, -0x10(%rbp)
    0x1000010f1 <+49>:  callq  0x100001d74               ; symbol stub for: std::__1::basic_istream<char, std::__1::char_traits<char> >::operator>>(int&)
    0x1000010f6 <+54>:  movl   -0x8(%rbp), %edi
    0x1000010f9 <+57>:  movq   %rax, -0x18(%rbp)
    0x1000010fd <+61>:  callq  0x1000011a0               ; ___lldb_unnamed_symbol3$$verify
    0x100001102 <+66>:  testb  $0x1, %al
    0x100001104 <+68>:  jne    0x10000110f               ; <+79>
    0x10000110a <+74>:  jmp    0x10000112b               ; <+107>
    0x10000110f <+79>:  movq   0xef2(%rip), %rdi         ; (void *)0x00007fff78ba32f8: std::__1::cout
    0x100001116 <+86>:  leaq   0xe4f(%rip), %rsi         ; "Correct!\n"
    0x10000111d <+93>:  callq  0x100001150               ; ___lldb_unnamed_symbol2$$verify
    0x100001122 <+98>:  movq   %rax, -0x20(%rbp)
    0x100001126 <+102>: jmp    0x100001142               ; <+130>
    0x10000112b <+107>: movq   0xed6(%rip), %rdi         ; (void *)0x00007fff78ba32f8: std::__1::cout
    0x100001132 <+114>: leaq   0xe3d(%rip), %rsi         ; "Incorrect!\n"
    0x100001139 <+121>: callq  0x100001150               ; ___lldb_unnamed_symbol2$$verify
    0x10000113e <+126>: movq   %rax, -0x28(%rbp)
    0x100001142 <+130>: xorl   %eax, %eax
    0x100001144 <+132>: addq   $0x30, %rsp
    0x100001148 <+136>: popq   %rbp
    0x100001149 <+137>: retq
    0x10000114a <+138>: nopw   (%rax,%rax)
(lldb)

The verify function is called at 0x1000010fd if afterwards it jumps to 0x10000110f then the number is verified correctly, and if not it jumps to 0x10000112b. We still want it to ask for a number but completely skip the verification. The crude way is to insert NOPs from 0x1000010f6 to 0x10000110f because it will arrive straight at the part where it prints "Correct!". That means 25 NOPs.

I created the “nop.py” script to insert NOPs:

% ./nop.py verify 0x10f6 25
Patching "verify" at offset 4342 with 25 NOPs
Read 14416 bytes
Changing values at 4342 to 4367
Writing new data

% ./verify
Serial number: 83
Correct!

Thus 83 is now deemed correct!

Let’s take a look at the actual contents of the binary now:

(lldb) dis
verify`___lldb_unnamed_symbol1$$verify:
->  0x1000010c0 <+0>:   pushq  %rbp
    0x1000010c1 <+1>:   movq   %rsp, %rbp
    0x1000010c4 <+4>:   subq   $0x30, %rsp
    0x1000010c8 <+8>:   movq   0xf39(%rip), %rdi         ; (void *)0x00007fff78ba32f8: std::__1::cout
    0x1000010cf <+15>:  leaq   0xe86(%rip), %rsi         ; "Serial number: "
    0x1000010d6 <+22>:  movl   $0x0, -0x4(%rbp)
    0x1000010dd <+29>:  callq  0x100001150               ; ___lldb_unnamed_symbol2$$verify
    0x1000010e2 <+34>:  movq   0xf17(%rip), %rdi         ; (void *)0x00007fff78ba3250: std::__1::cin
    0x1000010e9 <+41>:  leaq   -0x8(%rbp), %rsi
    0x1000010ed <+45>:  movq   %rax, -0x10(%rbp)
    0x1000010f1 <+49>:  callq  0x100001d74               ; symbol stub for: std::__1::basic_istream<char, std::__1::char_traits<char> >::operator>>(int&)
    0x1000010f6 <+54>:  nop
    0x1000010f7 <+55>:  nop
    0x1000010f8 <+56>:  nop
    ...
    0x10000110c <+76>:  nop
    0x10000110d <+77>:  nop
    0x10000110e <+78>:  nop
    0x10000110f <+79>:  movq   0xef2(%rip), %rdi         ; (void *)0x00007fff78ba32f8: std::__1::cout
    0x100001116 <+86>:  leaq   0xe4f(%rip), %rsi         ; "Correct!\n"
    0x10000111d <+93>:  callq  0x100001150               ; ___lldb_unnamed_symbol2$$verify
    0x100001122 <+98>:  movq   %rax, -0x20(%rbp)
    0x100001126 <+102>: jmp    0x100001142               ; <+130>
    0x10000112b <+107>: movq   0xed6(%rip), %rdi         ; (void *)0x00007fff78ba32f8: std::__1::cout
    0x100001132 <+114>: leaq   0xe3d(%rip), %rsi         ; "Incorrect!\n"
    0x100001139 <+121>: callq  0x100001150               ; ___lldb_unnamed_symbol2$$verify
    0x10000113e <+126>: movq   %rax, -0x28(%rbp)
    0x100001142 <+130>: xorl   %eax, %eax
    0x100001144 <+132>: addq   $0x30, %rsp
    0x100001148 <+136>: popq   %rbp
    0x100001149 <+137>: retq
    0x10000114a <+138>: nopw   (%rax,%rax)

The 25 NOPs are easily spotted.

Recompile and strip the “verify” program again to get rid of the NOPs.

Another approach is to insert a jump instruction that takes us directly to 0x10000110f. We want to insert the jump at 0x1000010f6 where we have 3 bytes of room to change, so a relative jump would count from the next instruction (after inserting) to 0x10000110f. Since we are within a one-byte short jump distance of +/-128 we can use a JMP rel8 with jump instruction opcode 0xeb (for other jump instructions take a look here). This leaves one byte dangling which we will fill with a NOP. However, this effectively will change the address of the next instruction after our JMP to be 0x1000010f8. The distance from that to 0x10000110f is 0x17=23, which means we will insert the bytes eb 17 90 at 0x1000010f6.

I wrote another script, “patch_bytes.py”, that will allow us to insert those bytes:

% ./patch_bytes.py verify 0x10f6 0xeb 0x17 0x90
Patching "verify" at offset 4342 with: [235, 23, 144] (3 bytes)
Read 14416 bytes
Changing values at 4342 to 4345
Writing new data

% ./verify
Serial number: 83
Correct!

Let’s take a look at the resulting binary:

(lldb) dis -b
verify`___lldb_unnamed_symbol1$$verify:
->  0x1000010c0 <+0>:   55                    pushq  %rbp
    0x1000010c1 <+1>:   48 89 e5              movq   %rsp, %rbp
    ...
    0x1000010f6 <+54>:  eb 17                 jmp    0x10000110f               ; <+79>
    0x1000010f8 <+56>:  90                    nop
    0x1000010f9 <+57>:  48 89 45 e8           movq   %rax, -0x18(%rbp)
    ...
    0x10000110f <+79>:  48 8b 3d f2 0e 00 00  movq   0xef2(%rip), %rdi         ; (void *)0x00007fff78ba32f8: std::__1::cout
    0x100001116 <+86>:  48 8d 35 4f 0e 00 00  leaq   0xe4f(%rip), %rsi         ; "Correct!\n"
    0x10000111d <+93>:  e8 2e 00 00 00        callq  0x100001150               ; ___lldb_unnamed_symbol2$$verify
    0x100001122 <+98>:  48 89 45 e0           movq   %rax, -0x20(%rbp)
    0x100001126 <+102>: e9 17 00 00 00        jmp    0x100001142               ; <+130>
    0x10000112b <+107>: 48 8b 3d d6 0e 00 00  movq   0xed6(%rip), %rdi         ; (void *)0x00007fff78ba32f8: std::__1::cout
    0x100001132 <+114>: 48 8d 35 3d 0e 00 00  leaq   0xe3d(%rip), %rsi         ; "Incorrect!\n"
    0x100001139 <+121>: e8 12 00 00 00        callq  0x100001150               ; ___lldb_unnamed_symbol2$$verify
    0x10000113e <+126>: 48 89 45 d8           movq   %rax, -0x28(%rbp)
    0x100001142 <+130>: 31 c0                 xorl   %eax, %eax
    0x100001144 <+132>: 48 83 c4 30           addq   $0x30, %rsp
    0x100001148 <+136>: 5d                    popq   %rbp
    0x100001149 <+137>: c3                    retq
    0x10000114a <+138>: 66 0f 1f 44 00 00     nopw   (%rax,%rax)

Take a look in particular at the following to view our inserted instructions:

0x1000010f6 <+54>:  eb 17                 jmp    0x10000110f               ; <+79>
0x1000010f8 <+56>:  90                    nop

Continue to read the third part about changing values in a program, and live modification of the memory of a running program.

Next in series:
Patching Binaries: Live Modification from Wed 07 September 2016 How to do modification of running programs

Related Posts