13. Hook原理介紹

13.1 Objective-C消息傳遞(Messaging)

對(duì)于C/C++這類靜態(tài)語(yǔ)言刹泄,調(diào)用一個(gè)方法其實(shí)就是跳到內(nèi)存中的某一點(diǎn)并開(kāi)始執(zhí)行一段代碼算色。沒(méi)有任何動(dòng)態(tài)的特性,因?yàn)檫@在編譯時(shí)就決定好了遭铺。

而在 Objective-C 中巷屿,[object foo] 語(yǔ)法并不會(huì)立即執(zhí)行 foo 這個(gè)方法的代碼。它是在運(yùn)行時(shí)給 object 發(fā)送一條叫 foo 的消息滤蝠。這個(gè)消息豌熄,也許會(huì)由 object 來(lái)處理,也許會(huì)被轉(zhuǎn)發(fā)給另一個(gè)對(duì)象物咳,或者不予理睬假裝沒(méi)收到這個(gè)消息锣险。多條不同的消息也可以對(duì)應(yīng)同一個(gè)方法實(shí)現(xiàn)。這些都是在程序運(yùn)行的時(shí)候動(dòng)態(tài)決定的览闰。

事實(shí)上芯肤,在編譯時(shí)你寫(xiě)的 Objective-C 函數(shù)調(diào)用的語(yǔ)法都會(huì)被翻譯成一個(gè) C 的函數(shù)調(diào)用 objc_msgSend() 。比如压鉴,下面兩行代碼就是等價(jià)的:

[people TailName:@"Luz" Age:18];

objc_msgSend(people, @selector(TailName:Age:), "Luz", 18);

在 Objective-C 中崖咨,類、對(duì)象和方法都是一個(gè)C的結(jié)構(gòu)體油吭,從 objc/objc.h 和 objc/runtime.h 頭文件中击蹲,我們可以找到他們的定義:

  typedef struct objc_class *Class;
  struct objc_object {
      Class isa  OBJC_ISA_AVAILABILITY;
  };
  typedef struct objc_object *id;
  
//Class 是一個(gè) objc_class 結(jié)構(gòu)類型的指針, id是一個(gè) objc_object 結(jié)構(gòu)類型的指針.

struct objc_class {
  Class isa  OBJC_ISA_AVAILABILITY;

  #if !__OBJC2__
      Class super_class                                        
      const char *name                                         
      long version                                             
      long info                                                
      long instance_size                                       
      struct objc_ivar_list *ivars                             
      struct objc_method_list **methodLists                    
      struct objc_cache *cache                                 
      struct objc_protocol_list *protocols                     
  #endif
} OBJC2_UNAVAILABLE;
  • isa
    是一個(gè) objective-c Class 類型的指針. 實(shí)例對(duì)象有個(gè)isa的屬性,指向Class, 而Class里也有個(gè)isa的屬性, 指向meteClass. 這里就有個(gè)點(diǎn), 在Objective-C中任何的類定義都是對(duì)象.

  • super_class
    指向該類的父類, 如果該類已經(jīng)是最頂層的根類(如 NSObject 或 NSProxy),那么 super_class 就為 NULL.

objective-c.png
  • name
    類的名字

  • version
    類的版本信息,默認(rèn)為0

  • info
    供運(yùn)行期使用的一些位標(biāo)識(shí)。

  • instance_size
    該類的實(shí)例變量大小

  • ivars
    成員變量的鏈表

struct objc_ivar_list {
    int ivar_count                                          
    /* variable length structure */
    struct objc_ivar ivar_list[1]                           
}       
  • methodLists
    方法定義的鏈表
     struct objc_method_list {  
          struct objc_method_list *obsolete;
          int method_count;
    
          struct objc_method method_list[1];
     };
     
     struct objc_method {  
      SEL method_name;
      char *method_types;   
      IMP method_imp;
    

};


- objc_cache
指向最近使用的方法.用于方法調(diào)用的優(yōu)化  

struct objc_cache {
unsigned int mask /* total = mask + 1 */;
unsigned int occupied;
Method buckets[1];
};


- protocols
    協(xié)議的鏈表

struct objc_protocol_list {
struct objc_protocol_list *next;
long count;
Protocol *list[1];
};


objc_method_list 本質(zhì)是一個(gè)有 objc_method 元素的可變長(zhǎng)度的數(shù)組婉宰。一個(gè) objc_method 結(jié)構(gòu)體中:
- 函數(shù)名歌豺,也就是SEL
- 表示函數(shù)原型的字符串 (見(jiàn) Type Encoding) 
- 函數(shù)的實(shí)現(xiàn)IMP

#### 13.2 Method Swizzling示例
  以上面可知方法的名字(SEL)跟方法的實(shí)現(xiàn)(IMP,指向 C 函數(shù)的指針)一一對(duì)應(yīng)。Swizzle 一個(gè)方法其實(shí)就是在程序運(yùn)行時(shí)對(duì) objc_method_list 里做點(diǎn)改動(dòng),讓這個(gè)方法的名字(SEL)對(duì)應(yīng)到另個(gè)IMP还惠。 
  
   Method Swizzling(方法調(diào)配技術(shù)),僅針對(duì)Objective-C方法有效痕惋。Method Swizzling 利用 Runtime 特性把一個(gè)方法的實(shí)現(xiàn)與另一個(gè)方法的實(shí)現(xiàn)進(jìn)行替換。
  //涉及到的主要方法
  class_addMethod
  class_replaceMethod
  method_exchangeImplementations
- 為什么在load里面調(diào)用娃殖?
   一般情況下值戳,類別里的方法會(huì)重寫(xiě)掉主類里相同命名的方法。如果有兩個(gè)類別實(shí)現(xiàn)了相同命名的方法珊随,只有一個(gè)方法會(huì)被調(diào)用述寡。
但 +load是個(gè)特例,當(dāng)一個(gè)類被讀到內(nèi)存的時(shí)候叶洞, runtime 會(huì)給這個(gè)類及它的每一個(gè)類別都發(fā)送一個(gè) +load: 消息鲫凶。(多個(gè)類別時(shí)需要防止多次執(zhí)行)

- object_getClass(obj)與[obj class]的區(qū)別?
參考資料:http://www.reibang.com/p/ae5c32708bc6

13.3 Fishhook

fishhook蘋(píng)果系統(tǒng)下的一種C函數(shù)的hook方案衩辟,是facebook提供的一個(gè)動(dòng)態(tài)修改鏈接Mach-O符號(hào)表的開(kāi)源工具螟炫。Mach-O為Mach Object文件格式的縮寫(xiě),也是用于iOS可執(zhí)行文件,目標(biāo)代碼艺晴,動(dòng)態(tài)庫(kù)昼钻,內(nèi)核轉(zhuǎn)儲(chǔ)的文件格式掸屡。Mach-O有自己的dylib規(guī)范(/usr/include/mach-o/loader.h文件里面)。

官網(wǎng):https://github.com/facebook/fishhook

  • Hook示例
#import <Foundation/Foundation.h>
#import <dlfcn.h>
#import "fishhook.h"

static int (*orig_close)(int);
static int (*orig_open)(const char *, int, ...);

int my_close(int fd) {
    printf("Calling real close(%d)\n", fd);
    return orig_close(fd);
}

int my_open(const char *path, int oflag, ...) {
    va_list ap = {0};
    mode_t mode = 0;
    
    if ((oflag & O_CREAT) != 0) {
        // mode only applies to O_CREAT
        va_start(ap, oflag);
        mode = va_arg(ap, int);
        va_end(ap);
        printf("Calling real open('%s', %d, %d)\n", path, oflag, mode);
        return orig_open(path, oflag, mode);
    } else {
        printf("Calling real open('%s', %d)\n", path, oflag);
        return orig_open(path, oflag, mode);
    }
}


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        //NSLog(@"Hello, World!");
        struct rebinding rbd[2];
        rbd[0].name = "close";
        rbd[0].replacement = my_close;
        rbd[0].replaced = (void*)&orig_close;
        
        rbd[1].name = "open";
        rbd[1].replacement = my_open;
        rbd[1].replaced = (void*)&orig_open;
        
        rebind_symbols(rbd, 2);
        
        // Open our own binary and print out first 4 bytes (which is the same
        // for all Mach-O binaries on a given architecture)
        int fd = open(argv[0], O_RDONLY);
        uint32_t magic_number = 0;
        read(fd, &magic_number, 4);
        printf("Mach-O Magic Number: 0x%x \n", magic_number);
        close(fd);
    }
    return 0;
}

  • 使用fishHook監(jiān)聽(tīng)微信文件讀寫(xiě)
    Makefile 文件
THEOS_DEVICE_IP = 192.168.1.113
DEBUG = 1
ARCHS = armv7 arm64 
TARGET = iphone:latest:8.0  
include $(THEOS)/makefiles/common.mk

TWEAK_NAME = WeChatReProject
WeChatReProject_FILES = Tweak.xm fishhook.c
WeChatReProject_FRAMEWORKS = UIKit Foundation CoreLocation
WeChatReProject_CFLAGS = -fobjc-arc
include $(THEOS_MAKE_PATH)/tweak.mk

after-install::
    install.exec "killall -9 WeChat"

