Android Hooking

I recently found myself in situation where I had to analyze another process in order to understand what was really happening . In order to achieve this , I had to monitor the target process in one way or another. In the process I had to implement a simple hook just to do evil stuff 😈 .

For purposes of demonstration and for the blog I will simply look at how you can hook onto another process changing the flow of execution. In this case we will simply have a sleep program as the target and a hooking program that arguments the sleeping behavior of the sleep program .

Let’s get our hands dirty 😎 .

By definition

Hooking refers to the process of changing the behavior of software or its components. The code responsible of the aforementioned is referred to as a hook.

Target program

In this case we will have a simple sleep program that sleeps for 10 seconds.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const* argv[]) {
int x = 1;
while (x) {
printf("%s\n", "Sleep ...");
sleep(10);
}
return 0;
}

Evil Function

1
2
3
4
5
void
evil_function()
{
//empty for now lets keep it that way
}

Linux process basics

From the basics of processes , any program that is started has a process id (pid). Every process has its own Virtual Memory Area (VMA) which is using memory management schemes it translates into a physical address.

Understanding the target

In order do anything its important to always understand you target. This can be easily achieved using the File utility . In this case, the binary is an elf file . It is important to understand how the linker and the loader see this file. Won’t go into too much details since this could be another blogpost look at http://www.cirosantilli.com/elf-hello-world/ for a detailed insight on elf files (also look at PLT and GOT sections https://www.technovelty.org/linux/plt-and-got-the-key-to-code-sharing-and-dynamic-libraries.html)

GOT stands for global offset table which holds the various symbol offsets that are part of the binary and are from loaded the libraries

Game Plan

From the above we can easily formulate a plan 💡

i) First we need to monitor the process of interest in order to inspect registers etc.. more like a debugger. This can be easily achieved using ptrace
ii) Get the GOT offset of sleep symbol and calculate its address.
iii)Write our evil function into the target’s memory
iv) Patching GOT address with a pointer to our evil function.

i) The almighty debugger

In order to inspect contents of the remote target we need to use ptrace. So we will first attach ourselves to the target process.
Ptrace is very straight forward from its mans page.

1
2
3
4
if (ptrace(PTRACE_ATTACH, target_pid, NULL, NULL) == -1) {
perror("[^] Unable to attach target\n");
return -1;
}

ii) Get the G.O.T

