Linux Reverse Engineering CTFs for Beginners

After a while, I decided a write a short blog post about Linux binary reversing CTFs in general. How to approach a binary and solving for beginners. I personally am not a fan of Linux reverse engineering challenges in general, since I focus more time on Windows reversing. I like windows reverse engineering challenges more. A reason me liking Windows is as a pentester daily I encounter Windows machines and it’s so rare I come across an entire network running Linux. Even when it comes to exploit development it’s pretty rare you will manually develop an exploit for a Linux software while pentesting. But this knowledge is really useful when it comes to IoT, since almost many devices are based on Linux embedded. If you want to begin reverse engineering and exploit development starting from Linux would be a good idea. I too started from Linux many years ago. Saying that since some people when they see a reverse engineering challenge they try to run away. So if you are a newbie I hope this content might be useful for you to begin with.

The ELF Format

Let’s first have a look at the ELF headers. The best way to learn more about this in detail is to check the man pages for ELF.

Here’s in more detail. The “e_shoff” member holds the offset to the section header table. The “sh_offset” member holds the address to the section’s first byte.

            +-------------------+
            | ELF header        |---+
+---------> +-------------------+   | e_shoff
|           |                   |<--+
| Section   | Section header 0  |
|           |                   |---+ sh_offset
| Header    +-------------------+   |
|           | Section header 1  |---|--+ sh_offset
| Table     +-------------------+   |  |
|           | Section header 2  |---|--|--+
+---------> +-------------------+   |  |  |
            | Section 0         |<--+  |  |
            +-------------------+      |  | sh_offset
            | Section 1         |<-----+  |
            +-------------------+         |
            | Section 2         |<--------+
            +-------------------+

Executable Header

Any ELF file starts with an executable header. This contains information about which type of an ELF file, the offsets to different headers. Everything is self-explanatory if you look at the comments. For this example, I am using 32-bit structures. For x86_64 the sizes may change and the naming convention would start with “Elf64_”.

#define EI_NIDENT (16)

typedef struct {
  unsigned char e_ident[EI_NIDENT];     /* Magic number and other info */
  Elf32_Half    e_type;                 /* Object file type */
  Elf32_Half    e_machine;              /* Architecture */
  Elf32_Word    e_version;              /* Object file version */
  Elf32_Addr    e_entry;                /* Entry point virtual address */
  Elf32_Off     e_phoff;                /* Program header table file offset */
  Elf32_Off     e_shoff;                /* Section header table file offset */
  Elf32_Word    e_flags;                /* Processor-specific flags */
  Elf32_Half    e_ehsize;               /* ELF header size in bytes */
  Elf32_Half    e_phentsize;            /* Program header table entry size */
  Elf32_Half    e_phnum;                /* Program header table entry count */
  Elf32_Half    e_shentsize;            /* Section header table entry size */
  Elf32_Half    e_shnum;                /* Section header table entry count */
  Elf32_Half    e_shstrndx;             /* Section header string table index */
} Elf32_Ehdr;

This is an example using readelf.

# readelf -h /bin/ls
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:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x6130
  Start of program headers:          64 (bytes into file)
  Start of section headers:          137000 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         11
  Size of section headers:           64 (bytes)
  Number of section headers:         29
  Section header string table index: 28

To calculate the size of the entire binary we can use the following calculation

size = e_shoff + (e_shnum * e_shentsize)
size = Start of section headers + (Number of section headers * Size of section headers)
size = 137000 + (29*64) = 138856

As you can see our calculation is correct.

# ls -l /bin/ls
-rwxr-xr-x 1 root root 138856 Aug 29 21:20 /bin/ls

Program Headers

These headers describe the segments of the binary which important for the loading of the binary. This information is useful for the kernel to map the segments to memory from disk. The members of the structure are self-explanatory. I won’t be explaining in depth about this for this post as I try to keep things basic. However, every section is important to understand in doing cool things in reverse engineering in ELF 🙂

typedef struct {
  Elf32_Word    p_type;                 /* Segment type */
  Elf32_Off     p_offset;               /* Segment file offset */
  Elf32_Addr    p_vaddr;                /* Segment virtual address */
  Elf32_Addr    p_paddr;                /* Segment physical address */
  Elf32_Word    p_filesz;               /* Segment size in file */
  Elf32_Word    p_memsz;                /* Segment size in memory */
  Elf32_Word    p_flags;                /* Segment flags */
  Elf32_Word    p_align;                /* Segment alignment */
} Elf32_Phdr;

Section Headers

These headers contain the information for the binary’s segments. It references the size, location for linking and debugging purposes. These headers are not really important for the execution flow of the binary. In some cases, this is stripped and tools like gdb, objdump are useless as they rely on these headers to locate symbol information.

typedef struct {
  Elf32_Word    sh_name;                /* Section name (string tbl index) */
  Elf32_Word    sh_type;                /* Section type */
  Elf32_Word    sh_flags;               /* Section flags */
  Elf32_Addr    sh_addr;                /* Section virtual addr at execution */
  Elf32_Off     sh_offset;              /* Section file offset */
  Elf32_Word    sh_size;                /* Section size in bytes */
  Elf32_Word    sh_link;                /* Link to another section */
  Elf32_Word    sh_info;                /* Additional section information */
  Elf32_Word    sh_addralign;           /* Section alignment */
  Elf32_Word    sh_entsize;             /* Entry size if section holds table */
} Elf32_Shdr;