clean::
    rm -rf ./packages/* 

Tweak.xm文件

#import<UIKit/UIKit.h>
#import<CoreLocation/CoreLocation.h>
#import<CoreLocation/CLLocation.h>
#import "fishhook.h"

@interface SeePeopleNearByLogicController
- (void)onRetrieveLocationOK:(id)arg1;
@end

static int (*orig_close)(int);
static int (*orig_open)(const char *, int, ...);

int my_close(int fd) {
    printf("Calling real close(%d)\n", fd);
    return orig_close(fd);
}

int my_open(const char *path, int oflag, ...) {
    va_list ap = {0};
    mode_t mode = 0;
    
    if ((oflag & O_CREAT) != 0) {
        // mode only applies to O_CREAT
        va_start(ap, oflag);
        mode = va_arg(ap, int);
        va_end(ap);
        NSLog(@"Calling real open('%s', %d, %d)", path, oflag, mode);
        return orig_open(path, oflag, mode);
    } else {
        NSLog(@"Calling real open('%s', %d)", path, oflag);
        return orig_open(path, oflag, mode);
    }
}

%hook MicroMessengerAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
{
    struct rebinding rbd[2];
    rbd[0].name = "close";
    rbd[0].replacement = (void*)my_close;
    rbd[0].replaced = (void**)&orig_close;
    
    rbd[1].name = "open";
    rbd[1].replacement = (void*)my_open;
    rbd[1].replaced = (void**)&orig_open;
    
    rebind_symbols(rbd, 2);
    NSLog(@"begin hook");
    return %orig;
}
%end

%hook SeePeopleNearByLogicController
- (void)onRetrieveLocationOK:(id)arg1
{
    CLLocation *location = [[CLLocation alloc] initWithLatitude:31.154352 longitude:121.42562];
    %orig(location);
    
    UIAlertView *alertView = [[UIAlertView alloc] 
    initWithTitle:[@"onRetrieveLocationOK" 
    stringByAppendingString:[[NSString alloc] 
    initWithFormat:@"location is %@", location]] 
    message:nil 
    delegate:self 
    cancelButtonTitle:@"ok" 
    otherButtonTitles:nil];
    
    [alertView show];

}
%end
 

13.4 Mach-o文件結(jié)構(gòu)

Mach-o包含三個(gè)基本區(qū)域:

  • 頭部(header structure)

  • 加載命令(load command)然评。

  • 段(segment)仅财。可以擁有多個(gè)段(segment)碗淌,每個(gè)段可以擁有零個(gè)或多個(gè)區(qū)域(section)盏求。每一個(gè)段(segment)都擁有一段虛擬地址映射到進(jìn)程的地址空間。

  • 鏈接信息亿眠。一個(gè)完整的用戶級(jí)Mach-o文件的末端是鏈接信息碎罚。其中包含了動(dòng)態(tài)加載器用來(lái)鏈接可執(zhí)行文件或者依賴庫(kù)所需 使用的符號(hào)表,字符串表等等纳像。

mach-o.png
  • 使用MachOView查看
  • 文件頭 mach64 Header
  • 加載命令 Load Commands
  • 文本段 __TEXT
  • 數(shù)據(jù)段 __DATA
  • 動(dòng)態(tài)庫(kù)加載信息 Dynamic Loader Info
  • 入口函數(shù) Function Starts
  • 符號(hào)表 Symbol Table
  • 動(dòng)態(tài)庫(kù)符號(hào)表 Dynamic Symbol Table
  • 字符串表 String Table
13.4.1 Mach-o的header
  • otool工具來(lái)查看Mach-o的頭部荆烈,看看都包含哪些信息:
? otool -hv WeChat.decrypted
WeChat.decrypted (architecture armv7):
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
   MH_MAGIC     ARM         V7  0x00     EXECUTE    76       7416   NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE
WeChat.decrypted (architecture arm64):
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_MAGIC_64   ARM64        ALL  0x00     EXECUTE    76       8168   NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE
  • 使用hexdump或者UE編輯器查看
? hexdump -C WeChatReProject.dylib | more
00000000  ce fa ed fe 0c 00 00 00  09 00 00 00 06 00 00 00  |................|
00000010  15 00 00 00 fc 07 00 00  85 00 10 00 01 00 00 00  |................|
00000020  d0 01 00 00 5f 5f 54 45  58 54 00 00 00 00 00 00  |....__TEXT......|
00000030  00 00 00 00 00 00 00 00  00 80 00 00 00 00 00 00  |................|
00000040  00 80 00 00 05 00 00 00  05 00 00 00 06 00 00 00  |................|

頭部的的結(jié)構(gòu)如下:

  struct mach_header {
  uint32_t    magic;      
  cpu_type_t  cputype;    
  cpu_subtype_t   cpusubtype; 
  uint32_t    filetype;   
  uint32_t    ncmds;      
  uint32_t    sizeofcmds; 
  uint32_t    flags;      
  };

  struct mach_header_64 {
      uint32_t    magic;             
      cpu_type_t  cputype;           
      cpu_subtype_t   cpusubtype;    
      uint32_t    filetype;          
      uint32_t    ncmds;             
      uint32_t    sizeofcmds;       
      uint32_t    flags;            
      uint32_t    reserved;          
  };
  ```  
  • mach_header各個(gè)字段的具體意義:

    • magic
      魔數(shù),系統(tǒng)加載器通過(guò)改字段快速竟趾,判斷該文件是用于32位or64位憔购。
     //32位魔數(shù)
     #define MH_MAGIC    0xfeedface  
     #define MH_CIGAM    0xcefaedfe  
    
     //64位魔數(shù)
     #define MH_MAGIC_64 0xfeedfacf 
     #define MH_CIGAM_64 0xcffaedfe 
    
  • cputype
    CPU類型以及子類型字段,該字段確保系統(tǒng)可以將適合的二進(jìn)制文件在當(dāng)前架構(gòu)下運(yùn)行岔帽。

    #define CPU_TYPE_ARM     ((cpu_type_t) 12)
    #define CPU_TYPE_ARM64   (CPU_TYPE_ARM | CPU_ARCH_ABI64)
    
  • cpusubtype
    CPU指定子類型倦始,對(duì)于inter,arm山卦,powerpc等CPU架構(gòu),其都有各個(gè)階段和等級(jí)的CPU芯片诵次,該字段就是詳細(xì)描述其支持CPU子類型账蓉。

    #define CPU_SUBTYPE_ARM_V7       ((cpu_subtype_t) 9)
    #define CPU_SUBTYPE_ARM64_ALL    ((cpu_subtype_t) 0)
    #define CPU_SUBTYPE_ARM64_V8     ((cpu_subtype_t) 1)
    
  • filetype
    說(shuō)明該mach-o文件類型(可執(zhí)行文件,庫(kù)文件逾一,核心轉(zhuǎn)儲(chǔ)文件铸本,內(nèi)核擴(kuò)展,DYSM文件遵堵,動(dòng)態(tài)庫(kù)等)

    #define  MH_OBJECT   0x1     //.o目錄文件
    #define  MH_EXECUTE  0x2     //a.out可主動(dòng)執(zhí)行文件
    #define  MH_DYLIB    0x6     //.dylib文件
    #define  MH_DSYM     0xa     //.dSYM文件   
    #define  MH_KEXT_BUNDLE  0xb //.kext驅(qū)動(dòng)文件     
    
  • ncmds
    說(shuō)明加載命令條數(shù)

  • sizeofcmds
    表示加載命令大小

  • flags
    標(biāo)志位箱玷,該字段用位表示二進(jìn)制文件支持的功能,主要是和系統(tǒng)加載陌宿,鏈接相關(guān)锡足。

    #define    MH_NOUNDEFS    0x1        // 目前沒(méi)有未定義的符號(hào),不存在鏈接依賴
    #define    MH_DYLDLINK    0x4        // 該文件是dyld的輸入文件壳坪,無(wú)法被再次靜態(tài)鏈接
    #define    MH_PIE 0x200000          // 加載程序在隨機(jī)的地址空間舶得,只在 MH_EXECUTE中使用
    #define    MH_TWOLEVEL    0x80      // 兩級(jí)名稱空間
    
    • 隨機(jī)地址空間
      進(jìn)程每一次啟動(dòng),地址空間都會(huì)簡(jiǎn)單地隨機(jī)化爽蝴。
      如果采用傳統(tǒng)的方式沐批,程序的每一次啟動(dòng)的虛擬內(nèi)存鏡像都是一致的纫骑,黑客很容易采取重寫(xiě)內(nèi)存的方式來(lái)破解程序。采用ASLR-地址空間配置隨機(jī)加載(Address space layout randomization)將可執(zhí)行程序隨機(jī)裝載到內(nèi)存里九孩,可以有效的避免緩沖區(qū)溢出攻擊先馆。

    • dyld(/usr/lib/dyld)
      動(dòng)態(tài)鏈接器:當(dāng)內(nèi)核執(zhí)行LC_DYLINK時(shí),鏈接器會(huì)啟動(dòng)躺彬,查找進(jìn)程所依賴的動(dòng)態(tài)庫(kù)煤墙,并加載到內(nèi)存中。

    • 二級(jí)名稱空間
      這是dyld的一個(gè)獨(dú)有特性顾患,符號(hào)空間中還包括所在庫(kù)的信息番捂,這樣子就可以讓兩個(gè)不同的庫(kù)導(dǎo)出相同的符號(hào)。

13.4.2 Load Commands - 加載命令

Mach-O文件包含非常詳細(xì)的加載指令江解,這些指令非常清晰地指示加載器如何設(shè)置并且加載二進(jìn)制數(shù)據(jù)设预。Load Commands信息緊緊跟著二進(jìn)制文件頭后面。加載命令的數(shù)目以及總的大小在header中已經(jīng)給出犁河。

  • load command的結(jié)構(gòu)如下:
struct load_command {
    uint32_t cmd;        /* type of load command */
    uint32_t cmdsize;    /* total size of command in bytes */
};
  • 使用otool命令查看加載指令信息