In order to argument the sleep function we need to get its Global Offest so as we can overwrite it with a pointer to our evil function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/**
* Returns the GOT address of target function
* @param module_path Module holding symbol of interest
* @param symbol_name symbol name
* @param pid target proccess id
* @return address from GOT
*/
uint32_t
find_got_entry_address(const char* module_path, const char* symbol_name,
pid_t pid)
{
uint32_t module_base = findLoadingAddress(module_path, pid);
if (module_base == 0) {
return 0;
}
printf("[+] base address of %s: 0x%x\n", module_path, module_base);
int fd = open(module_path, O_RDONLY);
if (fd == -1) {
return 0;
}
Elf32_Ehdr* elf_header = (Elf32_Ehdr*)malloc(sizeof(Elf32_Ehdr));
if (read(fd, elf_header, sizeof(Elf32_Ehdr)) != sizeof(Elf32_Ehdr)) {
return 0;
}
uint32_t sh_base = elf_header->e_shoff;
uint32_t ndx = elf_header->e_shstrndx;
uint32_t shstr_base = sh_base + ndx * sizeof(Elf32_Shdr);
lseek(fd, shstr_base, SEEK_SET);
Elf32_Shdr* shstr_shdr = (Elf32_Shdr*)malloc(sizeof(Elf32_Shdr));
if (read(fd, shstr_shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)) {
return 0;
}
char* shstrtab = (char*)malloc(sizeof(char) * shstr_shdr->sh_size);
lseek(fd, shstr_shdr->sh_offset, SEEK_SET);
if (read(fd, shstrtab, shstr_shdr->sh_size) != shstr_shdr->sh_size) {
return 0;
}
Elf32_Shdr* shdr = (Elf32_Shdr*)malloc(sizeof(Elf32_Shdr));
Elf32_Shdr* relplt_shdr = (Elf32_Shdr*)malloc(sizeof(Elf32_Shdr));
Elf32_Shdr* dynsym_shdr = (Elf32_Shdr*)malloc(sizeof(Elf32_Shdr));
Elf32_Shdr* dynstr_shdr = (Elf32_Shdr*)malloc(sizeof(Elf32_Shdr));
lseek(fd, sh_base, SEEK_SET);
if (read(fd, shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)) {
return 0;
}
int i = 1;
char* s = NULL;
for (; i < elf_header->e_shnum; i++) {
s = shstrtab + shdr->sh_name;
if (strcmp(s, ".rel.plt") == 0)
memcpy(relplt_shdr, shdr, sizeof(Elf32_Shdr));
else if (strcmp(s, ".dynsym") == 0)
memcpy(dynsym_shdr, shdr, sizeof(Elf32_Shdr));
else if (strcmp(s, ".dynstr") == 0)
memcpy(dynstr_shdr, shdr, sizeof(Elf32_Shdr));
if (read(fd, shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)) {
printf("[-] read %s error! i = %d, in %s at line %d\n", module_path, i,
__FILE__, __LINE__);
return 0;
}
}
// read dynmaic symbol string table
char* dynstr = (char*)malloc(sizeof(char) * dynstr_shdr->sh_size);
lseek(fd, dynstr_shdr->sh_offset, SEEK_SET);
if (read(fd, dynstr, dynstr_shdr->sh_size) != dynstr_shdr->sh_size) {
printf("[-] read %s error!\n", module_path);
return 0;
}
// read dynamic symbol table
Elf32_Sym* dynsymtab = (Elf32_Sym*)malloc(dynsym_shdr->sh_size);
lseek(fd, dynsym_shdr->sh_offset, SEEK_SET);
if (read(fd, dynsymtab, dynsym_shdr->sh_size) != dynsym_shdr->sh_size) {
printf("[-] read %s error!\n", module_path);
return 0;
}
// read each entry of relocation table
Elf32_Rel* rel_ent = (Elf32_Rel*)malloc(sizeof(Elf32_Rel));
lseek(fd, relplt_shdr->sh_offset, SEEK_SET);
if (read(fd, rel_ent, sizeof(Elf32_Rel)) != sizeof(Elf32_Rel)) {
printf("[-] read %s error!\n", module_path);
return 0;
}
for (i = 0; i < relplt_shdr->sh_size / sizeof(Elf32_Rel); i++) {
ndx = ELF32_R_SYM(rel_ent->r_info);
if (strcmp(dynstr + dynsymtab[ndx].st_name, symbol_name) == 0) {
printf("[+] got entry offset of %s: 0x%x\n", symbol_name,
rel_ent->r_offset);
break;
}
if (read(fd, rel_ent, sizeof(Elf32_Rel)) != sizeof(Elf32_Rel)) {
printf("[-] read %s error!\n", module_path);
return 0;
}
}
uint32_t offset = rel_ent->r_offset;
Elf32_Half type = elf_header->e_type; // ET_EXEC or ET_DYN
free(elf_header);
free(shstr_shdr);
free(shstrtab);
free(shdr);
free(relplt_shdr);
free(dynsym_shdr);
free(dynstr_shdr);
free(dynstr);
free(dynsymtab);
free(rel_ent);
// GOT entry offset is different between ELF executables and shared libraries
if (type == ET_EXEC)
return offset;
else if (type == ET_DYN)
return offset + module_base;
return 0;
}

iii) Writing the evil function on target process

The big question here is how do we write data to a process we have no access to and and how do we affect the memory area of another process bearing in mind all the limitations thats are a result of Copy On Write (COW).
So far we have the GOT address and we have the ability to inspect the target’s memory. The aforementioned means that we can use registers of the attached process to do our dirty work 😁 .

