首页
社区
课程
招聘
[原创]破解Hopper Disassembler v3.7.8 for mac的艰难历程
2015-3-1 14:27 73477

[原创]破解Hopper Disassembler v3.7.8 for mac的艰难历程

2015-3-1 14:27
73477
Hopper Disassembler是一个很牛的逆向分析工具,虽然我个人觉得比不上IDA,但是聊胜于无,感觉字符处理得不错。
这个工具在2.x时代是很容西xx的,但是到了3.x翅膀就硬了。
我这里xx的是最新的3.7.8,在这个版本中自我保护手段是很多的,主要是:
1.无法用gdb lldb载入分析,一附加就出错退出。
2、Section偏移量篡改。
3、加密了__TEXT里的代码,字符串,还有objc相关的信息。
这样的结果就是,无法进行动态分析,无法载入静态分析。
以下Hopper Disassembler简称hd
想破解它,首先就要修复其Section。
关于Mach-O的结构,不懂的百度一下吧。还有就是用010 Editor来编辑简直好用到不行
要修复Section,我们先分析以下内存布局,根据其布局来修复
这是otool -l输出的__TEXT Segment的信息
Load command 1
      cmd LC_SEGMENT_64
  cmdsize 1032
  segname __TEXT
   vmaddr 0x0000000100000000
   vmsize 0x00000000003d1000
  fileoff 0
 filesize 4001792
  maxprot 0x00000007
 initprot 0x00000007
   nsects 12
    flags 0x0

主要看vmaddr 和fileoff,vmaddr是这个二进制文件加载到内存中的位置。fileoff是加载到内存里的数据在文件中得偏移量,这里是0.也就是说从头加载。
为什么要看这里呢,这是因为根据分析发现hd主要是对Section的offet进行修改,修改为任意值。Section中得offset和fileoff类似,也是指在文件中得偏移量。静态分析工具需要这个偏移量来定位一些静态信息,如果将其修改,静态分析就无法继续。还有就是修改Section对运行时是没有影响的。这样我们就得知了fileoff和vmaddr。
我们再看一下__TEXT Segment的__text Section,以下是otool -l输出的__text的信息。
Section
  sectname __text
   segname __TEXT
      addr 0x0000000100006fa0
      size 0x00000000003022ea
    offset 28576
     align 2^4 (16)
    reloff 0
    nreloc 0
     flags 0x80000400
 reserved1 0
 reserved2 0

addr同样是这个Section在内存中的位置,offset是在文件中的偏移量。可以这么算
offset = fileoff + addr - vmaddr
将offset回填就行了。
按照上面说的方法,逐一修正所有的Section就ok了。还要注意的地方就是各个Segment的大小filesize和vmsize是否相等。
至此Section的修复就算完成了。
第二步,就是恢复被加密的Section的内容。
因为无法附加调试,vm_read的动态基址获取很麻烦,于是我这里选择指定DYLD_INSERT_LIBRARIES环境变量的方式注入一个dylib,在dylib的构造方法里启动一个线程,在线程中等待运行时Section的解密完成。一旦发现解密完成之后就把解密后的内容dump到硬盘上。
/*

	export DYLD_FORCE_FLAT_NAMESPACE=1
	export DYLD_INSERT_LIBRARIES=~/xd.dylib

*/



#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include "pthread.h"

#include <sys/types.h>
#include <sys/ptrace.h>
#include <sys/sysctl.h>

#include <mach/mach.h>
#include <mach/mach_init.h>
#include <mach/mach_vm.h>

