Basic OSX Memory Hacking

Back in July, I wrote about how to use task_for_pid on modern OSX releases. I ended the article stating that you can do anything with a task, but left out that there were a few caveats. To demonstrate this, I've written a simple POC for CS2D that reads a value from memory and then writes a value. More specifically, we read from the base_client structure the location of the player structure, add an offset to get to our health address, and then write a value to that address to adjust our health.

A gist of the code is available here.

The code is based off the template created in the previous article, available here.

Reading

The prototype for vm_read:

kern_return_t vm_read( 
    vm_task_t target_task,
    vm_address_t address,
    vm_size_t size,
    size data_out,
    target_task data_count);

In practice, this tends to look like:

uint32_t size = 0;
pointer_t buffer_pointer = 0;

kern_return = vm_read( task, 0xDEADBEEF, sizeof( int ), &buffer_pointer, &size );

 

Downcasting

One of those odd quirks of the mach operations is you can't directly read into a variable type you want (or at least, I haven't found a way). Looking at the documentation:

data_out: Out-pointer to dynamic array of bytes returned by the read.

Since we have a pointer to an array, the onus is on us to downcast it to a type we want. The best way I've found is to always downcast to a character array and then cast to whatever type you want. This has two benefits:

  1. Character arrays are incredibly versatile. Having data in one basically ensures you can modify it however you want.
  2. Casting from a character array to a dword or qword solves any endianness issues you may have.

Examples for both these points:

  1. In mem_scan, I hold the results of a read region to a large character array. This allows me to avoid the bottleneck of constantly reading from a processes' memory and also allows me to easily cast values up to an int on dword boundaries.
  2. In CS2D, pointers are stored in little-endian due to the architecture. By downcasting to a character array and then again to an integer, we change the encoding to big-endian, which is what we need to use in vm_write.

To downcast, we use memcpy:

unsigned char buffer[ 4 ] = { 0 };
    
memcpy( buffer, (const void*)buffer_pointer, size );

This will place the exact contents at the memory address into buffer. For example:

    0xDEADBEEF 00 00 00 64
buffer[ 0 ] = 00;
buffer[ 1 ] = 00;
buffer[ 2 ] = 00;
buffer[ 3 ] = 0x64;

If we know 0xDEADBEEF is an integer type:

uint32_t num = 0;

memcpy( &num, buffer, sizeof( uint32_t ) );

// num now equals 100 (0x64)

For our CS2D POC, the memory at 0x3136e0 looks something like this

    0x3136e0 00 2a 55 1c

Where the address of our player structure is dynamically allocated (in this example, it would be at 0x1c552a00). To get this value, we downcast to our character array and then cast again to an unsigned int:

unsigned char buffer[ 4 ] = { 0 };
unsigned int address = 0;

memcpy( buffer, (const void*)buffer_pointer, size );
memcpy( &address, buffer, sizeof( address ) );

// address now equals 0x1c552a00

Now to get our health address, we can add our offset directly to our address:

address += 0x164;

 

Writing

The prototype for vm_write:

kern_return_t vm_write(
    vm_task_t target_task,
    vm_address_t address,
    pointer_t data,
    mach_msg_type_number_t data_count);

Writing is a lot more straight-forward. The only "gotcha" is that data needs to be a pointer to the array of bytes you wish to write. An example from our POC:

uint32_t value = 0;    //provided in argv[ 2 ]

kern_return = vm_write( task, address, (pointer_t)( unsigned char * )&value, sizeof( uint32_t ) );

If your write target lives in code, you will have to give that address the write protection flag (generally code segments only have the execute flag set). You can do this using vm_protect:

vm_protect( task, address, sizeof( uint32_t ), 0, VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE );