iOS逆向分析笔记


layout: wiki
title: iOS逆向分析笔记
categories: Reverse_Engineering
description: iOS逆向分析笔记
keywords:
url: https://lichao890427.github.io/ https://github.com/lichao890427/


Android命令和IOS命令对照关系

Android命令 iOS命令
安装应用 adb install -r <pkgname> 真机安装:fruitstrap -b UCWEB.app/XXX.ipa
模拟器安装:xcrun simctl install booted <XXX.app/XXX.ipa>
ideviceinstaller -i
卸载应用 adb uninstall -k <pkgname> crun simctl erase [device ID]
ideviceinstaller -u
查看设备 adb devices instruments -s devices
xcrun simctl list
打开进程 am start open
端口转发 adb forward tcprelay.py

Mac/iOS环境配置

Mac环境配置

安装brew 
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
安装wget
brew install binutils
brew install wget
brew install python
安装pip
wget https://bootstrap.pypa.io/get-pip.py
sudo su
python get-pip.py
pip install -U pip
安装应用
pip install frida
brew install gcc
brew install llvm
brew install automake
brew install cmake
brew install git
brew install gdbre
https://sourceware.org/gdb/wiki/BuildingOnDarwin
codesign -s gdb-cert /usr/local/bin/gdb

iOS环境配置

安装CYDIA工具:
OpenSSH             基本命令
Tcpdump
AppSync             绕过系统验证,随意安装ipa
Apple File Conduit      安装后可在手机助手显示系统目录
samba windows       文件共享
syslog              日志存放在/var/log/syslog
安装python pip
Ncdu    du command
Lsof        lsof command
File        file command
Less        less command
Cyscript
Apt struct 提供apt-get
Adv-cmds    finger fingerd lsvfs last md ps
File-cmds       chflags compress ipcrm ipcs pax
Basic-cmds  msg uudecode uuencode write
Shell-cmds  killall mktemp renice time which
System-cmds iostat login passwd sync sysctl
Diskdev-cmds    mount quota fsck fstyp fdisk tunefs
Network     arp ifconfig netstat route traceroute
Syslog      syslogon syslogoff       /var/log/syslog
Wget
GNU Debugger    ar nm objdump ranlib strip addr2line c++filt gdb objcopy objdump readelf        
(compile your code with –mcpu=arm1176jzf-s)

CYDIA常用源:
http://apt.thebigboss.org
http://apt.saurik.com
http://apt.modmyi.com
http://repo666.ultrasn0wn.com
http://ctdua.zodttd.com

http://apt.weifeng.com
http://apt.feng.com
http://repo.feng.com
http://repo.xarold.com
http://julio.xarold.com
http://crak.cn/repo/
http://iphone.tgbus.com/cydia/
https://build.frida.re

分析工具

Android Mac iOS
跟踪工具 strace ltrace Introspy dtruss dtrace Frida Cycript Introspy
文件操作 adb push/pull scp
日志 logcat idevicesyslog /var/log/syslog
调试工具 gdb jdb IDA gkidbg gdb IDA lldb gdb IDA lldb gikdbg
Hook框架 XPosed/Cydia Substrate Cydia Substrate
静态分析 IDA dex2jar Apktool jadx jeb jd-gui IDA classdump iNalyzer Hopper IDA classdump iNalyzer Hopper

Class-Dump用法

Class-dump是mac上的命令行工具用于解析Objective-C类接口,class-dump-z修复了一些bug并使用c++重写从而在mac, linux, win平台上运行,不支持x64 iphone,因此如果要解析mac os x程序的类,要用原始class-dump,而解析iphone的类使用class-dump-z

  • class-dump
      运行于mac的工具用于解析objectc 运行时信息,生成classes, categories protocols,和otool –ov的结果类似,以objectivec语法表示更可读

  • Class-dump-z
      速度快,便携且兼容各系统,修正ivar偏移处理,结构体名可读性高,属性化,隐藏继承和代理方法,参数名可读,修正头文件生成等

  若由于AppStore加密等原因无法直接用classdump导出类的情况下,可以用cycript脚本weak_classdump在运行时进行同等操作

IDA反汇编

寻找oc函数调用栈

  对于OC语法由于是通过消息机制进行函数调用的,因此无法直接找到调用者,这里通过脚本解决

import idc

def addxref(x, y, z):
    """
    add reference for objc_meth_addr <=> objc_methname_addr <=> msgsend_call_addr
    :param x: msgsend_call_addr
    :param y: objc_meth_addr
    :param z: objc_methname_addr
    :return: nothing
    """
    AddCodeXref(x, y, XREF_USER | fl_F)
#    AddCodeXref(y, x, XREF_USER | fl_F)
#    AddCodeXref(x, z, XREF_USER | fl_F)
#    AddCodeXref(z, x, XREF_USER | fl_F)
    AddCodeXref(y, z, XREF_USER | fl_F)
#    AddCodeXref(z, y, XREF_USER | fl_F)


def addobjcref():
    """
    add reference for math-o file
    :return: nothing
    """
    objc_meth_map = {}
    methnamebegin = 0
    methnameend = 0
    forbitmeth = [
        "alloc",
        "allocWithZone:",
        "allowsWeakReference",
        "autorelease",
        "class",
        "conformsToProtocol:",
        "copy",
        "copyWithZone:",
        "dealloc",
        "debugDescription",
        "description",
        "doesNotRecognizeSelector:",
        "finalize",
        "forwardingTargetForSelector:",
        "forwardInvocation:",
        "hash",
        "init",
        "initialize",
        "instanceMethodForSelector:"
        "instanceMethodSignatureForSelector:",
        "instancesRespondToSelector:",
        "isEqual",
        "isKindOfClass:",
        "isMemberOfClass:",
        "isProxy",
        "isSubclassOfClass:",
        "load",
        "methodForSelector:",
        "methodSignatureForSelector:",
        "mutableCopy",
        "mutableCopyWithZone:",
        "performSelector:",
        "performSelector:withObject:",
        "performSelector:withObject:withObject:",
        "respondsToSelector:",
        "release",
        "resolveClassMethod:",
        "resolveInstanceMethod:",
        "retain",
        "retainCount",
        "retainWeakReference",
        "superclass",
        "zone",
        ".cxx_construct",
        ".cxx_destruct",
    ]
    # find the segment which contains objc method names
    curseg = FirstSeg()
    while curseg != 0xffffffff:
        if "__objc_methname" == SegName(curseg):
            methnamebegin = SegStart(curseg)
            methnameend = SegEnd(curseg)
            break
        curseg = NextSeg(curseg)
    # get objc method names
    if methnamebegin != 0:
        while methnamebegin < methnameend:
            funcname = GetString(methnamebegin)
            objc_meth_map[funcname] = methnamebegin
            methnamebegin = methnamebegin + len(funcname) + 1
    # get objc func table
    funcmap = {}
    addr = PrevFunction(-1)
    while addr != 0xffffffff:
        curname = GetFunctionName(addr)
        if -1 != curname.find('['):
            curname = curname.replace("[", "").replace("]", "")
            curname = curname.split(" ")[1]
            # may be more than one function with same sel but differenct class
            if curname not in funcmap:
                funcmap[curname] = []
            funcmap[curname].append(addr)
        addr = PrevFunction(addr)
    # make xref
    for (k, v) in objc_meth_map.items():
        # find corresponding func addr
        if k in funcmap and k not in forbitmeth:
            farr = funcmap[k]
            # find xref to code and make xref for each
            curref = DfirstB(v)
            while curref != 0xffffffff:
                for f in farr:
                    addxref(curref, f, v)                    
                curref = DnextB(v, curref)
            print "added xref for " + k

if __name__ == "__main__":
addobjcref()

正常显示unicode中文字符

  由于ida使用python对中文支持不好,这里通过脚本解决一定程度的问题

def find_utf16_string(addr):
    start = SegStart(addr)
    end = SegEnd(addr)
    addr = start
    while addr < end:
        # get length
        len = 1
        while Name(addr + len) == "":
            len = len + 1
        totalstr = ""
        for i in range(0, len, 2):
            if Word(addr + i) > 0x100:
                # read an unicode char
                bytes = GetString(addr + i, 2)
                try:  # some chinese character not supported by python
                    comm = bytes.decode("utf-16")
                    if type(comm) == unicode:
                        comm = comm.encode("gbk")
                    else:
                        comm = '?'
                except Exception as e:
                    comm = '?'
            else:
                # extract as ascii
                comm = chr(Word(addr + i))
            totalstr = totalstr + comm
        MakeComm(addr, totalstr)
        addr = addr + len


tofind = ["__ustring"]
seg = FirstSeg()
while seg != 0xffffffff:
    if SegName(seg) in tofind:
        find_utf16_string(seg)
    seg = NextSeg(seg)

Debug for mac&ios

lldb调试

  • 安装lldb和usb调试环境
