Magisk原理之Boot补丁(二)

Magisk原理之Boot补丁

通过xml文件追踪到修补boot.img的入口为
com/topjohnwu/magisk/ui/install/InstallViewModel.kt

// 对原boot.img开始补丁
    fun install() {
        when (method) {
            R.id.method_patch -> FlashFragment.patch(data.value!!).navigate(true)
            R.id.method_direct -> FlashFragment.flash(0).navigate(true)
            R.id.method_inactive_slot -> FlashFragment.flash(1).navigate(true)
            R.id.method_direct_system -> FlashFragment.flash(2).navigate(true)
            else -> error("Unknown value")
        }
    }

明显这里我们要走FlashFragment.patch(data.value!!).navigate(true)

fun patch(uri: Uri) = MainDirections.actionFlashFragment(
            action = Const.Value.PATCH_FILE,
            additionalData = uri
        )

最终会来到com/topjohnwu/magisk/ui/flash/FlashFragment.kt
进入页面后的代码

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        defaultOrientation = activity?.requestedOrientation ?: -1
        activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
        if (savedInstanceState == null) {
            viewModel.startFlashing()
        }
    }


fun startFlashing() {
    ...
     Const.Value.PATCH_FILE -> {
                    uri ?: return@launch
                    showReboot = false
                    MagiskInstaller.Patch(uri, outItems, logItems).exec()
                }
    ...
}


 class Patch(
        private val uri: Uri,
        console: MutableList<String>,
        logs: MutableList<String>
    ) : MagiskInstaller(console, logs) {
        override suspend fun operations() = patchFile(uri)
    }


 protected fun patchFile(file: Uri) = extractFiles() && handleFile(file)

其实这里就简单了修补boot.img就分两个步骤
extractFiles 导出相关环境
handleFile 真正修补img

 /**
     * 这个相当于构建一个安装的工作目录,将相关的环境copy过去
     *
     *
     */
    private fun extractFiles(): Boolean {
        // 输出设备的 CPU 架构和版本信息到控制台
        console.add("- Device platform: ${Const.CPU_ABI}")
        console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")

        // 创建安装目录并清理旧数据
        installDir = localFS.getFile(context.filesDir.parent, "install")
        installDir.deleteRecursively() // 删除现有的安装目录及其内容
        installDir.mkdirs() // 创建新的安装目录

        try {
            // 判断当前是否作为 Stub(占位符)运行
            if (isRunningAsStub) {
                // 如果是 Stub 模式,从当前 APK 文件中提取二进制文件
                val zf = ZipFile(StubApk.current(context))

                // 定义是否提取 32 位的 magisk 库(适用于非纯 64 位设备)
                val is32lib = Const.CPU_ABI_32?.let {
                    { entry: ZipEntry -> entry.name == "lib/$it/libmagisk32.so" }
                } ?: { false }

                // 遍历 ZIP 文件中的条目,筛选出需要的库文件
                zf.entries().asSequence().filter {
                    !it.isDirectory && (it.name.startsWith("lib/${Const.CPU_ABI}/") || is32lib(it))
                }.forEach {
                    // 提取文件名
                    val n = it.name.substring(it.name.lastIndexOf('/') + 1)
                    val name = n.substring(3, n.length - 3) // 去掉前缀 "lib" 和后缀 ".so"
                    val dest = File(installDir, name) // 目标文件路径

                    // 将文件写入目标路径
                    zf.getInputStream(it).writeTo(dest)
                    dest.setExecutable(true) // 设置文件可执行权限
                }
                zf.close()
            } else {
                // 如果不是 Stub 模式,直接从 nativeLibraryDir 中加载库文件
                val info = context.applicationInfo

                // 获取库文件列表
                var libs = File(info.nativeLibraryDir).listFiles { _, name ->
                    name.startsWith("lib") && name.endsWith(".so")
                } ?: emptyArray()

                // 如果设备支持 32 位,添加 32 位的 magisk 库
                val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir")
                    .get(info) as String?
                if (lib32 != null) {
                    libs += File(lib32, "libmagisk32.so")
                }

                // 为每个库文件创建符号链接到安装目录
                for (lib in libs) {
                    val name = lib.name.substring(3, lib.name.length - 3) // 去掉前缀 "lib" 和后缀 ".so"
                    Os.symlink(lib.path, "$installDir/$name") // 创建符号链接
                }
            }

            // 提取脚本文件到安装目录
            for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh", "stub.apk")) {
                val dest = File(installDir, script)
                context.assets.open(script).writeTo(dest) // 从 assets 文件夹中读取并写入目标路径
            }

            // 提取 Chrome OS 工具到子目录 "chromeos"
            File(installDir, "chromeos").mkdir() // 创建子目录
            for (file in listOf("futility", "kernel_data_key.vbprivk", "kernel.keyblock")) {
                val name = "chromeos/$file"
                val dest = File(installDir, name)
                context.assets.open(name).writeTo(dest) // 从 assets 文件夹中读取并写入目标路径
            }
        } catch (e: Exception) {
            // 捕获异常,记录错误信息到控制台并返回失败状态
            console.add("! Unable to extract files")
            Timber.e(e)
            return false
        }

        // 如果需要使用 root 目录,将安装文件移动到 tmpfs
        if (useRootDir) {
            rootFS.getFile(Const.TMPDIR).also {
                arrayOf(
                    "rm -rf $it", // 删除目标目录中的现有内容
                    "mkdir -p $it", // 创建目标目录
                    "cp_readlink $installDir $it", // 复制安装目录内容到目标目录
                    "rm -rf $installDir" // 删除原始安装目录
                ).sh() // 执行 Shell 命令
                installDir = it // 更新安装目录为目标目录
            }
        }

        // 返回成功状态
        return true
    }