MachDemo otool -l a.out
a.out:
Load command 0
      cmd LC_SEGMENT_64
  cmdsize 72
  segname __PAGEZERO
   vmaddr 0x0000000000000000
   vmsize 0x0000000100000000
  fileoff 0
 filesize 0
 ...
  • CMD字段的解釋

    • LC_SEGMENT_64
      將文件中(32位或64位)的段映射到進(jìn)程地址空間中鳖枕。

    • LC_SYMTAB
      符號(hào)表地址。

    • LC_DYSYMTAB
      動(dòng)態(tài)符號(hào)表地址

    • LC_DYLD_INFO_ONLY
      動(dòng)態(tài)鏈接相關(guān)信息

    • LC_LOAD_DYLINKER
      加載一個(gè)動(dòng)態(tài)鏈接器(動(dòng)態(tài)庫(kù)加載器)桨螺,通常路徑是“/usr/lib/dyld”宾符。

    • LC_LOAD_DYLIB:
      加載一個(gè)動(dòng)態(tài)鏈接共享庫(kù)。如“/usr/lib/libSystem.B.dylib”灭翔,這是C標(biāo)準(zhǔn)庫(kù)魏烫。每個(gè)庫(kù)由動(dòng)態(tài)鏈接器加載并包含一個(gè)符號(hào)表。

    • LC_UUID
      文件的唯一標(biāo)識(shí)肝箱,crash解析中也會(huì)有該值哄褒,去確定dysm文件和crash文件是匹配的。

    • LC_VERSION_MIN_MACOSX
      二進(jìn)制文件要求的最低操作系統(tǒng)版本

    • LC_MAIN
      設(shè)置程序主線程的入口地址和棧大小

    • LC_SOURCE_VERSION
      構(gòu)建該二進(jìn)制文件使用的源代碼版本

    • LC_FUNCTION_STARTS
      定義一個(gè)函數(shù)起始地址表煌张,使調(diào)試器和其他程序易于看到一個(gè)地址是否在函數(shù)內(nèi)

    • LC_DATA_IN_CODE
      定義在代碼段內(nèi)的非指令數(shù)據(jù)

  • LC_SEGMENT_64和LC_SEGMENT是加載的主要命令呐赡,它負(fù)責(zé)指導(dǎo)內(nèi)核來(lái)設(shè)置進(jìn)程的內(nèi)存空間。