brew install lldb libplist libusb usbmuxd ldid
wget http://cgit.sukimashita.com/usbmuxd.git/snapshot/usbmuxd-1.0.8.tar.bz2
tar xjfv usbmuxd-1.0.8.tar.bz2
cd usbmuxd-1.0.8/python-client/
python tcprelay.py -t 22:22         留作iphone命令行操作
python tcprelay.py –t 23946:23946   留作iphone调试
  • 签名debugserver使之可以附加
    • 创建签名文件entitlements.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.springboard.debugapplications</key>
    <true/>
    <key>get-task-allow</key>
    <true/>
    <key>task_for_pid-allow</key>
    <true/>
    <key>run-unsigned-code</key>
    <true/>
</dict>
</plist>
    • 签名完成后送入终端
codesign -s - --entitlements entitlements.plist -f debugserver
scp debugserver root@127.0.0.1:/bin/

  上述过程在Xcode经历一次调试后自动完成,debugserver位于iOS /Developer/usr/bin

  • 拷贝ARMDisassembler,提升代码可读性
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/?.?/DeveloperDiskImage.dmg的/Library/PrivateFrameworks/ARMDisassembler.framework
scp –r –p ARMDisassembler.framework root@127.0.0.1:/System/Library/PrivateFrameworks
  • 启动lldb-server
./bin/debugserver 
    debugserver host:port [program-name program-arg1 program-arg2 ...]  启动调试
    debugserver host:port --attach=<pid>                                进程id附加调试
    debugserver host:port --attach=<process_name>                       进程名附加调试
  • 启动lldb-client
lldb -> process connect connect://127.0.0.1:23946

IDA调试

  由于lldb扩展了RSP协议(gdb remote serial debug protocol), Ida调试使用原始gdb调试协议,功能没有lldb和gikdbg强,对于app只能下断点跟踪,功能十分有限,目前还在研究协议转换中。由于默认编译的程序会有PIE标志,导致模块地址随机化,ida无法直接附加,因此使用010Editor删除PIE标志,上传到远程机器后chmod 777改为可执行即可。步骤如下:

  • 使用010editor等工具将可执行文件去除pie标志(mach_header的flags MH_PIE=0x200000,注意选择正确的架构)
  • 拷贝可执行文件并载入到ida:scp root@127.0.01:/path/to/file /path/to/file
  • 启动server端:debugserver –x backboard *:1234 /path/to/file (或附加调试)
  • 转发端口:python tcprelay.py –t 1234:1234
  • 根据程序架构选择ida(x86 x64)设置ida为gdb调试,设置入口断点,设置调试地址和端口为127.0.0.1:1234,即可

Gikdbg调试

  官网http://www.gikir.com/product.php,由ollydbg进一步开发的面向android和ios的汇编语言调试工具,支持静态分析elf/mach-o文件和动态调试android/iOS App,目前只支持arm系统,该软件运行在window上,适合调试dylib和可执行文件和简单的app

  • 1.配置服务器
从官网下载gikdbg
scp $(GIKDBG)/iserver/gikir_iserver.deb root@127.0.0.1:/var/tmp
ssh root@127.0.0.1
dpkg -i /var/tmp/gikir_iserver.deb
重启后打开gikir_server app(清除占用6080端口的进程)
另一种安装方式是添加cydia源http://apt.feng.com/geekneo,安装gikir_iserver
  • 2.启动客户端
执行Gikdbg.exe
iDebug/Login(USB)登录
iDebug/File/Attach附加调试   Open启动调试

  目前gikdbg可以调试控制台、动态库、app程序,支持usb/wifi,支持注入动态库,首次调试的程序需要打补丁:

  • 1) 删除MH_PIE标志,让进程每次加载基址固定;
  • 2) 记录App的UUID值;
  • 3) 如果是FAT格式的App则禁用最低以及最高的架构版本;
  • 4) 如果是加密的App则解密该App;
  • 5) 注入调试辅助动态库gikir_iserver_injecter.dylib;

Trace/Hook for mac/ios

系统支持

  mac&ios进程加载器dyld提供了设置环境变量DYLD_INSERT_LIBRARIES 的方式向目标进程注入动态库,另外mac&ios系统支持的hook为在mach-o的__DATA __interpose节数据,源码如下,编译成mac和ios的binary即可,该法适用于普通程序,不适用于app,因为app无法用命令行直接启动

#include <unistd.h>
#include <fcntl.h>
typedef struct interpose_s{
    void* new_func;
    void* orig_func;
} interpose_t;
int my_open(const char*, int ,mode_t);
__attribute__((used))
const interpose_t interposers[] __attribute__ ((section("__DATA, __interpose"))) =
{
    {(void*)my_open, (void*)open},
};
int my_open(const char* path, int flags, mode_t mode)
{
    int ret = open(path, flags, mode);
    printf("%d = open %s\n",ret, path);
    return ret;
}
void init() __attribute__((constructor));
void init()
{
    printf("im in\n");
}
//gcc -dynamiclib l.c -o 1.dylib -Wall  // compile to dylib
// lichao26de-iPhone:/tmp root# DYLD_INSERT_LIBRARIES=interpose.dylib cat 1
//im in
//3 = open 1

OC支持

  load重写使该类在第一次加载时交换swizzled_setHidden和setHidden函数指针,导致调用swizzled_setHidden实际调用的是setHidden,反之亦然

#import <objc/runtime.h>
@implementation UIView(Loghiding)
- (BOOL)swizzled_setHidden {
NSLog(@"We're calling setHidden now!");
BOOL result = [self swizzled_setHidden];
return result;
}
+ (void)load {
Method original_setHidden;
Method swizzled_setHidden;
original_setHidden = class_getInstanceMethod(self, @selector(setHidden));
swizzled_setHidden = class_getInstanceMethod(self, @selector(swizzled_setHidden));
method_exchangeImplementations(original_setHidden, swizzled_setHidden);
}
@end

Frida

  著名的跨平台注入&跟踪工具,普通安装方式pip install frida,越狱ios上安装方式:

  • 添加源http://build.frida.re,安装frida,确保27042 27043端口不被占用
  • 启动frida-server ./usr/sbin/frida-server
  • 转发端口 python tcprelay.py –t 27042:27042 27043:27043

Cycript

  著名的注入&跟踪工具,支持iOS/Mac/Android,支持ObjC/JavaScript1.7/C++11语法

远程连接Cycript

hcy=dlopen(”libcycript.dylib”,1)    (可以使用各种方式将libcycript.dylib加载到进程中)
CYListenServer=(typedef void(short))(dlsym(hcy,”CYListenServer”))   
CYListenServer(111)         调用函数
tcprelay –t 111:111             host上转发端口
cycript –r 127.0.0.1:111            host上连接server

编译JS

echo "[x*x for each(x in [1,2,3])]" | cycript -c > x.js
cat x.js
(function($cyv,x){$cyv=[];(function($cys){$cys=[1,2,3];for(x in $cys){x=$cys[x];$cyv.push(x*x)}})();return $cyv})()

?命令

?bypass 忽略错误
?debug  调试输出开关
?lower
?exit
?reparse    显示换行等字符
?syntax 语法高亮
?gc     强制js垃圾回收

语法特点

JS type ObjC type
number NSNumber (CFNumber)
boolean NSNumber (CFBoolean)
string NSString
Array NSArray
object NSDictionary
[[NSArray arrayWithObjects:
  [NSNumber numberWithInt:41],
  "foo",
  [NSNumber numberWithBool:YES],
  [NSArray arrayWithObjects:[NSNumber numberWithInt:8], [NSNumber numberWithInt:6], nil],
  [NSDictionary dictionaryWithObjectsAndKeys:
    [NSNumber numberWithInt:12], "a",
    [NSNumber numberWithInt:46], "b",
  nil],
  [NSNumber numberWithInt:36],
nil] indexOfObject:"foo"]
可以直接简写为:[[41,"foo",true,[8,6],{a:12,b:46},36] indexOfObject:"foo"]

兼容OC语法

[obj msg:var]
@selector(selname)
obj->ivar
*ptr            打印结构体或类成员
objc->[key] 获取实例的某成员
&var            获取Objc实例地址
@class classname : superclass {}        定义Objc类
 + methodname {function body}
- methodname {function body}
@end
new classname
@”str”      等价于”str”
[super …]

Selector(selname)       声明selector
Functor(function body, type encoding)   定义ObjC函数
    new Functor(function(x,y){return (x+y).toString(16);}, "*dd")       (double, double) → char*

block = ^ int (int value) { return value + 5; }     声明block

基本功能

  • 导入js模块
      import "/tmp/test.js"

  • 导入cy模块
      @import com.saurik.substrate.MS (对应/usr/lib/cycript0.9/com/saurik/substrate/MS.cy)

  • 导入nodejs/cy模块

util=require(“util”)
utils=require(“/tmp/utils.cy”)
  • 返回上一次执行结果
    _

  • 获取可执行程序参数
    system.args

  • 指针类型转换

Pointer(address, type encoding)         函数地址转换为encoding指定类型
Instance(address)                       对象地址转换为ObjC对象地址
pt=(typedef int*)(oldpt)                    强制类型转换
  • 定义结构体