创建一个/data/user_de/0/io.github.huskydg.magisk/install目录

.sh文件来自asset 二进制可执行文件来自于lib 它以lib***.so的方式存储 所以extractFiles的工作还将修改会正确的名称
将执行的.sh和magiskinit magiskboot拷贝到上面的工作目录

private fun handleFile(uri: Uri): Boolean {
        val outStream: OutputStream
        val outFile: MediaStoreUtils.UriFile

        // 处理输入文件
        try {
            uri.inputStream().buffered().use { src ->
                ...

                // 生成唯一的随机文件名
                val alpha = "abcdefghijklmnopqrstuvwxyz"
                val alphaNum = "$alpha${alpha.uppercase(Locale.ROOT)}0123456789"
                val random = SecureRandom()
                val filename = StringBuilder("magisk_patched-${BuildConfig.VERSION_CODE}_").run {
                    for (i in 1..5) {
                        append(alphaNum[random.nextInt(alphaNum.length)])
                    }
                    toString()
                }

                // 检查文件是否为 tar 格式
                srcBoot = if (tarMagic.contentEquals("ustar".toByteArray())) {
                    ...
                } else {
                    // 处理其他格式(如 img 或 zip)
                    outFile = MediaStoreUtils.getFile("$filename.img", true)
                    outStream = outFile.uri.outputStream()

                    try {
                        if (magic.contentEquals("CrAU".toByteArray())) {
                            processPayload(src)
                        } else if (magic.contentEquals("PK\u0003\u0004".toByteArray())) {
                            // 处理 zip 文件
                            processZip(ZipInputStream(src))
                        } else {
                            // 将文件直接复制到缓存
                            console.add("- Copying image to cache")
                            // 处理 img 文件
                            installDir.getChildFile("boot.img").also {
                                src.copyAndCloseOut(it.newOutputStream())
                            }
                        }
                    } catch (e: IOException) {
                        ...
                    }
                }
            }
        } catch (e: IOException) {
            ...
        }

        // 执行补丁处理 处理成新的new-boot.img 真正的补丁位置
        if (!patchBoot()) {
            outFile.delete() // 如果失败,删除输出文件
            return false
        }

        // 输出补丁后的文件  将new-boot.img 输出到最后的magisk_patched-${BuildConfig.VERSION_CODE}_"文件中
        try {
            val newBoot = installDir.getChildFile("new-boot.img")
            if (outStream is TarOutputStream) {
                // 根据文件路径设定名称
                val name = with(srcBoot.path) {
                    when {
                        contains("recovery") -> "recovery.img"
                        contains("init_boot") -> "init_boot.img"
                        else -> "boot.img"
                    }
                }
                outStream.putNextEntry(newTarEntry(name, newBoot.length())) // 写入新条目
            }
            newBoot.newInputStream().copyAndClose(outStream) // 复制新文件
            newBoot.delete() // 删除临时文件

        } catch (e: IOException) {
            ...
        }

        // 清理和修复二进制文件
        srcBoot.delete()
        "cp_readlink $installDir".sh()

        return true
    }

