Practical binary analysis book: CTF writeup for levels 5-7
  2020-05-04

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.

This post is about my solutions for the levels 5, 6 and 7.

Level 5

We have a classic, stripped binary offering a nice, warm welcome message :p

$ file ./lvl5
./lvl5: 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]=1c4f1d4d245a8e252b77c38c9c1ba936f70d8245, stripped

$ ./lvl5
nothing to see here

This binary does not apparently interact with its environment, as shown by strace and ltrace :

$ ltrace -i ./lvl5
[0x400549] __libc_start_main(0x400500, 1, 0x7fff5f45d768, 0x4006f0 <unfinished ...>
[0x40050e] puts("nothing to see here"nothing to see here
)                                                      = 20
[0xffffffffffffffff] +++ exited (status 1) +++
$ strace -i ./lvl5
[00007fded8bf4777] execve("./lvl5", ["./lvl5"], [/* 35 vars */]) = 0
[00007fef2080d4b9] brk(NULL)            = 0x90d000
[00007fef2080e3c7] access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
[00007fef2080e3c7] access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
[00007fef2080e367] open("/home/binary/code/chapter5/tls/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[00007fef2080e2b5] stat("/home/binary/code/chapter5/tls/x86_64", 0x7ffdc9dbdfe0) = -1 ENOENT (No such file or directory)
[00007fef2080e367] open("/home/binary/code/chapter5/tls/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[00007fef2080e2b5] stat("/home/binary/code/chapter5/tls", 0x7ffdc9dbdfe0) = -1 ENOENT (No such file or directory)
[00007fef2080e367] open("/home/binary/code/chapter5/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[00007fef2080e2b5] stat("/home/binary/code/chapter5/x86_64", 0x7ffdc9dbdfe0) = -1 ENOENT (No such file or directory)
[00007fef2080e367] open("/home/binary/code/chapter5/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[00007fef2080e2b5] stat("/home/binary/code/chapter5", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
[00007fef2080e367] open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
[00007fef2080e2f4] fstat(3, {st_mode=S_IFREG|0644, st_size=98537, ...}) = 0
[00007fef2080e4ba] mmap(NULL, 98537, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fef209ff000
[00007fef2080e467] close(3)             = 0
[00007fef2080e3c7] access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
[00007fef2080e367] open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
[00007fef2080e387] read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\t\2\0\0\0\0\0"..., 832) = 832
[00007fef2080e2f4] fstat(3, {st_mode=S_IFREG|0755, st_size=1868984, ...}) = 0
[00007fef2080e4ba] mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fef209fe000
[00007fef2080e4ba] mmap(NULL, 3971488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fef20429000
[00007fef2080e557] mprotect(0x7fef205e9000, 2097152, PROT_NONE) = 0
[00007fef2080e4ba] mmap(0x7fef207e9000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000) = 0x7fef207e9000
[00007fef2080e4ba] mmap(0x7fef207ef000, 14752, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fef207ef000
[00007fef2080e467] close(3)             = 0
[00007fef2080e4ba] mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fef209fd000
[00007fef2080e4ba] mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fef209fc000
[00007fef207f3bd5] arch_prctl(ARCH_SET_FS, 0x7fef209fd700) = 0
[00007fef2080e557] mprotect(0x7fef207e9000, 16384, PROT_READ) = 0
[00007fef2080e557] mprotect(0x600000, 4096, PROT_READ) = 0
[00007fef2080e557] mprotect(0x7fef20a18000, 4096, PROT_READ) = 0
[00007fef2080e537] munmap(0x7fef209ff000, 98537) = 0
[00007fef2051fc34] fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
[00007fef20525e19] brk(NULL)            = 0x90d000
[00007fef20525e19] brk(0x92e000)        = 0x92e000
[00007fef205202c0] write(1, "nothing to see here\n", 20nothing to see here
) = 20
[00007fef204f5748] exit_group(1)        = ?
[????????????????] +++ exited with 1 +++

An quick glance to the .rodata section gives a few interesting strings key = 0x%08x and decrypted flag = %s to start with:

$ objdump -s -j .rodata ./lvl5

./lvl5:     file format elf64-x86-64

Contents of section .rodata:
 400770 01000200 6b657920 3d203078 25303878  ....key = 0x%08x
 400780 0a006465 63727970 74656420 666c6167  ..decrypted flag
 400790 203d2025 730a006e 6f746869 6e672074   = %s..nothing t
 4007a0 6f207365 65206865 726500             o see here.

Let’s follow the initial code flow from the program entry point address 0x400520 :

$ readelf -h ./lvl5|grep Entry
  Entry point address:               0x400520

Disassembly of the code starting there with objdump -d -Mintel :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  400520:	31 ed                	xor    ebp,ebp
  400522:	49 89 d1             	mov    r9,rdx
  400525:	5e                   	pop    rsi
  400526:	48 89 e2             	mov    rdx,rsp
  400529:	48 83 e4 f0          	and    rsp,0xfffffffffffffff0
  40052d:	50                   	push   rax
  40052e:	54                   	push   rsp
  40052f:	49 c7 c0 60 07 40 00 	mov    r8,0x400760
  400536:	48 c7 c1 f0 06 40 00 	mov    rcx,0x4006f0
  40053d:	48 c7 c7 00 05 40 00 	mov    rdi,0x400500
  400544:	e8 87 ff ff ff       	call   4004d0 <__libc_start_main@plt>
  400549:	f4                   	hlt

Focus a bit on libc_start_main() called here on line 11: the signature of the libc __libc_start_main() initialization routine is:

BP_SYM __libc_start_main(int (*main) (int, char**, char**), int argc, char *__unbounded *__unbounded ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (*__unbounded stack_end));

After all the glibc internal initialization has been completed, control will be handed off to the main function of the binary, which is here the first parameter as a function pointer: int (*main) (int, char**, char**)

The calling convention for this system (System V AMD64 ABI) specifies that registers are used for the first 6 parameters:

The first six integer or pointer arguments are passed in registers RDI, RSI, RDX, RCX, R8, R9

So here, the address of the main() function is passed trough the rdi register, here 0x400500

  40053d:	48 c7 c7 00 05 40 00 	mov    rdi,0x400500

The function starting at this address is very short:

Lines comments:

  1. Increase the stack size by 1 byte
  2. Set the address of the string to be displayed in the edi register - 0x400797 “nothing to see here”
  3. Call puts() to actually display it
  4. Set the exit code to 1 - eax register
  5. Shrink back the stack by 1 byte
  6. Returns to the caller, because this is the main function this mean the program is over with an exit code of 1 as shown below:
$ ./lvl5
nothing to see here
$ echo $?
1

Note about the 0x400797 string address: if you paid attention to the previous .rodata dump, this is the address of the 'nothing to see here' string:

Contents of section .rodata:
 400770 01000200 6b657920 3d203078 25303878  ....key = 0x%08x
 400780 0a006465 63727970 74656420 666c6167  ..decrypted flag
 400790 203d2025 730a006e 6f746869 6e672074   = %s..nothing t  <--- 0x400797
 4007a0 6f207365 65206865 726500             o see here.
 

This leaves us with a perfectly valid program, which only purpose so far is to mock us :^)

Why are the code paths using the other strings like decrypted flag out of reach? There are no conditional checks that could be easily reverted.

I’ll take the approach of searching the code for references to the other strings in the .rodata section:

  • (S1) 0x400774 key = 0x%08x

  • (S2) 0x400782 decrypted flag = %s

These addresses are referenced only once in this part of the code - note it starts at the address 0x400620 :

Some instructions deal with interesting 64bits values in (2) (3) (4) (5) which are very likely involved in the flag computation:

  • (2) 0x6223331533216010
  • (3) 0x6675364134766545
  • (4) 0x6570331064756717
  • (5) 0x6671671162763518

The string key = 0x%08x is a format specifier, and __printf_chk@ a variation of the printf() function

I wasn’t able to find any instruction calling out this function.

What happen if we try to patch the program Entry point address from 0x400520 to 0x400620 to get this code executed? This can be done quickly with bvi -s 0x18 ./lvl5 :

$ ./lvl5
key = 0x00400500
decrypted flag = ea36cbE`64A35fb5d60e06bb1f
Segmentation fault (core dumped)

We got this code to run, the outcome is a segfault assorted with some hints: a corrupted decrypted flag and a key, which value 0x400520 is just the program entry point address we just patched, what a coincidence!

What is the purpose of that key? Let’s try to understand what is happening here, comments on the right:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
  400620: 53                    push   rbx
  400621: be 74 07 40 00        mov    esi,0x400774                ; esi=0x400774 "key = 0x%08x"
  400626: bf 01 00 00 00        mov    edi,0x1                     ; edi=0x1
  40062b: 48 83 ec 30           sub    rsp,0x30                    ; allocate 48 bytes on the stack
g 40062f: 64 48 8b 04 25 28 00  mov    rax,QWORD PTR fs:0x28       ; rax=0xc902d681f5be2700 stack-guard value
  400636: 00 00
  400638: 48 89 44 24 28        mov    QWORD PTR [rsp+0x28],rax    ; [rsp+0x28] = 0xc902d681f5be2700
  40063d: 31 c0                 xor    eax,eax                     ; eax=0
  40063f: 48 b8 10 60 21 33 15  movabs rax,0x6223331533216010      ; rax=0x6223331533216010
  400646: 33 23 62
  400649: c6 44 24 20 00        mov    BYTE PTR [rsp+0x20],0x0     ; [rsp+20] = 0x0
  40064e: 48 89 04 24           mov    QWORD PTR [rsp],rax         ; [rsp] = 0x6223331533216010
  400652: 48 b8 45 65 76 34 41  movabs rax,0x6675364134766545      ; rax = 0x6675364134766545
  400659: 36 75 66
  40065c: 48 89 44 24 08        mov    QWORD PTR [rsp+0x8],rax     ; [rsp+0x8] = 0x6675364134766545
  400661: 48 b8 17 67 75 64 10  movabs rax,0x6570331064756717      ; rax = 0x6570331064756717
  400668: 33 70 65
  40066b: 48 89 44 24 10        mov    QWORD PTR [rsp+0x10],rax    ; [rsp+0x10] = 0x6570331064756717
  400670: 48 b8 18 35 76 62 11  movabs rax,0x6671671162763518      ; rax = 0x6671671162763518
  400677: 67 71 66
  40067a: 48 89 44 24 18        mov    QWORD PTR [rsp+0x18],rax    ; [rsp+0x18] = 0x6671671162763518
g 40067f: 8b 1c 25 40 05 40 00  mov    ebx,DWORD PTR ds:0x400540   ; ebx = 0x400500 - this is the key for the xor
  400686: 31 c0                 xor    eax,eax                     ; eax = 0
  400688: 89 da                 mov    edx,ebx                     ; edx = 0x400500
  40068a: e8 51 fe ff ff        call   4004e0 <__printf_chk@plt>
  40068f: 48 8d 54 24 20        lea    rdx,[rsp+0x20]              ; rdx = 0x7fffffffe3c8 - end of the 4 64bits values in the stack
  400694: 48 89 e0              mov    rax,rsp                     ; rax = 0x7fffffffe3a8 - start of the values
  400697: 66 0f 1f 84 00 00 00  nop    WORD PTR [rax+rax*1+0x0]    ; padding
  40069e: 00 00                                                    ; the 4 values are on the stack, between rax and rdx
  4006a0: 31 18                 xor    DWORD PTR [rax],ebx         ; xor 4 bytes of the values with the key 0x400500
  4006a2: 48 83 c0 04           add    rax,0x4                     ; rax used as an index - increase it by 4 bytes
  4006a6: 48 39 d0              cmp    rax,rdx                     ; have we reached to end of the 4 values range?
  4006a9: 75 f5                 jne    4006a0 <__printf_chk@plt+0x1c0> ; no: keep xor'ing next 4 bytes - yes: display the key
  4006ab: 31 c0                 xor    eax,eax                     ; eax = 0
  4006ad: 48 89 e2              mov    rdx,rsp                     ; rdx = 0x7fffffffe3a8
  4006b0: be 82 07 40 00        mov    esi,0x400782                ; esi = "decrypted flag = %s"
  4006b5: bf 01 00 00 00        mov    edi,0x1                     ; edi=0x1
  4006ba: e8 21 fe ff ff        call   4004e0 <__printf_chk@plt>   ; actually display the flag
  4006bf: 31 c0                 xor    eax,eax
  4006c1: 48 8b 4c 24 28        mov    rcx,QWORD PTR [rsp+0x28]
  4006c6: 64 48 33 0c 25 28 00  xor    rcx,QWORD PTR fs:0x28
  4006cd: 00 00
  4006cf: 75 06                 jne    4006d7 <__printf_chk@plt+0x1f7>
  4006d1: 48 83 c4 30           add    rsp,0x30
  4006d5: 5b                    pop    rbx
  4006d6: c3                    ret

Lines prefixed with g mean I had to actually run the code to figure out the resulting values. This is the stack before the initial xor loop, we can see the 4 64 bits values:

(gdb) x/48xg $rsp 
0x7fffffffe3a8: 0x6223331533216010  0x6675364134766545
                ^ rax points here [rsp]

0x7fffffffe3b8: 0x6570331064756717  0x6671671162763518
                                    ^ rdx points here [rsp+20]

0x7fffffffe3c8: 0x0000000000000000  0xc902d681f5be2700

This is now clear what happen: the 4 64-bits values before are xor’ed with the key 0x00400500, 32 bytes at a time.

But the correct key still needs to be figured out to not have the segfault and the flag displayed entirely.

We can try to change the key from 0x400520 to 0x400620 which is the program Entry point address we have been messing up with.

I have no idea how the key intially ended up in ds:0x400540 instruction:

40067f: 8b 1c 25 40 05 40 00  mov    ebx,DWORD PTR ds:0x400540

so I will directly patch the mov instruction which sets the value of the rdi parameter to __libc_start_main we have seen earlier:

  400520: 31 ed                 xor    ebp,ebp
  400522: 49 89 d1              mov    r9,rdx
  400525: 5e                    pop    rsi
  400526: 48 89 e2              mov    rdx,rsp
  400529: 48 83 e4 f0           and    rsp,0xfffffffffffffff0
  40052d: 50                    push   rax
  40052e: 54                    push   rsp
  40052f: 49 c7 c0 60 07 40 00  mov    r8,0x400760
  400536: 48 c7 c1 f0 06 40 00  mov    rcx,0x4006f0
  40053d: 48 c7 c7 00 05 40 00  mov    rdi,0x400500  <-- patching here directly to 0x400620
  400544: e8 87 ff ff ff        call   4004d0 <__libc_start_main@plt>
  400549: f4                    hlt

Hopefully the following will maybe update the value that ends up in ds:0x400540 (the key for the xor loop):

  • Set back the program entry point to its initial value 0x400520

  • Patch the instruction at address 0x40053d to move into rdi the value 0x400620

Note that our ‘hidden’ code starting at 0x400620 will still be called due to this update, just from another path.

Using bvi here:

  • with the \ key, I searched for the bytes of the instruction mov rdi,0x400500 instruction, which are 48 c7 c7 00 05 40 00

  • replace them with 48 c7 c7 20 06 40 00

New objdump disassembly to check it has been correctly patched:

  40053d: 48 c7 c7 20 06 40 00  mov    rdi,0x400620
  400544: e8 87 ff ff ff        call   4004d0 <__libc_start_main@plt>

Patched successfully!

$ ./lvl5
key = 0x00400620
decrypted flag = 0fa355cbec64a05f7a5d050e836b1a1f

$ ./oracle 0fa355cbec64a05f7a5d050e836b1a1f
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Level 5 completed, unlocked lvl6         |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
Run oracle with -h to show a hint

It works.

This level took me way more effort than the previous ones, just to make the connection between the xor key and the ‘hidden’ function address/program entry point.

Level 6

Initial inspection:

$ ./lvl6
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
$ echo $?
0

$ objdump -s -j .rodata ./lvl6

./lvl6:     file format elf64-x86-64

Contents of section .rodata:
 400910 01000200 44454255 473a2061 7267765b  ....DEBUG: argv[
 400920 315d203d 2025730a 00676574 5f646174  1] = %s..get_dat
 400930 615f6164 64720030 78256a78 00444154  a_addr.0x%jx.DAT
 400940 415f4144 44520025 642000             A_ADDR.%d .

$ readelf -sh ./lvl6
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:               0x400790
  Start of program headers:          64 (bytes into file)
  Start of section headers:          4496 (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

Symbol table '.dynsym' contains 9 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND putchar@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND setenv@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4 (3)
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __printf_chk@GLIBC_2.3.4 (4)
     7: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __sprintf_chk@GLIBC_2.3.4 (4)
     8: 00000000004005b0     0 FUNC    GLOBAL DEFAULT  UND strcmp@GLIBC_2.2.5 (2)

We have here some starting points with the setenv() and strcmp() functions, the .rodata section eventually indicate the use of command line arguments, due to the argv string.

strace and ltrace don’t tell us more right now, no file involved, no specific functions calls:

$ ltrace -i ./lvl6
[0x4007b9] __libc_start_main(0x4005f0, 1, 0x7fffeafe61b8, 0x400890 <unfinished ...>
[0x400745] __printf_chk(1, 0x400914, 0, 0DEBUG: argv[1] = (null)
)                                                      = 24
[0x400704] __printf_chk(1, 0x400947, 2, 100)                                                    = 2
[0x400704] __printf_chk(1, 0x400947, 3, 0x7ffffffe)                                             = 2
[0x400704] __printf_chk(1, 0x400947, 5, 0x7ffffffe)                                             = 2
[0x400704] __printf_chk(1, 0x400947, 7, 0x7ffffffe)                                             = 2
[0x400704] __printf_chk(1, 0x400947, 11, 0x7ffffffe)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 13, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 17, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 19, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 23, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 29, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 31, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 37, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 41, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 43, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 47, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 53, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 59, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 61, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 67, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 71, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 73, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 79, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 83, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 89, 0x7ffffffd)                                            = 3
[0x400704] __printf_chk(1, 0x400947, 97, 0x7ffffffd)                                            = 3
[0x400713] putchar(10, 3, 0, 0x7ffffffd2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
)                                                        = 10
[0xffffffffffffffff] +++ exited (status 0) +++
$ strace -i ./lvl6
[00007f9741915777] execve("./lvl6", ["./lvl6"], [/* 35 vars */]) = 0
[00007fcc709674b9] brk(NULL)            = 0x1fa6000
[00007fcc709683c7] access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
[00007fcc709683c7] access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
[00007fcc70968367] open("/home/binary/code/chapter5/tls/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[00007fcc709682b5] stat("/home/binary/code/chapter5/tls/x86_64", 0x7ffcad19b090) = -1 ENOENT (No such file or directory)
[00007fcc70968367] open("/home/binary/code/chapter5/tls/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[00007fcc709682b5] stat("/home/binary/code/chapter5/tls", 0x7ffcad19b090) = -1 ENOENT (No such file or directory)
[00007fcc70968367] open("/home/binary/code/chapter5/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[00007fcc709682b5] stat("/home/binary/code/chapter5/x86_64", 0x7ffcad19b090) = -1 ENOENT (No such file or directory)
[00007fcc70968367] open("/home/binary/code/chapter5/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[00007fcc709682b5] stat("/home/binary/code/chapter5", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
[00007fcc70968367] open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
[00007fcc709682f4] fstat(3, {st_mode=S_IFREG|0644, st_size=98537, ...}) = 0
[00007fcc709684ba] mmap(NULL, 98537, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fcc70b59000
[00007fcc70968467] close(3)             = 0
[00007fcc709683c7] access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
[00007fcc70968367] open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
[00007fcc70968387] read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\t\2\0\0\0\0\0"..., 832) = 832
[00007fcc709682f4] fstat(3, {st_mode=S_IFREG|0755, st_size=1868984, ...}) = 0
[00007fcc709684ba] mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcc70b58000
[00007fcc709684ba] mmap(NULL, 3971488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fcc70583000
[00007fcc70968557] mprotect(0x7fcc70743000, 2097152, PROT_NONE) = 0
[00007fcc709684ba] mmap(0x7fcc70943000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000) = 0x7fcc70943000
[00007fcc709684ba] mmap(0x7fcc70949000, 14752, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fcc70949000
[00007fcc70968467] close(3)             = 0
[00007fcc709684ba] mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcc70b57000
[00007fcc709684ba] mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcc70b56000
[00007fcc7094dbd5] arch_prctl(ARCH_SET_FS, 0x7fcc70b57700) = 0
[00007fcc70968557] mprotect(0x7fcc70943000, 16384, PROT_READ) = 0
[00007fcc70968557] mprotect(0x600000, 4096, PROT_READ) = 0
[00007fcc70968557] mprotect(0x7fcc70b72000, 4096, PROT_READ) = 0
[00007fcc70968537] munmap(0x7fcc70b59000, 98537) = 0
[00007fcc70679c34] fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
[00007fcc7067fe19] brk(NULL)            = 0x1fa6000
[00007fcc7067fe19] brk(0x1fc7000)       = 0x1fc7000
[00007fcc7067a2c0] write(1, "DEBUG: argv[1] = (null)\n", 24DEBUG: argv[1] = (null)
) = 24
[00007fcc7067a2c0] write(1, "2 3 5 7 11 13 17 19 23 29 31 37 "..., 722 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
) = 72
[00007fcc7064f748] exit_group(0)        = ?
[????????????????] +++ exited with 0 +++

As a next step I’ll do a static analysis of the code, to learn more about what is happening in main()

There must be a better way, but as a beginner I did this analysis by adding comments to the asm listing issued by the dump of objdump -Mintel -d –insn-width=10 lvl6

  • the supposed functions boundaries by looking for functions prologue
  • educated guess on their purpose
  • references to strings
  • call to external functions

Starting with the instruction at the entry point address, 0x400790.

The entry point leads us to the glibc initial setup function, which tells us the main() function is located at 0x4005f0 (first argument to libc_start_main() stored in rdi)

400790: 31 ed                           xor    ebp,ebp
400792: 49 89 d1                        mov    r9,rdx
400795: 5e                              pop    rsi
400796: 48 89 e2                        mov    rdx,rsp
400799: 48 83 e4 f0                     and    rsp,0xfffffffffffffff0
40079d: 50                              push   rax
40079e: 54                              push   rsp
40079f: 49 c7 c0 00 09 40 00            mov    r8,0x400900
4007a6: 48 c7 c1 90 08 40 00            mov    rcx,0x400890
4007ad: 48 c7 c7 f0 05 40 00            mov    rdi,0x4005f0
4007b4: e8 e7 fd ff ff                  call   4005a0 <__libc_start_main@plt>

The main() function starts at the address 0x4005f0, below is the first part commented:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
00000000004005f0 <.text>:
  4005f0: 55                              push   rbp
  4005f1: 53                              push   rbx
  4005f2: 89 fd                           mov    ebp,edi                            ; argc stored in edi
  4005f4: 48 89 f3                        mov    rbx,rsi                            ; argv pointer stored in rsi
  4005f7: 48 81 ec a8 05 00 00            sub    rsp,0x5a8                          ; reserve 1448 bytes on the stack
  4005fe: 8b 15 60 0a 20 00               mov    edx,DWORD PTR [rip+0x200a60]       ; 0x60105e: within .data section
  400604: 64 48 8b 04 25 28 00 00 00      mov    rax,QWORD PTR fs:0x28              ; retrieve stack smash protection canary value
  40060d: 48 89 84 24 98 05 00 00         mov    QWORD PTR [rsp+0x598],rax          ; write it as the first 16 bytes of the stack
  400615: 31 c0                           xor    eax,eax
  400617: 85 d2                           test   edx,edx                            ; equivalent to cmp edx, 0
  400619: 0f 85 13 01 00 00               jne    400732                             ; not taken
  40061f: 83 fd 01                        cmp    ebp,0x1                            ; is argc == 1? (cmdline parameter count)
  400622: 48 c7 05 3b 0a 20 00 b0 05 40   mov    QWORD PTR [rip+0x200a3b],0x4005b0  ; 0x60105d: within .data section
  40062c: 00
  40062d: 7e 16                           jle    400645 <__sprintf_chk@plt+0x75>    ; jump taken
  40062f: 48 8b 7b 08                     mov    rdi,QWORD PTR [rbx+0x8]            ; move to rdi the first cmdline parameter
  400633: be 29 09 40 00                  mov    esi,0x400929                       ; "get_data_addr"
  400638: e8 73 ff ff ff                  call   4005b0 <strcmp@plt>                ; compare the 1st parameter to "get_data_addr"
  40063d: 85 c0                           test   eax,eax
  40063f: 0f 84 05 01 00 00               je     40074a <__sprintf_chk@plt+0x17a> 

How do I know about the .data section addresses range? These can be retrieved from $ readelf -S ./lvl6 output:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
(...)
  [25] .data             PROGBITS         0000000000601050  00001050
       0000000000000010  0000000000000000  WA       0     0     8
(...)

.data section

  • starts at 0x601050 (Address 0x601050)
  • ends at 0x601060 (Address 0x601050 + size 0x10)

The System V ABI specification - page 32 shows the full main() function prototype, argc (argument count) and argv (argument vector) being the command line arguments passed to main() :

    extern int main ( int argc , char *argv[ ] , char* envp[ ] );

Additionaly, the System V AMD64 ABI tells us the 2 first arguments, here argc and argv, are passed through the rdi and rsi registers.

Side note about stack smashing protection: line 8: what is this stack smash protection canary? This has greatly confused me at the beginning, especially because the canary value was changing on every execution. You don’t know what you don’t know :^) More details on this security mechanism directly handled by the compiler.

So this chunk of code checks if we are passing a single command line argument called get_data_addr , let’s try and ltrace it:

$ ./lvl6 get_data_addr
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
binary@binary-VirtualBox:~/code/chapter5$ ltrace -i ./lvl6 get_data_addr
[0x4007b9] __libc_start_main(0x4005f0, 2, 0x7ffede5c7ca8, 0x400890 <unfinished ...>
[0x40063d] strcmp("get_data_addr", "get_data_addr")                                             = 0
[0x40076c] __sprintf_chk(0x7ffede5c77a0, 1, 1024, 0x400937)                                     = 8
[0x400783] setenv("DATA_ADDR", "0x4006c1", 1)                                                   = 0
[0x400704] __printf_chk(1, 0x400947, 2, 100)                                                    = 2
[0x400704] __printf_chk(1, 0x400947, 3, 0x7ffffffe)                                             = 2

The output did not change, however ltrace shows us another clue setenv("DATA_ADDR", "0x4006c1", 1)

If we look at the program instruction at this address, the instructions starting on line2 seem very weird:

1
2
3
4
5
  4006c1: 2e 29 c6                        cs sub esi,eax
  4006c4: 4a 0f 03 a6 ee 2a 30 7f         rex.WX lsl rsp,WORD PTR [rsi+0x7f302aee]
  4006cc: ec                              in     al,dx

  4006cd: c8 c3 ff 42                     enter  0xffc3,0x42

If you count the number of bytes of these instructions, this is 16 bytes, which happen to be the length of our flag! I just have to concatenate them and try to submit to the oracle:

$ ./oracle 2e29c64a0f03a6ee2a307fecc8c3ff42
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Level 6 completed, unlocked lvl7         |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
Run oracle with -h to show a hint

Level cleared!

That was a nice level which a red herring - the suite of numbers. I have to admit I’ve wasted time trying to understand all the logic behind the numbers generation, kudos to the author!

Level 7

Not a binary file, but several within lvl7, which is a compressed tarball:

  • stage1 - binary executable
  • stage2.zip - password protected zip archive

TIL: file can sneak into compressed files with -z:

$ file lvl7
lvl7: gzip compressed data, last modified: Sat Dec  1 17:30:15 2018, from Unix

$ file -z lvl7
lvl7: POSIX tar archive (GNU) (gzip compressed data, last modified: Sat Dec  1 17:30:15 2018, from Unix)

$ tar tvzf lvl7
-rwxr-xr-x dnx/dnx        6304 2018-12-01 18:25 stage1
-rw-r--r-- dnx/dnx        7738 2018-08-09 17:20 stage2.zip
$ file ./stage1
./stage1: 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]=ac71081a0951af729a4064c1dafbc5713b1537e3, stripped

$ unzip stage2.zip
Archive:  stage2.zip
[stage2.zip] tmp password:

stage1 does not call any external libraries function:

$ readelf -s ./stage1

Symbol table '.dynsym' contains 3 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

There is a somehow crippled string in the .rodata section:

$ objdump -s -j .rodata ./stage1

./stage1:     file format elf64-x86-64

Contents of section .rodata:
 4005a0 01000200 20532954 411147fa deff4532  .... S)TA.G...E2
 4005b0 204b458a 5900                         KE.Y.

While starting to follow the code from the program entry point 0x400420 to the main function at 0x0x4003e0 , I did notice again some weird instructions, especially the ones starting at 0x4003f7

00000000004003e0 <.text>:
  4003e0: ba a4 05 40 00                  mov    edx,0x4005a4                  
  4003e5: 0f 1f 00                        nop    DWORD PTR [rax]
  4003e8: 0f be 02                        movsx  eax,BYTE PTR [rdx]
  4003eb: 83 f8 30                        cmp    eax,0x30
  4003ee: 7c 12                           jl     400402 
  4003f0: 83 f8 5a                        cmp    eax,0x5a
  4003f3: 7f 0d                           jg     400402 
  4003f5: eb 09                           jmp    400400 
  4003f7: 64 75 6d                        fs jne 400467 
  4003fa: 70 20                           jo     40041c 
  4003fc: 65 63 78 00                     movsxd edi,DWORD PTR gs:[rax+0x0]
  400400: 89 c1                           mov    ecx,eax
  400402: 48 83 c2 01                     add    rdx,0x1
  400406: 48 81 fa b5 05 40 00            cmp    rdx,0x4005b5
  40040d: 75 d9                           jne    4003e8
  40040f: 31 c0                           xor    eax,eax
  400411: c3                              ret

0x4003f7 being obviously in the .text section we are currently within, there might be something hidden? I first noticed that with a .text section dump with objdump:

$ objdump -sj .text  ./stage1

./stage1:     file format elf64-x86-64

Contents of section .text:
 4003e0 baa40540 000f1f00 0fbe0283 f8307c12  ...@.........0|.
 4003f0 83f85a7f 0deb0964 756d7020 65637800  ..Z....dump ecx.  <--- dump ecx
 400400 89c14883 c2014881 fab50540 0075d931  ..H...H....@.u.1
 400410 c0c3662e 0f1f8400 00000000 0f1f4000  ..f...........@.
 400420 31ed4989 d15e4889 e24883e4 f0505449  1.I..^H..H...PTI
 400430 c7c09005 400048c7 c1200540 0048c7c7  ....@.H.. .@.H..
 400440 e0034000 e877ffff fff4660f 1f440000  ..@..w....f..D..
 400450 b8371060 0055482d 30106000 4883f80e  .7.`.UH-0.`.H...
 400460 4889e576 1bb80000 00004885 c074115d  H..v......H..t.]
 400470 bf301060 00ffe066 0f1f8400 00000000  .0.`...f........
 400480 5dc30f1f 4000662e 0f1f8400 00000000  ]...@.f.........
 400490 be301060 00554881 ee301060 0048c1fe  .0.`.UH..0.`.H..
 4004a0 034889e5 4889f048 c1e83f48 01c648d1  .H..H..H..?H..H.
 4004b0 fe7415b8 00000000 4885c074 0b5dbf30  .t......H..t.].0
 4004c0 106000ff e00f1f00 5dc3660f 1f440000  .`......].f..D..
 4004d0 803d590b 20000075 11554889 e5e86eff  .=Y. ..u.UH...n.
 4004e0 ffff5dc6 05460b20 0001f3c3 0f1f4000  ..]..F. ......@.
 4004f0 bf200e60 0048833f 007505eb 930f1f00  . .`.H.?.u......
 400500 b8000000 004885c0 74f15548 89e5ffd0  .....H..t.UH....
 400510 5de97aff ffff662e 0f1f8400 00000000  ].z...f.........
 400520 41574156 4189ff41 5541544c 8d25de08  AWAVA..AUATL.%..
 400530 20005548 8d2dde08 20005349 89f64989   .UH.-.. .SI..I.
 400540 d54c29e5 4883ec08 48c1fd03 e83ffeff  .L).H...H....?..
 400550 ff4885ed 742031db 0f1f8400 00000000  .H..t 1.........
 400560 4c89ea4c 89f64489 ff41ff14 dc4883c3  L..L..D..A...H..
 400570 014839eb 75ea4883 c4085b5d 415c415d  .H9.u.H...[]A\A]
 400580 415e415f c390662e 0f1f8400 00000000  A^A_..f.........
 400590 f3c3

See the string dump ecx " ! That could have been noticed within gdb as well at runtime:

gdb> x/s 0x4003f7
0x4003f7: "dump ecx"

What do we do with that clue?

I’ll start tracing the main function starting at 0x4003e0 with gdb and see how ecx value changes over time, since in this code ecx is only written to from eax, never copied somewhere else so there is no ‘history’ of the ecx previous values:

  400400: 89 c1                           mov    ecx,eax
gdb ./stage1
>>> b *0x4003e0
Breakpoint 1 at 0x4003e0
>>> r

Start the program and wait for the breakpoint on main() address to be reached. We only want to monitor ecx changes from that part of the code on, not from the very beginning with all the initial glibc housekeeping.

Breakpoint 1, 0x00000000004003e0 in ?? ()
>>> watch $ecx
Watchpoint 1: $ecx

Set a memory watch on the ecx register (watchpoint), which mean the program will stop and bring us back to the gdb prompt when ecx value change, with the older and new value. We can now continue the execution, hopefully this is what dump ecx meant!

>>> c

>>> c
Continuing.

Watchpoint 1: $ecx

Old value = 0
New value = 83
0x0000000000400402 in ?? ()
>>>
Continuing.

Watchpoint 1: $ecx

Old value = 83
New value = 84
0x0000000000400402 in ?? ()
>>>
Continuing.

Watchpoint 1: $ecx

Old value = 84
New value = 65
0x0000000000400402 in ?? ()
>>>
Continuing.

Watchpoint 1: $ecx

Old value = 65
New value = 71
0x0000000000400402 in ?? ()

I’ve trimmed the output, but you get the grist. So far the sequential changes of ecx have been: 83 -> 84 -> 65 -> 71…

Do it until the program exits abruptly:

Old value = 89
New value = -136500144
0x00007ffff7a46f43 in __run_exit_handlers (status=0, listp=0x7ffff7dd15f8 <__exit_funcs>, run_list_atexit=run_list_atexit@entry=true) at exit.c:50
50  exit.c: No such file or directory.

Finally we got these values: 838465716950756989 , using their ascii representation we got:

83 84 65 71 69 50 75 69 89
S  T  A  G  E   2  K  E  Y  

And here we see we should have tried what was already visible on the first overview of the .rodata section! Remember:

Contents of section .rodata:
 4005a0 01000200 20532954 411147fa deff4532  .... S)TA.G...E2
 4005b0 204b458a 5900   

Trying our password on the zip password protected stage2.zip file, it works:

 $ unzip stage2.zip
Archive:  stage2.zip
[stage2.zip] tmp password:
  inflating: tmp
  inflating: stage2

2 new binary files to analyse!

  • stage2 binary file
  • tmp binary file
$ file stage2
stage2: 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]=377416069d2f4238cb934608d3e580a1df3d0b58, stripped