mach_vm_address_t getBasicAddress(mach_port_t task){
	mach_vm_size_t region_size = 0;
	mach_vm_address_t region = NULL;
	int ret = 0;

	  /* Get region boundaries */
#if defined(_MAC64) || defined(__LP64__)
	vm_region_basic_info_data_64_t info;
	mach_msg_type_number_t info_count = VM_REGION_BASIC_INFO_COUNT_64;
	vm_region_flavor_t flavor = VM_REGION_BASIC_INFO_64;
	if ((ret = mach_vm_region(mach_task_self(), ®ion, ®ion_size, flavor, (vm_region_info_t)&info, 
		(mach_msg_type_number_t*)&info_count, (mach_port_t*)&task)) != KERN_SUCCESS)
	{
		printf("mach_vm_region() message %s!\n",mach_error_string(ret));
		return NULL;
	}
#else
	vm_region_basic_info_data_t info;
	mach_msg_type_number_t info_count = VM_REGION_BASIC_INFO_COUNT;
	vm_region_flavor_t flavor = VM_REGION_BASIC_INFO;
	if ((ret = vm_region(mach_task_self(), ®ion, ®ion_size, flavor, (vm_region_info_t)&info, 
		(mach_msg_type_number_t*)&info_count, (mach_port_t*)&task)) != KERN_SUCCESS)
	{
		printf("vm_region() message %s!\n",mach_error_string(ret));
		return NULL;
	}
#endif
	return region;
}
vm_size_t readRemotoMemory(char *buf,vm_size_t len,mach_port_t task,vm_address_t address)
{
	vm_size_t outSize = 0;

	int ret = vm_read_overwrite(task,address,len,(vm_address_t)buf,&outSize);
	if (ret != 0)
	{
		printf("vm_read_overwrite() message %s!\n",mach_error_string(ret));
		return 0;
	}
	return outSize;
}

//int main(int argc, char const *argv[])
void* handler(void *p)
{
	//int pid = 16057;
	int pid = getpid();
	char buffer[512];
	mach_vm_address_t address = 0;
	mach_port_t task = 0;

	int waitTime = 15;
	while(waitTime){
		sleep(1);
		printf("thread waiting! %d\n",waitTime);
		waitTime --;
	}

	int ret = task_for_pid(mach_task_self(),pid,&task);
	if (ret != 0)
	{	
		printf("task_for_pid() message %s!\n",mach_error_string(ret));
		return NULL;
	}
	address = getBasicAddress(task);

	printf("pid     : %d\n",pid);
	printf("task    : %x\n", task);
	printf("address : %llx\n", address);

	if (address == 0)
	{
		printf("getBasicAddress() faild!\n");
		return NULL;
	}

	uint32_t writeSize = 0;
	FILE *fp = fopen("dump.bin","wb");
	readRemotoMemory(buffer,512,task,address);
	printf("%x\n",*(uint*)buffer);

	while (writeSize <= 0x1F8E){
		readRemotoMemory(buffer,512,task,address);
		//printf("%x\n",*(uint*)buffer);
		address += 512;
		writeSize += 512;
		fwrite(buffer,512,1,fp);
	}

	return NULL;
}

void __attribute__((constructor)) init()
{
	int err;
	pthread_t ntid;
    err = pthread_create(&ntid, NULL, handler, NULL);
    if (err != 0)
    {
    	printf("can't create thread: %s\n", strerror(err));
    	return ;
    }
}

