iOS使用shell脚本批量修改属性

背景

公司需要做一系列的壳版本,壳版本如果内容雷同提交到App Store会有被拒绝的风险,除了我在上一篇文章中说道的在壳版本中注入混淆的代码,防止被苹果检测到内容太过雷同而导致审核被拒绝。还有另一种可行的方法是批量修改源文件中的类名、属性、方法名称等会在二进制文件中留下符号标记的信息,绕过苹果的机器审核。
这篇文章介绍的是如何使用脚本批量修改属性名称,后续还有系列的包括使用脚本批量修改类名称、方法名称等信息的文章。

结果

本文的Demo代码YTTInjectedContentKit

使用方法

  • 打开测试工程

测试工程位于项目目录下面的DevPods/InjectedContentKit/Example/目录下,打开InjectedContentKit.xcworkspace即可

  • 执行命令

在命令行中进入到项目目录下面的DevPods/InjectedContentKit/Example/injectContentShell子目录,在我的电脑对应的目录为/Users/aron/git-repo/YTTInjectedContentKit/DevPods/InjectedContentKit/Example/injectContentShell,然后执行./RenameProperty.sh批量替换属性

➜  injectContentShell git:(master) pwd
/Users/aron/git-repo/YTTInjectedContentKit/DevPods/InjectedContentKit/Example/injectContentShell
➜  injectContentShell git:(master) ./RenameProperty.sh 
检测到配置文件存在 /Users/aron/git-repo/YTTInjectedContentKit/DevPods/InjectedContentKit/Example/injectContentShell/RenameProperties.cfg
需处理源码目录存在 /Users/aron/git-repo/YTTInjectedContentKit/DevPods/InjectedContentKit/Example/injectContentShell/../InjectedContentKit
检测到配置文件存在 /Users/aron/git
// 省略...
正在处理属性 invitationCode.....
正在处理属性 organizer.....
正在处理属性 ruleCardBack.....
done.

下面是执行脚本替换了属性的结果图,脚本把所有需要替换的属性添加了abc后缀,当然依然是可以正常编译运行的

替换结果图

分析

原理分析

objc代码中的类名、属性、方法、源文件路径等信息最终会被打包到二进制文件中,保存在二进制文件中的.sym符号表段中,可以使用objdump -t命令查看二进制符号信息,以下的命令把objdump -t的结果写入到文件InjectedContentKit_Example_Symbols中去。

objdump -t InjectedContentKit_Example > InjectedContentKit_Example_Symbols

文件的内容会很大,所以选择了几个代表性的内容说明:

0000000100026350 l    d  __TEXT,__text  __text
# 这里保存的是类源文件的路径符号信息
0000000000000000 l    d  *UND*  /Users/aron/PuTaoWorkSpace/project/sscatch/DevPods/InjectedContentKit/InjectedContentKit/Classes/Composer/PubSearchDataComposer.h

# 这里保存的是属性对应的var信息
0000000000000000 l    d  *UND*  _OBJC_IVAR_$_TextCardItem._title
0000000000000000 l    d  *UND*  _OBJC_IVAR_$_TextCardItem._showReact
0000000000000000 l    d  *UND*  _OBJC_IVAR_$_TextCardItem._topChart
0000000000000000 l    d  *UND*  _OBJC_IVAR_$_TextCardItem._reaction

# 这里保存的是属性信息对应的getter方法信息
00000001000264a0 l     F __TEXT,__text  -[TextCardItem title]
00000001000264c0 l     F __TEXT,__text  -[TextCardItem showReact]
00000001000264f0 l     F __TEXT,__text  -[TextCardItem topChart]
0000000100026510 l     F __TEXT,__text  -[TextCardItem setTopChart:]

# 这里保存的是属性信息对应的setter方法信息
00000001000028a0 l     F __TEXT,__text  -[SSCatchInviteScheduler setOrganizer:]
00000001000028e0 l     F __TEXT,__text  -[SSCatchInviteScheduler setInputCardBack:]
0000000100002920 l     F __TEXT,__text  -[SSCatchInviteScheduler setInputTextBack:]

# 这里保存的是类文件的文件名信息
0000000000000000 l    d  *UND*  PubSearchDataComposer.m
000000005a937587 l    d  __TEXT,__stub_helper   __stub_helper
00000001000251c0 l    d  __TEXT,__text  __text

