Skip to main content
  1. Writeups/

Practical Binary Analysis - Chapter 5

· loading · loading · ·
Théo Abel
Author
Théo Abel
Student and cybersecurity enthusiast, I share my discoveries and projects on this blog.
practical-binary-analysis - This article is part of a series.
Part 1: This Article

Chapter 5 of Practical Binary Analysis features a short CTF to put into practice the knowledge acquired in previous chapters. The aim is to find a flag hidden in a binary, using the tools and techniques seen previously.

The chapter provides us with the following files:

 ➜ exa -T
.
├── levels.db
├── oracle
└── payload

 ➜ find . | xargs -I {} file {}
./levels.db: ASCII text, with very long lines (12832)
./oracle: 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]=083d1ffa7acfcf775c1e5f95dd870c8106aaf1f0, stripped
./payload: ASCII text

The most interesting file seems to be payload, which contains plain text encoded in base64 :

 ➜ head payload
H4sIABzY61gAA+xaD3RTVZq/Sf+lFJIof1r+2aenKKh0klJKi4MmJaUvWrTSFlgR0jRN20iadpKX
UljXgROKjbUOKuOfWWfFnTlzZs/ZXTln9nTRcTHYERhnZ5c/R2RGV1lFTAFH/DNYoZD9vvvubd57
bcBl1ln3bL6e9Hvf9+733e/+v+/en0dqId80WYAWLVqI3LpooUXJgUpKFy6yEOsCy6KSRQtLLQsW
EExdWkIEyzceGVA4JLmDgkCaA92XTXel9/9H6ftVNcv0Ot2orCe3E5RiJhuVbUw/fH3SxkbKSS78
v47MJtkgZynS2YhNxYeZa84NLF0G/DLhV66X5XK9TcVnsXSc6xQ8S1UCm4o/M5moOCHCqB3Geny2
rD0+u1HFD7I4junVdnpmN8zshll6zglPr1eXL5P96pm+npWLcwdL51CkR6r9UGrGZ8O1zN+1NhUv
ZelKNXb3gl02+fpkZnwFyy9VvQgsfs55O3zH72sqK/2Ov3m+3xcId8/vLi+bX1ZaHOooLqExmVna
6rsbaHpejwKLeQqR+wC+n/ePA3n/duKu2kNvL175+MxD7z75W8GC76aSZLv1xgSdkGnLRV0+/KbD
7+UPnnhwadWbZ459b/Wsl/o/NZ468olxo3P9wOXK3Qe/a8fRmwhvcTVdl0J/UDe+nzMp9M4U+n9J
oX8jhT5HP77+ZIr0JWT8+NvI+OnvTpG+NoV/Qwr9Vyn0b6bQkxTl+ixF+p+m0N+qx743k+wWGlX6

 ➜ cat payload | base64 -d | file -
/dev/stdin: gzip compressed data, last modified: Mon Apr 10 19:08:12 2017, from Unix

You can see that it’s actually a gzip file. It can be decompressed to view its contents:

 ➜ cat payload | base64 -d > archive.tar.gz
 ➜ tar -xzf archive.tar.gz
 ➜ l
.rw-rw-r-- 786k abel abel 10 avril  2017  67b8601
.rwxrwxr-x  11k abel abel 10 avril  2017  ctf
.rw-rw-r-- 6,4k abel abel 27 juil. 11:58  archive.tar.gz
.rw-rw-r-- 8,7M abel abel  1 déc.   2018  levels.db
.rwxr-xr-x  11k abel abel 30 nov.   2018  oracle
.rw-rw-r-- 8,6k abel abel 19 avril  2018  payload

The result is two interesting files, an executable ctf and an image file 67b8601.

 ➜ file ctf
ctf: 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]=29aeb60bcee44b50d1db3a56911bd1de93cd2030, stripped

 ➜ file 67b8601
67b8601: PC bitmap, Windows 3.x format, 512 x 512 x 24, image size 786434, resolution 7872 x 7872 px/m, 1165950976 important colors, cbSize 786488, bits offset 54

We can’t run ctf because of a missing library:

 ➜ ./ctf
./ctf: error while loading shared libraries: lib5ae9b7f.so: cannot open shared object file: No such file or directory

 ➜ ldd ctf
    linux-vdso.so.1 (0x00007ffc0ebf3000)
    lib5ae9b7f.so => not found
    libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x0000777a10e00000)
    libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x0000777a110ce000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000777a10a00000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x0000777a10d19000)
    /lib64/ld-linux-x86-64.so.2 (0x0000777a11111000)

The only way to find this library is to find it in the files we have, in particular that image we retrieved earlier, which makes the least sense. It’s easy to “fool” file, if you take a closer look at the image’s magic-byte :

 ➜ xxd 67b8601 | head
