That's not how LD_PRELOAD
is meant to work*. And I'm really struggling to find how it could ever worked. When I perform the following on the latest linux kernel (4.7), latest glibc
(2.27) and latest ld
(binutils 2.27) I get the following:
$ LD_PRELOAD='0000000' less pp.c
ERROR: ld.so: object '0000000' from LD_PRELOAD cannot be preloaded (cannot open shared object file): ignored.
[4]+ Stopped LD_PRELOAD='0000000' less pp.c
So yeah, ld
simply complains that the object (here 0x00000) is not an ELF. This comes from elf/rtld.c
in glibc
, here:
static unsigned int
do_preload (const char *fname, struct link_map *main_map, const char *where)
{
const char *objname;
const char *err_str = NULL;
struct map_args args;
bool malloced;
args.str = fname;
args.loader = main_map;
args.mode = __RTLD_SECURE;
unsigned int old_nloaded = GL(dl_ns)[LM_ID_BASE]._ns_nloaded;
(void) _dl_catch_error (&objname, &err_str, &malloced, map_doit, &args);
if (__glibc_unlikely (err_str != NULL))
{
_dl_error_printf ("\
ERROR: ld.so: object '%s' from %s cannot be preloaded (%s): ignored.\n",
fname, where, err_str);
/* No need to call free, this is still before
the libc's malloc is used. */
}
else if (GL(dl_ns)[LM_ID_BASE]._ns_nloaded != old_nloaded)
/* It is no duplicate. */
return 1;
/* Nothing loaded. */
return 0;
}
do_preload
is called for every object in LD_PRELOAD
(separated either by space or colon). Now ubuntu 3.0.0 was released around July 2011, at that time the latest glibc
was 2.15; I looked at do_preload
in glibc-2.15
and found the exact same code.
Now, maybe the ubuntu guys used an old glibc, say glibc-2.11
(November 2009, and that's pushing it far). If you look at that code, you see that do_preload
is still the same. That code has been added in 1998 and never changed.
The only thing I can think of are features added by ubuntu at the time (but that's pretty dubious). It should have nothing to do with the linux kernel since it is the linker's work to add the correct shared objects (Just in case, I went to one of those code indexers for the kernel code and checked all 3.x and 4.x kernels. And, as expected, LD_PRELOAD is not used anywhere).
The only thing I can do is to argue about how LD_PRELOAD
should be used. And how to achieve a similar effect taking into consideration the above excerpt from glibc
. The first option to get that shellcode into memory uses LD_PRELOAD
properly (but that is very different from what they do in that blog post), the second option uses the environment (which I believe is what they are seeing in the blog post and misunderstanding it as a loaded shared object).
Option 1: use a real shared object in LD_PRELOAD
The current ld
will check whether the object pointed by LD_PRELOAD
is an ELF, i.e. that it starts with \x7fELF
. A better way to achieve the loading of specific code into a program's memory is to create an ELF yourself and then point to it with LD_PRELOAD
:
$ perl -e 'print "int yay(void){char *x=\"" . "A"x10000 . "\"; return 0;}\n"' > pp.c
$ gcc -o pp.so -fPIC -shared pp.c
$ LD_PRELOAD=./pp.so less pp.c
# And Ctrl+Z to send SIGTSTP
$ jobs -l
[1]+ 15476 Stopped LD_PRELOAD=./pp.so less pp.c
$ cat /proc/15476/maps
00400000-00422000 r-xp 00000000 08:03 3811056 /usr/bin/less
00621000-00622000 r--p 00021000 08:03 3811056 /usr/bin/less
00622000-00626000 rw-p 00022000 08:03 3811056 /usr/bin/less
00626000-0062a000 rw-p 00000000 00:00 0
01336000-01357000 rw-p 00000000 00:00 0 [heap]
7f0328272000-7f0328525000 r--p 00000000 08:03 3845397 /usr/lib/locale/locale-archive
7f0328525000-7f032853d000 r-xp 00000000 08:03 3804385 /usr/lib/libpthread-2.24.so
7f032853d000-7f032873c000 ---p 00018000 08:03 3804385 /usr/lib/libpthread-2.24.so
7f032873c000-7f032873d000 r--p 00017000 08:03 3804385 /usr/lib/libpthread-2.24.so
7f032873d000-7f032873e000 rw-p 00018000 08:03 3804385 /usr/lib/libpthread-2.24.so
7f032873e000-7f0328742000 rw-p 00000000 00:00 0
7f0328742000-7f03288d7000 r-xp 00000000 08:03 3804421 /usr/lib/libc-2.24.so
7f03288d7000-7f0328ad6000 ---p 00195000 08:03 3804421 /usr/lib/libc-2.24.so
7f0328ad6000-7f0328ada000 r--p 00194000 08:03 3804421 /usr/lib/libc-2.24.so
7f0328ada000-7f0328adc000 rw-p 00198000 08:03 3804421 /usr/lib/libc-2.24.so
7f0328adc000-7f0328ae0000 rw-p 00000000 00:00 0
7f0328ae0000-7f0328b52000 r-xp 00000000 08:03 3811050 /usr/lib/libpcre.so.1.2.7
7f0328b52000-7f0328d51000 ---p 00072000 08:03 3811050 /usr/lib/libpcre.so.1.2.7
7f0328d51000-7f0328d52000 r--p 00071000 08:03 3811050 /usr/lib/libpcre.so.1.2.7
7f0328d52000-7f0328d53000 rw-p 00072000 08:03 3811050 /usr/lib/libpcre.so.1.2.7
7f0328d53000-7f0328dba000 r-xp 00000000 08:03 3804648 /usr/lib/libncursesw.so.6.0
7f0328dba000-7f0328fba000 ---p 00067000 08:03 3804648 /usr/lib/libncursesw.so.6.0
7f0328fba000-7f0328fbe000 r--p 00067000 08:03 3804648 /usr/lib/libncursesw.so.6.0
7f0328fbe000-7f0328fc0000 rw-p 0006b000 08:03 3804648 /usr/lib/libncursesw.so.6.0
7f0328fc0000-7f0328fc3000 r-xp 00000000 08:05 3801272 /home/grochmal/tmp/pp.so
7f0328fc3000-7f03291c2000 ---p 00003000 08:05 3801272 /home/grochmal/tmp/pp.so
7f03291c2000-7f03291c3000 r--p 00002000 08:05 3801272 /home/grochmal/tmp/pp.so
7f03291c3000-7f03291c4000 rw-p 00003000 08:05 3801272 /home/grochmal/tmp/pp.so
7f03291c4000-7f03291e7000 r-xp 00000000 08:03 3804420 /usr/lib/ld-2.24.so
7f03293b0000-7f03293b2000 rw-p 00000000 00:00 0
7f03293e4000-7f03293e6000 rw-p 00000000 00:00 0
7f03293e6000-7f03293e7000 r--p 00022000 08:03 3804420 /usr/lib/ld-2.24.so
7f03293e7000-7f03293e8000 rw-p 00023000 08:03 3804420 /usr/lib/ld-2.24.so
7f03293e8000-7f03293e9000 rw-p 00000000 00:00 0
7fff77a97000-7fff77ab8000 rw-p 00000000 00:00 0 [stack]
7fff77bb5000-7fff77bb7000 r--p 00000000 00:00 0 [vvar]
7fff77bb7000-7fff77bb9000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
The important bit that /proc/<PID>/maps
tells us is the memory location of the object, the "AAAAA...AAAA" string should be somewhere in there:
7f0328fc0000-7f0328fc3000 r-xp 00000000 08:05 3801272 /home/grochmal/tmp/pp.so
7f0328fc3000-7f03291c2000 ---p 00003000 08:05 3801272 /home/grochmal/tmp/pp.so
7f03291c2000-7f03291c3000 r--p 00002000 08:05 3801272 /home/grochmal/tmp/pp.so
7f03291c3000-7f03291c4000 rw-p 00003000 08:05 3801272 /home/grochmal/tmp/pp.so
We can now use GDB to check it (note that I'm again using the PID I got from jobs -l
):
# gdb -q -p 15476
...
warning: Could not load shared library symbols for ./pp.so
...
That line appears if GDB is running in a different working directory than the process was started. But that's fine, we are after memory not symbols.
(gdb) p 0x7f03291c4000 - 0x7f0328fc0000
$1 = 2113536
(gdb) x/2113536x 0x7f0328fc0000
(I took the numbers from the maps and used GDB as a calculator)
That will print a lot of stuff (all memory of the shared object we created, including bits added by ELF and PIC), but will show all the "A"s we are after:
...
0x7f0328fc2ae0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7f0328fc2af0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7f0328fc2b00: 0x41414141 0x41414141 0x41414141 0x41414141
0x7f0328fc2b10: 0x41414141 0x41414141 0x41414141 0x41414141
0x7f0328fc2b20: 0x41414141 0x41414141 0x41414141 0x41414141
0x7f0328fc2b30: 0x41414141 0x41414141 0x41414141 0x41414141
0x7f0328fc2b40: 0x41414141 0x41414141 0x41414141 0x41414141
0x7f0328fc2b50: 0x41414141 0x41414141 0x41414141 0x41414141
0x7f0328fc2b60: 0x41414141 0x41414141 0x41414141 0x41414141
0x7f0328fc2b70: 0x41414141 0x41414141 0x41414141 0x41414141
0x7f0328fc2b80: 0x41414141 0x41414141 0x41414141 0x41414141
0x7f0328fc2b90: 0x41414141 0x41414141 0x41414141 0x41414141
0x7f0328fc2ba0: 0x41414141 0x41414141 0x41414141 0x41414141
...
Option 2: use the environment
Now, this does not need the environment variable to be called LD_PRELOAD
. The variable can be anything. Let's start by adding our shellcode ("AAAA...") to LD_PRELOAD
as they did in the blog post:
$ LD_PRELOAD=`perl -e 'print "A"x30'` less pp.c
ERROR: ld.so: object 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' from LD_PRELOAD cannot be preloaded (cannot open shared object file): ignored.
[1]+ Stopped LD_PRELOAD=`perl -e 'print "A"x30'` less pp.c
$ jobs -l
[1]+ 1566 Stopped LD_PRELOAD=`perl -e 'print "A"x30'` less pp.c
We got the warning alright, we will ignore it and attach GDB:
# gdb -q -p 1566
(gdb) info variable environ
All variables matching regular expression "environ":
Non-debugging symbols:
0x00007fb7bd5babd8 last_environ
0x00007fb7bd5bbf18 __environ
0x00007fb7bd5bbf18 _environ
0x00007fb7bd5bbf18 environ
0x00007fb7bdcc30c0 __environ
0x00007fb7bdcc30c0 _environ
0x00007fb7bdcc30c0 environ
And search for the environ
variable (man 3 environ
). We have two of them, that's a peculiarity of kernel 4. environ
is a char**
so lets look at finding those strings:
(gdb) x/3gx 0x00007fb7bd5bbf18
0x7fb7bd5bbf18 <environ>: 0x00007ffd1c3884a0 0x0000000000000000
0x7fb7bd5bbf28 <buflen.8581>: 0x0000000000000000
(gdb) x/3gx 0x00007fb7bdcc30c0
0x7fb7bdcc30c0 <environ>: 0x00007ffd1c3884a0 0x0000000000000000
0x7fb7bdcc30d0: 0x0000000000000000
Both environ
symbols are pointing to the same place in memory (namely 0x00007ffd1c3884a0
). Let's see what is in there:
(gdb) x/20gx 0x00007ffd1c3884a0
0x7ffd1c3884a0: 0x00007ffd1c388ba3 0x00007ffd1c388bcd
0x7ffd1c3884b0: 0x00007ffd1c388bd8 0x00007ffd1c388bea
0x7ffd1c3884c0: 0x00007ffd1c388c05 0x00007ffd1c388c15
0x7ffd1c3884d0: 0x00007ffd1c388c27 0x00007ffd1c388c4c
0x7ffd1c3884e0: 0x00007ffd1c388c6c 0x00007ffd1c388c8b
0x7ffd1c3884f0: 0x00007ffd1c388c99 0x00007ffd1c388cd1
0x7ffd1c388500: 0x00007ffd1c388cfa 0x00007ffd1c388d49
0x7ffd1c388510: 0x00007ffd1c388d67 0x00007ffd1c388e01
0x7ffd1c388520: 0x00007ffd1c388e10 0x00007ffd1c388e27
0x7ffd1c388530: 0x00007ffd1c388e38 0x00007ffd1c388ebd
I bet those are strings, let's see:
(gdb) x/s 0x00007ffd1c388ba3
0x7ffd1c388ba3: "LD_PRELOAD=", 'A' <repeats 30 times>
(gdb) x/s 0x00007ffd1c388bcd
0x7ffd1c388bcd: "XDG_VTNR=2"
(gdb) x/s 0x00007ffd1c388bd8
0x7ffd1c388bd8: "XDG_SESSION_ID=c1"
Correct, strings. And we also found our shellcode. We only need to print it in a different way to get the 0x41s:
(gdb) x/20wx 0x00007ffd1c388ba3
0x7ffd1c388ba3: 0x505f444c 0x4f4c4552 0x413d4441 0x41414141
0x7ffd1c388bb3: 0x41414141 0x41414141 0x41414141 0x41414141
0x7ffd1c388bc3: 0x41414141 0x41414141 0x44580041 0x54565f47
0x7ffd1c388bd3: 0x323d524e 0x47445800 0x5345535f 0x4e4f4953
0x7ffd1c388be3: 0x3d44495f 0x54003163 0x3d4d5245 0x74767872
Important note: This has nothing to do with LD_PRELOAD
! The program start as:
$ LD_PRELOAD=`perl -e 'print "A"x30'` less pp.c
Could be pretty much done with:
$ YAY=`perl -e 'print "A"x30'` less pp.c
And we would achieve the same result. But I strongly believe this is what they're actually seeing in that blog post.
* I do not have anything running Ubuntu 3.0.0-12-generic, but I suppose that its ld
was more permissive and allowed non ELF objects to be loaded. I'm confident that my Arch 4.7 simply rejects the object in LD_PRELOAD
if it is not a proper ELF and has no PIC indicators.