从上面可以看出,二进制中保留了很多信息和源代码有很大关系,我们做个简单的猜测苹果后台机器审查二进制的时候会通过二进制中的符号进行对比,如果两个二进制(一个主版本、一个壳版本)代码中的符号重合度超过某个阈值,就会判定这是发布壳版本的行为,而这是苹果说不允许的,所以可行的方法是修改源文件中的这些信息来绕过苹果的审查机制。

另外猜测苹果应该是不会根据代码中的流程控制来判断的,因为二进制中的控制流程已经是机器码了,反编译出来也就是汇编代码,只要稍微做点改动二进制(.text段)就会变化很大。所以从这个方面来判断就难度很大了。

步骤分析

主要有以下几个步骤

  1. 寻找到需要替换的源文件中的所有的属性,处理之后保存在配置文件中
  2. 用户自定义一个黑名单配置文件
  3. 某部分需要隔离的代码中的属性生成黑名单配置文件
  4. 把需要替换的源文件中的所有匹配的属性做批量的替换

这里说明下为什么第一步需要保存在配置文件中,因为第三步的操作有部分和第一步是相同的,所有这部分单独出来一个模块共用,都是输入一个文件夹,最终保存在指定的文件中,后面的代码中可以看到这部分。

实现

单步实现

1、寻找到需要替换的源文件中的所有的属性,处理之后保存在配置文件中

这一步的功能是客户端输入一个需要处理的源码文件夹,递归遍历该源码文件夹获取所有源码文件(.h .m 文件)。使用正则匹配找到属性名称,暂时保存到数组中,最后经过黑名单过滤、去重过滤、其他过滤条件过滤,最终把待处理的属性保存到客户端输入的输出文件中。

可以分解为一下几个小步骤

  • 递归遍历文件夹获取源码文件
  • 正则匹配源码文件的属性
  • 过滤属性(可选)
  • 保存属性到文件

这部分功能的源码如下:
文件名: GetAndStoreProperties.sh
该脚本在多个地方都有用到,所以作为一个单独的模块,定义了一些参数,以适应不同的应用场景。在下面可以看到使用该脚本的地方。

#!/bin/bash
########################
# 脚本功能:从指定目录获取和保存属性到指定的文件
# 输入参数 -i 输入的文件夹
# 输入参数 -o 保存的文件
# 输入参数 -f 使用黑名单和自定义过滤条件的参数
# 输入参数 -c 自定义的黑名单文件
########################

####### 参数定义
param_input_dir=""
param_output_file=""
param_custom_filter_file=""
param_should_use_filter=0

####### 参数解析
while getopts :i:o:c:f opt
do
    case "$opt" in
        i) param_input_dir=$OPTARG
            echo "Found the -i option, with parameter value $OPTARG"
            ;;
        o) param_output_file=$OPTARG
            echo "Found the -o option, with parameter value $OPTARG"
            ;;
        c) param_custom_filter_file=$OPTARG
            echo "Found the -c option, with parameter value $OPTARG"
            ;;
        f) echo "Found the -f option" 
            param_should_use_filter=1
            ;;
        *) echo "Unknown option: $opt";;
    esac
done


####### 配置

# 属性黑名单配置文件
blacklist_cfg_file="$(pwd)/DefaultBlackListPropertiesConfig.cfg"

####### 数据定义

# 定义保存源文件的数组
declare -a implement_source_file_array
implement_source_file_count=0


# 定义保存属性的数组
declare -a tmp_props_array
props_count=0


# mark: p384
# 递归函数读取目录下的所有.m文件
function read_source_file_recursively {
    echo "read_implement_file_recursively"
    if [[ -d $1 ]]; then
        for item in $(ls $1); do
            itemPath="$1/${item}"
            if [[ -d $itemPath ]]; then
                # 目录
                echo "处理目录 ${itemPath}"
                read_source_file_recursively $itemPath
                echo "处理目录结束====="
            else 
                # 文件
                echo "处理文件 ${itemPath}"
                if [[ $(expr "$item" : '.*\.m') -gt 0 ]] || [[ $(expr "$item" : '.*\.h') -gt 0 ]]; then
                    echo ">>>>>>>>>>>>mmmmmmm"
                    implement_source_file_array[$implement_source_file_count]=${itemPath}
                    implement_source_file_count=$[ implement_source_file_count + 1 ];
                fi
                echo ""
            fi
        done
    else
        echo "err:不是一个目录"
    fi
}