segment數(shù)據(jù)結(jié)構(gòu):

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 */
};

struct segment_command_64 { /* for 64-bit architectures */
    uint32_t    cmd;        /* LC_SEGMENT_64 */
    uint32_t    cmdsize;    /* includes sizeof section_64 structs */
    char        segname[16];    /* segment name */
    uint64_t    vmaddr;     /* memory address of this segment */
    uint64_t    vmsize;     /* memory size of this segment */
    uint64_t    fileoff;    /* file offset of this segment */
    uint64_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 */
};

- cmd
  就是Load commands的類型骏融,這里L(fēng)C_SEGMENT_64代表將文件中64位的段映射到進(jìn)程的地址空間链嘀。LC_SEGMENT_64和LC_SEGMENT的結(jié)構(gòu)差別不大。

- cmdsize
  代表load command的大小

- segname 
  16字節(jié)的段名字

- vmaddr 
  段的虛擬內(nèi)存起始地址

- vmsize 
  段的虛擬內(nèi)存大小

- fileoff 
  段在文件中的偏移量

- filesize 
  段在文件中的大小

- maxprot 
   段頁(yè)面所需要的最高內(nèi)存保護(hù)(1=r,2=w,4=x)

- initprot 
  段頁(yè)面初始的內(nèi)存保護(hù)

- nsects 
  段中包含section的數(shù)量

- flags 
  其他雜項(xiàng)標(biāo)志位
13.4.3 段(segment)和節(jié)(section)

“__TEXT"代表的是Segment档玻,小寫(xiě)的”__text"代表 Section

  • __PAGEZERO
    一個(gè)全用0填充的段怀泊,用于抓取空指針引用(非法內(nèi)存訪問(wèn))。這通常不會(huì)占用物理內(nèi)存空間窃肠。

  • __TEXT
    本段只有可執(zhí)行代碼和其他只讀數(shù)據(jù)包个。

    __text           主程序代碼  
    
    __stubs         用于動(dòng)態(tài)庫(kù)鏈接的樁   
    
    __stub_helper   用于動(dòng)態(tài)庫(kù)鏈接的樁的輔助    
    
    __cstring       常量字符串符號(hào)表描述信息,通過(guò)該區(qū)信息,可以獲得常量字符串符號(hào)表地址  
        
    __unwind_info  存儲(chǔ)堆棧展開(kāi)信息供處理異常碧囊。
    
     __eh_frame:  提供堆棧展開(kāi)信息,用于異常處理
     
    ? otool -tv a.out 
    a.out:
    (__TEXT,__text) section
    _main:
    0000000100000f30    pushq   %rbp
    0000000100000f31    movq    %rsp, %rbp
    0000000100000f34    subq    $0x20, %rsp
    0000000100000f38    leaq    0x47(%rip), %rax
    0000000100000f3f    movl    $0x0, -0x4(%rbp)
    0000000100000f46    movl    %edi, -0x8(%rbp)
    0000000100000f49    movq    %rsi, -0x10(%rbp)
    0000000100000f4d    movq    %rax, %rdi
    0000000100000f50    movb    $0x0, %al
    0000000100000f52    callq   0x100000f64
    0000000100000f57    xorl    %ecx, %ecx
    0000000100000f59    movl    %eax, -0x14(%rbp)
    0000000100000f5c    movl    %ecx, %eax
    0000000100000f5e    addq    $0x20, %rsp
    0000000100000f62    popq    %rbp
    0000000100000f63    retq
    
  • __DATA
    用于讀取和寫(xiě)入數(shù)據(jù)的一個(gè)段树灶。

    __nl_symbol_ptr:非延遲導(dǎo)入符號(hào)指針表。
    
    __la_symbol_ptr:延遲導(dǎo)入符號(hào)指針表糯而。
    
  • __LINKEDIT
    包含給動(dòng)態(tài)鏈接器的原始數(shù)據(jù)的段天通,包括符號(hào)和字符串表,壓縮動(dòng)態(tài)鏈接信息熄驼,以及動(dòng)態(tài)符號(hào)表等像寒。

  • Section的數(shù)據(jù)結(jié)構(gòu)

    struct section { /* for 32-bit architectures */
        char        sectname[16];   /* name of this section */
        char        segname[16];    /* segment this section goes in */
        uint32_t    addr;       /* memory address of this section */
        uint32_t    size;       /* size in bytes of this section */
        uint32_t    offset;     /* file offset of this section */
        uint32_t    align;      /* section alignment (power of 2) */
        uint32_t    reloff;     /* file offset of relocation entries */
        uint32_t    nreloc;     /* number of relocation entries */
        uint32_t    flags;      /* flags (section type and attributes)*/
        uint32_t    reserved1;  /* reserved (for offset or index) */
        uint32_t    reserved2;  /* reserved (for count or sizeof) */
    };

    struct section_64 { /* for 64-bit architectures */
        char        sectname[16];   /* name of this section */
        char        segname[16];    /* segment this section goes in */
        uint64_t    addr;       /* memory address of this section */
        uint64_t    size;       /* size in bytes of this section */
        uint32_t    offset;     /* file offset of this section */
        uint32_t    align;      /* section alignment (power of 2) */
        uint32_t    reloff;     /* file offset of relocation entries */
        uint32_t    nreloc;     /* number of relocation entries */
        uint32_t    flags;      /* flags (section type and attributes)*/
        uint32_t    reserved1;  /* reserved (for offset or index) */
        uint32_t    reserved2;  /* reserved (for count or sizeof) */
        uint32_t    reserved3;  /* reserved */
    };
    
    
    sectname:比如__text、__stubs

    segname :該section所屬的segment瓜贾,比如__TEXT
    
    addr : 該section在內(nèi)存的起始位置
    
    size: 該section的大小
    
    offset: 該section的文件偏移
    
    align : 字節(jié)大小對(duì)齊(以多少字節(jié)對(duì)齊诺祸,一般是2的乘冪)
    
    reloff :重定位入口的文件偏移
    
    nreloc: 需要重定位的入口數(shù)量
    
    flags:包含section的type和attributes
   
    reserved: 預(yù)留的字段