Before we get lost in excitement it is important to understand and identify that no process is aware of the existence of the other or knows about the other processes’s VMA. Now with this fundamental principle comes another challenge how do we do a simple mmap (well this is important if we want to write into a memory region). The only option we have is to find a way to call functions from the target process 🤔 .

Remember we have control over the registers so we can do pretty much what we want to do as long as we are creative about it 💪 .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
long CallRemoteFunction(pid_t pid, long function_addr, long* args, size_t argc)
{
struct pt_regs regs;
// backup the original regs
struct pt_regs backup_regs;
ptrace(PTRACE_GETREGS, pid, NULL, &regs);
memcpy(&backup_regs, &regs, sizeof(struct pt_regs));
// put the first 4 args to r0-r3
int i;
for (i = 0; i < argc && i < 4; ++i) {
regs.uregs[i] = args[i];
}
// push the remainder to stack
if (argc > 4) {
regs.ARM_sp -= (argc - 4) * sizeof(long);
long* data = args + 4;
ptrace_writedata(pid, (uint8_t*)regs.ARM_sp, (uint8_t*)data,
(argc - 4) * sizeof(long));
}
// set return addr to 0, so we could catch SIGSEGV
regs.ARM_lr = 0;
regs.ARM_pc = function_addr;
if (regs.ARM_pc & 1) {
// thumb
regs.ARM_pc &= (~1u);
regs.ARM_cpsr |= CPSR_T_MASK;
} else {
// arm
regs.ARM_cpsr &= ~CPSR_T_MASK;
}
ptrace(PTRACE_SETREGS, pid, NULL, &regs);
ptrace(PTRACE_CONT, pid, NULL, NULL);
waitpid(pid, NULL, WUNTRACED);
// to get return value;
ptrace(PTRACE_GETREGS, pid, NULL, &regs);
ptrace(PTRACE_SETREGS, pid, NULL, &backup_regs);
// Fuction return value
printf("Call remote function %lx with %d arguments, return value is %lx\n",
function_addr, argc, regs.ARM_r0);
return regs.ARM_r0;
}

one important concept to understand is that android has both ARM and Thumb states it is important to always check which state is in use else this will haunt you trust me this is from experience . This deserves a blogpost too.

1
2
3
4
5
6
7
8
if (regs.ARM_pc & 1) {
// thumb
regs.ARM_pc &= (~1u);
regs.ARM_cpsr |= CPSR_T_MASK;
} else {
// arm
regs.ARM_cpsr &= ~CPSR_T_MASK;
}

now since all is set lets create a function that calls mmap in the target process.
Still another problem we don’t know the address of mmap on target process so we cant call it. Now ASLR becomes a problem here 😩 .

So again we use what we have. Assuming we know where Libc is loaded and it’s distance from mmap is always the same that means we can calculate the address of mmap in the remote function assuming mathematically

Using the formula

1
remote_function_address = local_function_address - (remote_libc_load_address + local_libc_load_address )

and just like that we defeated ASLR 😎 .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* This function bypasses KASLR to give address of remote function
* @param library [target library]
* @param pid [target process id]
* @param local_function_address [address of remote function]
*/
void* functionAddress(const char* library, pid_t pid, void* local_function_address)
{
uintptr_t remote_loading_address, remote_function_address,
local_loading_address;
local_loading_address = findLoadingAddress(library, getpid());
remote_loading_address = findLoadingAddress(library, pid);
remote_function_address = (uintptr_t)local_function_address +
(remote_loading_address - local_loading_address);
return (void*)remote_function_address;
}

Now lets progress and call mmap in the target processs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uint32_t
page_mmap(pid_t pid)
{
void* _mmap;
_mmap = functionAddress(LIB_C, pid, (void*)mmap);
long params[6];
params[0] = 0;
params[1] = 0x400;
params[2] = PROT_READ | PROT_WRITE | PROT_EXEC;
params[3] = MAP_PRIVATE | MAP_ANONYMOUS;
params[4] = 0;
params[5] = 0;
return CallRemoteFunction(pid, _mmap, params, 6);
}