# 读取源码中的属性,保存到数组中
# 参数一: 源码文件路径
function get_properties_from_source_file {
    local class_file=$1;
    echo "class_file=${class_file}"

    properties=$(grep "@property.*" ${class_file})
    IFS_OLD=$IFS
    IFS=$'\n'
    for prop_line in $properties; do
        echo ">>>>>${prop_line}"

        asterisk_seperator_pattern="\*"
        if [[ ${prop_line} =~ ${asterisk_seperator_pattern} ]]; then
            # 从左向右截取最后一个string后的字符串
            prop_name=${prop_line##*${asterisk_seperator_pattern}}
            # 从左向右截取第一个string后的字符串
            seal_pattern=";*"
            seal_pattern_replacement=""
            prop_name=${prop_name//${seal_pattern}/${seal_pattern_replacement}}
            subsring_pattern="[ |;]"
            replacement=""
            prop_name=${prop_name//${subsring_pattern}/${replacement}}

            if [[ ${param_should_use_filter} -gt 0 ]]; then
                grep_result=$(grep ${prop_name} ${blacklist_cfg_file})
                echo "grep_result = >>${grep_result}<<"
                custom_grep_result=""
                if [[ -n ${param_custom_filter_file} ]]; then
                    custom_grep_result=$(grep ${prop_name} ${param_custom_filter_file})
                fi
                if [[ -n ${grep_result} ]] || [[ -n ${custom_grep_result} ]]; then
                    echo "--${prop_name}--存在配置文件中"
                else
                    echo "--${prop_name}--XXX不存在配置文件中"

                    tmp_props_array[$props_count]=$prop_name
                    props_count=$[ props_count + 1 ]
                    echo ">>>>>>>result_prop_name=${prop_name}"
                fi
            else
                tmp_props_array[$props_count]=$prop_name
                props_count=$[ props_count + 1 ]
            fi          
        fi
    done
    IFS=$IFS_OLD
}

# 获取目录下的所有源文件,读取其中的属性
function get_properties_from_source_dir {

    local l_classed_folder=$1

    echo "获取需要处理的源文件... ${l_classed_folder}"
    # 读取需要处理目标文件
    read_source_file_recursively ${l_classed_folder}

    echo "读取源文件中的属性..."
    for(( i=0;i<${#implement_source_file_array[@]};i++)) 
    do 
        class_file=${implement_source_file_array[i]}; 
        echo "处理源文件:${class_file}"
        get_properties_from_source_file ${class_file}
    done;
}

# 把获取到的属性过滤之后写入文件中
# 过滤步骤包含去重、去掉简单词汇、去掉长度少于多少的词汇
# 如果在执行的过程中遇到特殊情况,添加到黑名单配置(DefaultBlackListPropertiesConfig.cfg文件中添加配置)
function post_get_properties_handle {

    local prop_config_file=$1

    # 写入文件中
    echo "# Properties Configs" > ${prop_config_file}
    for key in $(echo ${!tmp_props_array[*]})
    do
        # echo "$key : ${tmp_props_array[$key]}"
        echo ${tmp_props_array[$key]} >> ${prop_config_file}
    done

    # 去重
    cfg_back_file="${prop_config_file}.bak"
    mv ${prop_config_file} ${cfg_back_file}
    sort ${cfg_back_file} | uniq > ${prop_config_file}
    
    # 过滤
    if [[ ${param_should_use_filter} -gt 0 ]]; then
        mv ${prop_config_file} ${cfg_back_file}
        echo "# Properties Configs Filtered" > ${prop_config_file}
        IFS_OLD=$IFS
        IFS=$'\n'
        # 上一行的内容
        lastLine="";
        for line in $(cat ${cfg_back_file} | sed 's/^[ \t]*//g')
        do
            if [[ ${#line} -le 6 ]] || [[ $(expr "$line" : '^#.*') -gt 0 ]]; then
                # 长度小于等于6或者注释内容的行不处理
                echo "less then 6 char line or comment line"
            else
                if [[ -n ${lastLine} ]]; then
                    # 上一行是非空白行
                    # 比较上一行内容是否是当前行的一部分,不是添加上一行
                    if [[ ${line} =~ ${lastLine} ]]; then
                        echo "${line} 和 ${lastLine} 有交集"
                    else
                        echo ${lastLine} >> ${prop_config_file}
                    fi
                fi
                # 更新上一行
                lastLine=${line}
            fi  
        done
        IFS=${IFS_OLD}
    fi

    # 删除临时文件
    rm -f ${cfg_back_file}
}


get_properties_from_source_dir ${param_input_dir}
post_get_properties_handle ${param_output_file}

使用以上脚本生成的配置文件 PropertiesConfigs.cfg 部分如下:

# Properties Configs Filtered
UserRestrictionLabel
aboutusButton
activitySamplers
addAddressPress
addressSamplers
addressTextBox
appealPress
appliedGroupedSamplers
appliedSamplers
applyPress
asyncArray
asyncListSampler
audioPlayer

2. 用户自定义一个黑名单配置文件

在实践的过程中,替换属性的符号有时候会把系统类的属性替换了,比如

  • AppDelegate 中的 window 属性替换了,导致了编译链接没错,但是界面出不来了,因为初始的window对象找不到了
  • UIButton 中的 titleLabel 属性替换了,直接导致了编译出错

对于这类问题,需要在黑名单中配置一些默认的过滤属性,对于黑名单中的这些属性不处理即可,在我的业务场景下,黑名单文件的配置如下:

文件名:DefaultBlackListPropertiesConfig.cfg

# BlackListPropertiesConfig.cfg
# 属性黑名单配置,在此配置文件中的属性不需要替换名称
window
name
title
titleLabel
layout
appealSamplers

GetAndStoreProperties.sh 脚本使用到的代码片段如下,其实就是使用了 grep 命来查找,判断时候有找到,如果有就不处理,具体的可以看上面提供的完整的 GetAndStoreProperties.sh 脚本代码

if [[ ${param_should_use_filter} -gt 0 ]]; then
    grep_result=$(grep ${prop_name} ${blacklist_cfg_file})
    echo "grep_result = >>${grep_result}<<"
    custom_grep_result=""
    if [[ -n ${param_custom_filter_file} ]]; then
        custom_grep_result=$(grep ${prop_name} ${param_custom_filter_file})
    fi
    if [[ -n ${grep_result} ]] || [[ -n ${custom_grep_result} ]]; then
        echo "--${prop_name}--存在配置文件中"
    else
        echo "--${prop_name}--XXX不存在配置文件中"

        tmp_props_array[$props_count]=$prop_name
        props_count=$[ props_count + 1 ]
        echo ">>>>>>>result_prop_name=${prop_name}"
    fi
else
    tmp_props_array[$props_count]=$prop_name
    props_count=$[ props_count + 1 ]
fi  

3. 某部分需要隔离的代码中的属性生成黑名单配置文件

这部分的功能其实就是调用 GetAndStoreProperties.sh 这个脚本,最终把文件输出的文件以追加的方式写入到用户自定义的黑名单属性文件中。

#...
# 黑名单类目录
declare -a custom_blacklist_search_dirs
custom_blacklist_search_dirs=("/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/SSCatchAPI" 
    "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Categories" 
    "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Components" 
    "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/External" 
    "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/HandyTools" 
    "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Macros" )
# ...

# 属性黑名单配置文件
custom_blacklist_cfg_file="$(pwd)/CustomBlackListPropertiesConfig.cfg"

# ...
# 获取自定义的黑名单属性并保存到文件中
echo "" > ${custom_blacklist_cfg_file}
for (( i = 0; i < ${#custom_blacklist_search_dirs[@]}; i++ )); do
    custom_blacklist_search_dir=${custom_blacklist_search_dirs[${i}]}
    ./GetAndStoreProperties.sh \
        -i ${custom_blacklist_search_dir}\
        -o ${custom_blacklist_cfg_tmp_file}
    cat ${custom_blacklist_cfg_tmp_file} >> ${custom_blacklist_cfg_file}
done
#...

最终生成的用户自定义的黑名单文件部分如下
文件:CustomBlackListPropertiesConfig.cfg

# Properties Configs
DBFilePath
ValidityString
accessQueue
age
attributedNameString
avatarURLString
avatarUrlString
backColorString
bodyScheduler
bodyView
catchDateString
cellHeight
channelKey
cityName
conditionString
# ....

4. 把需要替换的源文件中的所有匹配的属性做批量的替换

这一步在前面三部的基础上,查找并替换源码目录中在 PropertiesConfigs.cfg 配置文件中出现的属性和属性的引用,查找使用grep命令、替换使用了sed命令。脚本代码如下

#!/bin/bash
# 属性重命名脚本

####### 配置
# classes类目录
classes_dir="$(pwd)/../InjectedContentKitx"
# 黑名单类目录
declare -a custom_blacklist_search_dirs
custom_blacklist_search_dirs=("/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/SSCatchAPI" 
    "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Categories" 
    "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Components" 
    "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/External" 
    "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/HandyTools" 
    "/Users/aron/PuTaoWorkSpace/project/sscatch/sscatch/Classes/Macros" )
# 配置文件
cfg_file="$(pwd)/PropertiesConfigs.cfg"
# 属性黑名单配置文件
blacklist_cfg_file="$(pwd)/DefaultBlackListPropertiesConfig.cfg"
# 属性黑名单配置文件
custom_blacklist_cfg_file="$(pwd)/CustomBlackListPropertiesConfig.cfg"
custom_blacklist_cfg_tmp_file="$(pwd)/TmpCustomBlackListPropertiesConfig.cfg"
# 属性前缀,属性前缀需要特殊处理
class_prefix=""
# 属性后缀
class_suffix="abc"


# 检测文件是否存在,不存在则创建
checkOrCreateFile() {
    file=$1
    if [[ -f $file ]]; then
        echo "检测到配置文件存在 $file"
    else
        echo "创建配置文件 $file"
        touch $file
    fi
}

# 配置文件检查
checkOrCreateFile $cfg_file

# 循环检测输入的文件夹
function checkInputDestDir {
    echo -n "请输入需处理源码目录: "
    read path
    if [[ -d $path ]]; then
        classes_dir=$path
    else
        echo -n "输入的目录无效,"
        checkInputDestDir
    fi
}

# 需处理源码目录检查
if [[ -d $classes_dir ]]; then
    echo "需处理源码目录存在 $classes_dir"
else
    echo "请确认需处理源码目录是否存在 $classes_dir"
    checkInputDestDir
fi


####### 数据定义

# 定义属性保存数组
declare -a rename_properties_config_content_array
cfg_line_count=0


# 读取属性配置文件
function read_rename_properties_configs {
    IFS_OLD=$IFS
    IFS=$'\n'
    # 删除文件行首的空白字符 http://www.jb51.net/article/57972.htm
    for line in $(cat $cfg_file | sed 's/^[ \t]*//g')
    do
        is_comment=$(expr "$line" : '^#.*')
        echo "line=${line} is_common=${is_comment}"
        if [[ ${#line} -eq 0 ]] || [[ $(expr "$line" : '^#.*') -gt 0 ]]; then
            echo "blank line or comment line"
        else
            rename_properties_config_content_array[$cfg_line_count]=$line
            cfg_line_count=$[ $cfg_line_count + 1 ]
            # echo "line>>>>${line}"
        fi  
    done
    IFS=${IFS_OLD}
}

function print_array {
    # 获取数组
    local newarray
    newarray=($(echo "$@"))
    for (( i = 0; i < ${#newarray[@]}; i++ )); do
        item=${newarray[$i]}
        echo "array item >>> ${item}"
    done
}

# 重命名所有的属性
function rename_properties {

    # 读取属性配置文件
    read_rename_properties_configs
    # print_array ${rename_properties_config_content_array[*]}

    # 执行替换操作
    for (( i = 0; i < ${#rename_properties_config_content_array[@]}; i++ )); do
        original_prop_name=${rename_properties_config_content_array[i]};
        result_prop_name="${class_prefix}${original_prop_name}${class_suffix}"
        sed -i '{
            s/'"${original_prop_name}"'/'"${result_prop_name}"'/g
        }' `grep ${original_prop_name} -rl ${classes_dir}`
        echo "正在处理属性 ${original_prop_name}....."
    done
}

checkOrCreateFile ${custom_blacklist_cfg_tmp_file}

# 获取自定义的黑名单属性并保存到文件中
echo "" > ${custom_blacklist_cfg_file}
for (( i = 0; i < ${#custom_blacklist_search_dirs[@]}; i++ )); do
    custom_blacklist_search_dir=${custom_blacklist_search_dirs[${i}]}
    ./GetAndStoreProperties.sh \
        -i ${custom_blacklist_search_dir}\
        -o ${custom_blacklist_cfg_tmp_file}
    cat ${custom_blacklist_cfg_tmp_file} >> ${custom_blacklist_cfg_file}
done


# 获取和保存属性到熟悉配置文件
./GetAndStoreProperties.sh \
    -i ${classes_dir}\
    -o ${cfg_file}\
    -f \
    -c ${custom_blacklist_cfg_file}


# 执行属性重命名
rename_properties

echo "done."

总结

以上就是基于shell脚本,以壳版本为场景,把属性的批量替换做了一个半自动化的实现步骤,如果不妥之处,还请不吝赐教。

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

推荐阅读更多精彩内容