CGRect=(typedef struct {int x;int y;})
rect = new (struct CGRect)
rect.size                               获取结构体大小
  • 定义数组
      arr=new (typedef char[10])

  • 获取对象类型
      obj.class

OC运行时功能

  • 获取所有类
      ObjectiveC.classes

  • 获取所有接口
      ObjectiveC.protocols

  • 获取某实例所有方法
      [MYCLASS (tab-key)]

  • 获取实例的所有变量
      *obj

  • 由内存地址获取对象
      [#0x18b6c8d0 show]

  • 获取所有类实例
      choose(SBIconView)

  • 获取成员函数类型描述

selector.type(class)
selector(copyWithZone:).type(NSString)  =>   @12@0:4^{_NSZone=}8.
  • 修改函数
var oldm = NSObject.prototype.description
NSObject.prototype.description = function() { return oldm.call(this) + ' (of doom)'; }
[new NSObject init]
#"<NSObject: 0x100d11520> (of doom)"

调试功能

  • 获取加载模块
utils.get_dyld_info()
ObjectiveC.images
  • 修改内存权限
      utils.mprotect(addr, size, utils.constants.PROT_READ)

  • 读写内存

var foo = new int
*foo = 0x12345678
utils.hexdump(foo, 4)
  • 获取当前回溯栈
function bt(){
return [NSThread callStackSymbols];
}
  • 执行命令
      utils.getOutputFromTask(“/bin/ls”, [])

  • 调用函数

[obj msg: var]          调用oc函数
fopen(“/tmp”,”r”)       调用c函数
utils.apply("printf", ["%s %.3s, %d -> %c, float: %f\n", "foo", "barrrr", 97, 97, 1.5])     反射调用c函数
  • 反汇编
var method = class_getInstanceMethod(NSNumber,@selector(intValue));
var imp = method_getImplementation(method);
utils.disasm(imp, 10)
0x7fff83363b8c   1                       55  push rbp
0x7fff83363b8d   3                   4889e5  mov rbp, rsp
0x7fff83363b90   2                     4157  push r15
0x7fff83363b92   2                     4156  push r14
0x7fff83363b94   2                     4155  push r13
  • 汇编
var n = [NSNumber numberWithLongLong:10]
var method = class_getInstanceMethod([n class], @selector(longLongValue));
var imp = method_getImplementation(method);
utils.asm(imp, 'mov eax, 42; ret;')

Hook功能

  • 记录OC函数调用(需要substrate)
utils.logify(NSNumber, @selector(numberWithDouble:))
[NSNumber numberWithDouble:1.5]     //触发logifyt
2014-07-28 02:26:39.805 cycript[71213:507] +[<NSNumber: 0x10032d0c4> numberWithDouble:1.5]

  注意:对静态成员函数,第一参为object_getClass(类名);对普通成员函数,第一参为object_getClass(实例)
底层实现:

cy# @import com.saurik.substrate.MS
cy# var oldm = {};
cy# MS.hookMessage(NSObject, @selector(description), function() {
        return oldm->call(this) + " (of doom)";
    }, oldm)
cy# [new NSObject init]
#"<NSObject: 0x100203d10> (of doom)"
  • 记录C函数调用(需要substrate)
utils.logifyFunc("fopen", 2)
apply("fopen", ["/etc/passwd", "r"]);       //触发logifyt
    2015-01-14 07:01:08.009 cycript[55326:2042054] fopen(0x10040d4cc, 0x10040d55c)
cy# @import com.saurik.substrate.MS
cy# extern "C" void *fopen(char *, char *);
cy# var oldf = {}
cy# var log = []
cy# MS.hookFunction(fopen, function(path, mode) {
        var file = (*oldf)(path, mode);
        log.push([path.toString(), mode.toString(), file]);
        return file;
    }, oldf)
cy# fopen("/etc/passwd", "r");
(typedef void*)(0x7fff774ff2a0)
cy# log
[["/etc/passwd","r",(typedef void*)(0x7fff774ff2a0)]]

其他功能

  • 获取所有控件元素
      utils.find_subviews()

  • 获取所有viewcontroller
      utils.find_subview_controllers()

  • 获取cpu类型
      utils.getCpuType()

  • 获取坐标

manager=choose(CLLocationManager)[0]
[manager location]
  • 获取bundleid
      NSBundle.mainBunble.bundleIdentifier

其他CYCRIPT模块

iOS实例分析

  • 方式一:MachO格式注入
      在mach-o格式中增加LOAD_DYLIB command节,添加dylib,重签名即可

  • 方式二:调试器(LLDB/GDB/Cycript等)注入

LLDB/GDB:po dlopen("/usr/lib/test.dylib",1)
Cycript:dlopen("/usr/lib/test.dylib",1)     调试状态下也可使用cycript
  • 方式三:MobileLoader注入
      CydiaSubstrate的MobileLoader组件用于加载第三方dylib给指定程序,MobileLoader首先在启动时使用DYLD_INSERT_LIBRARIES加载自身,之后加载/Library/MobileSubstrate/DynamicLibraries下的所有动态库,由于是全局的默认会在所有程序中加载,可以采用过滤配置plist文件加载dylib(iOS9以后必须存在plist才准予加载),plist文件名与dylib名相同:
  • Bundle:必须匹配app(s)的bundle-id才准予加载
  • Classes:必须在目标进程中实现指定类(s)才准予加载
  • Executables:必须匹配可执行文件名才准予加载
Filter = {
    Executables = (“mediaserverd”);
    Bundles = (“com.apple.sprintboard”, “net.whatsapp.WhatsApp”);
    Mode = “Any”
};
  • 方式一:Cydia Hook框架
MSImageRef MSMapImage(const char* file)                         加载dylib
cont void* MSImageAddress(MSImageRef image)                     
bool MSHookProcess(pid_t pid, const char* library)                  远程线程方式(vm_)注入dylib
MSImageRef MSGetImageByName(const char* file)                   获取模块基址,优于dlopen
Void* MSFindSymbol(MSImageRef image, const char* name)          获取函数地址,优于dlsym
char* MSFindAddress(MSImageRef image, void** address)
Void MSHookFunction(void* symbol, void* replace, void** result)     hook c函数
IMP MSHookMessage(Class _class, SEL sel, IMP imp, const char* prefix)   hook oc消息
Void MSHookMessageEx(Class _class, SEL sel, IMP imp, IMP* result)       hook oc消息
void MSHookClassPair(Class target, Class hook, Class old)               封装MSHookMessageEx
Hook c函数底层实现仍然是arm汇编的inline hook
Hook oc函数底层实现则是利用objective-c runtime function

对于  rettype funcname(type1 param1, type2 param2)的函数:
hook c function 方式1 -- MSHookFunction:
rettype (*old_funcname)(type1 param1, type2 param2);
rettype new_funcname(type1 param1, type2 param2)
{
    ……….work before hook……….
    old_funcname(param1, param2);
    ……….work after hook…………
}
MSHookFunction((void*) funcname,  (void*)&new_funcname,  (void**)&old_funcname);

hook c function 方式2 – MSHookFunction-MSHook-MSHack:
MSHook(rettype, funcname, type1 param1, type2 param2)
{
    ……….work before hook……….
    _funcname(param1, param2);//注意前面加’_’
    ……….work after hook…………
}
MSHookFunction(funcname, MSHake(funcname))

hook oc function 方式1 - MSHookMessageEx
hook oc function 方式2 - MSHookClassPair
hook oc function 方式3 - MSHookInterface

1.  Theos越狱框架开发
优点:方便,一键部署,缺点:调试麻烦
$THEOS/bin/nic.pl
iphone/tweak
export THEOS_DEVICE_IP=???
make package install

2.  XCode开发
特点:和前者相反,调试方便,只需要如前述修改mach-o type为可执行程序即可调试
#include <CydiaSubstrate.h>
void* handle = dlopen(“libsubstrate.dylib”, 1);
typedef void (*HOOK)(void*, void*, void**);
HOOK MSHookFunction = (HOOK)dlsym(handle, “MSHookFunction”);
MSHookFunction((void*)funcname, (void*)&oldfunc, (void**)&newfunc);
  • 方式二:frida/frida-trace
      frida安装:mac/linux/win下执行pip install frida,iOS上从frida源安装服务端,安好后服务端每次开机启动,占用端口27042/27043,因此在客户端执行python tcprelay.pt –t 27042:27042 27043:27043
frida-ps –R     枚举所有进程
frida-ps –R –a 枚举所有app进程
frida-ps –R –a –i 枚举所有安装的app及其bundle name
frida-trace –R –p PID 附加到进程(按进程id)
frida-trace –R –n name 附加到进程(按进程名,例如百度商户)
frida-trace –R –f FILE 拉起进程并跟踪(例如com.baidu.bshoppush)

hook c function     frida-trace –i “recv*” –i “send*” ….
hook oc function    frida-trace –m “-[NS* draw*]” …