$ file tmp
tmp: 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]=377416069d2f4238cb934608d3e580a1df3d0b58, stripped

They look to be exactly the same binaries:

$ md5sum tmp stage2
1eb7e8a43c001ecc3ab13de2dd999f75  tmp
1eb7e8a43c001ecc3ab13de2dd999f75  stage2

when executed, they display an obfuscated c++ source code: the whole program is stored in char array char q[] :

$ ./stage2

#include <stdio.h>
#include <string.h>
#include <vector>
#include <algorithm>

int main()
{
std::vector<char> hex;
char q[] = "#include <stdio.h>\n#include <string.h>\n#include <vector>\n#include <algorithm>\n\nint main()\n{\nstd::vector<char> hex;\nchar q[] = \"%s\";\nint i, _0F;\nchar c, qc[4096];\n\nfor(i = 0; i < 32; i++) for(c = '0'; c <= '9'; c++) hex.push_back(c);\nfor(i = 0; i < 32; i++) for(c = 'A'; c <= 'F'; c++) hex.push_back(c);\nstd::srand(55);\nstd::random_shuffle(hex.begin(), hex.end());\n\n_0F = 0;\nfor(i = 0; i < strlen(q); i++)\n{\nif(q[i] == 0xa)\n{\nqc[_0F++] = 0x5c;\nqc[_0F] = 'n';\n}\nelse if(q[i] == 0x22)\n{\nqc[_0F++] = 0x5c;\nqc[_0F] = 0x22;\n}\nelse if(!strncmp(&q[i], \"0F\", 2) && (q[i-1] == '_' || i == 545))\n{\nchar buf[3];\nbuf[0] = q[i];\nbuf[1] = q[i+1];\nbuf[2] = 0;\nunsigned j = strtoul(buf, NULL, 16);\nqc[_0F++] = q[i++] = hex[j];\nqc[_0F] = q[i] = hex[j+1];\n}\nelse qc[_0F] = q[i];\n_0F++;\n}\nqc[_0F] = 0;\n\nprintf(q, qc);\n\nreturn 0;\n}\n";
int i, _0F;
char c, qc[4096];

for(i = 0; i < 32; i++) for(c = '0'; c <= '9'; c++) hex.push_back(c);
for(i = 0; i < 32; i++) for(c = 'A'; c <= 'F'; c++) hex.push_back(c);
std::srand(55);
std::random_shuffle(hex.begin(), hex.end());

_0F = 0;
for(i = 0; i < strlen(q); i++)
{
if(q[i] == 0xa)
{
qc[_0F++] = 0x5c;
qc[_0F] = 'n';
}
else if(q[i] == 0x22)
{
qc[_0F++] = 0x5c;
qc[_0F] = 0x22;
}
else if(!strncmp(&q[i], "0F", 2) && (q[i-1] == '_' || i == 545))
{
char buf[3];
buf[0] = q[i];
buf[1] = q[i+1];
buf[2] = 0;
unsigned j = strtoul(buf, NULL, 16);
qc[_0F++] = q[i++] = hex[j];
qc[_0F] = q[i] = hex[j+1];
}
else qc[_0F] = q[i];
_0F++;
}
qc[_0F] = 0;

printf(q, qc);

return 0;
}

