ipa-medit是什么?

跑起来

先写个简单的 tap10000 程序,代码如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int count = 10000;
        char none[30] = {0};
        NSLog(@"[tap1000]pid -> %d", getpid());
        NSLog(@"-> %d", count);

        while (count>0) {
            gets(none);
            count--;
            NSLog(@"-> %d", count);
        }
    }
    return 0;
}

测试一下功能,完美!

Untitled


先从 find 和 patch 两个功能入手,核心功能在 ipa-medit/cmd/cmd.go 文件下:

package cmd

import (
...
)

type Found struct {
	addrs     []int
	converter func(string) ([]byte, error)
	dataType  string
}

func Find(pid string, targetVal string, dataType string) ([]Found, error) {
	...
}
...

func Patch(pid string, targetVal string, targetAddrs []Found) error {
	...
}
...

源码分析

Find

函数使用 vmmap --wide ${pid} 查看进程的虚拟内存区域,再调用 GetWritableAddrRanges(vmmapResult) 函数遍历可写区域获取地址范围,以"==== Writable regions for process" 开始的行即为可写的区域;

vmmap 输出示例:

Process:         tap10000 [19769]
Path:            /Users/USER/Downloads/tap10000
Load Address:    0x100350000
Identifier:      tap10000
Version:         ???
Code Type:       ARM64
Platform:        macOS
Parent Process:  zsh [11605]

...

Physical footprint:         1297K
Physical footprint (peak):  1297K
----

Virtual Memory Map of process 19769 (tap10000)
Output report format:  2.4  -- 64-bit process
VM page size:  16384 bytes

==== Non-writable regions for process 19769
REGION TYPE                    START - END         [ VSIZE  RSDNT  DIRTY   SWAP] PRT/MAX SHRMOD PURGE    REGION DETAIL
__TEXT                      100350000-100354000    [   16K    16K     0K     0K] r-x/r-x SM=COW          /Users/USER/Downloads/tap10000
...

==== Writable regions for process 19769
REGION TYPE                    START - END         [ VSIZE  RSDNT  DIRTY   SWAP] PRT/MAX SHRMOD PURGE    REGION DETAIL
Kernel Alloc Once           100460000-100468000    [   32K    16K    16K     0K] rw-/rwx SM=PRV
...
__DATA                      100640000-100644000    [   16K    16K    16K     0K] rw-/rw- SM=COW          /usr/lib/dyld
...

解析拿到可写内存区域地址范围后存入 addrRanges [][2]int,接下来就是读取内存并查找值为 targetVal 的地址:

splitSize = 0x5000000

func FindDataInAddrRanges(pid int, targetBytes []byte, addrRanges [][2]int) ([]int, error) {
	foundAddrs := []int{}
	searchLength := len(targetBytes)
	for _, s := range addrRanges {
		beginAddr := s[0]
		endAddr := s[1]
		memSize := endAddr - beginAddr
		for i := 0; i < (memSize/splitSize)+1; i++ {
			// target memory is too big to read all of it, so split it and then search in memory
			splitIndex := (i + 1) * splitSize
			splittedBeginAddr := beginAddr + i*splitSize
			splittedEndAddr := endAddr
			if splitIndex < memSize {
				splittedEndAddr = beginAddr + splitIndex
			}
			b := bufferPool.Get().([]byte)[:(splittedEndAddr - splittedBeginAddr)]
			task := GetTaskForPid(pid)
			if err := ReadMemory(task, b, splittedBeginAddr, splittedEndAddr); err == nil {
				findDataInSplittedMemory(&b, targetBytes, searchLength, splittedBeginAddr, 0, &foundAddrs)
				bufferPool.Put(b)
				if len(foundAddrs) > 500000 {
					fmt.Println("Too many addresses with target data found...")
					return foundAddrs, TooManyErr{&Err{errors.New("Error: Too many addresses")}}
				}
			} else {
				fmt.Printf("0x%x: %s\\\\n", beginAddr, err)
			}
		}
	}
	return foundAddrs, nil
}