实例:跟踪商户app二维码操作
frida-trace -R -f com.baidu.bshoppush -m "-[QRCode* *]" -f com.baidu.bshoppush
对生成的js进行编辑,自定义输出数据可以在控制台得到相应显示
获取JSPatch下发代码:
frida-trace –U –f com.baidu.waimai –m “+[JPEngine *evaluate*]”
js脚本内容
var data=new ObjC.Object(args[2]);
log(data.toString());
log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join("\n") + "\n");

Jailbreak Development Tools

Theos

$ $THEOS/bin/nic.pl
NIC 1.0 - New Instance Creator
------------------------------
  [1.] iphone/application
  [2.] iphone/library
  [3.] iphone/preference_bundle
  [4.] iphone/tool
  [5.] iphone/tweak
Choose a Template (required): 1
Project Name (required): iPhoneDevWiki
Package Name [com.yourcompany.iphonedevwiki]: net.howett.iphonedevwiki
Authour/Maintainer Name [Dustin L. Howett]:              
Instantiating iphone/application in iphonedevwiki/...
Done.
$

IOSOpenDev

  用于XCode的越狱程序开发插件http://iphonedevwiki.net/index.php/IOSOpenDev

Mac&iOS file format analysis

otool       类似于objdump,可以解析objc类信息
class-dump  objc类接口信息解析成可读objc头文件
OBJC_HELP   环境变量打日志
OBJC_HELP=1 ./build/Debug/HelloWorld
objc: OBJC_HELP: describe Objective-C runtime environment variables
objc: OBJC_PRINT_OPTIONS: list which options are set
objc: OBJC_PRINT_IMAGES: log image and library names as the runtime loads
them

    NSObjCMessageLoggingEnabled 环境变量用于打印objc_msgSend调用日志
NSObjCMessageLoggingEnabled=Yes ./hello 
Hello World!
-[dcbz@megatron:~/code/HelloWorld/build]$ cat /tmp/msgSends-6686 
+ NSRecursiveLock NSObject initialize
+ NSRecursiveLock NSObject new
+ NSRecursiveLock NSObject alloc
....
+ Talker NSObject initialize
+ Talker NSObject alloc
+ Talker NSObject allocWithZone:
- Talker NSObject init
- Talker Talker say:
- Talker NSObject release
- Talker NSObject dealloc
    machoview   查看格式的gui工具 https://github.com/gdbinit/MachOView.git
    dtrace  跟踪mac上objective-c函数调用

  分析iOS二进制文件的过程:

  • 1.如果是app store下载的app,需要先用工具砸壳,将代码数据区内存解密
  • 2.从手机将文件拷贝到主机使用ida分析
  • 3.将砸壳生成的文件修改PIE标志并重新签名,替换原始app,方便动态分析

砸壳

  由于class-dump等工具的流行,App Store上发布的软件都经过加密处理(LC_ENCRYPTION_INFO所标志的区域),加载器dyld对可执行文件校验,根据fat头选择合适的架构,处理所有的command,使用posix_spawn函数启动进程。ios上所有第三方代码都需要使用developer id代码签名,而代码签名作为数据存储在mach-o格式command结构中,因此fat格式中得每个架构的文件都分别签名,并由内核验证,如果验证失败则会收到停止信号而退出。在越狱机上可以通过ldid进行伪签名通过签名校验。进行了加密后,无法直接用ida查看内部结构

  • dumpencrypted
      https://github.com/stefanesser/dumpdecrypted/blob/master/dumpdecrypted.c,该工具注入目标进程内存,利用解密后的内存转储数据得到脱壳文件,时机在dyld加载后,init(__mod_init_func)节加载前

  • clutch
      https://github.com/KJCracks/Clutch/releases,命令行工具。该工具使用posix_spawn函数以暂停态(POSIX_SPAWN_START_SUSPENDED)和ASLR关闭模式创建目标程序子进程,从而使目标进程不执行任何代码而得到系统解密的内存,后使用task_for_pid从mach port得到目标进程内存,最后更新头部的LC_ENCRYPTION_COMMAND,合并成文件。

mach-o格式分析

  相关数据结构定义在/Developer/SDKs/iPhoneOS.sdk/usr/include/mach-o/loader.h,总体结构包括:header结构、command表、数据区。header结构:用于指明cpu类型(x86?arm?...),文件类型(动态库?可执行文件?...),command表位置;如果文件中包含多个cpu的可执行文件,则会存在FAT header头指明每个cpu的文件位置,因此一个mach-o文件的开头可能是mach_header结构,此时文件只包含一种cpu架构的可执行文件,也可能是fat_header,存储不同mach_header的偏移

struct mach_header {
    uint32_t    magic;      /* mach magic number identifier */
    cpu_type_t  cputype;    /* cpu specifier */
    cpu_subtype_t   cpusubtype; /* machine specifier */
    uint32_t    filetype;   //静态库.a  目标文件.o  动态库.dylib   可执行文件  ………….
    uint32_t    ncmds;      /* number of load commands */
    uint32_t    sizeofcmds; /* the size of all the load commands */
    uint32_t    flags;      /* flags */
};

  command表相当于pe的节表,描述文件和内存进行映射的表,包括__PAGEZERO(标记可执行文件的第一个节)、__TEXT、__DATA、__OBJC(objective-c运行库表用于描述类信息)、__IMPORT、__LINKEDIT(符号、字符串、重定位表),常用的command如下:

LC_SEGMENT/LC_SEGMENT_64        描述文件中得节和内存映射关系
struct segment_command { /* for 32-bit architectures */
    uint32_t    cmd;        /* LC_SEGMENT */
    uint32_t    cmdsize;    /* includes sizeof section structs */
    char        segname[16];    /* segment name */
    uint32_t    vmaddr;     /* memory address of this segment */
    uint32_t    vmsize;     /* memory size of this segment */
    uint32_t    fileoff;    /* file offset of this segment */
    uint32_t    filesize;   /* amount to map from the file */
    vm_prot_t   maxprot;    /* maximum VM protection */
    vm_prot_t   initprot;   /* initial VM protection */
    uint32_t    nsects;     /* number of sections in segment */
    uint32_t    flags;      /* flags */
};
LC_LOAD_DYLIB               要加载的动态库
struct dylib {
    union lc_str  name;         /* library's path name */
    uint32_t timestamp;         /* library's build time stamp */
    uint32_t current_version;       /* library's current version number */
    uint32_t compatibility_version; /* library's compatibility vers number*/
};
struct dylib_command {
    uint32_t    cmd;        /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,
                       LC_REEXPORT_DYLIB */
    uint32_t    cmdsize;    /* includes pathname string */
    struct dylib    dylib;      /* the library identification */
};
LC_MAIN                 描述入口点
struct entry_point_command {
    uint32_t  cmd;  /* LC_MAIN only used in MH_EXECUTE filetypes */
    uint32_t  cmdsize;  /* 24 */
    uint64_t  entryoff; /* file (__TEXT) offset of main() */
    uint64_t  stacksize;/* if not zero, initial stack size */
};
    LC_LOAD_DYLINKER        描述mach-o可执行文件加载器
struct dylinker_command {
    uint32_t    cmd;        /* LC_ID_DYLINKER, LC_LOAD_DYLINKER or
                       LC_DYLD_ENVIRONMENT */
    uint32_t    cmdsize;    /* includes pathname string */
    union lc_str    name;       /* dynamic linker's path name */
};
    LC_CODE_SIGNATURE       用codesign和ldid(plist)签名生成的结构,用于突破沙盒等权限限制
struct linkedit_data_command {
    uint32_t    cmd;        /* LC_CODE_SIGNATURE, LC_SEGMENT_SPLIT_INFO,
                                   LC_FUNCTION_STARTS, LC_DATA_IN_CODE,
                   LC_DYLIB_CODE_SIGN_DRS or
                   LC_LINKER_OPTIMIZATION_HINT. */
    uint32_t    cmdsize;    /* sizeof(struct linkedit_data_command) */
    uint32_t    dataoff;    /* file offset of data in __LINKEDIT segment */
    uint32_t    datasize;   /* file size of data in __LINKEDIT segment  */
};
    LC_ENCRYPTION_INFO/LC_ENCRYPTION_INFO_64        用于appstore加密程序
struct encryption_info_command {
   uint32_t cmd;        /* LC_ENCRYPTION_INFO */
   uint32_t cmdsize;    /* sizeof(struct encryption_info_command) */
   uint32_t cryptoff;   /* file offset of encrypted range */
   uint32_t cryptsize;  /* file size of encrypted range */
   uint32_t cryptid;    /* which enryption system,
                   0 means not-encrypted yet */
};
    LC_SYMTAB               符号表
    LC_UUID                 文件唯一标识

App目录和文件

  用户App位置/var/mobile/Applications/[GUID]/

  • AppName.app 目录存放app静态数据和代码
  • Documents目录存放持久化数据,和iTunes同步;包括sql数据库
  • Library目录存放配置文件、缓存和cookie
  • tmp目录存放临时文件