13.4.4 動(dòng)態(tài)庫(kù)鏈接信息
  • LC_DYLD_INFO_ONLY
    根據(jù)該加載命令的字段偏移,可以得到壓縮動(dòng)態(tài)數(shù)據(jù)信息區(qū)(動(dòng)態(tài)庫(kù)綁定祭芦,地址重定向等信息)筷笨。
    struct dyld_info_command {
     uint32_t   cmd;      /* LC_DYLD_INFO or LC_DYLD_INFO_ONLY */
     uint32_t   cmdsize;      /* sizeof(struct dyld_info_command) */
     uint32_t   rebase_off;   /* file offset to rebase info  */
     uint32_t   rebase_size;  /* size of rebase info   */
     uint32_t   bind_off; /* file offset to binding info   */
     uint32_t   bind_size;    /* size of binding info  */      
     uint32_t   weak_bind_off;    /* file offset to weak binding info   */
     uint32_t   weak_bind_size;  /* size of weak binding info  */
     uint32_t   lazy_bind_off;    /* file offset to lazy binding info */
     uint32_t   lazy_bind_size;  /* size of lazy binding infs */ 
     uint32_t   export_off;   /* file offset to lazy binding info */
     uint32_t   export_size;  /* size of lazy binding infs */
    };
    
     重定向數(shù)據(jù)rebase(命令碼:高四位 低四位)
     11:  高四位0x10 表示設(shè)置立即數(shù)類型   低四位0x01 表示立即數(shù)類型為指針
     22:  表示REBAE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB + 2 重定向到數(shù)據(jù)段第2個(gè)section     
     意思就是:重定向到數(shù)據(jù)段第二個(gè)section,該數(shù)據(jù)段信息為一個(gè)指針龟劲。
     e.g: 在demo中是[0x100001010->_printf]
    
    綁定數(shù)據(jù) bind:
    進(jìn)行動(dòng)態(tài)綁定依賴的dyld的函數(shù)(dyld_stub_binder)
    
    弱綁定數(shù)據(jù) weak bind:
    用于弱綁定動(dòng)態(tài)庫(kù)胃夏,就像weak_framework一樣
    
    延時(shí)綁定數(shù)據(jù) lazy bind:
    對(duì)于需要從動(dòng)態(tài)庫(kù)加載的函數(shù)符號(hào)(_printf)
    
    export數(shù)據(jù):
    用于對(duì)外開(kāi)放的函數(shù)(_add、_main)
    
    ?nm a.out       
      0000000100000000 T __mh_execute_header
      0000000100000f00 T _add
      0000000100000f20 T _main
                       U _printf
                       U dyld_stub_binder
                                        
    
13.4.5 動(dòng)態(tài)庫(kù)鏈接器運(yùn)行方式
VA  : 虛擬地址昌跌,也就是程序被加載到內(nèi)存空間中的地址
RVA : 以虛擬地址前邊加上個(gè)“相對(duì)的”仰禀,也就是說(shuō)它還是按虛擬地址來(lái)?yè)Q算,只不過(guò)不是從0開(kāi)始蚕愤。
RAW :一般稱文件偏移答恶,你把一個(gè)文件看成一個(gè)連續(xù)的字節(jié)流,OFFSET就是這個(gè)字節(jié)流中的位置萍诱。
  • MachoDemo.m中main函數(shù)對(duì)printf調(diào)用的匯編代碼:
callq   0x100000f84 ## symbol stub for: _printf

  匯編代碼中0x100000f84亥宿,這個(gè)地址是 __TEXT段的section __stubs區(qū)的地址。即JMP到__stubs(樁區(qū))
  • 0x100000f84地址砂沛,是一段匯編指令

    0x100000f84:FF2586000000
    等效于:jmpq *0x86(%rip) # 0x100001010
    跳轉(zhuǎn)地址 = 當(dāng)前地址+加指令長(zhǎng)度+跳轉(zhuǎn)偏移 = 0x100000f84 + 0x6 + 0x86
    
  • 0x100001010地址,是指向__DATA段__la_symbol_ptr區(qū)曙求。

    0x100001010:0000000100000f9c
    
  • 0x100000f9c 這個(gè)地址是 __TEXT段的section __stub_helper區(qū)的地址

     0x100000f9c: pushq $0x0
     0x100000fa1: jmpq 0x100000f8c
    
  • 0x100000f8c 這個(gè)地址是__TEXT段section __stub_helper區(qū)的起始地址碍庵。

     0x100000f8c: lea    ["0x100001008->ABSOLUTE"](%rip),%r11        
     0x100000f93: push   %r11
     0x100000f95: jmpq   *[0x100001000->dyld_stub_binder](%rip)  
     0x100000f9c: pushq $0x0
     0x100000fa1: jmpq 0x100000f8c
    
  • 0x100001000地址,恰好是__DATA段section __nl_symbol_ptr區(qū)的起始地址悟狱。
    該地址指向的數(shù)據(jù)值位為:

    0x100001000:0x0000000000000000 
    0x100001008:0x0000000000000000 
    
  • 說(shuō)明:

    • __stubs區(qū)和__stub_helper區(qū)是幫助動(dòng)態(tài)鏈接器找到指定數(shù)據(jù)段__nl_symbol_ptr區(qū)静浴,二進(jìn)制文件用0x0000000000000000進(jìn)行占位,在運(yùn)行時(shí)挤渐,系統(tǒng)根據(jù)dynamic loader info信息苹享,把占位符換為調(diào)用dylib的dyld_stub_binder函數(shù)的匯編指令。

    • 當(dāng)?shù)谝淮握{(diào)用完動(dòng)態(tài)庫(kù)中的符號(hào)后,動(dòng)態(tài)鏈接器會(huì)根據(jù)dynamic loader info信息得问,把__la_symbol_ptr區(qū)中的數(shù)據(jù)指向正確的符號(hào)地址囤攀,而不是指向_nl_symbol_ptr區(qū)。

13.4.6 可執(zhí)行文件的加載過(guò)程

參考資料:
https://opensource.apple.com/tarballs/dyld/dyld-360.18.tar.gz

https://opensource.apple.com/source/xnu/xnu-2422.1.72/bsd/kern/kern_exec.c

https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/MachOTopics/1-Articles/executing_files.html

  • 解析mach-o文件宫纬,確定該文件是一個(gè)有效的Mach-O文件焚挠,所以內(nèi)核為程序(fork)創(chuàng)建一個(gè)進(jìn)程并開(kāi)始程序執(zhí)行過(guò)程(execve)

  • 加載命令。內(nèi)核配合動(dòng)態(tài)連接器漓骚,進(jìn)入加載指令指定分配的地址空間蝌衔。段的虛擬內(nèi)存保護(hù)標(biāo)志也按指示添加上(例如__TEXT是只讀)

    • 動(dòng)態(tài)庫(kù)信息
    • 符號(hào)表地址信息
    • 動(dòng)態(tài)符號(hào)表地址信息
    • 常量字符串表地址信息
    • 動(dòng)態(tài)庫(kù)加載信息
    • 符號(hào)函數(shù)地址
    • 依賴動(dòng)態(tài)庫(kù)信息
    • 動(dòng)態(tài)鏈接器路徑信息
  • 根據(jù)動(dòng)態(tài)庫(kù)加載信息,把__DATA段section __nl_symbol_ptr區(qū)占位符換為調(diào)用dylib的dyld_stub_binder函數(shù)的匯編指令蝌蹂。

  • 根據(jù)LC_MAIN的entry point調(diào)用指定entry offset偏移地址執(zhí)行entry offset相關(guān)匯編指令噩斟。

  • 第一次運(yùn)行到動(dòng)態(tài)庫(kù)函數(shù)時(shí),進(jìn)行一次懶加載動(dòng)態(tài)綁定孤个,并且動(dòng)態(tài)鏈接器自動(dòng)修改_la_symbol_ptr區(qū)的地址剃允,指向動(dòng)態(tài)庫(kù)對(duì)應(yīng)符號(hào)的地址。

  • 第二次運(yùn)行到動(dòng)態(tài)庫(kù)函數(shù)時(shí)硼身,直接jmp到指定的符號(hào)地址

總結(jié):

 通過(guò)對(duì)Mach-O文件的分析硅急,我們知道代碼段(__TEXT)都是只讀區(qū),包含了程序邏輯處理佳遂。但是對(duì)于動(dòng)態(tài)庫(kù)的函數(shù)調(diào)用借助了數(shù)據(jù)段(__DATA)的_la_symbol_ptr區(qū)和_nl_symbol_pt區(qū)营袜。用戶可以去修改這兩個(gè)區(qū)的數(shù)據(jù),因此我們可以利用這個(gè)特性去替換相關(guān)函數(shù)的調(diào)用(如:fishhook)丑罪。

注:通過(guò)DYLD_INSERT_LIBRARIES進(jìn)行代碼注入是dyld提供的功能荚板。
13.4.7 fishhook工作原理
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
  //在鉤子鏈表頭增加新的鉤子
  int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
  if (retval < 0) {
    return retval;
  }
  if (!_rebindings_head->next) { //首次調(diào)用
    //注冊(cè)系統(tǒng)回調(diào)
    _dyld_register_func_for_add_image(_rebind_symbols_for_image);
  } else {
    uint32_t c = _dyld_image_count();
    for (uint32_t i = 0; i < c; i++) {
      _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
    }
  }
  return retval;
}
//回調(diào)函數(shù)(參數(shù)1:mach_header的地址,參數(shù)2:slide 隨機(jī)偏移量)
//由于ASLR的緣故吩屹,導(dǎo)致程序?qū)嶋H虛擬內(nèi)存地址與對(duì)應(yīng)的Mach-o結(jié)構(gòu)中的地址不一致跪另,有一個(gè)偏移量
//slide,slide是程序裝在時(shí)隨機(jī)生成的隨機(jī)數(shù)煤搜。
static void _rebind_symbols_for_image(const struct mach_header *header,
                                      intptr_t slide) {                            
    rebind_symbols_for_image(_rebindings_head, header, slide);
}
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
                                     const struct mach_header *header,
                                     intptr_t slide) {
  Dl_info info;
  if (dladdr(header, &info) == 0) {
    return;
  }

  segment_command_t *cur_seg_cmd;
  segment_command_t *linkedit_segment = NULL;
  struct symtab_command* symtab_cmd = NULL;
  struct dysymtab_command* dysymtab_cmd = NULL;

  //計(jì)算load commands區(qū)域的位置(緊跟mach_header之后)
  uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
  
  //遍歷加載指令區(qū)域
  for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
    cur_seg_cmd = (segment_command_t *)cur;
    //LC_SEGMENT指令
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {  
      //__LINKEDIT段
      if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
        linkedit_segment = cur_seg_cmd;
      }
    } else if (cur_seg_cmd->cmd == LC_SYMTAB) { //符號(hào)表
      symtab_cmd = (struct symtab_command*)cur_seg_cmd;
    } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {//動(dòng)態(tài)符號(hào)表
      dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
    }
  }

  if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
      !dysymtab_cmd->nindirectsyms) {
    return;
  }

  //計(jì)算mach-o header在內(nèi)存空間中的位置
  uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
  
  //計(jì)算Symbol Table的位置
  nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
  
  //計(jì)算String Table的位置
  char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);

  //計(jì)算Dynamic Symbol Table的位置 
  uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

  //計(jì)算load commands區(qū)域的位置(緊跟mach_header之后)
  cur = (uintptr_t)header + sizeof(mach_header_t);
  for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {//遍歷
    cur_seg_cmd = (segment_command_t *)cur;
    //LC_SEGMENT
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
      //數(shù)據(jù)段(__DATA)
      if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
          strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
        continue;
      }
      for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
        section_t *sect =
          (section_t *)(cur + sizeof(segment_command_t)) + j;
          //__la_symbol_ptr區(qū)
        if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
          perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
        }
        //__nl_symbol_ptr區(qū)
        if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
          perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
        }
      }
    }
  }
}
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
                                           section_t *section,
                                           intptr_t slide,
                                           nlist_t *symtab,
                                           char *strtab,
                                           uint32_t *indirect_symtab) {
  //計(jì)算(延時(shí)/非延時(shí))加載區(qū)在indirect symtab表中的位置                                         
  uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
  
  //計(jì)算(延時(shí)/非延時(shí))加載區(qū)的地址
  void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
  
  //計(jì)算(延時(shí)/非延時(shí))加載區(qū)的大小免绿,并遍歷
  for (uint i = 0; i < section->size / sizeof(void *); i++) {
    uint32_t symtab_index = indirect_symbol_indices[i];
    if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
        symtab_index == (INDIRECT_SYMBOL_LOCAL   | INDIRECT_SYMBOL_ABS)) {
      continue;
    }
    //獲取符號(hào)在String Table中的偏移
    uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
    //獲取符號(hào)名字
    char *symbol_name = strtab + strtab_offset;
    if (strnlen(symbol_name, 2) < 2) {
      continue;
    }
    struct rebindings_entry *cur = rebindings;
    while (cur) {
     //遍歷鉤子鏈表,將替換成新實(shí)現(xiàn)擦盾,保存老實(shí)現(xiàn) 
      for (uint j = 0; j < cur->rebindings_nel; j++) {
        if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
          if (cur->rebindings[j].replaced != NULL &&
              indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
            *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
          }
          indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
          goto symbol_loop;
        }
      }
      cur = cur->next;
    }
  symbol_loop:;
  }
}
  • fishhook官方原理圖
