A few months ago I have started studying a wonderful book I bought some time ago: Practical Binary Analysis [1].

First and foremost, I strongly recommend this book to whoever would like to approach the world of the Linux binary analysis, I honestly believe that it is very clear, very well structured, detailed and comprehensive.

Now, why this post? Well, at the end of every chapter, this book has some exercises. In particular, the Chapter 5 focuses on basic Linux binary analysis and the exercise for this chapter consists in solving a CTF-like challenge which consists of multiple levels. For this, I did not find (luckily) any solution online, so I had to endure the frustration and try harder to solve each level. The challenge is probably easy for anyone with a decent amount of experience in the field, but I believe that it might be somewhat difficult for beginners. For this reason, I will write this post as a walkthrough for the levels in this CTF.

Introduction

The challenge can be found in the “binary” virtual machine that the author provides on the book’s page. In the ~/code/chapter5 folder, there is one binary called oracle which is used to input found flags and unlock new levels.

Level 1

The first level is the easiest: not because it is actually the easiest, but because it is solved in the book. The whole chapter uses this level to present common tools such as xxd, gdb, strace, ltrace and objdump. The first flag is written in the book, 84b34c124b2ba5ca224af8e33b077e9e. You can unlock the lvl2 by running ./oracle 84b34c124b2ba5ca224af8e33b077e9e.

Level 2

The binary created for the second level is called lvl2.

It is a standard ELF:

binary@binary-VirtualBox:~/code/chapter5$ file lvl2 
lvl2: 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]=457d7940f6a73d6505db1f022071ee7368b67ce9, stripped

It is a 64-bit ELF and it is stripped.

binary@binary-VirtualBox:~/code/chapter5$ ldd lvl2
        linux-vdso.so.1 =>  (0x00007ffc9cc58000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2bce606000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f2bce9d0000)

All the dependencies for the linking are satisfied.

Running it, it seems to spit out a random byte encoded in hexadecimal:

binary@binary-VirtualBox:~/code/chapter5$ ./lvl2
4f
binary@binary-VirtualBox:~/code/chapter5$ ./lvl2
f8

Now, the first thing that I personally did was to understand how big was the pool of the bytes generated.

To do this, one way would be a simple script such as:

def gather_output():
    output = []
    for i in range(300):
        print "[+] %s/300" % i
        out = subprocess.check_output(['/home/binary/code/chapter5/lvl2'])
        output.append(out.rstrip('\n'))
        time.sleep(1 + random.randint(1, 10)/10)
    print "[+] Finished gathering outputs."
    return output


raw_list = gather_output()
set_bytes = set(raw_list)

Obviously it is possible to do easily with bash as well. Running this script and checking the content of the set shows that just 16 different bytes are output. This sounds reasonable as the flag for the lvl1 was 32 characters in hexadecimal. All it is needed to be figured out is the order of these characters.

To get an idea of what the program does, it is possible to use ltrace.