Objective C Reversing

  研究方式:命令行编译+二进制对比+调试

Debug:      clang/gcc –g -fobjc-arc -framework Foundation FKPerson.m main.m
Release:    clang/gcc –O3 -fobjc-arc -framework Foundation FKPerson.m main.m
交叉编译arm: clang/gcc -x objective-c -arch armv7 -g -fobjc-arc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS9.3.sdk -framework Foundation main.m    (arch=i386 x86_64 armv7 arm64)
objective-c编译为c++源码:clang/gcc –rewrite-objc –framework Foundation main.m
  • meta-class 每个类都存在元类,
  • super-class 父类
  • root-class 根类
  • selector 选择器(存储为字符串,其内存位置与方法一一对应)
  • imp 普通函数指针
  • id 通用数据类型

杂项

字符串存储:
@”” => 实际编译为CFString结构
Class CFString : objc_object
{
    longlong info;
    char* data;//真正的字符串存储位置
    longlong length;//字符串长度
}

synchronized锁:
@synchronized(expression1){
    expression2;
}
实际编译生成为:
id lock = expression1
objc_sync_enter(lock)
expression2;
objc_sync_exit(lock)

选择器@selector:
@selector(x) => 实际编译为”x”

关键字@encode:
@encode(type) => 实际编译为 该类型的描述符

关键字@autorelease:
@autorelease{expression;} => 实际编译为
objc_autoreleasePoolPush(…)
expression;
objc_autoreleasePoolPop

快速枚举for:
for(type a in b) {expression;}=> 实际编译为
for(int i=0;i<b.selRef_countByEnumeratingWithState_objects_count_;i++){
    expression;
}

nil值:=> (void*)0

Function

  传参所用寄存器,适用于普通函数和成员函数(id,sel)

arm架构:
a1  R0
a2  R1
a3  R2
a4  R3
a5  [sp+0]
a6  [sp+4]
….  arm64架构:
a1  W0
a2  W1
a3  W2
a4  W3
a5  W4
a6  W5
a7  W6
a8  [sp+0]
a9  [sp+8]
a10 [sp+16]
a11 [sp+24]
…….
    X86架构(默认调用约定):
a1  [esp+0]
a2  [esp+4]
a3  [esp+8]
a4  [esp+8]
a5  [esp+12]
a6  [esp+16]
…….
    x86_64架构:
a1  rdi
a2  rsi
a3  rdx
a4  rcx
a5  r8
a6  r9
a7  [rsp+0]
a8  [rsp+8]
……

Block

用于定义匿名函数,等价于lambda表达式,形式如下:
^ [返回值类型] (类型1 形参1, 类型2 形参2, ...)
{
}
定义Block变量形式如下:
返回值类型 (^块变量名) (类型1, 类型2, ...);
   int (^hypot)(int, int) = ^(int num1, int num2)
        {
                 returnnum1 * num1 + num2 * num2;
        };
NSLog(@"%d",hypot(3,4));
编译得到:
  v3= ((int (__fastcall *)(_QWORD, _QWORD, _QWORD))*(&__block_literal_global8 +2))(&__block_literal_global8, 3LL, 4LL);
 NSLog(&cfstr_D, (unsigned int)v3);
        其中__block_literal_global8将函数等相关信息封装成类(这点和vs-win一致),___main_block_invoke_2正是函数体实现:
__const:0000000100001060 ___block_descriptor_tmp7dq 0           ; DATA XREF:__const:0000000100001098o
__const:0000000100001068                 dq 20h
__const:0000000100001070                 dq offset aI16@?0i8i12  ; "i16@?0i8i12"
__const:0000000100001078                 align 20h
__const:0000000100001080___block_literal_global8 dq offset __NSConcreteGlobalBlock
__const:0000000100001080                                         ; DATAXREF: _main+87o
__const:0000000100001088                 dq 50000000h
__const:0000000100001090                 dq offset ___main_block_invoke_2
__const:0000000100001098                 dq offset___block_descriptor_tmp7
 
从源码Block_private.h可以得到构造的Block结构体
struct Block_layout 
{
   void *isa;
   volatile int32_t flags; // contains ref count
   int32_t reserved; 
   void (*invoke)(void *, ...);//实际调用的函数
   struct Block_descriptor_1 *descriptor;
   // imported variables
};
struct Block_descriptor_1
{
   uintptr_t reserved;
uintptr_t size;
};
struct Block_descriptor_2 
{
   void (*copy)(void *dst, const void *src);
   void (*dispose)(const void *);
};
struct Block_descriptor_3 
{
   const char *signature;
   const char *layout;     //contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
 
从内部实现看,Block代码能生成3种类型:
NSGlobalBlock         代码中未操作外部变量或操作全局变量(如上例)
NSStackBlock          代码中操作外部栈变量
NSMallocBlock        代码中操作外部堆变量
下面分别讨论
第一种情况:
代码为最开始的例子,可见其中没有用到外部变量
实际产生的代码为:
int ___main_block_invoke(Block_layout this,int num1, int num2)
{
        returnnum1 * num1 + num2 * num2;
}
Block_layout __block_literal_global =
{
        __NSConcreteGlobalBlock,
        0x50000000,
        0,
        &___main_block_invoke,
        &___block_descriptor_tmp,
}
__block_literal_global. ___main_block_invoke(&__block_literal_global,3, 4);
 
第二种情况:
代码如下
        __blockint my = argc;
        int(^hypot)(int, int) = ^(int num1, int num2)
        {
                 my+= 1;
                 returnnum1 * num1 + num2 * num2;
        };
        NSLog(@"%d%d", hypot(3, 4), my);
实际产生的代码为:
block_descriptor ___block_descriptor_tmp = 
{
        0,
        28,
        ___copy_helper_block_,
        ___destroy_helper_block_,
        "i16@?0i8i12",
        16
};
int ___main_block_invoke(Block_layout this,int num1, int num2)
{
        this->___stack_variable->my+= 1;
        returnnum1 * num1 + num2 * num2;
}
Block_layout __block_literal_global =
{
        __NSConcreteStackBlock,
        0xC2000000,
        0,
        &___main_block_invoke,
        &___block_descriptor_tmp,
        &___stack_variable//存放所有栈变量
};
void __copy_helper_block_()
{
        _Block_object_assign(my,argc)
}
void __destroy_helper_block_()
{
        _Block_object_dispose(my,argc)
}
 
__block_literal_global.___block_descriptor_tmp.___copy_helper_block_();
__block_literal_global. ___main_block_invoke(&__block_literal_global,3, 4);
..................
__block_literal_global.___block_descriptor_tmp.___destroy_helper_block_();
 
第三种情况:
需要开启arc,暂无研究

Class

类型定义

Object描述通用对象,所有类继承自该类   struct objc_object{
    Class isa;      //描述类型
}
Class描述类,相当于模板,创建实例和使用静态方法时使用   struct objc2_class : objc2_object{//runtime/
    //Class isa;            //Class对象的Class即meta class
    Class superclass;       //父类Class
    cache_t cache;      //缓存调用过的成员函数
 objc2_class_rw* data;
}
class_rw动态类数据,内存中呈现形式   struct objc2_class_rw{//runtime/objc-runtime-new.h class_rw_t
    int flags;          //标志位
    int version;
    objc2_class_ro* ro;
    method_array_t methods;     //链表结构方便随时添加函数
    property_array_t properties
    protocol_array_t protocols;
    Class firstSubclass;
    Class nextSiblingClass;
    char* demangledName;
}
类属性flags
RW_REALIZING 0x80000            class has started realizing
RW_HAS_INSTANCE_SPECIFI
class_ro静态类数据,二进制文件中呈现形式,在初始化后设置REALIZE转化成新结构class_rw   struct objc2_class_ro{//runtime/objc-runtime-new.h class_ro_t
    int flags;          //标志位
    int instanceStart;  //在Instance中第一个ivar偏移
    int instanceSize;   //Instance大小
    int reserved;
    byte* ivar_layout;
    char* name;     //对应类名
    objc2_meth_list* base_meths;    //类拥有的成员方法(静态成员在metaclass中)
    objc2_prot_list* base_prots;        //类遵守的接口
    objc2_ivar_list* ivars;         //类拥有的成员变量
    byte* weak_ivar_layout;
    objc2_prop_list* base_props;        //使用@property定义的属性
}
类属性flags
RO_META 1                   meta-class
RO_ROOT 2                   root-class
RO_HAS_CXX_STRUCTORS 4      has .cxx_construct/destruct
RO_HAS_LOAD_METHOD 8        has +load
RO_HIDDEN 16                visibility=hidden
RO_EXCEPTION 32             has attribute(objc_exception)
RO_REUSE_ME 64              available for reassignment
RO_IS_ARR 128               class compiled with –fobjc-arc
RO_HAS_CXX_DTOR_ONLY 256    has .cxx_destruct but no .cxx_construct
RO_FROM_BUNDLE 0x20000000   class is in unloadable bundle
RO_FUTURE 0x40000000        class is unrealized future
RO_REALIZED 0x80000000      class is realized
Instance——实例结构,操作实例或类成员函数中使用    struct objc_instance : objc_object{
    //Class isa;
    Member1;    //成员变量1,2,3….
    Member2;
}
Method List——描述类结构中包含的成员函数  struct objc2_method_list{
    int entrySize;          //每个objc2_method结构大小
    int count;              //后接count个objc2_method
}
Method——描述单个成员函数    struct objc2_method {
    SEL method_name     //方法名       setName:andAge:
    char *method_types  //方法类型  v28@0:8@16i24
    IMP method_imp      //实际地址  ptr of setNameandAge
} 
成员函数指针定义:typedef id (*IMP)(id, SEL, ...);

函数修饰符method_types   runtime.h
‘b’-bitfield
‘B’-bool
‘c’-char
‘C’-uchar
‘d’-double
‘f’-float
‘i’-int
‘I’-uint
‘l’-long
‘L’-ulong
‘n’-in      for input
‘N’-inout       both for input and output
‘o’-out     for ouput
‘O’-bycopy  instead of using a proxy/NSDistantObject, pass or return a copy of the object
‘q’-longlong
‘Q’-ulonglong
‘r’-const       constant
‘R’-byref       use a proxy(default)
‘s’-short
‘S’-ushort
‘v’-void
‘V’-oneway  允许在不同线程和进程使用,不可阻塞调用线程直到返回
‘^’-pointers
‘@’-object
‘[‘-array begin
‘]’-array end
‘{‘-structure begin
‘}’-structure end
‘(‘-union begin
‘)’-union end
‘#’-class
‘:’-selector
‘*’-char pointer
‘%’-atom
‘!’-vector
‘?’-undefine
Structure: returntype—stacksize—[argumenttype—bitoffset]*
v28@0:8@16i24 -> void stacksize=28 (pointer, selector, pointer, int)
Ivar List   struct objc2_ivar_list{
    int entrySize;          //每个objc2_ivar结构大小
    int count;              //后接count个objc2_ivar
}
Ivar    struct objc2_ivar{
    int* offset;            //存储该变量相对Instance结构偏移
    char* name;         //变量名
    char* type;         //类型描述符
    int alignment_raw;      //对齐
    int size;               //变量占用空间
}
Protocol List——描述遵守的接口  struct objc2_protocol_list{
    longlong count;//后接count个Protocol
}
Protocol——描述单个接口    struct objc2_protocol : objc_object{
    //Class isa;
    char* mangledName;
    objc2_protocol_list* protocols
    objc2_method_list* instanceMethods;
    objc2_method_list* classMethods;
    objc2_method_list* optionalInstanceMethods;
    objc2_method_list* optionalClassMethods;
    objc2_method_list* instanceProperties;
    int size;
    int flag;
    char** extendedMethodTypes;
    char* _demangledName;
}
Property List——描述使用@property关键字定义的成员变量(和普通成员变量分开存放) struct objc2_prop_list{
    int entrySize;          //每个objc2_prop结构大小
    int count;              //后接count个objc2_prop
}
Property——描述单个@property成员变量 struct objc2_prop{
    char* name;
    char* attributes;// T@"NSString",&,V_a1
}
返回普通类型的静态成员函数调用 
[FKPerson foo]  objc_msgSend([FKPerson class], “foo”)
void _cdecl foo(FKPerson* self, SEL selector)
返回普通类型的普通成员函数调用 
[person say]    objc_msgSend(person, “say”)
void _cdecl say(FKPerson * self, SEL selector)
返回普通类型的多参数成员函数调用    
[person setName:@”1” andAge:500]    objc_msgSend(person, “setName:andAge:”, @”1”, 500)
void _cdecl setName:andAge:(FKPerson* self, SEL selector, NSString* name, int age)
成员函数中调用父类函数,父函数返回普通类型   
[super init]    objc_msgSendSuper(make_super super, “init”)
返回栈结构体的成员函数调用
[person func]   objc_msgSend_stret(person, “func”)
成员函数中调用父类函数,父函数返回栈结构体
[super func]    objc_msgSendSuper_stret(self, “func”)
返回栈浮点数  arm:不使用objc_msgSend_fpret
i386:float|double|long double使用objc_msgSend_fpret
x86-64:long double使用objc_msgSend_fpret

成员函数分析

