Android插件化与热修复(四)---DroidPlugin hook 系统service

内容概要

本篇文章主要回答以下问题:

  1. 为什么要hook 系统service
  2. 应该在哪些环节来hook,为什么是这些环节

前提说明

  • 首先,本篇文章需要对Hook机制有比较清晰的了解,关于Hook机制,可以参考上篇文章《 Android插件化与热修复(三)---DroidPlugin Hook机制》

  • 另外,最好参考着DroidPlugin的源码跟着文章一步步来看,有助于理解文章当前讲的内容。

Binder机制介绍

几点说明

  • 了解Binder机制,有助于理解本文内容。
  • 以下Binder机制介绍的内容来自网上的一篇博客。
  • 大家有个粗略的认识即可,对于Binder机制的介绍并不是本文的重点,也不影响对本文的理解,想深入了解的,可以参考该博客的文章。

Binder机制

Binder 是Android系统采用的进程间通信(IPC)机制。 IPC机制有好多种, Binder 只是其中一种。
Binder机制中包含四个组件Client、Server、Service Manager和Binder驱动程序。

Binder机制
  1. Client、Server和Service Manager实现在用户空间中,Binder驱动程序实现在内核空间中
  2. Binder驱动程序和Service Manager在Android平台中已经实现,开发者只需要在用户空间实现自己的Client和Server
  3. Binder驱动程序提供设备文件/dev/binder与用户空间交互,Client、Server和Service Manager通过open和ioctl文件操作函数与Binder驱动程序进行通信
  4. Client和Server之间的进程间通信通过Binder驱动程序间接实现
  5. Service Manager是一个守护进程,用来管理Server,并向Client提供查询Server接口的能力

文章链接:http://blog.csdn.net/luoshengyang/article/details/6618363

使用AIDL调用远程Service的过程

为了搞清楚应该在哪些环节来hook,为什么是这些环节,我们需要对使用AIDL调用远程Service的过程有个清楚的了解。

新建一个aidl文件

定义通讯的接口
IRemoteService.aidl

interface IRemoteService {

    String getValue();

}

IDE会为我们自动生成IRemoteService.java
将该文件格式化后,我们来看看该文件的结构

IRemoteService.java

将Stub类折叠后,我们发现这里面的结构其实很简单

Stub类

看一下Stub类的声明:
public static abstract class Stub extends android.os.Binder implements com.example.jst.androidtest.IRemoteService

  • extends android.os.Binder
    android.os.Binder 实现了 IBinder接口,看一下IBinder接口源码的介绍

Base interface for a remotable object

所以Stub对象就是一个remotable object (可远程化的对象),通俗点,就是这个对象可以跨进程来使用(当然,肯定不是直接使用另一个进程的对象,具体实现原理就牵涉到底层实现了)。

  • implements IRemoteService
    Stub并没有真正的实现,而是定义为abstract,这样子类就必须具体实现这个接口。

综上,也就是说Stub对象是一个实现了IRemoteService接口的可以跨进程使用的对象。

下面在RemoteService里会看到它的使用。

实现远程服务 RemoteService

public class RemoteService extends Service {

    public class RemoteServiceImpl extends IRemoteService.Stub {
        @Override
        public String getValue() throws RemoteException {
            return "从RemoteService获得的值.";
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new RemoteServiceImpl();
    }
}

这里主要就用到了IRemoteService.Stub类,你定义一个IRemoteService.Stub类的子类来具体实现IRemoteService接口,另外因为Stub类实现了IBinder接口所以可以作为onBind方法的返回值,同时因为实现了IBinder接口,所以Stub对象是一个可以跨进程使用的对象。

Manifest文件里注册RemoteService

        <service
            android:name="com.example.jst.androidtest.RemoteService"
            android:process=":remote"/>

android:process=":remote" 指定了该服务是在另一个进程中。

程序中bind RemoteService

public class MainActivity extends Activity {