binary@binary-VirtualBox:~/code/chapter5$ ltrace ./lvl2
__libc_start_main(0x400500, 1, 0x7ffe7d63b5c8, 0x400640 <unfinished ...>
time(0)                                                                                                                                            = 1547976179
srand(0x5c443df3, 0x7ffe7d63b5c8, 0x7ffe7d63b5d8, 0)                                                                                               = 0
rand(0x7f618d7a9620, 0x7ffe7d63b4ac, 0x7f618d7a90a4, 0x7f618d7a911c)                                                                               = 0x1e1cf80
puts("03"03
)                                                                                                                                         = 3
+++ exited (status 0) +++

We can see that the binary calls time, srand, rand and then puts. It’s pretty obvious that it gets the current time, uses this as a seed for the rand function, gets some random value and prints something to console.

Disassembling the .text section leads to the following assembly code:

Disassembly of .text section

The key is therefore to understand what is there at 0x601060.

For this we use gdb:

(gdb) x/8x 0x601060
0x601060:       0xc4    0x06    0x40    0x00    0x00    0x00    0x00    0x00

We can see that there is the address 0x4006c4, this is most likely the address of the beginning of the ‘array’, so let’s print it:

(gdb) x/16s 0x4006c4
0x4006c4:       "03"
0x4006c7:       "4f"
0x4006ca:       "c4"
0x4006cd:       "f6"
0x4006d0:       "a5"
0x4006d3:       "36"
0x4006d6:       "f2"
0x4006d9:       "bf"
0x4006dc:       "74"
0x4006df:       "f8"
0x4006e2:       "d6"
0x4006e5:       "d3"
0x4006e8:       "81"
0x4006eb:       "6c"
0x4006ee:       "df"
0x4006f1:       "88"

Clearly it is the source of the 16 different bytes, let’s use the order as it is laid out in memory:

binary@binary-VirtualBox:~/code/chapter5$ ./oracle 034fc4f6a536f2bf74f8d6d3816cdf88
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Level 2 completed, unlocked lvl3         |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
Run oracle with -h to show a hint

Level 3

The binary created for the third level is called lvl3.

If we check what file this is:

binary@binary-VirtualBox:~/code/chapter5$ file lvl3 
lvl3: ERROR: ELF 64-bit LSB executable, Motorola Coldfire, version 1 (Novell Modesto) error reading (Invalid argument)

It seems that the file is corrupted. Using readelf to parse the header we see:

binary@binary-VirtualBox:~/code/chapter5$ 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               # This is probably broken
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Motorola Coldfire              # This is probably broken
  Version:                           0x1
  Entry point address:               0x4005d0
  Start of program headers:          4022250974 (bytes into file)   # This is definitely broken
  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

So, at the moment we can see three fields most likely corrupted:

  • The OS/ABI.
  • The machine type.
  • The program headers table address.
binary@binary-VirtualBox:~/code/chapter5$ xxd lvl3 | head -n 4
00000000: 7f45 4c46 0201 010b 0000 0000 0000 0000  .ELF............
00000010: 0200 3400 0100 0000 d005 4000 0000 0000  ..4.......@.....
00000020: dead beef 0000 0000 8011 0000 0000 0000  ................
00000030: 0000 0000 4000 3800 0900 4000 1d00 1c00  ....@.8...@.....

So we need to change:

  • 0b -> 00 byte 8. (OS/ABI version)
  • 34 -> 3e byte 19 (Machine)
  • dead beef -> 4000 0000 on byte 32-40 (Program Header address)

After the changes the header looks like this:

binary@binary-VirtualBox:~/code/chapter5$ xxd test3 | head -n 4
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
00000010: 0200 3e00 0100 0000 d005 4000 0000 0000  ..>.......@.....
00000020: 4000 0000 0000 0000 8011 0000 0000 0000  @...............
00000030: 0000 0000 4000 3800 0900 4000 1d00 1c00  ....@.8...@.....

NOTE: To make the changes I have personally used hexedit command. Using vim with xxd (:%!xxd) is also possible.

After making this change, I tried to disassemble the program to verify what the code actually does, but the .text section did not show up. The section header table is probably also corrupted somehow.

Using readelf -S to print the section information we can see:

[14] .text             NOBITS           0000000000400550  00000550
       00000000000001f2  0000000000000000  AX       0     0     16

The .text section is marked as NOBITS instead of PROGBITS. We need therefore to change the type of .text in the section header table. First, we need to find it in it.

From the header we can see that the Section Headers start 4480 bytes into the file. We know also that each entry is 64 bytes and that .text is the 14th section starting from 0 (since there is an empty section).

4480+(64x14)=5376 Should give us the address of the beginning of .text section header. If we convert 5376 in hexadecimal we get 1500.

00001500: 8d00 0000 0800 0000 0600 0000 0000 0000  ................
00001510: 5005 4000 0000 0000 5005 0000 0000 0000  P.@.....P.......
00001520: f201 0000 0000 0000 0000 0000 0000 0000  ................
00001530: 1000 0000 0000 0000 0000 0000 0000 0000  ................

If the calculations are correct, these 64 bytes are the header of the .text section. From the ABI document or from man elf 5 we see that a section header has the following structure.

           typedef struct {
               uint32_t   sh_name;
               uint32_t   sh_type;
               uint64_t   sh_flags;
               Elf64_Addr sh_addr;
               Elf64_Off  sh_offset;
               uint64_t   sh_size;
               uint32_t   sh_link;
               uint32_t   sh_info;
               uint64_t   sh_addralign;
               uint64_t   sh_entsize;
           } Elf64_Shdr;

The first 4 bytes are the name of the section, then the next 4 bytes are the type of section. In this case the current type bytes are:

0800 0000 

Which makes sense since 8 corresponds to NOBITS. The code for PROGBITS is 1, so we change:

  • 0800 0000 -> 0100 0000

Using readelf now confirms that .text is NOBITS.

Running the program now leads to:

binary@binary-VirtualBox:~/code/chapter5$ ./lvl3 
3a5c381e40d2fffd95ba4452a0fb4a40  ./lvl3

Feeding this flat to the oracle:

binary@binary-VirtualBox:~/code/chapter5$ ./oracle 3a5c381e40d2fffd95ba4452a0fb4a40
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Level 3 completed, unlocked lvl4         |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
Run oracle with -h to show a hint

Level 4

The binary for the fourth level is called lvl4.

This level was extremely easy or very lucky. Among the first routing checks that I do on new binaries, I usually run the program with strace and ltrace to check what syscalls and library functions are called, respectively.

For this level, it was enough to run ltrace:

binary@binary-VirtualBox:~/code/chapter5$ ltrace ./lvl4
__libc_start_main(0x4004a0, 1, 0x7ffeea9555d8, 0x400650 <unfinished ...>
setenv("FLAG", "656cf8aecb76113a4dece1688c61d0e7"..., 1)                                                                                         = 0
+++ exited (status 0) +++

It is clear that the program sets some environment variable as the flag.

Just taking that flag:

binary@binary-VirtualBox:~/code/chapter5$ ./oracle 656cf8aecb76113a4dece1688c61d0e7
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Level 4 completed, unlocked lvl5         |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
Run oracle with -h to show a hint

Next levels

I am going to make a final post about the next levels, I will attempt to crack level 5 soon. I am not sure how many levels there are in total, but running oracle with ltrace:

binary@binary-VirtualBox:~/code/chapter5$ ltrace ./oracle 656cf8aecb76113a4dece1688c61d0e4
__libc_start_main(0x400c00, 2, 0x7ffc78bb7318, 0x401570 <unfinished ...>
strlen("656cf8aecb76113a4dece1688c61d0e4"...)                                                                                                      = 32
crypt("656cf8aecb76113a4dece1688c61d0e4"..., "$1$pba")                                                                                             = "$1$pba$NIzB29sPpsOXx7G7U/pQi0"
strcmp("$1$pba$NIzB29sPpsOXx7G7U/pQi0", "$1$pba$cC.J56kTHt0f2BeCmPY0S0")                                                                           = -21
strcmp("$1$pba$NIzB29sPpsOXx7G7U/pQi0", "$1$pba$ThmQr44j/SzgLnGGr3z0t.")                                                                           = -6
strcmp("$1$pba$NIzB29sPpsOXx7G7U/pQi0", "$1$pba$bVbBZVMYF.yq/D95S14hi/")                                                                           = -20
strcmp("$1$pba$NIzB29sPpsOXx7G7U/pQi0", "$1$pba$eZVsO8xkHTfWXQJlVj3vF.")                                                                           = -23
strcmp("$1$pba$NIzB29sPpsOXx7G7U/pQi0", "$1$pba$9dVFs3IW334QFtvvh1ZkF0")                                                                           = 21
strcmp("$1$pba$NIzB29sPpsOXx7G7U/pQi0", "$1$pba$l26Zuo8AFAT.rHXQQ0FTX0")                                                                           = -30
strcmp("$1$pba$NIzB29sPpsOXx7G7U/pQi0", "$1$pba$KjeCu9tJUVtd24nC4g75L/")                                                                           = 3
strcmp("$1$pba$NIzB29sPpsOXx7G7U/pQi0", "$1$pba$mMAAETuTP0ixunRdwG9PT0") 

I can see that there are 8 comparisons, probably the input flag is checked against the flag of each level, so I would say that there are 8 levels in this CTF.


For any correction, feedback or question feel free to drop a mail to security[at]coolbyte[dot]eu.

References

  1. Practical Binary Analysis, Dennis Andriesse, Nostarch 2018