Sections

As any binary, these are the sections. Some sections are familiar with the PE’s headers. However, I won’t be discussing all the sections as I try to keep it basic.

.bss Section

This section contains the program’s uninitialized global data.

.data Section

This section contains the program’s initialized global variables.

.rodata Section

This section contains read-only data such as strings of the program used.

.text Section

This section contains the program’s actual code, the logic flow.

$ readelf -S --wide /bin/ls
There are 29 section headers, starting at offset 0x21728:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        00000000000002a8 0002a8 00001c 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            00000000000002c4 0002c4 000020 00   A  0   0  4
  [ 3] .note.gnu.build-id NOTE            00000000000002e4 0002e4 000024 00   A  0   0  4
  [ 4] .gnu.hash         GNU_HASH        0000000000000308 000308 0000c0 00   A  5   0  8
  [ 5] .dynsym           DYNSYM          00000000000003c8 0003c8 000c90 18   A  6   1  8
  [ 6] .dynstr           STRTAB          0000000000001058 001058 0005d8 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          0000000000001630 001630 00010c 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         0000000000001740 001740 000070 00   A  6   1  8
  [ 9] .rela.dyn         RELA            00000000000017b0 0017b0 001350 18   A  5   0  8
  [10] .rela.plt         RELA            0000000000002b00 002b00 0009f0 18  AI  5  24  8
  [11] .init             PROGBITS        0000000000004000 004000 000017 00  AX  0   0  4
  [12] .plt              PROGBITS        0000000000004020 004020 0006b0 10  AX  0   0 16
  [13] .plt.got          PROGBITS        00000000000046d0 0046d0 000018 08  AX  0   0  8
  [14] .text             PROGBITS        00000000000046f0 0046f0 01253e 00  AX  0   0 16
  [15] .fini             PROGBITS        0000000000016c30 016c30 000009 00  AX  0   0  4
  [16] .rodata           PROGBITS        0000000000017000 017000 005129 00   A  0   0 32
  [17] .eh_frame_hdr     PROGBITS        000000000001c12c 01c12c 0008fc 00   A  0   0  4
  [18] .eh_frame         PROGBITS        000000000001ca28 01ca28 002ed0 00   A  0   0  8
  [19] .init_array       INIT_ARRAY      0000000000021390 020390 000008 08  WA  0   0  8
  [20] .fini_array       FINI_ARRAY      0000000000021398 020398 000008 08  WA  0   0  8
  [21] .data.rel.ro      PROGBITS        00000000000213a0 0203a0 000a38 00  WA  0   0 32
  [22] .dynamic          DYNAMIC         0000000000021dd8 020dd8 0001f0 10  WA  6   0  8
  [23] .got              PROGBITS        0000000000021fc8 020fc8 000038 08  WA  0   0  8
  [24] .got.plt          PROGBITS        0000000000022000 021000 000368 08  WA  0   0  8
  [25] .data             PROGBITS        0000000000022380 021380 000268 00  WA  0   0 32
  [26] .bss              NOBITS          0000000000022600 0215e8 0012d8 00  WA  0   0 32
  [27] .gnu_debuglink    PROGBITS        0000000000000000 0215e8 000034 00      0   0  4
  [28] .shstrtab         STRTAB          0000000000000000 02161c 00010a 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

Solving a Basic CTF Challenge

Now that you have a basic understanding about the headers, let’s pick a random challenge CTF and explire. Download the binary from here.

When we pass in some random string we get [+] No flag for you. [+] text displayed.

$ ./nix_5744af788e6cbdb29bb41e8b0e5f3cd5 aaaa

[+] No flag for you. [+]

Strings

Let’s start by having a look at strings and see any interesting strings.

$ strings nix_5744af788e6cbdb29bb41e8b0e5f3cd5 

/lib/ld-linux.so.2
Mw1i#'0
libc.so.6
_IO_stdin_used
exit
sprintf
puts
strlen
__cxa_finalize
__libc_start_main
GLIBC_2.1.3
Y[^]
[^_]
UWVS
[^_]
Usage: script.exe 
Length of argv[1] too long.
[+] The flag is: SAYCURE{%s} [+]
[+] No flag for you. [+]
%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c
;*2$"
GCC: (Debian 8.2.0-8) 8.2.0
crtstuff.c

We found all the strings printed out from the binary. The “%c” is the format string where our flag gets printed, we can determine the flag must be of 15 characters.

Usage: script.exe
Length of argv[1] too long.
[+] The flag is: SAYCURE{%s} [+]
[+] No flag for you. [+]
%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c

We can get a better view of these strings if we look at the ‘.rodata’ section with the offsets.

 
$ readelf -x .rodata nix_5744af788e6cbdb29bb41e8b0e5f3cd5