    private IRemoteService remoteService;

    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //IRemoteService.Stub.asInterface 将 经过跨进程传输过来的IBinder对象 转换成了我们自定义的接口对象IRemoteService。
            remoteService = IRemoteService.Stub.asInterface(service);

            //通过IRemoteService对象我们才能调用我们自定义的接口方法getValue。
            try {
                remoteService.getValue();
            } catch (RemoteException e) {
                e.printStackTrace();
            }

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };
}

bind RemoteService时最主要的就是ServiceConnection,在onServiceConnected里会将我们在RemoteService的onBind方法里返回的IBinder对象作为参数传递过来(实际上IBinder对象是被跨进程传输的),这里有一个关键方法IRemoteService.Stub.asInterface,这个asInterface方法将IBinder对象转换成了我们能使用的IRemoteService对象。

android 系统Service 使用过程

以TELEPHONY_SERVICE为例,我们在使用系统的service的时候一般都是这么用的:

TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
tm.getSimState();

系统提供的Service也是运行在其他进程的,我们在使用系统的Service时内部也是用的AIDL这种标准的方式,只不过是系统帮我们封装好了一些便利的工具类而已。

在介绍应该在哪些环节来hook 之前,我们先来了解为什么要hook 系统service。

hook 系统service的目的

遇到的问题

上面讲了我们在应用中使用系统Service的方式,我们在插件apk中肯定也想使用系统的Service,但是系统Service在被使用的过程中会对使用者的包名进行验证,因为插件apk没有执行真正的安装,所以系统Service不认识它,就会通不过验证,从而不能使用系统Service.

解决方法

我们把在使用系统Service的过程中的某些步骤hook掉,把插件apk的包名替换成宿主的包名,因为宿主是真实安装的,所以可以通过系统Service的验证。实际上就是偷梁换柱。

应该在哪个步骤来进行hook

参考前面 "程序中bind RemoteService" 那一节的介绍

    private IRemoteService remoteService;

    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
           
           remoteService = IRemoteService.Stub.asInterface(service);
           remoteService.getValue();
        }
    }

我们应用最终使用的是 IRemoteService.Stub.asInterface(service)转换后的IRemoteService接口对象,是在这个接口对象上调用的接口方法。我们的调用方法传的参数什么的也是在调用这个接口对象上的方法时传的,所以如果我们能把这个asInterface方法转换后生成的接口对象hook掉,就能把插件apk的包名替换成宿主的包名。

如何hook掉asInterface转换后的接口对象

以ITelephonyBinderHook为例,这里hook的原理跟上节讲的一样,可以参考上节的文章。

ITelephonyBinderHook

public class ITelephonyBinderHook extends BinderHook {

    public ITelephonyBinderHook(Context hostContext) {
        super(hostContext);
    }


    private final static String SERVICE_NAME = Context.TELEPHONY_SERVICE;

    @Override
    Object getOldObj() throws Exception {
        //获取该Service对应的IBinder对象
        IBinder iBinder = MyServiceManager.getOriginService(SERVICE_NAME);
        //调用asInterface方法将该iBinder对象转换为 接口对象 该返回值是作为被hook的oldObj被存储起来的
        return ITelephonyCompat.asInterface(iBinder);
    }

    @Override
    public String getServiceName() {
        return SERVICE_NAME;
    }

    @Override
    protected BaseHookHandle createHookHandle() {
        return new ITelephonyHookHandle(mHostContext);
    }
}

看看父类BinderHook

abstract class BinderHook extends Hook implements InvocationHandler {

    private Object mOldObj;

    public BinderHook(Context hostContext) {
        super(hostContext);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//与上节讲的一样,略
    }

    abstract Object getOldObj() throws Exception;

    void setOldObj(Object mOldObj) {
        this.mOldObj = mOldObj;
    }

    public abstract String getServiceName();

