How to have fun with ptrace syscall
Cyberdude
- Cyberdude - How to have fun with ptrace syscall - Hi boys in this text i want show to you how is possible to "hack" one process using some assembly strings and the Dynamic linker. If you search in internet you can find it : a dynamic linker is the part of an operating system that loads and links the shared libraries for an executable when it is run. Such linkers typically also have a shared library that is linked with the executable when it is compiled and may determine the actions of the linker. One shared library,in addition to being loaded statically or dynamically, are also often classified according to how they are shared among programs. In Linux the dynamic linker shared libraries, tipically are based on a common set of environment variables, including LD_LIBRARY_PATH and LD_PRELOAD. In this text we will use the LD_PRELOAD variable. When LD_PRELOAD is set, the dynamic linker will use the specified library before any other when it searches for shared libraries. Now imagine that our process is the next code: -------------------------------------------------------process.c------------- #include <stdio.h> #include <unistd.h> int main() { printf("Userid = %d\n",getuid()); return 0; } ---------------------------------------------------------------------------- if we compile and run it... cyberdude@cyberdude-laptop:~$ gcc process.c -o process cyberdude@cyberdude-laptop:~$ ./process Userid = 1000 cyberdude@cyberdude-laptop:~$ strace ./process execve("./process", ["./process"], [/* 32 vars */]) = 0 brk(0) = 0x804a000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7f3c000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=67304, ...}) = 0 mmap2(NULL, 67304, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7f2b000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("/lib/tls/i686/cmov/libc.so.6", O_RDONLY) = 3 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\0`\1\000"..., 512) = 512 fstat64(3, {st_mode=S_IFREG|0644, st_size=1307104, ...}) = 0 mmap2(NULL, 1312164, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb7dea000 mmap2(0xb7f25000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x13b) = 0xb7f25000 mmap2(0xb7f28000, 9636, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb7f28000 close(3) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7de9000 set_thread_area({entry_number:-1 -> 6, base_addr:0xb7de96c0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0 mprotect(0xb7f25000, 4096, PROT_READ) = 0 munmap(0xb7f2b000, 67304) = 0 getuid32() = 1000 fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 3), ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7f3b000 write(1, "Userid = 1000\n", 14Userid = 1000 ) = 14 exit_group(0) = ? Process 6885 detached The line that we want change is the next: getuid32() = 1000 Imagine that we want obtain that when the process call the syscall getuid32() the returnament is not 1000 but is an id that we decide. I can just use a new library and call it tnk to LD_PRELOAD. In my lib i will create a new method named getuid() so when the operativ sistem will catch the string: printf("Userid = %d\n",getuid()) will use my persona getuid() and not the original. My lib is very simply and is showed next -----------------------------------------------------------lib.s------------- .globl getuid .type getuid, @function getuid: movl $0, %eax ret ----------------------------------------------------------------------------- This code is very simply, i just declare the name of my funcion as getuid and after move the value 0 in the eax registry. When i call the ret syscall, the funcion will return the value that is in the eax registry, so it will return the value 0. In this way when you will run the function getuid () from this library, it will return to you the value 0 and not the true id. At this point you have just to complie the lib like a shared library and run the prev code using the LD_PRELOAD and the shared lib as showed next cyberdude@cyberdude-laptop:~$ gcc -shared lib.s -o lib cyberdude@cyberdude-laptop:~$ LD_PRELOAD=./lib ./process Userid = 0 cyberdude@cyberdude-laptop:~$ LD_PRELOAD=./lib strace ./process execve("./process", ["./process"], [/* 33 vars */]) = 0 brk(0) = 0x804a000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7fd4000 open("./lib", O_RDONLY) = 3 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\0\3\0\000"..., 512) = 512 fstat64(3, {st_mode=S_IFREG|0755, st_size=5215, ...}) = 0 getcwd("/home/cyberdude", 128) = 16 mmap2(NULL, 5436, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb7fd2000 mmap2(0xb7fd3000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0xb7fd3000 mprotect(0xbfbbc000, 4096, PROT_READ|PROT_WRITE|PROT_EXEC|PROT_GROWSDOWN) = 0 close(3) = 0 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=67304, ...}) = 0 mmap2(NULL, 67304, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7fc1000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("/lib/tls/i686/cmov/libc.so.6", O_RDONLY) = 3 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\0`\1\000"..., 512) = 512 fstat64(3, {st_mode=S_IFREG|0644, st_size=1307104, ...}) = 0 mmap2(NULL, 1312164, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb7e80000 mmap2(0xb7fbb000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x13b) = 0xb7fbb000 mmap2(0xb7fbe000, 9636, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb7fbe000 close(3) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7e7f000 set_thread_area({entry_number:-1 -> 6, base_addr:0xb7e7fac0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0 mprotect(0xb7fbb000, 4096, PROT_READ) = 0 munmap(0xb7fc1000, 67304) = 0 fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 3), ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7fd1000 write(1, "Userid = 0\n", 11Userid = 0 ) = 11 exit_group(0) = ? Process 6932 detached As you can see, the difference between the two exection of the same prcess is in this block of istruction. The first block is the block that we runned without using the LD_PRELOAD variable. The second block is the block that we runned with the LD_PRELOAD. The difference is that in the first block there is a called to getuid32() with returnament 1000, in the second block this system call is not execute. The write is different too becouse the Write call in the first block return the original id, the Write call in the second block return the userid = 0 as decided in our lib -[BLOCK 1]------------------------------------------------------------------- mprotect(0xb7f25000, 4096, PROT_READ) = 0 munmap(0xb7f2b000, 67304) = 0 getuid32() = 1000 fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 3), ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7f3b000 write(1, "Userid = 1000\n", 14Userid = 1000 ----------------------------------------------------------------------------- -[BLOCK 2]------------------------------------------------------------------- mprotect(0xb7fbb000, 4096, PROT_READ) = 0 munmap(0xb7fc1000, 67304) = 0 fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 3), ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7fd1000 write(1, "Userid = 0\n", 11Userid = 0 ----------------------------------------------------------------------------- But unluckly, this tecnique is not good if the process is compiled like a static process, as showed next : cyberdude@cyberdude-laptop:~$ gcc -static process.c -o process cyberdude@cyberdude-laptop:~$ LD_PRELOAD=./lib ./process Userid = 1000 cyberdude@cyberdude-laptop:~$ LD_PRELOAD=./lib strace ./process execve("./process", ["./process"], [/* 33 vars */]) = 0 uname({sys="Linux", node="cyberdude-laptop", ...}) = 0 brk(0) = 0x80bc000 brk(0x80bccb0) = 0x80bccb0 set_thread_area({entry_number:-1 -> 6, base_addr:0x80bc830, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0 brk(0x80ddcb0) = 0x80ddcb0 brk(0x80de000) = 0x80de000 getuid32() = 1000 fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 3), ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7f19000 write(1, "Userid = 1000\n", 14Userid = 1000 ) = 14 exit_group(0) = ? Process 6985 detached In this case we have to change our strategy using differents system call There is one system call named ptrace(). We can do some requests to ptrace, but we are interested just to the next 4 requests : * PTRACE_TRACEME: This request let that we can trace the execution of the process like if we are making a debug on the current process * PTRACE_SYSCALL: If we make a fork and add this request to the parent,it will notify everytime that the child enter or exit from one systemcall * PTRACE_GETREGS: We can read the registry of the current process. The values are readed in a structure named user_regs_struct. In this struct there is an information named eax (that we know very well :D) * PTRACE_SETREGS: With this request we can write in one registry all that we want So our strategy is the next : [1] Create a new process [2] make a fork() on this process to make a chil [3] The child will make a request: PTRACE_TRACEME so the parent can trace he [4] The child will run the stati process that we want hack [5] The parent will make a request: PTRACE_SYSCALL on his child,so the parent always know what is the syscall that the child is running in the current moment [6] The parent will make a request: PTRACE_GETREGS to know the registry of everyone syscall called by the child [7] The parent will control the orig_eax value of the current syscall called by the child. If the value is the same that i search (in our case is SYS_getuid32), the parent will change the value of eax (the result of the syscall) with the value that we want (in out case the value 0) To explain it better we let that the process call the true getuid procedure but we will interect the returnament of this procedure and we will change it before that the process can return it to the operativ system. In this way we can set the returnament as we want ----------------------------------------------------------hack.s------------- patch: .string "/home/cyberdude/process" /* The path of the process that we want hack */ .globl main /* The main function */ main: movl $0, -4(%ebp) /* In this address I will store the variable to wait the child process*/ movl $0, -8(%ebp) /* In this address I will store the output value that i will read from eax registry of sys call runned by child */ movl $1, -12(%ebp) /* Thi is a value that i will use to control if the parent is intercepting a entering syscall or exiting it */ call fork /* I call the fork syscall to obtain the child */ movl %eax, -16(%ebp) /* And i store the child' s pid in -16(%ebp) */ cmpl $0, -16(%ebp) /* Now compare this pid with 0 to know if i'm in parent process or child it */ jne parent /* If the pid is not 0 i'm in the parent execution so i jump to label named parent */ child: /* Else i continue with child processing */ movl $26,%eax /* The syscall ptrace */ movl $0, %ebx /* The first Parameter setted to 0 is TRACEME*/ movl $0, %ecx /* second parameter */ movl $0, %edx /* therd parameter */ movl $0, %esi /* fourtf parameter */ int $0x80 /* i run it in this way the child accept that the parent trace he during his execution */ pushl $0 /* Now i run the process */ pushl $patch /* created previously */ pushl $patch /* with the syscall */ call execlp /* execlp */ jmp quit /* Here is finished the child's life */ parent: /* Here start the life of parent */ leal -4(%ebp), %eax /* I leal the space of variable previously created */ movl %eax, (%esp) /* push this space */ call wait /* and call the wait to wait the execution of child */ whileCicle: movl $26,%eax /* The ptrace function */ movl $24,%ebx /* The first parameter is 24 = PTRACE_SYSCALL */ movl -16(%ebp),%ecx /* In -16(%ebp there is the pid, it is the second parameter of the ptrace function */ movl $0,%edx movl $0,%esi int 0x80 leal -4(%ebp), %eax movl %eax, (%esp) call wait movl -4(%ebp), %eax /* Now i control if the */ andl $127, %eax /* returnament of the */ testl %eax, %eax /* the wait funcion is equals to 127 (if is equals the child is finished */ je quit /* If is finished jump to quit */ leal -92(%ebp), %eax /* Else i leal the address -92(%ebp) in %eax */ pushl %eax pushl $0 movl -16(%ebp), %eax pushl %eax pushl $12 /* This parameter rapresent GETREGS, i use it to obtain the registrs used for the intercepted sys call */ call ptrace movl -48(%ebp), %eax /* In -48(%ebp) there is the value orig_eax that say to me the number of the systemcall catched */ movl %eax, -8(%ebp) cmpl $199, -8(%ebp) /* I control if this number is 199 (getuid() number */ jne whileCicle /* If is not 199 i come back to the while Cicle to intercept the next call */ cmpl $0, -12(%ebp) /* Else i control the value in -12(%ebp)! i set this variable previously to 1 so if his value is still 1 it does mean that i never change this value so i'm in the moment that i'm entering in the syscall */ je changeEaxValue /* If is equals to 0 it is the moment that i'm in in the out of syscall so i can take the output value and change it */ movl $0, -12(%ebp) /* Else if i'm in the entering moment, i have to change the value of my variable to 0 to remember that the next intercept time, will be the out time */ jmp whileCicle /* After that i change the variable value i can come back to the cicle to incercept the next syscall */ changeEaxValue: leal -92(%ebp), %eax /* If i'm in the exiting of syscall i leal some space from ebp and push it */ pushl %eax pushl $0 movl -16(%ebp), %eax pushl %eax pushl $12 /* I obtain the GETREGS */ call ptrace movl $0, -68(%ebp) /* I change the output value that is stored in -68(%ebp) to 0! in this way i'm changing the returnament of the intercepted sys call (that in our case is the getuid()) */ leal -92(%ebp), %eax /* I repush all the */ pushl %eax /* structure of ptrace's */ pushl $0 /* function */ movl -16(%ebp), %eax pushl %eax pushl $13 /* and call the SETREGS */ call ptrace /* function on ptrace() */ movl $1, -12(%ebp) /* After i change another time the variable value to remember that in the next time, the call interception will be to entering syscall */ jmp whileCicle quit: movl $0, %eax call exit ----------------------------------------------------------------------------- The next code is the same of the showed previously, just in asm inline version, in this way, maybe you can understand better the process execution -------------------------------------------------------AsmInLineVersion.c---- #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <linux/user.h> #include <sys/syscall.h> #include <sys/reg.h> int main() { char* path = "/home/cyberdude/process"; int status = 0; int callIdNumber = 0; int isEntering = 1; struct user_regs_struct regs; int pid = fork(); if(!pid) { asm( "movl $26,%%eax;" "movl $0, %%ebx;" "movl $0, %%ecx;" "movl $0, %%edx;" "movl $0, %%esi;" "int $0x80;" "movl $11,%%eax;" "movl %%edi,%%ebx;" "movl $0,%%ecx;" "movl $0,%%edx;" "int $0x80;" :: "D"(path) ); } else { wait(&status); while(1) { asm( "movl $26,%%eax;" "movl $24,%%ebx;" "movl $0,%%edx;" "movl $0,%%esi;" "int $0x80;" :: "c"(pid) ); wait(&status); if ( WIFEXITED( status ) ) break; asm( "movl $26,%%eax;" "movl $12,%%ebx;" "movl $0,%%edx;" "int $0x80;" :: "c"(pid), "S"(&regs) ); callIdNumber= regs.orig_eax; if ( callIdNumber == 199 ) { if ( isEntering ) isEntering = 0; else { regs.eax = 0; asm( "movl $26,%%eax;" "movl $13,%%ebx;" "movl $0,%%edx;" "int $0x80;" :: "c"(pid), "S"(&regs) ); isEntering = 1; } } } } return 0; } ----------------------------------------------------------------------------- And with this code is finished this text ... see you to the next article bye bye Cyberdude! Happy code to everybody :D