The “Practical binary analysis” book
I’ve only reached chapter 5, but so far this book is awesome!
After an introduction to the whole compilation process, a detailed walk-through of the ELF format, a primer on the PE Windows format, how to write your binary tools with libbfd and a step-by-step introduction to the first level of a CTF, up to the reader to get his hands dirty by tackling the next levels of the CTF.
A VM (Virtualbox) image is even provided to be able to enjoy the challenge safely.
This post is about my findings for the levels 2, 3 and 4 during the covid19 lock-down -_-
More to come!
The CTF challenge
The Capture The Flag challenge offered in the book consists of finding a hidden flag (a string) in a binary, without access to its source code, by using reverse engineering techniques.
Once discovered, the flag unlocks the next levels and so on and so forth.
Only basics tools like a hexeditor, gdb, objdump, nm, readelf, strings will be used, and not more complex tools like IDA, Ghidra or Binary Ninja to be sure to understand the basics first.
Everything has been completed on a Kali Linux VM or on the Linux VM provided by the book author.
The flag?
At the end of the chapter 5 (walk-through of the initial level of the CTF) we are given the flag to be fed to the provided oracle binary, that will in turn generate the next level binaries, namely lvlXX.
The goal is to find for every level the flag to unlock the next level, using oracle <flag> that will:
- validate the flag is the expected one for the level
- generate the next level binary
Example with the level 1: the flag 84b34c124b2ba5ca224af8e33b077e9e has been found after the lvl1 binary analysis, we feed it to oracle that confirms the flag is correct
$ ./oracle 84b34c124b2ba5ca224af8e33b077e9e
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Level 1 completed, unlocked lvl2 |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
Run oracle with -h to show a hint
A new binary lvl2 has been generated waiting for our investigation :)
Level 2
When executed (remember, in a VM) a different hexadecimal number is generated on every execution, with sometimes some repetitions (time based?):
$ ./lvl2
6c
$ ./lvl2
6c
$ ./lvl2
d3
$ ./lvl2
03
$ ./lvl2
df
$ ./lvl2
df
$ ./lvl2
a5
These strings can be found in the .rodata section of the binary:
$ objdump -s --section .rodata ./lvl2
./lvl2: file format elf64-x86-64
Contents of section .rodata:
4006c0 01000200 30330034 66006334 00663600 ....03.4f.c4.f6.
4006d0 61350033 36006632 00626600 37340066 a5.36.f2.bf.74.f
4006e0 38006436 00643300 38310036 63006466 8.d6.d3.81.6c.df
4006f0 00383800 .88.
If combined all together the resulting string would be 32 characters long which matches the expected flag length, so as a first try I’ll concatenate them in order of appearance in the binary:
$ ./oracle 034fc4f6a536f2bf74f8d6d3816cdf88
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Level 2 completed, unlocked lvl3 |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
Run oracle with -h to show a hint
That’s it!
Level 3
The binary can’t be executed on my VM because of an architecture mismatch; my VM system architecture being x86-64:
$ ./lvl3
-bash: ./lvl3: cannot execute binary file: Exec format error
$ file lvl3
lvl3: ERROR: ELF 64-bit LSB executable, Motorola Coldfire, version 1 (Novell Modesto) error reading (Invalid argument)
Manage to run this binary on a Motorola Coldfire system is very likely a waste of time. I think it worth trying first to patch the ELF header to make it behave like a x86-64 binary that can be executed in the VM.
There is more: readlelf shows that at least the binary ELF header is somehow corrupted:
$ readelf -h lvl3
ELF Header:
Magic: 7f 45 4c 46 02 01 01 0b 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: Novell - Modesto
ABI Version: 0
Type: EXEC (Executable file)
Machine: Motorola Coldfire
Version: 0x1
Entry point address: 0x4005d0
Start of program headers: 4022250974 (bytes into file)
Start of section headers: 4480 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
readelf: Error: Reading 0x1f8 bytes extends past end of file for program headers
I suppose the ELF header has been corrupted on purpose: given the lvl3 binary is only 6.2kb long, the Start of program headers: 4022250974 (bytes into file) entry is definitively not possible.
Using an ELF header reference to get the offsets and fields lengths, with bvi (binary editor) I am going to patch the appropriate fields of the ELF header:
- e_ident[EI_OSABI]
from
0x0b
(Novell Modesto) to0x00
(System V) - e_machine
from
0x34
(Motoroal Coldfire) to0x3e
(amd64) - e_phoff
from
0xdeadbeef
(note the reference) to0x40
(64 bytes) because here the program headers table follows directly the file header
Notes:
- when reading the ELF header reference - the offset/size to pick are the 64-bits ones
- in bvi:
- move to the field offset (leftmost number on bottom right)
- type ‘r’ to replace a single byte (keep in mind the little endian format)
- save and exit by typing ‘:x’
The patched file header now reads as:
$ readelf -h ./lvl3
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4005d0
Start of program headers: 64 (bytes into file)
Start of section headers: 4480 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
It can be executed, but the flag is wrong:
$ ./lvl3
0e2ada7381d04d4d2ed31be82b121aa3 ./lvl3
$ ./oracle 0e2ada7381d04d4d2ed31be82b121aa3
Invalid flag: 0e2ada7381d04d4d2ed31be82b121aa3
The investigation needs to be pushed further, looking at the .rodata section of the binary gives us a hint:
$ objdump -s --section .rodata ./lvl3
./lvl3: file format elf64-x86-64
Contents of section .rodata:
400750 01000200 6d643573 756d2000 ....md5sum .
Maybe the flag is the md5 checksum of the flag just found? It isn’t:
$ echo "0e2ada7381d04d4d2ed31be82b121aa3"|md5sum
f37e16344d2f01e876da023a0b60f82a
$ ./oracle f37e16344d2f01e876da023a0b60f82a
Invalid flag: f37e16344d2f01e876da023a0b60f82a
Using ltrace to check what happen under the hood:
$ ltrace -f ./lvl3
[pid 9006] __libc_start_main(0x400550, 1, 0x7fffa04dc178, 0x4006d0 <unfinished ...>
[pid 9006] __strcat_chk(0x7fffa04dbc70, 0x400754, 1024, 0) = 0x7fffa04dbc70
[pid 9006] __strncat_chk(0x7fffa04dbc70, 0x7fffa04dd69d, 1016, 1024) = 0x7fffa04dbc70
[pid 9006] system("md5sum ./lvl3" <no return ...>
[pid 9007] --- Called exec() ---
[pid 9008] --- Called exec() ---
[pid 9008] __libc_start_main(0x401750, 2, 0x7fff2d59e608, 0x4066b0 <unfinished ...>
[pid 9008] strrchr("md5sum", '/') = nil
[pid 9008] setlocale(LC_ALL, "") = nil
[pid 9008] bindtextdomain("coreutils", "/usr/share/locale") = "/usr/share/locale"
[pid 9008] textdomain("coreutils") = "coreutils"
[pid 9008] __cxa_atexit(0x402e60, 0, 0, 0x736c6974756572) = 0
[pid 9008] setvbuf(0x7f4d140bc620, 0, 1, 0) = 0
[pid 9008] getopt_long(2, 0x7fff2d59e608, "bctw", 0x407280, nil) = -1
[pid 9008] fopen("./lvl3", "r") = 0x1bf85b0
[pid 9008] fileno(0x1bf85b0) = 3
[pid 9008] fileno(0x1bf85b0) = 3
[pid 9008] posix_fadvise(3, 0, 0, 2) = 0
[pid 9008] malloc(32840) = 0x1bfa0d0
[pid 9008] fread_unlocked(0x1bfa0d0, 1, 32768, 0x1bf85b0) = 6336
[pid 9008] free(0x1bfa0d0) = <void>
[pid 9008] fileno(0x1bf85b0) = 3
[pid 9008] __freading(0x1bf85b0, 0, 0, 0) = 1
[pid 9008] fileno(0x1bf85b0) = 3
[pid 9008] lseek(3, 0, 1) = 6336
[pid 9008] __freading(0x1bf85b0, 0, 1, -1) = 1
[pid 9008] fflush(0x1bf85b0) = 0
[pid 9008] fclose(0x1bf85b0) = 0
[pid 9008] strchr("./lvl3", '\\') = nil
[pid 9008] strchr("./lvl3", '\n') = nil
[pid 9008] __printf_chk(1, 0x4068df, 14, 0x1bf87d0)
What we got here is that our lvl3 binary executes the md5sum binary on itself through system() , which is easy to confirm:
$ md5sum lvl3
0e2ada7381d04d4d2ed31be82b121aa3 lvl3
So there is something else that needs to be modified on the binary itself, in order the get the expected md5sum, i.e. the flag to unlock the next level.
What strikes me a bit is the absence of the .text section when disassembling the file:
$ objdump -d -Mintel lvl3|grep section
Disassembly of section .init:
Disassembly of section .plt:
Disassembly of section .plt.got:
Disassembly of section .fini:
Can readelf tell us more?
$ readelf -S lvl3|grep text
[14] .text NOBITS 0000000000400550 00000550
The .text section is here!
But with an incorrect type! It should be PROGBITS instead of NOBITS since it contains executable code.
This is why objdump skipped it during the disassembly earlier.
In an ELF section header this is the sh_type
field, which should be set to 0x1
SHT_PROGBITS
. This will be a bit more challenging to patch with bvi, but I’ll try anyway before resorting to develop a program with libbfd.
First we need to get the offset from the start of the file to the .text section header:
(output trimmed for readability)
$ readelf -S --wide lvl3
There are 29 section headers, starting at offset 0x1180:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
(...)
[14] .text NOBITS 0000000000400550 000550 0001f2 00 AX 0 0 16
(...)
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
What is relevant to us:
-
There are 29 section headers, starting at offset 0x1180:
-
The .text section is the 14th entry within all section headers- [14] hex value: 0xe
Given that a section header is 64 bytes long (hex 0x40), the .text offset from the start of the file will be:
0x1180 (section headers offset) + 0xe * 0x40 = 0x1500
Start the bvi editor with such offset:
$ bvi -s 0x1500 ./lvl3
We patch the current value 0x8
NOBITS
to 0x1
PROGBITS
, check our patching was successful:
$ readelf -S --wide lvl3|grep .text
[14] .text PROGBITS 0000000000400550 000550 0001f2 00 AX 0 0 16
It is, as readelf output confirms the PROGBITS value
Incidentally objdump -d -Mintel ./lvl3 now disassemble the .text section:
$ objdump -d -Mintel lvl3|grep .text
Disassembly of section .text:
0000000000400550 <.text>:
Trying to execute the binary and checking if its output still match its md5 checksum (shouldn’t have changed), maybe that was the only other thing the author wanted us to check:
$ md5sum ./lvl3
3a5c381e40d2fffd95ba4452a0fb4a40 ./lvl3
$ ./lvl3
3a5c381e40d2fffd95ba4452a0fb4a40 ./lvl3
$ ./oracle 3a5c381e40d2fffd95ba4452a0fb4a40
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Level 3 completed, unlocked lvl4 |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
Run oracle with -h to show a hint
Here we go to level4!
Level 4
Looks like a standard stripped binary, no output when executed:
$ file ./lvl4
./lvl4: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f8785d89a1f11e7b413c08c6176ad1ed7b95ca08, stripped
$ ./lvl4
$ echo $?
0
ltrace gives us a first clue about an environment variable that is set by the execution:
$ ltrace -i ./lvl4
[0x400579] __libc_start_main(0x4004a0, 1, 0x7ffcde74f508, 0x400650 <unfinished ...>
[0x400529] setenv("FLAG", "656cf8aecb76113a4dece1688c61d0e7"..., 1) = 0
[0xffffffffffffffff] +++ exited (status 0) +++
Is that the flag? yes!
$ ./oracle 656cf8aecb76113a4dece1688c61d0e7
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Level 4 completed, unlocked lvl5 |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
Run oracle with -h to show a hint
Next levels writeup in an upcoming post.
Feedback
Positive criticism always welcome! (comments here, contact in About ) as I would be more than happy to learn more!