    @Override
    protected void onInstall(ClassLoader classLoader) throws Throwable {
        //暂时忽略 一会会介绍
        new ServiceManagerCacheBinderHook(mHostContext, getServiceName()).onInstall(classLoader);
        //获取子类实现的getOldObj()产生的mOldObj
        mOldObj = getOldObj();
        //生成代理对象
        Class<?> clazz = mOldObj.getClass();
        List<Class<?>> interfaces = Utils.getAllInterfaces(clazz);
        Class[] ifs = interfaces != null && interfaces.size() > 0 ? interfaces.toArray(new Class[interfaces.size()]) : new Class[0];
        Object proxiedObj = MyProxy.newProxyInstance(clazz.getClassLoader(), ifs, this);
        //将代理对象存储起来
        MyServiceManager.addProxiedObj(getServiceName(), proxiedObj);
    }
}

跟上节讲的hook机制的原理是一样的。

HookedMethodHandler跟上节有一点不太一样。

来看看ITelephonyHookHandle

public class ITelephonyHookHandle extends BaseHookHandle {

    @Override
    protected void init() {

        sHookedMethodHandlers.put("dial", new MyBaseHandler(mHostContext));
        sHookedMethodHandlers.put("call", new MyBaseHandler(mHostContext));
        sHookedMethodHandlers.put("endCall", new MyBaseHandler(mHostContext));
        sHookedMethodHandlers.put("endCallForSubscriber", new MyBaseHandler(mHostContext));
        sHookedMethodHandlers.put("answerRingingCall", new MyBaseHandler(mHostContext));
        sHookedMethodHandlers.put("answerRingingCallForSubscriber", new MyBaseHandler(mHostContext));
  //……
    }

    private static class MyBaseHandler extends ReplaceCallingPackageHookedMethodHandler {
        public MyBaseHandler(Context context) {
            super(context);
        }
    }
}

你会发现所有的HookedMethodHandler对象都是同一个MyBaseHandler,说明hook这些方法的目的都是一个,具体目的是什么呢,来看一下ReplaceCallingPackageHookedMethodHandler

ReplaceCallingPackageHookedMethodHandler

class ReplaceCallingPackageHookedMethodHandler extends HookedMethodHandler {

    public ReplaceCallingPackageHookedMethodHandler(Context hostContext) {
        super(hostContext);
    }

    @Override
    protected boolean beforeInvoke(Object receiver, Method method, Object[] args) throws Throwable {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
            if (args != null && args.length > 0) {
                for (int index = 0; index < args.length; index++) {
                    if (args[index] != null && (args[index] instanceof String)) {
                        String str = ((String) args[index]);
                        //如果是插件apk的包名
                        if (isPackagePlugin(str)) {
                            //替换成宿主的包名
                            args[index] = mHostContext.getPackageName();
                        }
                    }
                }
            }
        }
        return super.beforeInvoke(receiver, method, args);
    }

    private static boolean isPackagePlugin(String packageName) throws RemoteException {
        return PluginManager.getInstance().isPluginPackage(packageName);
    }
}

这里就做了一件事,就是将插件apk的包名替换成宿主的包名,这样在插件apk里就能正常的使用系统的service了。

代理对象挂载的问题:

一个非常重要的问题
回看一下BinderHook类的onInstall方法

//将代理对象存储起来
MyServiceManager.addProxiedObj(getServiceName(), proxiedObj);

hook是hook掉了,但是这里只是把我们产生的代理对象存储了起来,并没有把它挂载到系统上,从而让应用在使用系统Service的时候使用到我们生成的代理对象。
为什么这里没法将我们产生的代理对象挂载上去呢。
我们来回顾一下上节讲到的代理对象是如何挂载上去的。

IPackageManagerHook的onInstall方法

public class IPackageManagerHook extends ProxyHook {