Unsurprisingly (they have the same md5sum) the tmp binary has the same output:

$ diff -u <(./stage2) <(./tmp)
$

However things start to be interesting when the obfuscated c++ source code is compiled, and its output compared to the binary output it came from.

Here we compare the output of stage2 to the binary issued from the stage2 source code written to the console:

$ ./stage2 |  g++ -x c++ - -o out
$ diff -u <(./stage2) <(./out)
--- /dev/fd/63  2020-05-05 19:50:59.776913999 +0200
+++ /dev/fd/62  2020-05-05 19:50:59.776913999 +0200
@@ -6,8 +6,8 @@
 int main()
 {
 std::vector<char> hex;
-char q[] = "#include <stdio.h>\n#include <string.h>\n#include <vector>\n#include <algorithm>\n\nint main()\n{\nstd::vector<char> hex;\nchar q[] = \"%s\";\nint i, _0F;\nchar c, qc[4096];\n\nfor(i = 0; i < 32; i++) for(c = '0'; c <= '9'; c++) hex.push_back(c);\nfor(i = 0; i < 32; i++) for(c = 'A'; c <= 'F'; c++) hex.push_back(c);\nstd::srand(55);\nstd::random_shuffle(hex.begin(), hex.end());\n\n_0F = 0;\nfor(i = 0; i < strlen(q); i++)\n{\nif(q[i] == 0xa)\n{\nqc[_0F++] = 0x5c;\nqc[_0F] = 'n';\n}\nelse if(q[i] == 0x22)\n{\nqc[_0F++] = 0x5c;\nqc[_0F] = 0x22;\n}\nelse if(!strncmp(&q[i], \"0F\", 2) && (q[i-1] == '_' || i == 545))\n{\nchar buf[3];\nbuf[0] = q[i];\nbuf[1] = q[i+1];\nbuf[2] = 0;\nunsigned j = strtoul(buf, NULL, 16);\nqc[_0F++] = q[i++] = hex[j];\nqc[_0F] = q[i] = hex[j+1];\n}\nelse qc[_0F] = q[i];\n_0F++;\n}\nqc[_0F] = 0;\n\nprintf(q, qc);\n\nreturn 0;\n}\n";
-int i, _0F;
+char q[] = "#include <stdio.h>\n#include <string.h>\n#include <vector>\n#include <algorithm>\n\nint main()\n{\nstd::vector<char> hex;\nchar q[] = \"%s\";\nint i, _25;\nchar c, qc[4096];\n\nfor(i = 0; i < 32; i++) for(c = '0'; c <= '9'; c++) hex.push_back(c);\nfor(i = 0; i < 32; i++) for(c = 'A'; c <= 'F'; c++) hex.push_back(c);\nstd::srand(55);\nstd::random_shuffle(hex.begin(), hex.end());\n\n_25 = 0;\nfor(i = 0; i < strlen(q); i++)\n{\nif(q[i] == 0xa)\n{\nqc[_25++] = 0x5c;\nqc[_25] = 'n';\n}\nelse if(q[i] == 0x22)\n{\nqc[_25++] = 0x5c;\nqc[_25] = 0x22;\n}\nelse if(!strncmp(&q[i], \"25\", 2) && (q[i-1] == '_' || i == 545))\n{\nchar buf[3];\nbuf[0] = q[i];\nbuf[1] = q[i+1];\nbuf[2] = 0;\nunsigned j = strtoul(buf, NULL, 16);\nqc[_25++] = q[i++] = hex[j];\nqc[_25] = q[i] = hex[j+1];\n}\nelse qc[_25] = q[i];\n_25++;\n}\nqc[_25] = 0;\n\nprintf(q, qc);\n\nreturn 0;\n}\n";
+int i, _25;
 char c, qc[4096];

 for(i = 0; i < 32; i++) for(c = '0'; c <= '9'; c++) hex.push_back(c);
@@ -15,33 +15,33 @@
 std::srand(55);
 std::random_shuffle(hex.begin(), hex.end());

-_0F = 0;
+_25 = 0;
 for(i = 0; i < strlen(q); i++)
 {
 if(q[i] == 0xa)
 {
-qc[_0F++] = 0x5c;
-qc[_0F] = 'n';
+qc[_25++] = 0x5c;
+qc[_25] = 'n';
 }
 else if(q[i] == 0x22)
 {
-qc[_0F++] = 0x5c;
-qc[_0F] = 0x22;
+qc[_25++] = 0x5c;
+qc[_25] = 0x22;
 }
-else if(!strncmp(&q[i], "0F", 2) && (q[i-1] == '_' || i == 545))
+else if(!strncmp(&q[i], "25", 2) && (q[i-1] == '_' || i == 545))
 {
 char buf[3];
 buf[0] = q[i];
 buf[1] = q[i+1];
 buf[2] = 0;
 unsigned j = strtoul(buf, NULL, 16);
-qc[_0F++] = q[i++] = hex[j];
-qc[_0F] = q[i] = hex[j+1];
+qc[_25++] = q[i++] = hex[j];
+qc[_25] = q[i] = hex[j+1];
 }
-else qc[_0F] = q[i];
-_0F++;
+else qc[_25] = q[i];
+_25++;
 }
-qc[_0F] = 0;
+qc[_25] = 0;

 printf(q, qc);

The differences are roughlya bunch of '0F' replaced by '25'

Running that one more time by compiling the source code generated by out , we are now at stage2 -> out -> out2 iteration:

$ ./out |  g++ -x c++ - -o out2
$ diff -u <(./out) <(./out2)

(diff truncated)
-qc[_25++] = 0x5c;
-qc[_25] = 'n';
+qc[_E5++] = 0x5c;
+qc[_E5] = 'n';

This time '25' are superseded by 'E5'

Let’s go down the rabbit hole on that generated c++ source code: the only source of change would come from these 2 lines:

 std::srand(55);
 std::random_shuffle(hex.begin(), hex.end())

std::srand(55) indicates that the random number generator is initialized every time with the same seed, here 55 , therefore random number sequence will always be the same, as confirmed by man rand(3)

   The  srand() function sets its argument as the seed for a new sequence of pseudo-random integers to be returned by rand().  These sequences
   are repeatable by calling srand() with the same seed value.

Does the changing variable name be a part of the flag we are looking for?

I’ll try to repeat the following steps until I have 16 bytes worth of data (32 hex characters, expected flag length), starting with the provided stage2 binary, using something along the lines:

./stage$N |  g++ -x c++ - -o stage$N+1

As this is tedious to run manually, this has to be automated (the egrep thingy is to extract the changing values we are trying to track):

#!/bin/bash
for i in {2..17}
do
  ((j=i+1))
  ./stage$i | g++ -x c++ - -o stage$j
  ./stage$i|egrep '^int i, _'|cut -f2 -d','|tr -d '_; '
done

Remove the newlines characters, because we are lazy and it saves a lot of copy/paste operations:

$ ./auto.sh
0F
25
E5
12
A7
76
3E
EF
B7
69
6B
3A
ED
A1
F9
64

$ ./auto.sh |tr -d '\n'
0F25E512A7763EEFB7696B3AEDA1F964

It does the trick :))

$ ./oracle 0F25E512A7763EEFB7696B3AEDA1F964
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Level 7 completed, unlocked lvl8         |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
Run oracle with -h to show a hint

Next levels writeup in an upcoming post.

Feedback

Constructive criticism always welcome! (comments here, contact in About ) as I would be more than happy to learn more!