这里可以看出handleFile主要就是为补丁后的boot.img取一个新的名称这个也是我们在Magisk修补的输出文件magisk_patched-${BuildConfig.VERSION_CODE}_.img
主要的修补工作是在patchBoot()中

private fun patchBoot(): Boolean {
        val newBoot = installDir.getChildFile("new-boot.img")
        if (!useRootDir) {
            // Create output files before hand
            newBoot.createNewFile()
            File(installDir, "stock_boot.img").createNewFile()
        }

        Log.i("SharkChilli", "cd $installDir ")
        Log.i(
            "SharkChilli", "KEEPFORCEENCRYPT=${Config.keepEnc} " +
                    "KEEPVERITY=${Config.keepVerity} " +
                    "PATCHVBMETAFLAG=${Info.patchBootVbmeta} " +
                    "RECOVERYMODE=${Config.recovery} " +
                    "LEGACYSAR=${Info.legacySAR} " +
                    "sh boot_patch.sh $srcBoot"
        )

        // cd /data/user_de/0/io.github.huskydg.magisk/install
        // KEEPFORCEENCRYPT=true KEEPVERITY=true PATCHVBMETAFLAG=false RECOVERYMODE=true LEGACYSAR=true sh boot_patch.sh /data/user_de/0/io.github.huskydg.magisk/install/boot.img
        val cmds = arrayOf(
            "cd $installDir",
            "KEEPFORCEENCRYPT=${Config.keepEnc} " +
                    "KEEPVERITY=${Config.keepVerity} " +
                    "PATCHVBMETAFLAG=${Info.patchBootVbmeta} " +
                    "RECOVERYMODE=${Config.recovery} " +
                    "LEGACYSAR=${Info.legacySAR} " +
                    "sh boot_patch.sh $srcBoot"
        )
        val isSuccess = cmds.sh().isSuccess

        shell.newJob().add("./magiskboot cleanup", "cd /").exec()

        return isSuccess
    }

实际就是使用shell脚本进入到前面创建的install工作目录,使用boot_patch.sh进行修补,所以可以了解到真正的patch的工作都是在boot_patch.sh脚本中

用于boot_patch.sh过长我截取一些关键的部分进行说明

...
./magiskboot unpack "$BOOTIMAGE"  # 解包 boot 镜像

...
# 检查并恢复 ramdisk 状态
if [ -e ramdisk.cpio ]; then
  ./magiskboot cpio ramdisk.cpio test
  STATUS=$?
  SKIP_BACKUP=""
else
  # Stock A only legacy SAR, or some Android 13 GKIs
  STATUS=0
  SKIP_BACKUP="#"
fi
...

case $((STATUS & 3)) in
  0 )  # Stock boot 镜像
    ui_print "- Stock boot image detected"
    SHA1=$(./magiskboot sha1 "$BOOTIMAGE" 2>/dev/null)
    cat $BOOTIMAGE > stock_boot.img
    cp -af ramdisk.cpio ramdisk.cpio.orig 2>/dev/null

...

SKIP32="#"
SKIP64="#"
# 压缩文件以节省 ramdisk 空间
if [ -f magisk64 ]; then
  $BOOTMODE && [ -z "$PREINITDEVICE" ] && PREINITDEVICE=$(./magisk64 --preinit-device)
  ./magiskboot compress=xz magisk64 magisk64.xz
  unset SKIP64
fi
if [ -f magisk32 ]; then
  $BOOTMODE && [ -z "$PREINITDEVICE" ] && PREINITDEVICE=$(./magisk32 --preinit-device)
  ./magiskboot compress=xz magisk32 magisk32.xz
  unset SKIP32
