Practical case: Patch Me 0x01

In this practical case, a binary is analysed and altered in order to obtain the win-condition. This case follows up on the Conditions and loops theory. At the end of the case, the source code is provided. The binary can be downloaded here.

To obtain as much information as possible and to avoid accidental execution, the file command in GNU Linux can be used to determine the target platform for the binary. When analysing malware, it often is a good practice to work on an operating system which differs from the target operating system of the malware. If the binary is accidentally executed, it can’t execute (i.e. an ELF on Windows or a PE on Linux). Obviously, when debugging a sample, the host platform and the target platform should be the same, which is why a virtual machine is used in these cases.

Additionally, this also provides the target architecture and the way the binary is compiled (in this case, it is stripped, which has been explained in the set-up article in the GNU Compiler Collection paragraph). The output of the file command provides the insight that the binary is a stripped 32-bit ELF binary.

libra@laptop:~/Desktop$ file ./PatchMe0x01.bin
PatchMe0x01.bin: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=9c2af072758b15bb1e0f7a9d7a15e51b013ccddd, stripped

Upon executing the binary, a message is shown to the user, giving a hint on what to do, besides the name of the challenge.

libra@x86:~/Desktop$ ./PatchMe0x01.bin 
Try again!

Using strings, all human readable strings within a file, with a length of 4 or more characters, are displayed. Doing this, provides both the Try again! string and the win condition Win!.

libra@laptop:~/Desktop$ strings ./PatchMe0x01.bin 
/lib/ld-linux.so.2
libc.so.6
_IO_stdin_used
puts
__libc_start_main
__gmon_start__
GLIBC_2.0
PTRh
QVh$
UWVS
t$,U
[^_]
Try again!
Win!
;*2$"
GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609
.shstrtab
.interp
.note.ABI-tag
.note.gnu.build-id
.gnu.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rel.dyn
.rel.plt
.init
.plt.got
.text
.fini
.rodata
.eh_frame_hdr
.eh_frame
.init_array
.fini_array
.jcr
.dynamic
.got.plt
.data
.bss
.comment

A note before the analysis of this binary: it is possible to patch this binary in numerous ways, all of which provide the outcome that is required fulfill the goal of this challenge. Because this case is related to the Conditions and loops article, the conditions will be the main focus during this practical case.

First of all, Radare2 needs to open the file in the write mode, which is done by adding the “-w” flag. To automatically analyse the binary, one can use the flag “-A”, which equals “aaa”. Using “-AA” equals “aaaa”.

libra@x86:~/Desktop$ r2 -w ./PatchMe0x01.bin 
 -- Assemble opcodes with the 'a' and 'A' keys in visual mode, which are bindings to the 'wa' and 'wA' commands
[0x08048310]> aaaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Emulate code to find computed references (aae)
[x] Analyze consecutive function (aat)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)

Note how Radare2 greets the user with a tip regarding the write mode, instead of a generic welcome message. Setting the current address to the main function in the program with s (which stands for seek) allows the disassembly of said function. UsingĀ pdda theĀ r2dec plug-in output is shown directly next to the assembly, providing more insight in the assembly instructions. Do note that the analysis of the assembly is not the same as the pdf command provides. The call to a function on address 0x08048462 is still recognisable due to the call instruction, but the function has no name and is only referred to with the address. Radare2 provides a name for the function when using the pdf command.

[0x08048310]> s main
[0x08048424]> pdda
    ; assembly                            | /* r2dec pseudo C output */
                                          | #include <stdio.h>
                                          |