  • 1.每增加一个成员函数,类模板会增加method,由于名称一一对应,同一个类不允许存在同名函数
  • 2.每个成员函数前两个参数分别是实例指针self和选择器SEL,之后才是用户为其定义的参数
  • 3.带(+)修饰的成员函数本质为静态成员,属于该类的meta-class类成员,因此位于meta-class函数表中,而普通成员函数位于该类的函数表中
  • 4.和c++不同的是,成员函数调用方式和普通函数相同,因此可以通过反射替换成普通函数

成员变量分析

  • 1.每增加一个成员变量,类模板Class会增加ivar,以后使用该类模板创建的实例的对象结构也会增加该元素
  • 2.只要有一个实际使用的成员变量,就会产生”类名.cxx_destruct”析构函数
  • 3.根据成员变量属性为weak/strong,在进行赋值操作时采用objc_storeWeak/objc_storeStrong,默认类型为strong
  • 4.对public成员变量的操作语法采用myclass->field形式,产生的逻辑也和c结构体相同,而更常规的方式是将成员变量写成@property中,这样编译器会自动为成员变量生成相应的getter和setter函数,使用kvc(键值编码)时会自动调用(msgsend)这些函数

meta-class存在的原因

  • 1.直接从类对象进行的操作,例如调用静态成员函数,并不属于某个实例,因此需要存在于类类型中
  • 2.当自身被子类化(setsuperclass)时,父类并不等同于所属类(isa != superclass),同理构造一个类要提供其isa

Objc_msgSend调用流程

[图片上传失败...(image-6df688-1516663329548)]