    @Override
    protected void onInstall(ClassLoader classLoader) throws Throwable {
        
        Object currentActivityThread = ActivityThreadCompat.currentActivityThread();
        //从主线程对象里通过反射拿到sPackageManager对象,作为原始对象赋值给mOldObj
        setOldObj(FieldUtils.readField(currentActivityThread, "sPackageManager"));
        Class<?> iPmClass = mOldObj.getClass();
        //生成代理对象
        List<Class<?>> interfaces = Utils.getAllInterfaces(iPmClass);
        Class[] ifs = interfaces != null && interfaces.size() > 0 ? interfaces.toArray(new Class[interfaces.size()]) : new Class[0];
        Object newPm = MyProxy.newProxyInstance(iPmClass.getClassLoader(), ifs, this);
        //用代理对象替换原始对象
        FieldUtils.writeField(currentActivityThread, "sPackageManager", newPm);
        //调用宿主的context的getPackageManager获取PackageManager对象
        PackageManager pm = mHostContext.getPackageManager();
        Object mPM = FieldUtils.readField(pm, "mPM");
        //如果该对象不是我们的代理对象,就把该对象也替换成我们的代理对象
        if (mPM != newPm) {
            FieldUtils.writeField(pm, "mPM", newPm);
        }
    }

}

这里,我们找到了被hook的原始对象在系统的哪些类中是作为成员变量的,这个例子中是ActivityThread里的sPackageManager和PackageManager里的mPM。
我们需要把这些成员变量都替换成我们的代理对象,因为这些类都有可能被使用到。
而对于asInterface方法转换后的对象,我猜测有可能是并没有作为某个系统类的成员变量来存储,所以我们无法像IPackageManagerHook那样去挂载。
那么,如何才能将我们产生的代理对象挂载上去呢?

解决方案

    private IRemoteService remoteService;

    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
           
           remoteService = IRemoteService.Stub.asInterface(service);
           remoteService.getValue();
        }
    }

作为onServiceConnected参数的IBinder对象是很容易被挂载的,所以可以通过IBinder对象来解决挂载的问题。为什么通过IBinder对象能解决挂载的问题,具体原理跟asInterface方法内部的实现原理有关,后面会介绍到。
为什么IBinder对象是很容易被挂载的,我们需要先看看ServiceManager

Service管理者--ServiceManager

package android.os;

/** @hide */
public final class ServiceManager {
    private static final String TAG = "ServiceManager";

    private static IServiceManager sServiceManager;
    private static HashMap<String, IBinder> sCache = new HashMap<String, IBinder>();

    private static IServiceManager getIServiceManager() {
        if (sServiceManager != null) {
            return sServiceManager;
        }

        // Find the service manager
        sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());
        return sServiceManager;
    }

    /**
     * Returns a reference to a service with the given name.
     * 
     * @param name the name of the service to get
     * @return a reference to the service, or <code>null</code> if the service doesn't exist
     */
    public static IBinder getService(String name) {
        try {
            IBinder service = sCache.get(name);
            if (service != null) {
                return service;
            } else {
                return getIServiceManager().getService(name);
            }
        } catch (RemoteException e) {
            Log.e(TAG, "error in getService", e);
        }
        return null;
    }

    /**
     * Place a new @a service called @a name into the service
     * manager.
     * 
     * @param name the name of the new service
     * @param service the service object
     */
    public static void addService(String name, IBinder service) {
        try {
            getIServiceManager().addService(name, service, false);
        } catch (RemoteException e) {
            Log.e(TAG, "error in addService", e);
        }
    }

    /**
     * Place a new @a service called @a name into the service
     * manager.
     * 
     * @param name the name of the new service
     * @param service the service object
     * @param allowIsolated set to true to allow isolated sandboxed processes
     * to access this service
     */
    public static void addService(String name, IBinder service, boolean allowIsolated) {
        try {
            getIServiceManager().addService(name, service, allowIsolated);
        } catch (RemoteException e) {
            Log.e(TAG, "error in addService", e);
        }
    }
    
}