00000000: 424d 3800 0c00 0000 0000 3600 0000 2800  BM8.......6...(.
00000010: 0000 0002 0000 0002 0000 0100 1800 0000  ................
00000020: 0000 0200 0c00 c01e 0000 c01e 0000 0000  ................
00000030: 0000 0000 7f45 4c46 0201 0100 0000 0000  .....ELF........
...

This is strangely similar to the magic of an ELF. With dd we will extract its content:

 ➜ binwalk 67b8601

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             PC bitmap, Windows 3.x format,, 512 x 512 x 24
52            0x34            ELF, 64-bit LSB shared object, AMD x86-64, version 1 (SYSV)

 ➜ dd if=67b8601 of=elf.bin skip=52 bs=1 status=progress
786436+0 records in
786436+0 records out
786436 bytes (786 kB, 768 KiB) copied, 0,876232 s, 898 kB/s

 ➜ file elf.bin
elf.bin: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=5279389c64af3477ccfdf6d3293e404fd9933817, stripped

By analyzing the new binary with bingrep, we know that it’s a shared object, a priori our missing library:

 ➜ bingrep -D elf.bin | rg is_lib
is_lib: true

After renaming it lib5ae9b7f.so and to be able to use it from the current directory, we need to tell the linker (ld) where the :

export LD_LIBRARY_PATH=$(pwd)
 ➜ ldd ctf
    linux-vdso.so.1 (0x00007ffccc7e7000)
    lib5ae9b7f.so => /home/abel/learning/practical-binary-analysis/code/chapter5/lib5ae9b7f.so (0x00007ce128e00000)
    ...

Several symbols are exported, and can be listed with nm. Note functions related to RC4, a deprecated symmetrical encryption algorithm.

 ➜ nm -D --demangle lib5ae9b7f.so
0000000000202058 B __bss_start
                 w __cxa_finalize@GLIBC_2.2.5
0000000000202058 D _edata
0000000000202060 B _end
0000000000000d20 T _fini
                 w __gmon_start__
00000000000008c0 T _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 w _Jv_RegisterClasses
                 U malloc@GLIBC_2.2.5
                 U memcpy@GLIBC_2.14
                 U __stack_chk_fail@GLIBC_2.4
0000000000000c60 T rc4_decrypt(rc4_state_t*, unsigned char*, int)
0000000000000c70 T rc4_decrypt(rc4_state_t*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)
0000000000000b40 T rc4_encrypt(rc4_state_t*, unsigned char*, int)
0000000000000bc0 T rc4_encrypt(rc4_state_t*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)
0000000000000cb0 T rc4_init(rc4_state_t*, unsigned char*, int)
                 U std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_create(unsigned long&, unsigned long)@GLIBCXX_3.4.21
                 U std::__throw_logic_error(char const*)@GLIBCXX_3.4

For the moment, we don’t know any more. We can try running ctf :

 ➜ ./ctf
 ➜ ./ctf test
checking 'test'

We can see that binary takes an argument, let’s look at the result of strings truncated to see if there are any :

 ➜ strings -6 ctf
...
DEBUG: argv[1] = %s
checking '%s'
show_me_the_flag
...
flag = %s
guess again!
It's kinda like Louisiana. Or Dagobah. Dagobah - Where Yoda lives!
...

We can therefore try to execute ctf with the show_me_the_flag argument:

 ➜ ./ctf show_me_the_flag
checking 'show_me_the_flag'
ok

Still no flag, but we seem to be on the right track. You can trace calls to dynamically imported functions with ltrace :

 ➜ ltrace -Cir ./ctf show_me_the_flag
  0.000000 [0x400fe9] __libc_start_main(0x400bc0, 2, 0x7ffe50b72f08, 0x4010c0 <unfinished ...>
  0.000159 [0x400c44] __printf_chk(1, 0x401158, 0x7ffe50b73759, 256checking 'show_me_the_flag'
)                                                      = 28
  0.000203 [0x400c51] strcmp("show_me_the_flag", "show_me_the_flag")                                                      = 0
  0.000189 [0x400cf0] puts("ok"ok
)                                                                                          = 3
  0.000123 [0x400d07] rc4_init(rc4_state_t*, unsigned char*, int)(0x7ffe50b72ca0, 0x4011c0, 66, 0x7e99c7114887)           = 0
  0.000123 [0x400d14] std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::assign(char const*)(0x7ffe50b72be0, 0x40117b, 58, 3) = 0x7ffe50b72be0
  0.000143 [0x400d29] rc4_decrypt(rc4_state_t*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)(0x7ffe50b72c40, 0x7ffe50b72ca0, 0x7ffe50b72be0, 0x7e889f91) = 0x7ffe50b72c40
  0.000121 [0x400d36] std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_assign(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)(0x7ffe50b72be0, 0x7ffe50b72c40, 0x7ffe50b72c50, 0) = 0
  0.000122 [0x400d53] getenv("GUESSME")                                                                                   = nil
  0.000411 [0xffffffffffffffff] +++ exited (status 1) +++

Note the call to basic_string, which at first glance may contain the data decrypted by RC4, and there’s also getenv, which could be a hint to provide an environment variable.

GUESSME=test ./ctf show_me_the_flag
checking 'show_me_the_flag'
ok
guess again!

Bingo! Now we just need to find the expected value for GUESSME. Let’s see how ltrace behaves with the environment variable.

GUESSME=test ltrace -Ci ./ctf show_me_the_flag
[0x400fe9] __libc_start_main(0x400bc0, 2, 0x7ffd5a2a51c8, 0x4010c0 <unfinished ...>
[0x400c44] __printf_chk(1, 0x401158, 0x7ffd5a2a574c, 256checking 'show_me_the_flag'
)                                                                 = 28
[0x400c51] strcmp("show_me_the_flag", "show_me_the_flag")                                                                 = 0
[0x400cf0] puts("ok"ok
)                                                                                                     = 3
[0x400d07] rc4_init(rc4_state_t*, unsigned char*, int)(0x7ffd5a2a4f60, 0x4011c0, 66, 0x748678f14887)                      = 0
[0x400d14] std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::assign(char const*)(0x7ffd5a2a4ea0, 0x40117b, 58, 3) = 0x7ffd5a2a4ea0
[0x400d29] rc4_decrypt(rc4_state_t*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)(0x7ffd5a2a4f00, 0x7ffd5a2a4f60, 0x7ffd5a2a4ea0, 0x7e889f91) = 0x7ffd5a2a4f00
[0x400d36] std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_assign(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)(0x7ffd5a2a4ea0, 0x7ffd5a2a4f00, 0x7ffd5a2a4f10, 0) = 0
[0x400d53] getenv("GUESSME")                                                                                              = "test"
[0x400d6e] std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::assign(char const*)(0x7ffd5a2a4ec0, 0x401183, 5, 0xffffffe0) = 0x7ffd5a2a4ec0
[0x400d88] rc4_decrypt(rc4_state_t*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)(0x7ffd5a2a4f20, 0x7ffd5a2a4f60, 0x7ffd5a2a4ec0, 49) = 0x7ffd5a2a4f20
[0x400d9a] std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_assign(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)(0x7ffd5a2a4ec0, 0x7ffd5a2a4f20, 0x1e0a330, 0) = 0
[0x400db4] operator delete(void*)(0x1e0a330, 0x1e0a330, 21, 0)                                                            = 0
[0x400dd7] puts("guess again!"guess again!
)                                                                                           = 13
[0x400c8d] operator delete(void*)(0x1e0a2e0, 1, 1, 0x748678f14887)                                                        = 0
[0xffffffffffffffff] +++ exited (status 1) +++

We can see that the GUESSME variable is decrypted and compared with a value. The call to puts is made at address 0x400dd7, so we can disassemble the binary to see what’s going on around this address.

 ➜ objdump -M intel -d ctf
  ...
  400dc0:       0f b6 14 03             movzx  edx,BYTE PTR [rbx+rax*1]
  400dc4:       84 d2                   test   dl,dl
  400dc6:       74 05                   je     400dcd <__gmon_start__@plt+0x21d>
  400dc8:       3a 14 01                cmp    dl,BYTE PTR [rcx+rax*1]           # test if the decoded character is equal to the expected one
  400dcb:       74 13                   je     400de0 <__gmon_start__@plt+0x230>
  400dcd:       bf af 11 40 00          mov    edi,0x4011af
  400dd2:       e8 d9 fc ff ff          call   400ab0 <puts@plt>                 # puts("guess again!")
  400dd7:       e9 84 fe ff ff          jmp    400c60 <__gmon_start__@plt+0xb0>  # direction end of program
  400ddc:       0f 1f 40 00             nop    DWORD PTR [rax+0x0]
  400de0:       48 83 c0 01             add    rax,0x1
  400de4:       48 83 f8 15             cmp    rax,0x15
  400de8:       75 d6                   jne    400dc0 <__gmon_start__@plt+0x210>
  ...

We know that the comparison is performed at address 0x400dc8. By running GDB, we can set a breakpoint at this address and see the expected value:

gef➤  b *0x400dc8
Breakpoint 1 at 0x400dc8
gef➤  set env GUESSME=test
gef➤  run show_me_the_flag
gef➤  display/i $pc          # prints current instruction
1: x/i $pc
=> 0x400dc8:    cmp    dl,BYTE PTR [rcx+rax*1]
gef➤  i reg rcx
rcx            0x6152e0            0x6152e0
gef➤  i reg rax
rax            0x0                 0x0
gef➤  x/s 0x6152e0
0x6152e0:       "Crackers Don't Matter"

We can see that the expected value is Crackers Don't Matter. We can therefore launch the binary with this value:

GUESSME="Crackers Don't Matter" ./ctf show_me_the_flag
checking 'show_me_the_flag'
ok
flag = 84b34c124b2ba5ca224af8e33b077e9e

And now we’ve found the 84b34c124b2ba5ca224af8e33b077e9e flag!

practical-binary-analysis - This article is part of a series.
Part 1: This Article

Related

pwn.college - Program misuse
· loading · loading