Lendo e escrevendo na stack de um processo usando ptrace

Olá pessoal! Depois de um longo tempo, resolvi postar mais alguma coisa interessante com o uso do ptrace. Desta vez irei demonstrar como podemos simular um breakpoint em um processo e até printar e modificar o valor de uma variável local.

Para quem não leu o primeiro post [1], vale a pena dar uma lida para compreender o que veremos a seguir.

O programa ao qual iremos usar o ptrace para testar, é o seguinte:

Arquivo: foo.c

#include <stdio.h>
 
int main(int argc, char **argv) {
	int i = 0;
 
	while (i < 100) {
		++i;
		sleep(1);
	}
 
	return 0;
}

E o programa que irá mostrar o valor da variável e modificá-lo será:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <inttypes.h>
#include <string.h>
#include <signal.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
 
static pid_t pid;
 
void attach() {
	int status;
 
	if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1) {
		printf("Error: Failed to attach to pid (%s)\n", strerror(errno));
		exit(1);
	}
	wait(&status);
	if (!WIFSTOPPED(status)) {
		printf("Error: Unexpected wait result (%s)\n", strerror(errno));
		exit(1);
	}
}
 
void detach() {	
	ptrace(PTRACE_DETACH, pid, NULL, NULL);
}
 
void ptrace_write_long(uintptr_t addr, long value) {
	ptrace(PTRACE_POKETEXT, pid, addr, value);
}
 
void ptrace_read_long(uintptr_t addr, void *vptr) {
	long word = ptrace(PTRACE_PEEKTEXT, pid, addr, NULL);
	memcpy(vptr, &word, sizeof(long));
}
 
int main(int argc, char **argv) {
	struct user_regs_struct regs;
	uintptr_t addr;
	int status;
	long value;
 
	if (argc < 3) {
		printf("Usage: dump <pid> <breakpoint>\n");
		exit(1);
	}
 
	pid = strtol(argv[1], NULL, 10);
	addr = strtol(argv[2], NULL, 16);
 
	attach();
 
	while (1) {
		if (ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL) != 0) {
			printf("Error: singlestep failed!\n");
			break;
		}		
		wait(&status);
 
		if (WIFEXITED(status)) {
			break;
		}
 
		ptrace(PTRACE_GETREGS, pid, NULL, &regs);
 
		ptrace_read_long(regs.rbp-4, &value);
 
		if (regs.rip == addr) {
			printf("value: %d\n", value);
 
			if (value == 15) {
				ptrace_write_long(regs.rbp-4, 90);
			}
		}		
	}
 
	detach();
 
	return 0;
}

O programa irá receber um pid de um processo a ser analisado (no caso, do executável referente ao foo.c), e um endereço (de uma parte do código do executável) para servir de breakpoint.

Como uma breve descrição do ptrace foi dada no primeiro post, neste irei apenas destacar o que é relevante para a compreensão do código acima.

Ao usarmos PTRACE_SINGLESTEP, estamos requisitando que o processo analisado pare a cada instrução executada, o que habilita o processo pai a analisar cada instrução do processo filho (seria como o stepi no GDB). Para isso, usamos a função wait() para identificar que uma instrução foi executada e o processo teve seu status modificado para STOPPED, e através da macro WSTOPSIG() poderemos ainda constatar que o signal que provocou o stop é o SIGTRAP (Trace/breakpoint trap).

Tendo detectado que a instrução foi executada, podemos então através da PTRACE_GETREGS (já conhecida do primeiro post) acessar os registradores (veja o header sys/user.h para uma relação deles).

Ao checar com "objdump -d" o executável do programa foo.c, constatamos que a variável local 'i' é acessada apartir da base da stack por subtraindo 4 bytes. (i.e. -0x4(%rbp)) Logo, é exatamente isso que necessita-se fazer no código, usando o endereço de regs.rbp e fazendo a subtração.

Com o endereço, basta agora usarmos PTRACE_PEEKTEXT para lermos um long apartir do endereço, e usar PTRACE_POKETEXT se quisermos alterar o valor do mesmo.

Para escolher o endereço para nosso breakpoint, basta olhar o dissamble do programa a ser analisado:

00000000004004e4 <main>:
  4004e4:	55                   	push   %rbp
  4004e5:	48 89 e5             	mov    %rsp,%rbp
  4004e8:	48 83 ec 20          	sub    $0x20,%rsp
  4004ec:	89 7d ec             	mov    %edi,-0x14(%rbp)
  4004ef:	48 89 75 e0          	mov    %rsi,-0x20(%rbp)
  4004f3:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)
  4004fa:	eb 13                	jmp    40050f <main+0x2b>
  4004fc:	83 45 fc 01          	addl   $0x1,-0x4(%rbp)
  400500:	bf 01 00 00 00       	mov    $0x1,%edi
  400505:	b8 00 00 00 00       	mov    $0x0,%eax
  40050a:	e8 e1 fe ff ff       	callq  4003f0 <sleep@plt>
  40050f:	83 7d fc 63          	cmpl   $0x63,-0x4(%rbp)
  400513:	7e e7                	jle    4004fc <main+0x18>
  400515:	b8 00 00 00 00       	mov    $0x0,%eax
  40051a:	c9                   	leaveq 
  40051b:	c3                   	retq   
  40051c:	90                   	nop
  40051d:	90                   	nop
  40051e:	90                   	nop
  40051f:	90                   	nop

Iremos usar 0x400500, que é logo abaixo da instrução que incrementa o valor da variável 'i'. Vamos ao teste!

$ ./foo &
[1] 2557
$ ./dump 2557 400500
value: 4
value: 5
value: 6
value: 7
value: 8
value: 9
value: 10
value: 11
value: 12
value: 13
value: 14
value: 15
value: 91
value: 92
value: 93
value: 94
value: 95
value: 96
value: 97
value: 98
value: 99
value: 100
[1]+  Done                    ./foo

Referências

[1] - http://bughunter.com.br/lendo-dados-de-syscall-via-ptrace

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options