ServiceManager主要作用是管理Service:

  • sCache:提供了Service的缓存 HashMap<String, IBinder> sCache, ActivityThread在bindApplication()的时候,会从ServiceManager那边获得service cache。有这个service cache之后可以减少和ServiceManager的IPC(具体参考下面getService执行过程的介绍)。
  • getService 方法:获取Service。 方法说明:先读缓存sCache ,如果没有再通过IPC来获取(具体实现看源码)
  • addService方法: 添加Service 。

这里ServiceManager里存储的IBinder对象就是远程Service跨进程传输过来的IBinder对象,就是我们调用asInterface转换成接口对象的参数对象。
这里的IBinder对象就是ServiceManager的一个成员变量sCache,所以很容易被挂载上去。

解释如何通过IBinder对象和asInterface方法来实现我们生成的代理对象的挂载问题,需要先看看asInterface方法的内部实现。

asInterface方法源码

还是以前面讲的 AIDL调用远程Service 的例子为例

    public static abstract class Stub extends android.os.Binder implements com.example.jst.androidtest.IRemoteService {
        private static final java.lang.String DESCRIPTOR = "com.example.jst.androidtest.IRemoteService";

        /**
         * Cast an IBinder object into an com.example.jst.androidtest.IRemoteService interface,
         * generating a proxy if needed.
         */
        public static com.example.jst.androidtest.IRemoteService asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            //如果iin 不为空  并且是IRemoteService类型的 就返回iin
            if (((iin != null) && (iin instanceof com.example.jst.androidtest.IRemoteService))) {
                return ((com.example.jst.androidtest.IRemoteService) iin);
            }
            //否则返回一个实现了IRemoteService接口的Proxy对象
            return new com.example.jst.androidtest.IRemoteService.Stub.Proxy(obj);
        }

        private static class Proxy implements com.example.jst.androidtest.IRemoteService {
//实现细节略
        }

因为是跨进程使用的,所以我们在我们的应用进程中拿到的IBinder对象的类型并不是我们在RemoteService里的那个继承了android.os.Binder 实现了 IRemoteService接口的 IRemoteService.Stub类的子类RemoteServiceImpl,因为是跨进程的,所以底层会进行一些处理,我们这里拿到的IBinder对象的类型实际上是BinderProxy类型。(如果RemoteService没有跨进程,这里拿到的IBinder对象就不是BinderProxy类型的,而是RemoteServiceImpl对象本身)

BinderProxy


final class BinderProxy implements IBinder {
//……
    public IInterface queryLocalInterface(String descriptor) {
        return null;
    }
//……
}

BinderProxy定义在android.os.Binder.java文件里,但是并不是Binder类的内部类。

需要注意的是BinderProxy的queryLocalInterface方法返回的是null。(如果不是跨进程的,queryLocalInterface方法就是android.os.Binder里的queryLocalInterface方法,并不是返回null,具体参考android.os.Binder源码)
通过看asInterface源码发现,如果queryLocalInterface返回null,asInterface方法会返回一个实现了IRemoteService接口的Proxy对象,所以一般情况下在跨进程的时候我们在外面使用的也就是这个Proxy对象。但是,这里还有一个分支是queryLocalInterface不返回null,而是返回一个实现了IRemoteService接口的对象,此时asInterface方法返回的就是这个对象。关键就在这里,如果我们hook掉这个IBinder对象的queryLocalInterface方法,让queryLocalInterface返回我们在前面生成的代理对象,asInterface方法就会返回这个代理对象,这样外界拿到的就是这个代理对象,不就解决了我们前面生成的代理对象的挂载问题了吗。

对IBinder对象的hook ServiceManagerCacheBinderHook

onInstall

    @Override
    protected void onInstall(ClassLoader classLoader) throws Throwable {
        //从ServiceManager对象里读出来sCache
        Object sCacheObj = FieldUtils.readStaticField(ServiceManagerCompat.Class(), "sCache");
        if (sCacheObj instanceof Map) {
            Map sCache = (Map) sCacheObj;
            //从sCache中读出来该mServiceName对应的service
            Object Obj = sCache.get(mServiceName);
            if (Obj != null && false) {
                //FIXME 已经有了怎么处理?这里我们只是把原来的给remove掉,再添加自己的。程序下次取用的时候就变成我们hook过的了。
                //但是这样有缺陷。
                throw new RuntimeException("Can not install binder hook for " + mServiceName);
            } else {
                //将该service移除
                sCache.remove(mServiceName);
                //这里调用ServiceManager的getService方法来取原始的IBinder对象,参考getService源码我们知道,因为sCache里的被移除了,
                //所以这里会执行一次IPC来获取原始的IBinder对象,这里为什么不用sCache里的对象作为原始的IBinder对象呢,我想应该是担心sCache
                //里有可能已经是替换过的代理对象了,比如你不知道在之前的哪个时机已经将代理对象替换了原始对象,所以执行一次IPC从系统全新获取
                //的这个对象一定是原始的IBinder对象。
                IBinder mServiceIBinder = ServiceManagerCompat.getService(mServiceName);
                if (mServiceIBinder == null) {
                    //这里做了异常补救,如果调用getService方法没拿到原始的IBinder对象 就看看从sCache里面的那个是不是原始对象,
                    // 如果不是代理对象,说明是原始对象
                    if (Obj != null && Obj instanceof IBinder && !Proxy.isProxyClass(Obj.getClass())) {
                        mServiceIBinder = ((IBinder) Obj);
                    }
                }
                if (mServiceIBinder != null) {
                    //MyServiceManager是工具类,用来存储数据,这里将原始的IBinder对象存起来
                    MyServiceManager.addOriginService(mServiceName, mServiceIBinder);
                    //生成代理对象
                    Class clazz = mServiceIBinder.getClass();
                    List<Class<?>> interfaces = Utils.getAllInterfaces(clazz);
                    Class[] ifs = interfaces != null && interfaces.size() > 0 ? interfaces.toArray(new Class[interfaces.size()]) : new Class[0];
                    IBinder mProxyServiceIBinder = (IBinder) MyProxy.newProxyInstance(clazz.getClassLoader(), ifs, this);
                    //将代理对象添加进sCache中
                    sCache.put(mServiceName, mProxyServiceIBinder);
                    //将代理对象也存储起来
                    MyServiceManager.addProxiedServiceCache(mServiceName, mProxyServiceIBinder);
                }
            }
        }
    }

该onInstall方法是在BinderHook的onInstall方法里被调用,具体看文章前面对BinderHook的介绍。

ServiceManagerHookHandle

    private class ServiceManagerHookHandle extends BaseHookHandle {

        private ServiceManagerHookHandle(Context context) {
            super(context);
        }

        @Override
        protected void init() {
            sHookedMethodHandlers.put("queryLocalInterface", new queryLocalInterface(mHostContext));
        }


        class queryLocalInterface extends HookedMethodHandler {
            public queryLocalInterface(Context context) {
                super(context);
            }

            @Override
            protected void afterInvoke(Object receiver, Method method, Object[] args, Object invokeResult) throws Throwable {
                Object localInterface = invokeResult;
                //这里是我们在 BinderHook.onInstall() 方法里生成的代理对象
                Object proxiedObj = MyServiceManager.getProxiedObj(mServiceName);
                if (localInterface == null && proxiedObj != null) {
                    setFakedResult(proxiedObj);
                }
            }
        }
    }

至此,hook 系统service的内容就讲解完了,为了让插件apk里能够透明的使用系统的Service真是不容易,这里比较难的点是hook了好几个地方,一定要知道hook各个地方的目的是什么,才能更好的理解其中的原理

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

推荐阅读更多精彩内容