This article was published on the 14th of July 2018.
In this practical case, a binary is analysed and altered in order to trigger 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 cannot be executed (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, meaning no debug symbols are present in the file). The output of file 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, aside from the challenge’s name.
libra@x86:~/Desktop$ ./PatchMe0x01.bin Try again!
Using strings, all human readable strings within a file are displayed. The default settings only show strings of four or more human readable characters that are sequentially present. Doing this, provides both the Try again! string and the win condition Win! string.
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 -A flag, which equals aaa. Using -AA equals aaaa, which includes the experimental analysis methods.
libra@x86:~/Desktop$ r2 -w ./PatchMe0x01.bin [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 apparent that the value of the argument count argc (saved in variable arg_4h at ESP+0x4) is saved in the count register named 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 (standard output) using the imported puts function, 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 printed with the help of the imported puts function. 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 often being the binary itself, but that depends on the used language. 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 will always fail, resulting in the Try again! message.
int a; if(argc == 0) { a = 1; if(a == 2) { printf("Win!"); } }
There are multiple options to patch the binary, since a lot of values can be altered to achieve the goal of evading both checks before the Win! condition is printed. The jne (jump not equal) instructions can be altered to be je (jump equal) 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 is given below 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 @Libranalysis.