Hex dump of section '.rodata':
  0x00002000 03000000 01000200 55736167 653a2073 ........Usage: s
  0x00002010 63726970 742e6578 65203c6b 65793e00 cript.exe .
  0x00002020 4c656e67 7468206f 66206172 67765b31 Length of argv[1
  0x00002030 5d20746f 6f206c6f 6e672e00 5b2b5d20 ] too long..[+] 
  0x00002040 54686520 666c6167 2069733a 20534159 The flag is: SAY
  0x00002050 43555245 7b25737d 205b2b5d 0a000a5b CURE{%s} [+]...[
  0x00002060 2b5d204e 6f20666c 61672066 6f722079 +] No flag for y
  0x00002070 6f752e20 5b2b5d00 25632563 25632563 ou. [+].%c%c%c%c
  0x00002080 25632563 25632563 25632563 25632563 %c%c%c%c%c%c%c%c
  0x00002090 25632563 256300                     %c%c%c.

Checking for Symbols

By checking the symbols of the binary we can realize it uses printf, puts, sprintf, strlen functions.

 
$ nm -D nix_5744af788e6cbdb29bb41e8b0e5f3cd5 

         w __cxa_finalize
         U exit
         w __gmon_start__
00002004 R _IO_stdin_used
         w _ITM_deregisterTMCloneTable
         w _ITM_registerTMCloneTable
         U __libc_start_main
         U printf
         U puts
         U sprintf
         U strlen

Tracing System Calls

We can use tools such as strace to trace the system calls used by the program.

 
$ strace ./nix_5744af788e6cbdb29bb41e8b0e5f3cd5 aaaa

execve("./nix_5744af788e6cbdb29bb41e8b0e5f3cd5", ["./nix_5744af788e6cbdb29bb41e8b0e"..., "aaaa"], 0x7ffd5ff92d18 /* 46 vars */) = 0
strace: [ Process PID=59965 runs in 32 bit mode. ]
brk(NULL)                               = 0x56f14000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xf7ef0000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=220471, ...}) = 0
mmap2(NULL, 220471, PROT_READ, MAP_PRIVATE, 3, 0) = 0xf7eba000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\1\1\1\3\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0 \233\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1930924, ...}) = 0
mmap2(NULL, 1940000, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xf7ce0000
mprotect(0xf7cf9000, 1814528, PROT_NONE) = 0
mmap2(0xf7cf9000, 1359872, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19000) = 0xf7cf9000
mmap2(0xf7e45000, 450560, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x165000) = 0xf7e45000
mmap2(0xf7eb4000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1d3000) = 0xf7eb4000
mmap2(0xf7eb7000, 10784, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xf7eb7000
close(3)                                = 0
set_thread_area({entry_number=-1, base_addr=0xf7ef10c0, limit=0x0fffff, seg_32bit=1, contents=0, read_exec_only=0, limit_in_pages=1, seg_not_present=0, useable=1}) = 0 (entry_number=12)
mprotect(0xf7eb4000, 8192, PROT_READ)   = 0
mprotect(0x5664d000, 4096, PROT_READ)   = 0
mprotect(0xf7f1e000, 4096, PROT_READ)   = 0
munmap(0xf7eba000, 220471)              = 0
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x2), ...}) = 0
brk(NULL)                               = 0x56f14000
brk(0x56f35000)                         = 0x56f35000
brk(0x56f36000)                         = 0x56f36000
write(1, "\n", 1
)                       = 1
write(1, "[+] No flag for you. [+]\n", 25[+] No flag for you. [+]
) = 25
exit_group(26)                          = ?
+++ exited with 26 +++

To get a better understanding, we can use ltrace to trace the library calls made by demangling C++ function names. We can see there is a string length check being done.

  
$ ltrace -i -C ./nix_5744af788e6cbdb29bb41e8b0e5f3cd5 aaaaaaaa