fi
./magiskboot compress=xz stub.apk stub.xz
# 将代码中的参数输出配置文件
echo "KEEPVERITY=$KEEPVERITY" > config
echo "KEEPFORCEENCRYPT=$KEEPFORCEENCRYPT" >> config
echo "RECOVERYMODE=$RECOVERYMODE" >> config
if [ -n "$PREINITDEVICE" ]; then
  ui_print "- Pre-init storage partition: $PREINITDEVICE"
  echo "PREINITDEVICE=$PREINITDEVICE" >> config
fi
[ -n "$SHA1" ] && echo "SHA1=$SHA1" >> config

# 更新 ramdisk 内容 此处其实就是真正的修补处
./magiskboot cpio ramdisk.cpio \
"add 0750 $INIT magiskinit" \
"mkdir 0750 overlay.d" \
"mkdir 0750 overlay.d/sbin" \
"$SKIP32 add 0644 overlay.d/sbin/magisk32.xz magisk32.xz" \
"$SKIP64 add 0644 overlay.d/sbin/magisk64.xz magisk64.xz" \
"add 0644 overlay.d/sbin/stub.xz stub.xz" \
"patch" \
"$SKIP_BACKUP backup ramdisk.cpio.orig" \
"mkdir 000 .backup" \
"add 000 .backup/.magisk config" \
|| abort "! Unable to patch ramdisk"

...
# 重新打包 boot 镜像
ui_print "- Repacking boot image"
./magiskboot repack "$BOOTIMAGE" || abort "! Unable to repack boot image"

这个脚本其实就是干了先将boot.img解压出来得到ramdisk,往ramdisk中修改替换我们的文件,然后重新打包会boot.img。这样就完成了patch
所以真正的修补逻辑应该就是

./magiskboot cpio ramdisk.cpio \
"add 0750 $INIT magiskinit" \
"mkdir 0750 overlay.d" \
"mkdir 0750 overlay.d/sbin" \
"$SKIP32 add 0644 overlay.d/sbin/magisk32.xz magisk32.xz" \
"$SKIP64 add 0644 overlay.d/sbin/magisk64.xz magisk64.xz" \
"add 0644 overlay.d/sbin/stub.xz stub.xz" \
"patch" \
"$SKIP_BACKUP backup ramdisk.cpio.orig" \
"mkdir 000 .backup" \
"add 000 .backup/.magisk config"

这些修改文件都是项目工程中的执行文件和内置apk,以及上面刚刚通过参数输出的配置文件,打包压缩的执行文件
我们接下来主要来关注一下magiskboot cpio

它的编译文件在此处
native/src/Android.mk

include $(CLEAR_VARS)
LOCAL_MODULE := magiskboot
LOCAL_STATIC_LIBRARIES := \
    libbase \
    libcompat \
    liblzma \
    liblz4 \
    libbz2 \
    libz \
    libzopfli \
    libboot-rs

LOCAL_SRC_FILES := \
    boot/main.cpp \
    boot/bootimg.cpp \
    boot/compress.cpp \
    boot/format.cpp \
    boot/boot-rs.cpp

include $(BUILD_EXECUTABLE)

那他的入口应该就是在boot/main.cpp

int main(int argc, char *argv[]) {
    ...
     else if (argc > 2 && action == "cpio") {
        return rust::cpio_commands(argc - 2, argv + 2) ? 0 : 1;
    }
    ...
}


bool cpio_commands(::std::int32_t argc, char const *const *argv) noexcept {
  return rust$cxxbridge1$cpio_commands(argc, argv);
}

这里可以看到这部分的逻辑使用了rust编写
对应的rust源码就是在native/src/boot/cpio.rs