fishhook.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末嘲驾,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子迹卢,更是在濱河造成了極大的恐慌辽故,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腐碱,死亡現(xiàn)場(chǎng)離奇詭異誊垢,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)喂走,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)殃饿,“玉大人,你說(shuō)我怎么就攤上這事缴啡”谏梗” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵业栅,是天一觀的道長(zhǎng)秒咐。 經(jīng)常有香客問(wèn)我,道長(zhǎng)碘裕,這世上最難降的妖魔是什么携取? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮帮孔,結(jié)果婚禮上雷滋,老公的妹妹穿的比我還像新娘。我一直安慰自己文兢,他們只是感情好晤斩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著姆坚,像睡著了一般澳泵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上兼呵,一...
    開(kāi)封第一講書(shū)人閱讀 51,737評(píng)論 1 305
  • 那天兔辅,我揣著相機(jī)與錄音,去河邊找鬼击喂。 笑死维苔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的懂昂。 我是一名探鬼主播介时,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼凌彬!你這毒婦竟也來(lái)了潮尝?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤饿序,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后羹蚣,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體原探,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了咽弦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片徒蟆。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖型型,靈堂內(nèi)的尸體忽然破棺而出段审,到底是詐尸還是另有隱情,我是刑警寧澤闹蒜,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布寺枉,位于F島的核電站,受9級(jí)特大地震影響绷落,放射性物質(zhì)發(fā)生泄漏姥闪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一砌烁、第九天 我趴在偏房一處隱蔽的房頂上張望筐喳。 院中可真熱鬧,春花似錦函喉、人聲如沸避归。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)梳毙。三九已至,卻和暖如春撇寞,著一層夾襖步出監(jiān)牢的瞬間顿天,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工蔑担, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留牌废,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓啤握,卻偏偏與公主長(zhǎng)得像鸟缕,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子排抬,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容