因为目标内存太大,无法读取所有内存,因此 FindDataInAddrRanges 函数请将其拆分,然后在内存中搜。

task := GetTaskForPid(pid) 获取目标进程任务端口,c代码如下:

task_t get_task_for_pid(int pid) {
	task_t task = 0;
	task_for_pid(mach_task_self(), pid, &task);
	return task;
}

ReadMemory 读取内存调用 mach_vm_read addr 为开始地址,len 为起始间长度endAddr - beginAddr 代码如下:

import "C"
/*
...
int read_memory(task_t task, mach_vm_address_t addr, void *d, mach_msg_type_number_t len) {
	pointer_t data;
	mach_msg_type_number_t count;
	kern_return_t kret = mach_vm_read((vm_map_t)task, addr, len, &data, &count);
	if (kret != KERN_SUCCESS) return -1;
	memcpy(d, (void *)data, len);
	return count;
}
...
*/
func ReadMemory(task C.task_t, buf []byte, beginAddr int, endAddr int) error {
	var (
		vmData = unsafe.Pointer(&buf[0])
		vmAddr = C.mach_vm_address_t(beginAddr)
		length = C.mach_msg_type_number_t(endAddr - beginAddr)
	)

	ret := C.read_memory(task, vmAddr, vmData, length)
	if ret < 0 {
		return fmt.Errorf("could not read memory")
	}
	return nil
}

接下就比较简单了 findDataInSplittedMemory 函数通过 bytes.Index 索引目标值并递归查找:

func findDataInSplittedMemory(memory *[]byte, targetBytes []byte, searchLength int, beginAddr int, offset int, results *[]int) {
	// use Rabin-Karp string search algorithm in bytes.Index
	index := bytes.Index((*memory)[offset:], targetBytes)
	if index == -1 {
		return
	} else {
		resultAddr := beginAddr + index + offset
		*results = append(*results, resultAddr)
		offset += index + searchLength
		findDataInSplittedMemory(memory, targetBytes, searchLength, beginAddr, offset, results)
	}
}

Patch

获取到地址后,接着看下 patch 功能逻辑:

func Patch(pid string, targetVal string, targetAddrs []Found) error {
	for _, found := range targetAddrs {
		targetBytes, _ := found.converter(targetVal)
		for _, targetAddr := range found.addrs {
			intPid, _ := strconv.Atoi(pid)
			task := memory.GetTaskForPid(intPid)
			if err := memory.WriteMemory(task, targetAddr, targetBytes); err != nil {
				return err
			}
		}
	}
	fmt.Println("Successfully patched!")
	return nil
}

同样是先获取获取目标进程任务端口,再对地址进行写操作 memory.WriteMemory(task, targetAddr, targetBytes)

import "C"
/*
...
int write_memory(task_t task, mach_vm_address_t addr, void *d, mach_msg_type_number_t len) {
	kern_return_t kret = mach_vm_write((vm_map_t)task, addr, (vm_offset_t)d, len);
	if (kret != KERN_SUCCESS) return -1;
	return 0;
}
...
*/
func WriteMemory(task C.task_t, addr int, data []byte) error {
	var (
		vmData = unsafe.Pointer(&data[0])
		vmAddr = C.mach_vm_address_t(addr)
		length = C.mach_msg_type_number_t(len(data))
	)

	if ret := C.write_memory(task, vmAddr, vmData, length); ret < 0 {
		return fmt.Errorf("could not write memory")
	}
	return nil
}

总结

本文到此结束,对vmmap、mach任务端口等部分稍稍地了解一下。

还记得上一篇反反调试小记中需要在 entitlements.xml 中添加 get-task-allow 选项吗?就是因为 debugserver 调用了 task_for_pid 来控制目标进程,没有给予com.apple.security.get-task-allow 权限时内核就会终止程序执行。

参考链接


Powered by Kali-Team