  • 1.根据对象的isa找到类,在类的dispatch table中查找selector
  • 2.如果未找到则找到该类的父类,并在父类的dispatch table中查找selector,直到NSObject类(该过程中优先查找cache)
  • 3.如果所有子类和父类都无法找到该函数,则进行msgForward,如果用户添加了动态实现(resolveInstanceMethod)则调用
  • 4.如果上一步失败,则尝试找到一个能响应该消息的对象(forwardingTargetForSelector),如果能找到则转发给他
  • 5.如果上一步失败,则尝试获取一个方法签名(methodSignatureForSelector),如果获取不到直接抛异常
  • 6.调用用户自己实现的forwardInvocation

Runtime Ability

Object  id object_copy(id obj, size_t size)             拷贝实例内容
Class object_getClass(id obj)               返回Instance对应的Class
Class object_setClass(id obj, Class cls)        绑定Instance的Class
BOOL object_isClass(id obj)             判断所属
char* object_getClassName(id obj)       获取类名
void* object_getIvar(id obj, Ivar ivar)     获取成员变量值(按Ivar)
void object_setIval(id obj, Ivarl ivar, id value)   设置成员变量值
Ivar object_getInstanceVariable(id obj, char* name, void* value)    获取成员变量值(按变量名)
Ivar object_setInstanceVariable(id obj, char* name, void* value)        设置成员变量值
Class   Class objc_getClass(char* name)         返回指定类名的(类型)对象
Class objc_getaMetaClass(char* name)        返回指定类名的meta-class对象
Class objc_lookUpClass(char* name)      返回指定类名且已注册的的(类型)对象
int objc_getClassList(Class* buffer, int bufferCount)   返回所有已注册类
Class* objc_copyClassList(int* outCount)    返回所有已注册类
char* class_getName(Class cls)          获取类名
BOOL class_isMetaClass(Class cls)           是否meta-class
Class class_getSuperClass(Class cls)        获取父类
Class class_setSuperClass(Class cls, Class newSuper)设置父类
int class_getVersion(Class cls)             获取版本
void class_setVersion(Class cls, int version)   设置版本
size_t class_getInstanceSize(Class cls)     获取实例大小
Ivar class_getInstanceVariable(Class cls, char*name)    获取实例Ivar
Ivar* class_copyIvarList(Class cls, int* outCount)
Method class_getInstanceMethod(Class cls, SEL name) 获取非静态方法
Method class_getClassMethod(Class cls, SEL name)        获取静态方法
IMP class_getMethodImplementation(Class cls, SEL name)  获取方法实现
BOOL class_conformsToProtocol(Class cls, Protocol* protocol)是否遵守协议
Method* class_copyMethodList(Class cls, int* outCount)
Protocol* class_copyProtocolList(Class cls, int* outCount)
objc_property_t class_getProperty(Class cls, char* name)
objc_property_t class_copyPropertyList(Class cls, int* outCount)
uchar* class_getIvarLayout(Class cls)
BOOL class_addMethod(Class cls, SEL name, IMP imp, char* types) 增加函数(绑定普通函数)
BOOL class_replaceMethod(Class cls, SEL name, IMP imp, char* types)替换函数
BOOL class_addIvar(Class cls, char* name, size_t size, uchar alignment, char* types)添加变量
BOOL class_addProtocol(Class cls, Protocol* protocol)           增加协议
BOOL class_addProperty(Class cls, char* name, objc_property_attribute_t* attrib,int count)
BOOL class_replaceProperty(Class cls, char* name, objc_property_attribute_t* attrib,int count)
void class_setIvarLayout(Class cls, uchar layout)
id class_createInstance(Class cls, size_t extrabytes)   创建实例
id objc_constructInstance(Class cls, void* bytes)       创建实例
void* objc_destructInstance(id obj)             
Class objc_allocateClassPair(Class superclass, char* name, size_t extrabytes)   创建类和元类
void objc_registerClassPair(Class cls)              注册类
Class objc_duplicateClass(Class original, char* name, size_t extraBytes)    复制类
Method  SEL method_getName(Method m)                    获取函数名
int method_getNumberOfArguments
char* method_getTypeEncoding                    获取函数类型字段
void method_getArgumentType                 获取参数类型
void method_getReturnType                       获取返回值类型
IMP method_getImplementation
IMP method_setImplementation(Method m, IMP imp)设置函数实现
void method_exchangeImplementations(Method m1, Method m2)
Ivar    char* ivar_getName(Ivar v)                      获取Ivar名
char* ivar_getTypeEncodeing(Ivar v)             获取Ivar类型字段
ptrdiff_t ivar_getOffset(Ivar v)                    获取该ivar在instance中得偏移
Attribute   ……
Protocol    objc_copyProtocolList
protocol_getName
protocol_copyProtocolList
protocol_allocateProtocol
protocol_registerProtocol
protocol_addProtocol
protocol_addProperty
其他  char** objc_copyImageNames(int* outcount)   获取加载的动态库
char* class_getImageName(Class cls)         获取某类所属动态库
char** objc_copyClassNamesForImage(char* image, int* outCount)获取动态库中所有类
objc_loadWeak               获取weak值
objc_storeWeak              设置weak值
objc_setAssociatedObject        设置关联
objc_getAssociatedObject        获取关联
objc_removeAssociatedObjects    移除所有关联,恢复对象到原始状态

[图片上传失败...(image-a1f9cd-1516663329549)]
[图片上传失败...(image-cd3ede-1516663329549)]
[图片上传失败...(image-5133fe-1516663329549)]
[图片上传失败...(image-e6184-1516663329549)]
[图片上传失败...(image-ca05ba-1516663329549)]

其他

arc类型转换:
普通指针和objc指针转换:(实现调试器中任意内存当作类操作)
id obj1 = [[class1 alloc] init];
void* p = (__bridge void*)obj1;
id obj2 = (__bridge id)p;

@property:
@property(?,?,…)用于快速生成类成员及getter setter,其修饰符如下:
atomic      原子操作,线程安全(默认) 
    objc_getProperty objc_setProperty_atomic
nonatomic   非线程安全                                                     ‘N’
readwrite       具有setter getter(默认)     
readonly        具有getter                                                     ‘R’
assign      简单赋值(默认)                             
copy            setter方法中深度复制传入对象                                   ‘C’
    objc_getProperty objc_setProperty_atomic_copy
retain      setter方法中对传入对象引用计数加一                             ‘&’
strong      强引用(默认),和retain相似                                     ‘&’
    初始化/赋值=    销毁objc_storeStrong
weak        对象消失后指针置nil                                           ‘W’
初始化objc_initWeak  赋值objc_loadWeakRetained  销毁objc_destroyWeak/objc_autoreleaseReturnValue
__unsafe_unretain   对象引用计数不加一,对象释放后不置nil
autorelease 对象加入自动释放池       对应objc_autorelease

异常处理

objc提供异常处理机制
@try{
    expr1;
}
@catch(NSException* ex){
    expr2;
}
@finally{
    expr3;
}
产生的流程如下:
    ......
    flag = 0
    expr1
label1:
    expr3
    ...
    if(flag & 1)
        objc_exception_rethrow()
    return
tail:
    if(..)
    {
        expr2;  
    }
    goto label1;

@throw语句层产生:objc_exception_throw()

Reflection

  Objective-C是一种反射型语言,可以在运行时获取和修改自身状态,其中的实现存在于libobjc.A.dylib库中,这些“运行时”能力源于objective-c类结构组织较为灵活,并提供了操作自身结构的接口,同时在生成的可执行文件(mach-o)中存在_OBJC节,这些节中提供了足够的类构成信息,而Mac端gdb可以解析这些结构,而正由于objc提供了如此多的信息,因此也比c++在同等情况下逆向难度低一些。

LC_SEGMENT.__OBJC.__cat_cls_meth 
    LC_SEGMENT.__OBJC.__cat_inst_meth 
    LC_SEGMENT.__OBJC.__string_object 
    LC_SEGMENT.__OBJC.__cstring_object 
    LC_SEGMENT.__OBJC.__message_refs 
    LC_SEGMENT.__OBJC.__sel_fixup 
    LC_SEGMENT.__OBJC.__cls_refs 
    LC_SEGMENT.__OBJC.__class 
    LC_SEGMENT.__OBJC.__meta_class
    LC_SEGMENT.__OBJC.__cls_meth 
    LC_SEGMENT.__OBJC.__inst_meth
    LC_SEGMENT.__OBJC.__protocol
    LC_SEGMENT.__OBJC.__category 
    LC_SEGMENT.__OBJC.__class_vars 
    LC_SEGMENT.__OBJC.__instance_vars 
    LC_SEGMENT.__OBJC.__module_info 
    LC_SEGMENT.__OBJC.__symbols

java与objc反射对比

objc java
获取类 NSClassFromString myClass.class [myClass class] Class.forName myClass.class
检查继承 isKindOfClass isMemberOfClass conformsToProtocol class.isAssignableFrom instanceOf class.isInstance
获取函数 @selector NSSelectorFromString getMethod
调用函数 perfromSelector objc_msgSend invoke

iOS Attack&Defense

AntiDebug - AntiAntiDebug