(fcn) main:                               | int32_t main () {
    0x08048424  lea ecx, [esp + 4]        |     ecx = arg_4h;
    0x08048428  and esp, 0xfffffff0       |
    0x0804842b  push dword [ecx - 4]      |
    0x0804842e  push ebp                  |
    0x0804842f  mov ebp, esp              |
    0x08048431  push ecx                  |
    0x08048432  sub esp, 0x14             |
    0x08048435  mov eax, ecx              |     eax = ecx;
    0x08048437  cmp dword [eax], 0        |
                                          |     if (*(eax) == 0) {
    0x0804843a  jne 0x8048462             |
    0x0804843c  mov dword [ebp - 0xc], 1  |         *(local_ch) = 1;
    0x08048443  cmp dword [ebp - 0xc], 2  |
                                          |         if (*(local_ch) == 2) {
    0x08048447  jne 0x804845b             |
    0x08048449  sub esp, 0xc              |
                                          |             /* const char *s */
    0x0804844c  push 0x804850b            |
    0x08048451  call 0x80482e0            |             puts ();
    0x08048456  add esp, 0x10             |
    0x08048459  jmp 0x8048467             |
                                          |         }
                                          |         else {
    0x0804845b  call 0x804840b            |             Try_again ();
    0x08048460  jmp 0x8048467             |
    0x08048462  call 0x804840b            |             Try_again ();
                                          |         }
                                          |     }
    0x08048467  mov eax, 0                |     eax = 0;
    0x0804846c  mov ecx, dword [ebp - 4]  |     ecx = *(local_4h);
    0x0804846f  leave                     |
    0x08048470  lea esp, [ecx - 4]        |     esp = ecx - 4;
    0x08048473  ret                       |     return eax;
                                          | }

Using the print disassembly function command pdf after selecting the main function in the binary, it becomes visible that the value of the argument count argc (saved in variable arg_4h at the address off ESP+0x4) is saved in the count register ECX.

[0x08048310]> s main
[0x08048424]> pdf
/ (fcn) main 80
|   main (int argc, char **argv, char **envp);
|           ; var unsigned int local_ch @ ebp-0xc
|           ; var int local_4h @ ebp-0x4
|           ; arg int arg_4h @ esp+0x4
|           ; DATA XREF from entry0 (0x8048327)
|           0x08048424      8d4c2404       lea ecx, [arg_4h]           ; 4
|           0x08048428      83e4f0         and esp, 0xfffffff0
|           0x0804842b      ff71fc         push dword [ecx - 4]
|           0x0804842e      55             push ebp
|           0x0804842f      89e5           mov ebp, esp
|           0x08048431      51             push ecx
|           0x08048432      83ec14         sub esp, 0x14
|           0x08048435      89c8           mov eax, ecx
|           0x08048437      833800         cmp dword [eax], 0
|       ,=< 0x0804843a      7526           jne 0x8048462
|       |   0x0804843c      c745f4010000.  mov dword [local_ch], 1
|       |   0x08048443      837df402       cmp dword [local_ch], 2     ; [0x2:4]=-1 ; 2
|      ,==< 0x08048447      7512           jne 0x804845b
|      ||   0x08048449      83ec0c         sub esp, 0xc
|      ||   0x0804844c      680b850408     push str.Win                ; 0x804850b ; "Win!" ; const char *s
|      ||   0x08048451      e88afeffff     call sym.imp.puts           ; int puts(const char *s)
|      ||   0x08048456      83c410         add esp, 0x10
|     ,===< 0x08048459      eb0c           jmp 0x8048467
|     |||   ; CODE XREF from main (0x8048447)
|     |`--> 0x0804845b      e8abffffff     call sub.Try_again_40b
|     |,==< 0x08048460      eb05           jmp 0x8048467
|     |||   ; CODE XREF from main (0x804843a)
|     ||`-> 0x08048462      e8a4ffffff     call sub.Try_again_40b
|     ||    ; CODE XREFS from main (0x8048459, 0x8048460)
|     ``--> 0x08048467      b800000000     mov eax, 0
|           0x0804846c      8b4dfc         mov ecx, dword [local_4h]
|           0x0804846f      c9             leave
|           0x08048470      8d61fc         lea esp, [ecx - 4]
\           0x08048473      c3             ret

The value in ECX value is later saved in the accumulating register EAX, which is then compared to 0 (at the address 0x08048437). If the value of EAX is not zero, the jump is taken. This jump leads to a function which is named by Radare2, since the binary is stripped. The name sub.Try_again_40b gives a clue regarding the function’s purpose. The disassembled function is given below.

[0x08048424]> s sub.Try_again_40b 
[0x0804840b]> pdf
/ (fcn) sub.Try_again_40b 25
|   sub.Try_again_40b ();
|           ; CALL XREFS from main (0x804845b, 0x8048462)
|           0x0804840b      55             push ebp
|           0x0804840c      89e5           mov ebp, esp
|           0x0804840e      83ec08         sub esp, 8
|           0x08048411      83ec0c         sub esp, 0xc
|           0x08048414      6800850408     push str.Try_again          ; 0x8048500 ; "Try again!" ; const char *s
|           0x08048419      e8c2feffff     call sym.imp.puts           ; int puts(const char *s)
|           0x0804841e      83c410         add esp, 0x10
|           0x08048421      90             nop
|           0x08048422      c9             leave
\           0x08048423      c3             ret

The string “Try again!” is pushed on the stack and is printed to the stdout using the imported function puts, after which the function returns. This seems to be the function that executes when the binary is executed.

In the main function, if the first jump is not taken, a second one appears at the address 0x08048447. If this jump is also skipped, the win condition is triggered, since the string Win! is pushed on the stack and then called with the imported function puts. The local variable local_ch is an integer, in which the value 1 is stored. In the next instruction, the value of local_ch is compared with the value 2. If the result of this comparison is not equal, the jump is taken. Since there is nothing to alter the value of the integer, this jump is always taken, resulting in a call to the Try again! method.

A quick recap to view the conditions for the win condition is given below in C. Note that the value of argc is (on nearly all systems) never zero. The total amount of arguments that have been provided to the binary are stored in this variable, with the first argument being the binary itself. Even if this binary were to run on a system in which this was not the case, the local variable a always equals 1 and is, directly after setting, compared to the value 2. Therefore, this condition always fails, resulting in the Try again! message.

    int a;
    if(argc == 0) {
        a = 1;
        if(a == 2) {
            printf("Win!");
        }
    }

To patch the binary, there are multiple options since a lot of values can be altered to achieve the goal of evading both checks before the Win! condition is printed. The jne instructions can be altered to be je instructions. Previously, the values were wrong by default. By flipping the jump operator (jumping on true instead of false and the other way around), these values now ensure the execution of the win condition.

In write mode in Radare2, the command wa (write assembly) is used to write assembly on the location that is set with s (seek).

[0x08048424]> s 0x0804843a
[0x0804843a]> wa je 0x8048462
Written 2 byte(s) (je 0x8048462) = wx 7426
[0x0804843a]> s 0x08048447
[0x08048447]> wa je 0x804845b
Written 2 byte(s) (je 0x804845b) = wx 7412

Be careful not to overwrite other instructions by providing an input that is bigger than the instruction that needs to be changed. This will result in a segmentation fault during the execution of the binary.

Executing the patched binary now yields the requested result, as can be seen below.

libra@x86:~/Desktop$ ./PatchMe0x01.bin
Win!

The source code of the PatchMe0x01 binary (in C):

#include <stdio.h>
#include <stdlib.h>
 
void fail() {
    printf("Try again!\n");
}
 
int main(int argc, char** argv) {
    if (argc == 0) {
        int x = 1;
        if (x == 2) {
            printf("Win!\n");
        } else {
            fail();
        }
    } else {
        fail();
    }
    return 0;
}

The next article regarding “Methods and macros: the call stack” can be found here!.


To contact me, you can e-mail me at [info][at][maxkersten][dot][nl], send me a PM on Reddit or DM me on Twitter @LibraAnalysis.