/// 该函数是处理 `magiskboot` 中 CPIO 命令的主要入口。
/// 它接收命令行参数(`argc` 和 `argv`),并对它们进行处理以操作 CPIO 文件。
/// 
/// # 参数
/// - `argc`: 命令行参数的数量。
/// - `argv`: 指向命令行参数数组的指针。
/// 
/// # 返回值
/// - `true`: 如果函数成功处理了所有命令。
/// - `false`: 如果执行过程中发生错误。
pub fn cpio_commands(argc: i32, argv: *const *const c_char) -> bool {
    /// 内部函数,执行实际的 CPIO 命令处理。
    /// 返回一个 `LoggedResult`,表示成功 (`Ok(())`) 或包含日志信息的错误。
    fn inner(argc: i32, argv: *const *const c_char) -> LoggedResult<()> {
        // 确保至少有一个参数;否则记录错误并返回。
        if argc < 1 {
            return Err(log_err!("没有提供任何参数"));
        }

        // 将命令行参数解析为结构化形式。
        let cmds = map_args(argc, argv)?;

        // 初始化用于处理 CPIO 命令的 CLI,设置早期退出时打印用法信息。
        let mut cli =
            CpioCli::from_args(&["magiskboot", "cpio"], &cmds).on_early_exit(print_cpio_usage);

        // 从 CLI 参数中获取 CPIO 文件路径,并检查是否存在。
        let file = Utf8CStr::from_string(&mut cli.file);
        let mut cpio = if Path::new(file).exists() {
            // 如果文件存在,则加载 CPIO 文件。
            Cpio::load_from_file(file)?
        } else {
            // 否则,创建一个新的 CPIO 对象。
            Cpio::new()
        };

        // 处理 CLI 命令列表中的每个命令。
        for cmd in cli.commands {
            // 忽略以 '#' 开头的命令(视为注释)。
            if cmd.starts_with('#') {
                continue;
            }

            // 解析特定的 CPIO 子命令及其参数。
            let mut cli = CpioCommand::from_args(
                &["magiskboot", "cpio", file],
                cmd.split(' ')
                    .filter(|x| !x.is_empty())
                    .collect::<Vec<_>>()
                    .as_slice(),
            )
            .on_early_exit(print_cpio_usage);

            // 匹配解析后的子命令,并执行对应的操作。
            match &mut cli.command {
                CpioSubCommand::Test(_) => exit(cpio.test()), // 测试 CPIO 文件的完整性。
                CpioSubCommand::Restore(_) => cpio.restore()?, // 从备份中恢复 CPIO。
                CpioSubCommand::Patch(_) => cpio.patch(), // 对 CPIO 文件应用补丁。
                CpioSubCommand::Exists(Exists { path }) => {
                    // 检查 CPIO 中是否存在特定路径。
                    if cpio.exists(path) {
                        exit(0);
                    } else {
                        exit(1);
                    }
                }
                CpioSubCommand::Backup(Backup {
                    origin,
                    skip_compress,
                }) => cpio.backup(Utf8CStr::from_string(origin), *skip_compress)?, // 备份 CPIO。

                CpioSubCommand::Remove(Remove { path, recursive }) => cpio.rm(path, *recursive), // 从 CPIO 中移除文件/目录。
                CpioSubCommand::Move(Move { from, to }) => cpio.mv(from, to)?, // 在 CPIO 中移动/重命名文件。
                CpioSubCommand::MakeDir(MakeDir { mode, dir }) => cpio.mkdir(mode, dir), // 在 CPIO 中创建目录。
                CpioSubCommand::Link(Link { src, dst }) => cpio.ln(src, dst), // 在 CPIO 中创建符号链接。
                CpioSubCommand::Add(Add { mode, path, file }) => cpio.add(mode, path, file)?, // 将文件添加到 CPIO。
                CpioSubCommand::Extract(Extract { paths }) => {
                    // 从 CPIO 中提取文件,处理可选的源路径和目标路径。
                    if !paths.is_empty() && paths.len() != 2 {
                        return Err(log_err!("无效的参数"));
                    }
                    cpio.extract(
                        paths.get(0).map(|x| x.as_str()),
                        paths.get(1).map(|x| x.as_str()),
                    )?;
                }
                CpioSubCommand::List(List { path, recursive }) => {
                    // 列出 CPIO 中的文件,可选择递归列出。
                    cpio.ls(path.as_str(), *recursive);
                    exit(0);
                }
            };
        }

        // 处理完所有命令后,将更新的 CPIO 保存到文件中。
        cpio.dump(file)?;
        Ok(())
    }

    // 执行内部函数,并记录遇到的任何错误。
    inner(argc, argv)
        .log_with_msg(|w| w.write_str("处理 CPIO 失败"))
        .is_ok()
}

当解压的文件中有cpio在这个cpio基础上操作,没有的话就创建一个新的文件