Now all we need to do is write into the maped memory our evil function . Once more Ptrace is BAE in this case since we will use PTRACE_POKETEXT which simply copies data into the provided address.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* Write data into memory
* @param pid target pid
* @param dest destination
* @param data data
* @param size size
*/
void ptrace_writedata(pid_t pid, uint8_t* dest, uint8_t* data, size_t size)
{
uint32_t i, j, remain;
uint8_t* laddr;
union u
{
long val;
char chars[sizeof(long)];
} d;
j = size / 4;
remain = size % 4;
laddr = data;
for (i = 0; i < j; i++) {
memcpy(d.chars, laddr, 4);
ptrace(PTRACE_POKETEXT, pid, dest, d.val);
dest += 4;
laddr += 4;
}
if (remain > 0) {
d.val = ptrace(PTRACE_PEEKTEXT, pid, dest, 0);
for (i = 0; i < remain; i++) {
d.chars[i] = *laddr++;
}
ptrace(PTRACE_POKETEXT, pid, dest, d.val);
}
}

finally the last piece in the writing puzzle

1
ptrace_writedata(target_pid, (uint8_t *)map_base, (uint8_t *)evil_function-1,sizeof(map_base));

Notice (uint8_t *)evil_function-1 has a minus one. Well if thats not there chaos erupt literally. Remember when I said Thumb and ARM may become a pain if not handled correctly ; this is what I meant.

Pointers to functions in thumb state

In order to allow interworking between the two states , If you have a thumb function the pointer to that function must have its least significant bit set. So that means a pointer to the function will have a plus one . So in order to get the actual pointer you have to subtract one from the pointer.

1
2
3
4
5
6
7
8
9
10
typedef int (*FN)();
myfunc() {
FN fnptrs[] = {
(FN)(0x8084 + 1), // Valid Thumb address
(FN)(0x8074) // Invalid Thumb address
};
FN* myfunctions = fnptrs;
myfunctions[0](); // Call OK
myfunctions[1](); // Call fails
}

Patching our GOT with a pointer to evil function

Well just incase I missed this , the GOT holds a pointer to the target function ie sleep in our case so if we patch this by placing a pointer to our evil function.

We need to check whether the GOT address actually contains a pointer to sleep before we progress and once we confirm all we do is just write

1
2
3
4
5
6
7
8
9
10
11
12
13
_remote_function = functionAddress(LIB_C, target_pid, (void*)sleep);
if (_remote_function == (uint32_t*)(size_t)ptrace(PTRACE_PEEKDATA, target_pid, GOT_entry_address, NULL)) {
uint32_t target_memory = (uint32_t)map_base;
target_memory = target_memory+1; //Dont forget thumb mode
ptrace_writedata(target_pid, (uint8_t*)GOT_entry_address, (uint8_t*)&target_memory,
sizeof(long));
}

And Voila we are DONE !!!!! 🎉🎉🎉 😎

The whole project can be found here

The expected output is as shown below ENJOY

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[+] Remote address found @ 0xb6f030b9
[+] base address of /data/local/tmp/target: 0xb6f87000
[+] got entry offset of sleep: 0x2fe8
[+] Sleep address found @ 0xb6f89fe8
Call remote function b6ef7e51 with 6 arguments, return value is b63ff000
[+] Map base @ 0xb63ff000
****************************************************
DUMP OF EVIL FUNCTION IN TARGET PROCESS (0xb63ff000)
****************************************************
70 47 00 00 pG..
00 00 00 00 ....
00 00 00 00 ....
00 00 00 00 ....
00 00 00 00 ....
00 00 00 00 ....
00 00 00 00 ....
00 00 00 00 ....
*********************************************
**********************************************
DUMP OF GOT ADDRESS
**********************************************
00 f0 3f b6 ..?.
**********************************************
**********************************************
DUMP OF ADDRESS IN GOT (After patching)
**********************************************
70 47 00 00 pG..
00 00 00 00 ....
00 00 00 00 ....
00 00 00 00 ....
00 00 00 00 ....
00 00 00 00 ....
00 00 00 00 ....
00 00 00 00 ....
**********************************************