这是我此次使用的dump的代码。将其编译成dylib,插入DYLD_INSERT_LIBRARIES后启动hd就行了。详细做法请看其代码。
dump出来的文件,其实就是一个解密后的二进制镜像。里面包含有解密了得Section。其文件结构也是一个mach-o文件。
这里我用010 Editor神器将dump出来的Section逐一复制粘贴到原本里hd里。我做的时候只还原了__TEXT Segment的Section,__Data Segment的没有理会。而且这样子修复后的hd,是可以运行的,只不过显示主界面大约一秒钟之后就闪退了。而且其反调试功能还在工作,还是无法动态调试。
这样就算解密完成了,经过这样的处理,就能顺利地加载到IDA分析了。
最后一步就是破解了
本人比较懒,有快捷的方法我也不啰嗦。因为要完全还原hd的可执行文件太麻烦,于是我这里使用运行时内存补丁来将其破解。
经过ida的分析,找到几处关键点checkRegistrationLicense,和checkRegistrationToken,然后用这里(http://malokch.xicp.net/?post=25)的工具生成补丁。
补丁的加载方式还是DYLD_INSERT_LIBRARIES插入dylib,~~太懒了没办法,补丁代码如下
 /*

	export DYLD_FORCE_FLAT_NAMESPACE=1
	export DYLD_INSERT_LIBRARIES=~/chd.dylib

*/



#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include "pthread.h"

#include <sys/types.h>
#include <sys/ptrace.h>
#include <sys/sysctl.h>

#include <mach/mach.h>
#include <mach/mach_init.h>
#include <mach/mach_vm.h>

#include "libkern/OSCacheControl.h"

mach_vm_address_t getBasicAddress(int pid){
	mach_vm_size_t region_size = 0;
	mach_vm_address_t region = 0;
	mach_port_t task = 0;
	int ret = 0;

	ret = task_for_pid(mach_task_self(),pid,&task);
	if (ret != 0)
	{	
		printf("task_for_pid() message %s!\n",mach_error_string(ret));
		return 0;
	}

	  /* Get region boundaries */
#if defined(_MAC64) || defined(__LP64__)
	vm_region_basic_info_data_64_t info;
	mach_msg_type_number_t info_count = VM_REGION_BASIC_INFO_COUNT_64;
	vm_region_flavor_t flavor = VM_REGION_BASIC_INFO_64;
	if ((ret = mach_vm_region(mach_task_self(), ®ion, ®ion_size, flavor, (vm_region_info_t)&info, 
		(mach_msg_type_number_t*)&info_count, (mach_port_t*)&task)) != KERN_SUCCESS)
	{
		printf("mach_vm_region() message %s!\n",mach_error_string(ret));
		return 0;
	}
#else
	vm_region_basic_info_data_t info;
	mach_msg_type_number_t info_count = VM_REGION_BASIC_INFO_COUNT;
	vm_region_flavor_t flavor = VM_REGION_BASIC_INFO;
	if ((ret = vm_region(mach_task_self(), ®ion, ®ion_size, flavor, (vm_region_info_t)&info, 
		(mach_msg_type_number_t*)&info_count, (mach_port_t*)&task)) != KERN_SUCCESS)
	{
		printf("vm_region() message %s!\n",mach_error_string(ret));
		return NULL;
	}
#endif
	return region;
}

vm_size_t readRemotoMemory(char *buf,vm_size_t len,int pid,vm_address_t address)
{
	vm_size_t outSize = 0;
	mach_port_t task = 0;

	int ret = task_for_pid(mach_task_self(),pid,&task);
	if (ret != 0)
	{	
		printf("task_for_pid() message %s!\n",mach_error_string(ret));
		return 0;
	}

	ret = vm_read_overwrite(task,address,len,(vm_address_t)buf,&outSize);
	if (ret != 0)
	{
		printf("vm_read_overwrite() message %s!\n",mach_error_string(ret));
		return 0;
	}
	return outSize;
}
 //
 int FakeCode(char *addr, char code)
 {
 	mach_port_t task;
 	mach_vm_size_t region_size = 0;
 	mach_vm_address_t region = (vm_address_t)addr;

  /* Get region boundaries */
 #if defined(_MAC64) || defined(__LP64__)
 	vm_region_basic_info_data_64_t info;
 	mach_msg_type_number_t info_count = VM_REGION_BASIC_INFO_COUNT_64;
 	vm_region_flavor_t flavor = VM_REGION_BASIC_INFO_64;
 	if (mach_vm_region(mach_task_self(), ®ion, ®ion_size, flavor, (vm_region_info_t)&info, (mach_msg_type_number_t*)&info_count, (mach_port_t*)&task) != 0)
 	{
 		return 0;
 	}
 #else
 	vm_region_basic_info_data_t info;
 	mach_msg_type_number_t info_count = VM_REGION_BASIC_INFO_COUNT;
 	vm_region_flavor_t flavor = VM_REGION_BASIC_INFO;
 	if (vm_region(mach_task_self(), ®ion, ®ion_size, flavor, (vm_region_info_t)&info, (mach_msg_type_number_t*)&info_count, (mach_port_t*)&task) != 0)
 	{
 		return 0;
 	}
 #endif

  /* Change memory protections to rw- */
 	if (vm_protect(mach_task_self(), region, region_size, 0, VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY) != KERN_SUCCESS)
 	{
 		//_LineLog();
 		return 0;
 	}

  /* Actually perform the write */
 	*addr = code;

  /* Flush CPU data cache to save write to RAM */
 	sys_dcache_flush(addr, sizeof(code));

  /* Invalidate instruction cache to make the CPU read patched instructions from RAM */
 	sys_icache_invalidate(addr, sizeof(code));

  /* Change memory protections back to r-x */
 	vm_protect(mach_task_self(), region, region_size, 0, VM_PROT_EXECUTE | VM_PROT_READ);
 	return 1;
 }


//int main(int argc, char const *argv[])
void* handler(void *p)
{
	//int pid = 16057;
	int pid = getpid();
	char buffer[512];
	mach_vm_address_t address = 0;

	address = getBasicAddress(pid);

	//printf("Target pid     : %d\n",pid);
	//printf("Base address   : %llx\n", address);

	if (address == 0)
	{
		printf("getBasicAddress() faild!\n");
		return NULL;
	}

	//Demo
	char *demo = (char*)address + 0x329a9e;
	demo[0] = ' ';
	demo[1] = ' ';
	demo[2] = ' ';
	demo[3] = ' ';

	//Demo version
	char *dv = (char*)address + 0x329E8C;
	dv[0] = 'F';
	dv[1] = 'u';
	dv[2] = 'l';
	dv[3] = 'l';

	//Waiting for decode __text
	sleep(1);

	//checkRegistrationLicense:
	//xor ebx,ebx    =>    mov     $1,%bl
	//xor edi,edi    =>    inc     %edi
	*(uint32_t*)(address + 0xb9b7) = 0xc7ff01b3;

	//checkRegistrationToken
	// xor r14d,r14d => inc r14d
	*(uint8_t*)(address + 0xb974) 	  = 0x41;
	*(uint8_t*)(address + 0xb974 + 1) = 0xff;
	*(uint8_t*)(address + 0xb974 + 2) = 0xc6;

	return NULL;
}

void __attribute__((constructor)) init()
{
	int err;
	pthread_t ntid;
    err = pthread_create(&ntid, NULL, handler, NULL);
    if (err != 0)
    {
    	printf("can't create thread: %s\n", strerror(err));
    	return ;
    }
}

这个补丁是结合具体情况来写的,看代码就知道,是直接用指针来修改内存的。这样子做在这个例子可行,但是在别的情况就可能不行了。为什么呢,因为在正常情况,__TEXT段是只读可执行的,直接修改会报错的。但是我们这里为什么可以直接修改呢,因为我们前面说过,__TEXT的内容是被加密的,运行后将会被解密,解密回填的时候必然要对其进行写操作。于是hd在运行时就自行对这些内存增加了写权限。结果却没有恢复原来的权限,于是就方便了我们(要是我,我也不会恢复原来的权限的,太麻烦,没必要)。
这里仅对内存做了两处补丁,这样子破解可能还不完全,我也没发现哪里不能用,不过能用就是了。没有了注册窗口,没有了调试面板的限制,没有Demo的水印等。
最后打包的时候,将MacOS下的Hopper Disassembler v3重命名为Hopper Disassembler v3_,然后新建一个shell脚本,名字叫Hopper Disassembler v3,脚本代码如下
#!/bin/bash
HD_PATH="`dirname "${0}"`"
HD_BIN="`dirname "${0}"`"/Hopper\ Disassembler\ v3_

export DYLD_INSERT_LIBRARIES="${HD_PATH}/chd.dylib"
"$HD_BIN"

我这里的补丁叫chd.dylib,将其放到MacOS下就OK了。简直完美。
若有不对之处敬请指正。
很多同学说搞不定,在此奉上传送门:http://pan.baidu.com/s/1bn94SDx

[培训]内核驱动高级班,冲击BAT一流互联网大厂工 作,每周日13:00-18:00直播授课

收藏
点赞2
打赏
分享
最新回复 (59)
雪    币: 71
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
LifeVscen 2015-3-1 15:25
2
0
学习一下,楼主真棒!!!!!!
雪    币: 1136
活跃值: (683)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
heihu 2015-3-1 16:29
3
0
小菜看不懂mac
雪    币: 3149
活跃值: (66)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
jerryme 2015-3-2 09:16
4
0
不明觉厉
雪    币: 628
活跃值: (348)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
AloneMonkey 2 2015-3-2 09:32
5
0
楼主厉!
雪    币: 11
活跃值: (31)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lixiangren 2015-3-2 09:44
6
0
高手只在明间呀!
雪    币: 71
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
LifeVscen 2015-3-2 09:51
7
0
高手只在阴间!!!!!!

雪    币: 37
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Hong丫丫 2015-3-2 10:18
8
0
楼主赞赞哒
雪    币: 131
活跃值: (153)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
川美 2015-3-2 10:23
9
0
步骤还是比较详细的
雪    币: 10891
活跃值: (2729)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
menglv 2015-3-6 12:17
10
0
动手能力差,补丁在哪里?
雪    币: 576
活跃值: (1374)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
我是土匪 4 2015-3-6 13:55
11
0
好文章,一直用这个软件。
雪    币: 32415
活跃值: (18960)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
kanxue 8 2015-3-8 19:53
12
0
帖子移到『iOS安全』存档,感谢malokch与大家分享心得!
雪    币: 29
活跃值: (499)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
万抽抽 2 2015-3-8 23:32
13
0
原来是沧海一声呵兄弟,厉害!
雪    币: 43
活跃值: (388)
能力值: ( LV9,RANK:140 )
在线值:
发帖
回帖
粉丝
malokch 2 2015-3-9 09:38
14
0
兄弟是?
雪    币: 10891
活跃值: (2729)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
menglv 2015-3-9 22:18
15
0
明天去虚拟机里面验证一下,谢谢了。
雪    币: 133
活跃值: (233)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
不追浮云的人 2015-3-10 09:47
16
0
很详细,代码还贴出来了,可以操刀试一下了,谢谢
雪    币: 2443
活跃值: (434)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
飘云 1 2015-3-10 12:48
17
0
好文,学习了!

*(uint8_t*)(address + 0xb974 + 2) = 0xc6; ??
雪    币: 43
活跃值: (388)
能力值: ( LV9,RANK:140 )
在线值:
发帖
回帖
粉丝
malokch 2 2015-3-10 13:52
18
0
[QUOTE=飘云;1357909]好文,学习了!

*(uint8_t*)(address + 0xb974 + 2) = 0xc6; ??[/QUOTE]

这是我的疏忽了,感谢飘云兄指出此处错误,现已修正。

这样的错误之下还能跑成功,说明可能checkRegistrationToken并非痛点,又或者误打误撞了。
不过经过分析有多处代码调用checkRegistrationToken,这可能还是挺关键的。

能跑成功也不是偶然,因为这里的修改主要是破坏对r14d的清零,就算错了依然有很大的可能会成功。
雪    币: 2443
活跃值: (434)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
飘云 1 2015-3-10 22:57
19
0
兄弟客气了...
按照以往版本经验,这样改会出现导出功能无效,30分钟时间到期提示等等,等有空了我也测试下。

再次谢谢分享好文!
雪    币: 43
活跃值: (388)
能力值: ( LV9,RANK:140 )
在线值:
发帖
回帖
粉丝
malokch 2 2015-3-10 23:04
20
0
就我测试,功能是能用的,30分钟限制倒是没有测试,主力还是IDA,用HD很少超过30分钟。。。
雪    币: 585
活跃值: (568)
能力值: ( LV13,RANK:290 )
在线值:
发帖
回帖
粉丝
guxinyi 5 2015-3-11 09:29
21
0
vmaddr 0x0000000100000000
  addr 0x0000000100006fa0
addr 的值比vmaddr大,怎么能vmadr减addr?
offset = fileoff + vmadr - addr

还有,楼主贴的那些c++代码,是在xcode里开发编译的吗?

如果是的话,能否把整个工程也传上来。
雪    币: 43
活跃值: (388)
能力值: ( LV9,RANK:140 )
在线值:
发帖
回帖
粉丝
malokch 2 2015-3-11 10:29
22
0
不好意思,这里写错了。因为这文章是事后补的,可能有点问题…
应该是offset = file off + addr - vmaddr
不是xcode的,是Makefile的,编译很简单,就不贴makefile了。
雪    币: 205
活跃值: (21)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
gapple 2015-3-11 11:03
23
0
函数_dyld_get_image_header 获取的地址 是否和和楼主的getBasicAddress 相同?
雪    币: 585
活跃值: (568)
能力值: ( LV13,RANK:290 )
在线值:
发帖
回帖
粉丝
guxinyi 5 2015-3-11 12:38
24
0
贴出来吧,,我们新手不会的
雪    币: 43
活跃值: (388)
能力值: ( LV9,RANK:140 )
在线值:
发帖
回帖
粉丝
malokch 2 2015-3-11 13:19
25
0
gcc -shared -o chd.dylib cracked.c
具体的Makefile我这找不到了,不过一行命令就ok了
游客
登录 | 注册 方可回帖
返回