"add 0750 $INIT magiskinit" \
"mkdir 0750 overlay.d" \
"mkdir 0750 overlay.d/sbin" \
"$SKIP32 add 0644 overlay.d/sbin/magisk32.xz magisk32.xz" \
"$SKIP64 add 0644 overlay.d/sbin/magisk64.xz magisk64.xz" \
"add 0644 overlay.d/sbin/stub.xz stub.xz" \
"patch" \
"$SKIP_BACKUP backup ramdisk.cpio.orig" \
"mkdir 000 .backup" \
"add 000 .backup/.magisk config"


对应的代码就是
CpioSubCommand::Add(Add { mode, path, file }) => cpio.add(mode, path, file)?, // 将文件添加到 CPIO。
CpioSubCommand::MakeDir(MakeDir { mode, dir }) => cpio.mkdir(mode, dir), // 在 CPIO 中创建目录。
CpioSubCommand::Patch(_) => cpio.patch(), // 对 CPIO 文件应用补丁。
CpioSubCommand::Backup(Backup {
                origin,
                skip_compress,
            }) => cpio.backup(Utf8CStr::from_string(origin), *skip_compress)?, // 备份 CPIO。

这里Add MakeDir就是对目录文件进行的操作,我们就不详细看了

所以补丁的核心就是

# 将名为 magiskinit 的文件添加到 ramdisk 中命令为init,并设置权限为 0750。 此处尤为重要这个就是上一部文章中第一阶段的init
# 参数含义:0750 表示文件拥有者有读、写、执行权限,同组用户有读和执行权限,其他用户无权限。
"add 0750 $INIT magiskinit" \

# 在 ramdisk 中创建名为 overlay.d 的目录,并设置权限为 0750。
# overlay.d 目录可能用于覆盖或扩展现有文件系统的某些功能。
"mkdir 0750 overlay.d" \

# 在 overlay.d 目录中创建名为 sbin 的子目录,并设置权限为 0750。
# overlay.d/sbin 目录可能用于存放补丁工具或可执行文件。
"mkdir 0750 overlay.d/sbin" \

# 如果 SKIP32 未定义,则将压缩的 magisk32.xz 添加到 overlay.d/sbin 目录中,设置权限为 0644。
# 参数含义:0644 表示文件拥有者有读写权限,其他用户只有读取权限。
"$SKIP32 add 0644 overlay.d/sbin/magisk32.xz magisk32.xz" \

# 如果 SKIP64 未定义,则将压缩的 magisk64.xz 添加到 overlay.d/sbin 目录中,设置权限为 0644。
"$SKIP64 add 0644 overlay.d/sbin/magisk64.xz magisk64.xz" \

# 将压缩的 stub.xz 文件添加到 overlay.d/sbin 目录中,并设置权限为 0644。
# stub.xz 可能是一个占位文件或轻量级工具文件。
"add 0644 overlay.d/sbin/stub.xz stub.xz" \

# 调用 patch 命令,修补 ramdisk 文件。
# patch 通常会应用特定逻辑修改 fstab 或其他重要文件,移除 `verity` 校验和强制加密等机制。
"patch" \

# 如果 SKIP_BACKUP 未定义,则备份当前的 ramdisk 文件到 ramdisk.cpio.orig。
# 备份是为了确保在修补出问题时可以还原。
"$SKIP_BACKUP backup ramdisk.cpio.orig" \

# 在 ramdisk 中创建名为 .backup 的隐藏目录,并设置权限为 000(不可读、写、执行)。
# 这种目录可能用于存放内部工具或配置文件,防止被直接访问。
"mkdir 000 .backup" \

# 在 .backup 目录中添加名为 .magisk 的隐藏文件,权限设置为 000。
# 该文件可能是用于存储 Magisk 的特定配置信息。
"add 000 .backup/.magisk config"

总结

总的来说boot补丁的核心就解压boot.img,得到ramdisk如果没有就new一个,然后将自己的文件都放进去,最后打包回去成一个新的boot.img
最重要的是将magiskinit替换到了ramdisk的init中,这个就是启动第一阶段的init,这样将新的boot.img刷入后启动入口就到了magiskinit

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

推荐阅读更多精彩内容