[0x565570e1] __libc_start_main(0x565571e9, 2, 0xffe3a584, 0x56557400 
[0x56557249] strlen("aaaaaaaa")                                                                                      = 8
[0x565572ca] puts("\n[+] No flag for you. [+]"
[+] No flag for you. [+]
)                                                                      = 26
[0xffffffffffffffff] +++ exited (status 26) +++

Disassembling the Text Section

Let’s have a look at the .text section’s disassembly and try to understand. In this binary the symbols are not stripped so we can see the function names which makes it easier to understand. If you can read assembly by now you will have figure out what is happening. If not let’s do some live debugging and try to understand better.

   
root@Omega:/mnt/hgfs/shared/Linux RE# objdump -D -M intel -j .text nix_5744af788e6cbdb29bb41e8b0e5f3cd5 

nix_5744af788e6cbdb29bb41e8b0e5f3cd5:     file format elf32-i386


Disassembly of section .text:

000010b0 <_start>:
    10b0:	31 ed                	xor    ebp,ebp
    10b2:	5e                   	pop    esi
    10b3:	89 e1                	mov    ecx,esp
    10b5:	83 e4 f0             	and    esp,0xfffffff0
    10b8:	50                   	push   eax
    10b9:	54                   	push   esp
    10ba:	52                   	push   edx
    10bb:	e8 22 00 00 00       	call   10e2 <_start+0x32>
    10c0:	81 c3 40 2f 00 00    	add    ebx,0x2f40
    10c6:	8d 83 60 d4 ff ff    	lea    eax,[ebx-0x2ba0]
    10cc:	50                   	push   eax
    10cd:	8d 83 00 d4 ff ff    	lea    eax,[ebx-0x2c00]
    10d3:	50                   	push   eax
    10d4:	51                   	push   ecx
    10d5:	56                   	push   esi
    10d6:	ff b3 f8 ff ff ff    	push   DWORD PTR [ebx-0x8]
    10dc:	e8 9f ff ff ff       	call   1080 <__libc_start_main@plt>
    10e1:	f4                   	hlt    
    10e2:	8b 1c 24             	mov    ebx,DWORD PTR [esp]
    10e5:	c3                   	ret    
    10e6:	66 90                	xchg   ax,ax
    10e8:	66 90                	xchg   ax,ax
    10ea:	66 90                	xchg   ax,ax
    10ec:	66 90                	xchg   ax,ax
    10ee:	66 90                	xchg   ax,ax

... Output Omitted ...

000011e9 
: 11e9: 8d 4c 24 04 lea ecx,[esp+0x4] 11ed: 83 e4 f0 and esp,0xfffffff0 11f0: ff 71 fc push DWORD PTR [ecx-0x4] 11f3: 55 push ebp 11f4: 89 e5 mov ebp,esp 11f6: 56 push esi 11f7: 53 push ebx 11f8: 51 push ecx 11f9: 83 ec 1c sub esp,0x1c 11fc: e8 ef fe ff ff call 10f0 <__x86.get_pc_thunk.bx> 1201: 81 c3 ff 2d 00 00 add ebx,0x2dff 1207: 89 ce mov esi,ecx 1209: c7 45 e4 00 00 00 00 mov DWORD PTR [ebp-0x1c],0x0 1210: c7 45 dc 07 00 00 00 mov DWORD PTR [ebp-0x24],0x7 1217: 83 3e 02 cmp DWORD PTR [esi],0x2 121a: 74 1c je 1238 121c: 83 ec 0c sub esp,0xc 121f: 8d 83 08 e0 ff ff lea eax,[ebx-0x1ff8] 1225: 50 push eax 1226: e8 15 fe ff ff call 1040 122b: 83 c4 10 add esp,0x10 122e: 83 ec 0c sub esp,0xc 1231: 6a 01 push 0x1 1233: e8 28 fe ff ff call 1060 1238: 8b 46 04 mov eax,DWORD PTR [esi+0x4] 123b: 83 c0 04 add eax,0x4 123e: 8b 00 mov eax,DWORD PTR [eax] 1240: 83 ec 0c sub esp,0xc 1243: 50 push eax 1244: e8 27 fe ff ff call 1070 1249: 83 c4 10 add esp,0x10 124c: 83 f8 0f cmp eax,0xf 124f: 76 1c jbe 126d 1251: 83 ec 0c sub esp,0xc 1254: 8d 83 20 e0 ff ff lea eax,[ebx-0x1fe0] 125a: 50 push eax 125b: e8 f0 fd ff ff call 1050 1260: 83 c4 10 add esp,0x10 1263: 83 ec 0c sub esp,0xc 1266: 6a 01 push 0x1 1268: e8 f3 fd ff ff call 1060 126d: c7 45 e0 00 00 00 00 mov DWORD PTR [ebp-0x20],0x0 1274: eb 1a jmp 1290 1276: 8b 46 04 mov eax,DWORD PTR [esi+0x4] 1279: 83 c0 04 add eax,0x4 127c: 8b 10 mov edx,DWORD PTR [eax] 127e: 8b 45 e0 mov eax,DWORD PTR [ebp-0x20] 1281: 01 d0 add eax,edx 1283: 0f b6 00 movzx eax,BYTE PTR [eax] 1286: 0f be c0 movsx eax,al 1289: 01 45 e4 add DWORD PTR [ebp-0x1c],eax 128c: 83 45 e0 01 add DWORD PTR [ebp-0x20],0x1 1290: 8b 45 e0 mov eax,DWORD PTR [ebp-0x20] 1293: 3b 45 dc cmp eax,DWORD PTR [ebp-0x24] 1296: 7c de jl 1276 1298: 81 7d e4 21 03 00 00 cmp DWORD PTR [ebp-0x1c],0x321 129f: 75 1a jne 12bb 12a1: e8 33 00 00 00 call 12d9 12a6: 83 ec 08 sub esp,0x8 12a9: 50 push eax 12aa: 8d 83 3c e0 ff ff lea eax,[ebx-0x1fc4] 12b0: 50 push eax 12b1: e8 8a fd ff ff call 1040 12b6: 83 c4 10 add esp,0x10 12b9: eb 12 jmp 12cd 12bb: 83 ec 0c sub esp,0xc 12be: 8d 83 5e e0 ff ff lea eax,[ebx-0x1fa2] 12c4: 50 push eax 12c5: e8 86 fd ff ff call 1050 12ca: 83 c4 10 add esp,0x10 12cd: 90 nop 12ce: 8d 65 f4 lea esp,[ebp-0xc] 12d1: 59 pop ecx 12d2: 5b pop ebx 12d3: 5e pop esi 12d4: 5d pop ebp 12d5: 8d 61 fc lea esp,[ecx-0x4] 12d8: c3 ret 000012d9 : 12d9: 55 push ebp 12da: 89 e5 mov ebp,esp 12dc: 57 push edi 12dd: 56 push esi 12de: 53 push ebx 12df: 83 ec 7c sub esp,0x7c 12e2: e8 09 fe ff ff call 10f0 <__x86.get_pc_thunk.bx> 12e7: 81 c3 19 2d 00 00 add ebx,0x2d19 12ed: c7 45 e4 00 00 00 00 mov DWORD PTR [ebp-0x1c],0x0 12f4: c7 45 a8 4c 00 00 00 mov DWORD PTR [ebp-0x58],0x4c 12fb: c7 45 ac 33 00 00 00 mov DWORD PTR [ebp-0x54],0x33 1302: c7 45 b0 74 00 00 00 mov DWORD PTR [ebp-0x50],0x74 1309: c7 45 b4 73 00 00 00 mov DWORD PTR [ebp-0x4c],0x73 1310: c7 45 b8 5f 00 00 00 mov DWORD PTR [ebp-0x48],0x5f 1317: c7 45 bc 67 00 00 00 mov DWORD PTR [ebp-0x44],0x67 131e: c7 45 c0 33 00 00 00 mov DWORD PTR [ebp-0x40],0x33 1325: c7 45 c4 74 00 00 00 mov DWORD PTR [ebp-0x3c],0x74 132c: c7 45 c8 5f 00 00 00 mov DWORD PTR [ebp-0x38],0x5f 1333: c7 45 cc 69 00 00 00 mov DWORD PTR [ebp-0x34],0x69 133a: c7 45 d0 6e 00 00 00 mov DWORD PTR [ebp-0x30],0x6e 1341: c7 45 d4 32 00 00 00 mov DWORD PTR [ebp-0x2c],0x32 1348: c7 45 d8 5f 00 00 00 mov DWORD PTR [ebp-0x28],0x5f 134f: c7 45 dc 52 00 00 00 mov DWORD PTR [ebp-0x24],0x52 1356: c7 45 e0 33 00 00 00 mov DWORD PTR [ebp-0x20],0x33 135d: 8b 55 e0 mov edx,DWORD PTR [ebp-0x20] 1360: 8b 75 dc mov esi,DWORD PTR [ebp-0x24] 1363: 8b 45 d8 mov eax,DWORD PTR [ebp-0x28] 1366: 89 45 a4 mov DWORD PTR [ebp-0x5c],eax 1369: 8b 4d d4 mov ecx,DWORD PTR [ebp-0x2c] 136c: 89 4d a0 mov DWORD PTR [ebp-0x60],ecx 136f: 8b 7d d0 mov edi,DWORD PTR [ebp-0x30] 1372: 89 7d 9c mov DWORD PTR [ebp-0x64],edi 1375: 8b 45 cc mov eax,DWORD PTR [ebp-0x34] 1378: 89 45 98 mov DWORD PTR [ebp-0x68],eax 137b: 8b 4d c8 mov ecx,DWORD PTR [ebp-0x38] 137e: 89 4d 94 mov DWORD PTR [ebp-0x6c],ecx 1381: 8b 7d c4 mov edi,DWORD PTR [ebp-0x3c] 1384: 89 7d 90 mov DWORD PTR [ebp-0x70],edi 1387: 8b 45 c0 mov eax,DWORD PTR [ebp-0x40] 138a: 89 45 8c mov DWORD PTR [ebp-0x74],eax 138d: 8b 4d bc mov ecx,DWORD PTR [ebp-0x44] 1390: 89 4d 88 mov DWORD PTR [ebp-0x78],ecx 1393: 8b 7d b8 mov edi,DWORD PTR [ebp-0x48] 1396: 89 7d 84 mov DWORD PTR [ebp-0x7c],edi 1399: 8b 45 b4 mov eax,DWORD PTR [ebp-0x4c] 139c: 89 45 80 mov DWORD PTR [ebp-0x80],eax 139f: 8b 7d b0 mov edi,DWORD PTR [ebp-0x50] 13a2: 8b 4d ac mov ecx,DWORD PTR [ebp-0x54] 13a5: 8b 45 a8 mov eax,DWORD PTR [ebp-0x58] 13a8: 83 ec 0c sub esp,0xc 13ab: 52 push edx 13ac: 56 push esi 13ad: ff 75 a4 push DWORD PTR [ebp-0x5c] 13b0: ff 75 a0 push DWORD PTR [ebp-0x60] 13b3: ff 75 9c push DWORD PTR [ebp-0x64] 13b6: ff 75 98 push DWORD PTR [ebp-0x68] 13b9: ff 75 94 push DWORD PTR [ebp-0x6c] 13bc: ff 75 90 push DWORD PTR [ebp-0x70] 13bf: ff 75 8c push DWORD PTR [ebp-0x74] 13c2: ff 75 88 push DWORD PTR [ebp-0x78] 13c5: ff 75 84 push DWORD PTR [ebp-0x7c] 13c8: ff 75 80 push DWORD PTR [ebp-0x80] 13cb: 57 push edi 13cc: 51 push ecx 13cd: 50 push eax 13ce: 8d 83 78 e0 ff ff lea eax,[ebx-0x1f88] 13d4: 50 push eax 13d5: 8d 83 30 00 00 00 lea eax,[ebx+0x30] 13db: 50 push eax 13dc: e8 af fc ff ff call 1090 13e1: 83 c4 50 add esp,0x50 13e4: 8d 83 30 00 00 00 lea eax,[ebx+0x30] 13ea: 8d 65 f4 lea esp,[ebp-0xc] 13ed: 5b pop ebx 13ee: 5e pop esi 13ef: 5f pop edi 13f0: 5d pop ebp 13f1: c3 ret 13f2: 66 90 xchg ax,ax 13f4: 66 90 xchg ax,ax 13f6: 66 90 xchg ax,ax 13f8: 66 90 xchg ax,ax 13fa: 66 90 xchg ax,ax 13fc: 66 90 xchg ax,ax 13fe: 66 90 xchg ax,ax ... Output Omitted ...

Debugging Live

I will use GDB-Peda for this which makes it easier to understand. Let’s first check the functions in the binary. We can see functions such as main, comp_key

  
gdb-peda$ info functions 
All defined functions:

Non-debugging symbols:
0x00001000  _init
0x00001040  printf@plt
0x00001050  puts@plt
0x00001060  exit@plt
0x00001070  strlen@plt
0x00001080  __libc_start_main@plt
0x00001090  sprintf@plt
0x000010a0  __cxa_finalize@plt
0x000010a8  __gmon_start__@plt
0x000010b0  _start
0x000010f0  __x86.get_pc_thunk.bx
0x00001100  deregister_tm_clones
0x00001140  register_tm_clones
0x00001190  __do_global_dtors_aux
0x000011e0  frame_dummy
0x000011e5  __x86.get_pc_thunk.dx
0x000011e9  main
0x000012d9  comp_key
0x00001400  __libc_csu_init
0x00001460  __libc_csu_fini
0x00001464  _fini

This is how you debug a program. We will hit a break point at the main function. Use n to step and ni to step each instruction. If you don’t know assembly, in a basic challenge like this, look for jumps, compare instructions. Try to understand what check the program does and build the logic in your mind. There are many good crash courses on assembly and I would recommend reading few.

  
gdb-peda$ break main
Breakpoint 1 at 0x11f9
gdb-peda$ run aaaaaaaa
Starting program: /mnt/hgfs/shared/Linux RE/nix_5744af788e6cbdb29bb41e8b0e5f3cd5 aaaaaaaa

[----------------------------------registers-----------------------------------]
EAX: 0xf7f95dd8 --> 0xffffd2f0 --> 0xffffd4d1 ("NVM_DIR=/root/.nvm")
EBX: 0x0 
ECX: 0xffffd250 --> 0x2 
EDX: 0xffffd274 --> 0x0 
ESI: 0xf7f94000 --> 0x1d5d8c 
EDI: 0x0 
EBP: 0xffffd238 --> 0x0 
ESP: 0xffffd22c --> 0xffffd250 --> 0x2 
EIP: 0x565561f9 (:	sub    esp,0x1c)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x565561f6 :	push   esi
   0x565561f7 :	push   ebx
   0x565561f8 :	push   ecx
=> 0x565561f9 :	sub    esp,0x1c
   0x565561fc :	call   0x565560f0 <__x86.get_pc_thunk.bx>
   0x56556201 :	add    ebx,0x2dff
   0x56556207 :	mov    esi,ecx
   0x56556209 :	mov    DWORD PTR [ebp-0x1c],0x0
[------------------------------------stack-------------------------------------]
0000| 0xffffd22c --> 0xffffd250 --> 0x2 
0004| 0xffffd230 --> 0x0 
0008| 0xffffd234 --> 0xf7f94000 --> 0x1d5d8c 
0012| 0xffffd238 --> 0x0 
0016| 0xffffd23c --> 0xf7dd79a1 (<__libc_start_main+241>:	add    esp,0x10)
0020| 0xffffd240 --> 0xf7f94000 --> 0x1d5d8c 
0024| 0xffffd244 --> 0xf7f94000 --> 0x1d5d8c 
0028| 0xffffd248 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x565561f9 in main ()
1: main = {} 0x565561e9 
2: puts = {} 0xf7e25e40 gdb-peda$

If you play with gdb for a little you realize how it works. Let’s try to understand the logic part by part.

The program first tries to compare the number of arguments. It’s stored in ecx register and moved to esi and it’s used to compare the value with 0x2. You can use gdb to go through the assembly instructions and understand better.

 
  0x56556207 <+30>:	mov    esi,ecx
   0x56556209 <+32>:	mov    DWORD PTR [ebp-0x1c],0x0
   0x56556210 <+39>:	mov    DWORD PTR [ebp-0x24],0x7
   0x56556217 <+46>:	cmp    DWORD PTR [esi],0x2
   0x5655621a <+49>:	je     0x56556238 
   0x5655621c <+51>:	sub    esp,0xc
   0x5655621f <+54>:	lea    eax,[ebx-0x1ff8]
   0x56556225 <+60>:	push   eax
   0x56556226 <+61>:	call   0x56556040 
   0x5655622b <+66>:	add    esp,0x10
   0x5655622e <+69>:	sub    esp,0xc
   0x56556231 <+72>:	push   0x1
   0x56556233 <+74>:	call   0x56556060 

We can write pseudo code like this.

 
 if(argc != 2) {
    printf("Usage: script.exe <key>");
    exit(1);
 }
 
   0x56556238 <+79>:	mov    eax,DWORD PTR [esi+0x4]
   0x5655623b <+82>:	add    eax,0x4
   0x5655623e <+85>:	mov    eax,DWORD PTR [eax]
   0x56556240 <+87>:	sub    esp,0xc
   0x56556243 <+90>:	push   eax
   0x56556244 <+91>:	call   0x56556070 
   0x56556249 <+96>:	add    esp,0x10
   0x5655624c <+99>:	cmp    eax,0xf
   0x5655624f <+102>:	jbe    0x5655626d 
   0x56556251 <+104>:	sub    esp,0xc
   0x56556254 <+107>:	lea    eax,[ebx-0x1fe0]
   0x5655625a <+113>:	push   eax
   0x5655625b <+114>:	call   0x56556050 
   0x56556260 <+119>:	add    esp,0x10
   0x56556263 <+122>:	sub    esp,0xc
   0x56556266 <+125>:	push   0x1
   0x56556268 <+127>:	call   0x56556060 

After translating:

 
if(strlen(argv[1]) > 15) {
    puts("Length of argv[1] too long.");
    exit(1);
}

If you check this code we can see there is a loop going through iterating each character of our supplied string.

 
   0x5655626d <+132>:	mov    DWORD PTR [ebp-0x20],0x0
   0x56556274 <+139>:	jmp    0x56556290 
   0x56556276 <+141>:	mov    eax,DWORD PTR [esi+0x4]
   0x56556279 <+144>:	add    eax,0x4
   0x5655627c <+147>:	mov    edx,DWORD PTR [eax]
   0x5655627e <+149>:	mov    eax,DWORD PTR [ebp-0x20]
   0x56556281 <+152>:	add    eax,edx
   0x56556283 <+154>:	movzx  eax,BYTE PTR [eax]
   0x56556286 <+157>:	movsx  eax,al
   0x56556289 <+160>:	add    DWORD PTR [ebp-0x1c],eax
   0x5655628c <+163>:	add    DWORD PTR [ebp-0x20],0x1
   0x56556290 <+167>:	mov    eax,DWORD PTR [ebp-0x20]
   0x56556293 <+170>:	cmp    eax,DWORD PTR [ebp-0x24]
   0x56556296 <+173>:	jl     0x56556276 
   0x56556298 <+175>:	cmp    DWORD PTR [ebp-0x1c],0x321
   0x5655629f <+182>:	jne    0x565562bb 
   0x565562a1 <+184>:	call   0x565562d9 
   0x565562a6 <+189>:	sub    esp,0x8
   0x565562a9 <+192>:	push   eax
   0x565562aa <+193>:	lea    eax,[ebx-0x1fc4]
   0x565562b0 <+199>:	push   eax
   0x565562b1 <+200>:	call   0x56556040 
   0x565562b6 <+205>:	add    esp,0x10
   0x565562b9 <+208>:	jmp    0x565562cd 
   0x565562bb <+210>:	sub    esp,0xc
   0x565562be <+213>:	lea    eax,[ebx-0x1fa2]
   0x565562c4 <+219>:	push   eax
   0x565562c5 <+220>:	call   0x56556050 
   0x565562ca <+225>:	add    esp,0x10
   0x565562cd <+228>:	nop
   0x565562ce <+229>:	lea    esp,[ebp-0xc]
   0x565562d1 <+232>:	pop    ecx
   0x565562d2 <+233>:	pop    ebx
   0x565562d3 <+234>:	pop    esi
   0x565562d4 <+235>:	pop    ebp
   0x565562d5 <+236>:	lea    esp,[ecx-0x4]
   0x565562d8 <+239>:	ret   

Up to how many characters does it loop? Here’s how I found it. Basically, our password must be of 7 characters in length.

 
[----------------------------------registers-----------------------------------]
EAX: 0x6 
EBX: 0x56559000 --> 0x3efc 
ECX: 0x6 
EDX: 0xffffd4c6 ("1234567890")
ESI: 0xffffd250 --> 0x2 
EDI: 0x0 
EBP: 0xffffd238 --> 0x0 
ESP: 0xffffd210 --> 0xf7f943fc --> 0xf7f95200 --> 0x0 
EIP: 0x56556293 (:	cmp    eax,DWORD PTR [ebp-0x24])
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x56556289 :	add    DWORD PTR [ebp-0x1c],eax
   0x5655628c :	add    DWORD PTR [ebp-0x20],0x1
   0x56556290 :	mov    eax,DWORD PTR [ebp-0x20]
=> 0x56556293 :	cmp    eax,DWORD PTR [ebp-0x24]
   0x56556296 :	jl     0x56556276 
   0x56556298 :	cmp    DWORD PTR [ebp-0x1c],0x321
   0x5655629f :	jne    0x565562bb 
   0x565562a1 :	call   0x565562d9 
[------------------------------------stack-------------------------------------]
0000| 0xffffd210 --> 0xf7f943fc --> 0xf7f95200 --> 0x0 
0004| 0xffffd214 --> 0x7 
0008| 0xffffd218 --> 0x6 
0012| 0xffffd21c --> 0x135 
0016| 0xffffd220 --> 0x2 
0020| 0xffffd224 --> 0xffffd2e4 --> 0xffffd487 ("/mnt/hgfs/shared/Linux RE/nix_5744af788e6cbdb29bb41e8b0e5f3cd5")
0024| 0xffffd228 --> 0xffffd2f0 --> 0xffffd4d1 ("NVM_DIR=/root/.nvm")
0028| 0xffffd22c --> 0xffffd250 --> 0x2 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x56556293 in main ()
gdb-peda$ print $ebp-0x24
$24 = (void *) 0xffffd214
gdb-peda$ x/x 0xffffd214
0xffffd214:	0x00000007

After translating to high-level code, it would look something similar to this.

 
for (i = 0; i < 7; i++) value += argv[1][i];
if (value != 801) return puts("\n[+] No flag for you. [+]");
return printf("[+] The flag is: SAYCURE{%s} [+]\n", comp_key());

Basically, the sum of each byte of our password must be equal to 801. Givens us 7 characters, we can sum up like this. You can use any calculation which sums up to 801. After this check is done it calls the comp_key function and prints out the flag. We don’t really need to dig the com_key function as it directly gives us the flag.

114 * 6 + 117 = 801

Let’s check those characters in the ASCII table. 114 is ‘r’ and 117 is ‘u’.

 
Dec Hex    Dec Hex    Dec Hex  Dec Hex  Dec Hex  Dec Hex   Dec Hex   Dec Hex  
  0 00 NUL  16 10 DLE  32 20    48 30 0  64 40 @  80 50 P   96 60 `  112 70 p
  1 01 SOH  17 11 DC1  33 21 !  49 31 1  65 41 A  81 51 Q   97 61 a  113 71 q
  2 02 STX  18 12 DC2  34 22 "  50 32 2  66 42 B  82 52 R   98 62 b  114 72 r
  3 03 ETX  19 13 DC3  35 23 #  51 33 3  67 43 C  83 53 S   99 63 c  115 73 s
  4 04 EOT  20 14 DC4  36 24 $  52 34 4  68 44 D  84 54 T  100 64 d  116 74 t
  5 05 ENQ  21 15 NAK  37 25 %  53 35 5  69 45 E  85 55 U  101 65 e  117 75 u
  6 06 ACK  22 16 SYN  38 26 &  54 36 6  70 46 F  86 56 V  102 66 f  118 76 v
  7 07 BEL  23 17 ETB  39 27 '  55 37 7  71 47 G  87 57 W  103 67 g  119 77 w
  8 08 BS   24 18 CAN  40 28 (  56 38 8  72 48 H  88 58 X  104 68 h  120 78 x
  9 09 HT   25 19 EM   41 29 )  57 39 9  73 49 I  89 59 Y  105 69 i  121 79 y
 10 0A LF   26 1A SUB  42 2A *  58 3A :  74 4A J  90 5A Z  106 6A j  122 7A z
 11 0B VT   27 1B ESC  43 2B +  59 3B ;  75 4B K  91 5B [  107 6B k  123 7B {
 12 0C FF   28 1C FS   44 2C ,  60 3C <  76 4C L  92 5C \  108 6C l  124 7C |
 13 0D CR   29 1D GS   45 2D -  61 3D =  77 4D M  93 5D ]  109 6D m  125 7D }
 14 0E SO   30 1E RS   46 2E .  62 3E >  78 4E N  94 5E ^  110 6E n  126 7E ~
 15 0F SI   31 1F US   47 2F /  63 3F ?  79 4F O  95 5F _  111 6F o  127 7F DEL

That’s it! We just solved a very simple binary 🙂

 
$ ./nix_5744af788e6cbdb29bb41e8b0e5f3cd5 rrrrrru
[+] The flag is: SAYCURE{L3ts_g3t_in2_R3} [+]

Check out my previous CTF solution posts here
Birthday Crackme/
Rootme No software breakpoints Cracking Challenge
Solving Root-me Ptrace challenge
https://asciinema.org/~Osanda

References

http://www.cirosantilli.com/elf-hello-world/

29 thoughts on “Linux Reverse Engineering CTFs for Beginners

  1. when running # ./nix_5744af788e6cbdb29bb41e8b0e5f3cd5 aaaa
    im getting no such file or directory

    any ideas?

  2. Can’t download the binary from link :- https://ufile.io/blvpm
    Tried using wget & Firefox. Failed. Firefox just keeps on loading the page forever. wget just stands at Connecting to uploadfiles.io (uploadfiles.io)|218.248.255.164|:443…

    Location: India
    Date: 14 Feb

  3. Thanks for the article! How did you get colorized, terminal output for readelf, ltrace/strace, and objdump?

  4. Great article! The executable is not available for free anymore. Any chance you could make it available again please? 🙂

  5. Testing this on Ubuntu 20 on WSL2, perhaps this is a 32-bit issue but there doesn’t appear to be a way to run these:

    strace ./nix_5744af788e6cbdb29bb41e8b0e5f3cd5 aaaa
    execve(“./nix_5744af788e6cbdb29bb41e8b0e5f3cd5”, [“./nix_5744af788e6cbdb29bb41e8b0e”…, “aaaa”], 0x7fffe6ed02f8 /* 21 vars */) = -1 ENOEXEC (Exec format error)
    strace: exec: Exec format error
    +++ exited with 1 +++

    ltrace -i -C ./nix_5744af788e6cbdb29bb41e8b0e5f3cd5 aaaaaaaa
    ./nix_5744af788e6cbdb29bb41e8b0e5f3cd5: 1: Syntax error: “(” unexpected
    [0xffffffffffffffff] +++ exited (status 2) +++

  6. desperately seeking assembler crash courses. you left a very tantalizing teaser above “There are many good crash courses on assembly and I would recommend reading few” . please do, recommend a few … cheers from canada !!!

Leave a Reply to Osanda Malith JayathissaCancel reply