  • sysctl P_TRACED标志 检测调试
      可以检测调试器和跟踪器,但是不能检测注入和cycript:
#include <sys/types.h>
#include <sys/sysctl.h>
static int check_debugger( ) __attribute__((always_inline));
int check_debugger( )
{
    size_t size = sizeof(struct kinfo_proc);
    struct kinfo_proc info;
    int ret,name[4];
    memset(&info, 0, sizeof(struct kinfo_proc));
    name[0] = CTL_KERN;
    name[1] = KERN_PROC;
    name[2] = KERN_PROC_PID;
    name[3] = getpid();
    if((ret = (sysctl(name, 4, &info, &size, NULL, 0)))){
        return ret;  //sysctl() failed for some reason
    }
    return (info.kp_proc.p_flag & P_TRACED) ? 1 : 0;
}
  • ptrace PT_DENY_ATTACH 防止调试
      可以阻止调试器附加:
#import <dlfcn.h>
#import <sys/types.h>
typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);
#if !defined(PT_DENY_ATTACH)
#define PT_DENY_ATTACH 31
#endif  // !defined(PT_DENY_ATTACH)
void disable_gdb() {
  void* handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);
  ptrace_ptr_t ptrace_ptr = dlsym(handle, "ptrace");
  ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
  dlclose(handle);
}
#ifdef __arm__
asm volatile(
    “mov r0,#31\n”
    “mov r1,#0\n”
    “mov r2,#0\n”
    “mov r12,#26\n”
    “svc #80\n”
#endif
#ifdef __arm64__
asm volatile(
    “mov x0,#26\n”
    “mov x1,#31\n”
    “mov x2,#0\n”
    “mov x3,#0\n”
    “mov x16,#0\n”
    “svc #128\n”
#endif

  直接附加调试器会产生segmentation fault:11 启动调试程序会在ptrace执行后退出
  调试已经被调试的进程:直接失败产生日志:(os/kern) invalid task Exiting

  • 反反调试:hook相应函数
#import <substrate.h>
#if !defined(PT_DENY_ATTACH)
#define PT_DENY_ATTACH 31
#endif
static int (*_ptraceHook)(int request, pid_t pid, caddr_t addr, int data); 
static int $ptraceHook(int request, pid_t pid, caddr_t addr, int data) {
        if (request == PT_DENY_ATTACH) { 
        request = -1; 
        }
        return _ptraceHook(request,pid,addr,data);  
}
%ctor {
        MSHookFunction((void *)MSFindSymbol(NULL,"_ptrace"), (void *)$ptraceHook, (void **)&_ptraceHook);
}
  • isatty检测调试
      isatty函数在给定文件描述符被附加到调试器控制台时返回1,否则返回0
if(isatty(1)){
    NSLog(@”Being Debugged isatty”);
}
else{
    NSLog(@”isatty() bypassed”);
}
  • task_get_exception_ports检测调试
      调试器通常会监听异常端口,因此可以用task_get_exception_ports循环遍历以校验该端口是否设置
struct ios_execp_info{
    exception_mask_t masks[EXC_TYPES_COUNT];
    mach_port_ports[EXC_TYPES_COUNT];
    exception_behavior_t behaviors[EXC_TYPES_COUNT];
    thread_state_flavor_t flavors[EXC_TYPES_COUNT];
    mach_msg_type_number_t count;
}
struct ios_execp_info* info = malloc(sizeof(struct ios_execp_info));
kern_return_t kr = task_get_exception_ports(mach_task_self(),EXC_MASK_ALL,info->masks,&info->count,info->ports
,info->behaviors,info->flavors);
for(int i=0;i<info->count;i++){
    if(info->ports[i] != 0 || info->flavors[i] == THREAD_STATE_NONE){
        NSLog(@”Beging debugged”);
    }
else{
    NSLog(@“bypassed”);
}
}
  • RESTRICT节——防注入
      加载器dyld(ios7.0以后)源码中关于DYLD
    环境变量的逻辑pruneEnvironmentVariables
switch (sRestrictedReason) {
case restrictedNot:
break;
case restrictedBySetGUid:
dyld::log("main executable (%s) is setuid or setgid\n", sExecPath);
break;
case restrictedBySegment:
dyld::log("main executable (%s) has __RESTRICT/__restrict section\n", sExecPath);
break;
case restrictedByEntitlements:
dyld::log("main executable (%s) is code signed with entitlements\n", sExecPath);
break;
}

3种情况下DYLD环境变量会被忽视

  • 1.可执行文件设置了setuid setgid位
  • 2.可执行文件有__restrict节
  • 3.可执行文件有特殊代码签名

  由于受app store的限制,1和3都不能实现,而2可以设置Other linker flags为-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null
使用该方法可以禁止dylib的注入,在生成的mach-o文件中会多出一个__RESTRICT节
这种方式是可以防止启动注入和运行时注入的,尝试用dumpdecrypted脱壳时会产生类似如下的日志:

dyld: warning, LC_RPATH @executable_path/Frameworks in /var/mobile/Applications/[id]/?.app/* being ignored in restricted program because of @executable_path

  尝试用cycript附加会产生如下输入:

dlopen(/usr/lib/libcript.lib, 5): Library not loaded: /System/Library/PrivateFrameworks/JavaScriptCore.framework/JavaScriptCore
referenced from: /usr/lib/libcript.dylib
reason: image not found
*** _assert(status == 0):../Inject.cpp(143):InjectLibrary

  而使用lldb则可以正常附加调试

  • anti-anti-debug
      改restrict节名,重签名(ldid –S)即可

JailBreak Detect – Anti JailBreak Detect

沙盒完整性检测

  iOS设备上,用户app安装在/var/mobile/Application中受沙盒限制,而系统app安装在/Application中不受沙盒限制。越狱设备上很多第三方app也安装在/Application下从而不受沙盒限制而拥有更多权限。一些越狱工具会移除沙盒限制以允许特定行为(如fork vfork popen)

int result = fork();
if(!result)
    exit(0);
if(result >= 0)//jail broken
    {sandbox_is_compromised = 1};
    监测点2:在沙盒中,执行opendir(“/dev”)会返回NULL
    监测点3:system()  getgid()  ??

文件系统检测

  检测常见的越狱工具目录和文件是否存在

struct stat s;
int is_jailbroken = stat(“/Applications/Cydia.app”, &s) == 0;
常见的目录和文件
/Applications/MxTube.app
/Applications/blackra1n.app
/Applications/RockApp.app
/Applications/WinterBoard.app
/Applications/SBSettings.app
/Library/LaunchDaemons/com.openssh/sshd.plist
/Applications/IntelliScreen.app
/Library/MobileSubstrate/DynamicLibraries/Veency.plist
/Applications/FakeCarrier.app
/private/var/mobile/Library/SBSettings/Themes
/System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist
/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist
/System/Library/LaunchDaemons/com.ikey.bbot.plist
/Applications/Icy.app
/Applications/Loader.app
/private/var/tmp/cydia.log

/Library/MobileSubstrate/MobileSubstrate.dylib
/private/var/stash
/private/var/lib/apt
/private/var/lib/cydia
/usr/libexec/cydia
/usr/libeec/sftp-server
/var/cache/apt
/var/lib/apt
/var/lib/cydia
/var/log/syslog
/var/tmp/cydia.log
/var/tmp/cydia.log
/bin/bash
/bin/sh
/usr/sbin/sshd
/bin/mv
/usr/libexec/ssh-keysign
/etc/ssh/sshd_config
/etc/apt

检测装载点

检测fstab

  越狱工具会替换/etc/fstab文件导致变小,IOS5上正常为80字节

struct stat s;
stat(“/etc/fstab”, &s);
return s.st_size;

statfs函数检测

  在非越狱机上statfs(“/”)应该返回如下标志:buf->f_flags = MNT_RDONLY + MNT_ROOTFS + MNT_DOVOLFS + MNT_JOURNALED + MNT_MULTILABEL,同时statfs(“/var/mobile/Container/Data/Application/<APP_GUID>”)应该返回如下标志:buf->f_flags = MNT_NOSUID + MNT_NODEV + MNT_DOVOLFS + MNT_JOURNALED + MNT_MULTILABEL

检测软链接

  检测/Appliations软链接,越狱工具会将其替换到/var/stash/…下

struct stat s;
if(lstat(“/Applications”, &s) != 0)[
    if(s.st_mode & S_IFLNK)
        exit(-1);
}

其他软链接

/Library/Ringtones
/Library/WallPaper
/usr/arm-apple-darwin9
/usr/include
/usr/libexec
/usr/share

/var/stash/Library/Ringtones
/var/stash/usr/include
/var/stash/Library/WallPaper
/var/stash/usr/libexec
/var/stash/usr/share
/var/stash/usr/arm-apple-darwin9

URL Scheme检测

  在越狱机上Cydia会创建一个cydia://的URL scheme,因此如果调用该Scheme返回成功则机器越狱
  [NSURL URLWithString @”cydia://package/com.example.package”]

系统内核环境变量检测

  越狱时会增加2个内核环境变量用于绕过iOS代码签名机制,sysctlbyname函数用于检测系统信息,在非越狱机上,下面值应该为1

sysctlbyname(security.mac.proc_enforce)
sysctlbyname(security.mac.vnode_enforce

检测DYLD_INSERT_LIBRARIES

  检测DYLD_INSERT_LIBRARIES是否存在,越狱环境下会出现”/Library/MobileSubstrate/MobileSubstrate.dylib”,getenv(“DYLD_INSERT_LIBRARIES”) ,检测返回NULL和”\0”

运行进程检测

@try{
    NSArray* processes = [self runningProcesses]
    for(NSDictionary* dict in processes){
        NSString* process = [dict objectForKey:@”ProcessName”];
        if([process isEqualToString:@”MobileCydia”]){
            return true;
        }
        else if([process isEqualToString:”Cydia”]){
            return true
}
}
}
@catch(NSException* exception){
return 0
}

+ (NSArray*)runningProcesses{
    int mib[4] = {CTL_KERN,KERN_PROC,KERN_PROC_ALL,0};
    size_t miblen =4;
    size_t size;
    int st = sysctl(mib,miblen,NULL,&size,NULL,0);
    struct kinfo_proc* process = NULL;
    struct kinfo_proc* newprocess = NULL;
    do{
        size += size/10
        newprocess = realloc(process,size);
        if(!newprocess){
            if(process){
                free(process);
            }
            return nil;
        }
        int st = sysctl(mib,miblen,NULL,&size,NULL,0);
        st = sysctl
    }while(st == -1 && errno == ENOMEM);
}
if(st == 0){
    if(size % sizeof(struct kinfo_proc) == 0){
        int nprocess = size/sizeof(struct kinfo_proc);
        if(nprocess){
            NSMutableArray* array = [[NSMutableArray alloc] init];
            for(int I = nprocess – 1;I >= 0;i--){
                NSString* processID = [[NSString alloc] initWithFormat:@”%d”,process[i].kp_proc.p_pid];
                NSString* processName = [[NSString alloc] initWithFormat:@”%d”,process[i].kp_proc.p_comm];
                NSString* processPriority = [[NSString alloc] initWithFormat:@”%d”,process[i].kp_proc.p_priority];
                NSDate* processStartDate = [NSDate dateWithTimeInternvalSince1970:process[i].kp_proc.p_un.__p_starttime.tv_sec];
                NSDictionary* dict = [[NSDictionary alloc] initWithObjects:[NSArray arrayWithObjects:processID, processPriority, processName, processStartDate, nil] forKeys:[NSarray arrayWithObject:@”ProcessID”, @”ProcessPriority”, @”ProcessName”, @”ProcessStartDate”, nil]];
                [array addObject:dict];
            }
            free(process);
            return array;
        }
    }
    return nil;
}

反检测方式:hook

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容