计算机病毒分析

图片发自简书作者發姨

一、ollydbg(od反汇编工具)

ollydbg(od反汇编工具是当前逆向工程主流的动态跟踪调试工具,ollydbg(od反汇编工具)适合32位动态调试,调试过程可随时插入全局标签,过程直观简练,是反汇编工作必备的调试工具。

来看一小段代码:

#include "stdafx.h"
int main(int argc, char* argv[]){
printf("Hello,world!\n");
return 0;
}

上述代码中只执行了两条命令:printf、return,用于实现打印输出和返回。

下面逐条解释反汇编代码的各个命令。

首先载入OD。图1显示了各个界面窗口。


图1 OD界面窗口

在每个窗口右击可得到对应窗口的菜单。

在一个程序被载入OD进行动态反汇编的时候,首要的一点就是找到入口点,不同IDE编译出的程序的入口点会有所不同。

VC 6.0编译出的程序,入口点是三个压栈指令push,这个虽然在int main(int argc, char*argv[])定义的时候没有写,但是执行的时候默认还是传递3个参数,如图2所示。


图2 main函数入口

实际在这三条指令之前执行了很多指令,其作用是初始化可执行文件的空间,并激活主线程。接下来会执行获取命令行的API以便获取命令行信息。

这里说一下Call指令,熟悉VB的朋友可能会知道Call指令,比如下面一个简短的Test()子函数。

Private Sub Form_Load()
Call Test
End Sub
Sub Test()
MsgBox "我是测试"
End Sub

程序执行后进入Load函数执行Call Test指令,其作用就是激活Test子函数。在汇编语言中有详细解释,Call执行后程序的流程就进入了Test子函数里,上述汇编代码则是进入了00401000这个地址内,因为Call支持直接进入Call地址、Call寄存器。

如果想看Call所指示的子函数的话,可按OD的快捷键F7。这里直接按下F7键跟进,看到如图3所示结果。


图3 子函数

这里有个类似中括号的符号,括住的内容就是一段子函数,其结构如图4所示。


图4 子函数结构

参数是保存在栈中的,而栈是通过ss:sp来定位的。每次用Call调用子函数的时候,如果有参数必然会调用push,因为push是压栈操作,意思是将数据压入栈内。

00401000 /$ 68 30704000 push 00407030 ;
ASCII "Hello World!",
LF
//这一条指令的意思是将00407030地址内的ASCII字符"Hello World"压入堆栈
00401005 |. E8 06000000 call 00401010
//跳转到 00401010 地址去执行代码,这里的00401010就相当于Sub Test()
0040100A |. 83C4 04 add esp, 4
//平衡栈空间
0040100D |. 33C0 xor eax, eax
//清空寄存器
0040100F \. C3 retn
//返回

至此可以大概地梳理出代码的反汇编代码执行流程。

#include "stdafx.h"
int main(int argc, char* argv[])
{
printf("Hello,world!\n");
return 0;
}

首先执行主函数main。因为main有3个参数,所以要把这3个参数事先压入到栈空间,以便调用main的时候可以在main函数内部得到参数。

压入完成后执行Call函数,跳转到main主程序。main程序首先调用printf函数,因为printf函数有一个参数,所以在调用函数之前先把参数压入到栈中再调用,所以就有如下的反汇编代码。

00401000 /$ 68 30704000 push 00407030 ; ASCII “Hello World!”,LF
00401005 |. E8 06000000 call 00401010

调用完成之后需要平衡栈空间,由于这里的系统是32位的,所以参数就是4。

0040100A |. 83C4 04 add esp, 4

清空eax寄存器后返回,执行return 0。这就是上述代码的执行流程。

再看一段代码。

#include "stdafx.h"
int main(int argc, char* argv[])
{
int a;
scanf("%d",&a);
a--;
if(a==1)
{printf("成功\n");}
else
{printf("失败\n");}
return 0;
}

从代码逻辑看,可以知道输入1会提示成功,其他值均为失败。这里是最基本的判断,其中只是将a--变形了一下。

下面是上面代码的反汇编的main代码。

00401000 /$ 51 push ecx ; OllyDbgT.004080C8
//保护现场
00401001 |. 8D4424 00 lea eax, dword ptr [esp]
//获取int a 变量的地址保存进eax寄存器内
00401005 |. 50 push eax
//将a的地址压栈
00401006 |. 68 40804000 push 00408040 ; ASCII "%d"
//将字符压栈
0040100B |. E8 71000000 call 00401081
//调用scanf函数
00401010 |. 8B4424 08 mov eax, dword ptr [esp+8]
//将输入的数字保存进eax寄存器内,这里的esp+8实际因为a地址是第二个参数
00401014 |. 83C4 08 add esp, 8
//平衡栈空间
00401017 |. 48 dec eax
//这里是eax内的数据进行减1操作
00401018 |. 83F8 01 cmp eax, 1
//将eax寄存器的内容与1进行比较
0040101B |. 894424 00 mov dword ptr [esp], eax
//将eax寄存器里的内容保存进a变量内
0040101F |. 75 11 jnz short 00401032
//如果不为2则跳转
00401021 |. 68 38804000 push 00408038
//压入“成功”字符
00401026 |. E8 25000000 call 00401050
//执行打印输出函数
0040102B |. 83C4 04 add esp, 4
//平衡栈
0040102E |. 33C0 xor eax, eax
//清零
00401030 |. 59 pop ecx
//弹出ecx
00401031 |. C3 retn
//返回
00401032 |> 68 30804000 push 00408030
//压入“失败”字符
00401037 |. E8 14000000 call 00401050
//执行打印输出函数
0040103C |. 83C4 04 add esp, 4
//平衡栈
0040103F |. 33C0 xor eax, eax
//清零
00401041 |. 59 pop ecx
//弹出ecx
00401042 \. C3 retn
//返回

可以看到00401018 |. 83F8 01 cmp eax,1是个关键,把1修改为2试试?

下面来解释软件调试分析中的部分术语。

栈是一组连续的内存地址,其大小为<=2^操作系统位数为32,ss:sp为两个与栈有关的寄存器。

ss为段寄存器。

sp为偏移寄存器。

ss:sp在任意时刻都指向栈顶,即栈空间最大值加上一个字节。

当栈为空的时候,栈里没有元素,也就没有了所谓的栈顶元素,只能指向下一个内存单元。例如:将10000H~1000FH作为栈,当其为空的时候,其中ss:sp为10000H~10010H;当要压入元素时ax=2266H,则执行push ax时,ss:sp指向的栈顶为10000H~10010H-2(这里的2可能是位数),也就是10000H~1000EH,然后写入数据,高地址数据存入高地址,低地址数据存入低地址。

ah=22存入1000FH,al=66存入1000EH,压入栈的时候sp+2,弹出栈的时候sp-2。

拿到样本(这里的样本默认为可执行文件)后,要用杀病毒软件进行查杀,然后得到样本的杀毒软件定义名称,接着用搜索引擎进行搜索查询,看看有没有有用的信息。接着还要用PEiD查看这个样本的输入表,看都有什么函数,然后再看这个程序是否是窗口程序。

如果是:确认这个窗口是否是MFC窗口,如果是就检查MFC窗口启动前有没有多余的操作。分析的时候结合IDA能够更快地得到启动函数的内存地址,然后可以对比MFC启动函数的源代码。

如果不是:也需要找到启动函数。这个大家可以自行编写一些窗口程序进行分析。

首先打开一款HIPS软件(能够记录较多行为的软件),然后直接在OllyDbg运行(已经对样本输入表进行下断)。这一遍只是粗略地结合HIPS日志,大体地了解这个样本的功能;然后才是根据这些零散的功能做具体分析。

例如下载者样本,在运行后由于是下载者,必要的工作是验证是否联网,(有些编写者不验证网络直接下载,这对分析没有太大影响),然后寻找服务器,得到下载地址路径。如果是批量下载可能还会遇到字符的处理问题,比如以下格式的下载地址。

http:\\1.1.1.1\1.txt

内容如下:

http:\\1.1.1.1\1.exe,NULL,运行
http:\\1.1.1.1\2.DLL,注入到QQ.exe,运行
http:\\1.1.1.1\3.exe,NULL,运行
http:\\1.1.1.1\4.exe,NULL,3秒运行

当从http:\\1.1.1.1\1.txt获取样本后,会获取到上述格式的字符串。

这时就要进行字符截取,首先是回车符,其次是逗号,然后定义一个数组。这里只给出伪代码:

字符串=获取网络地址字符(“http:\\1.1.1.1\1.txt”);
数组=分割字符(字符串,回车符);
数组1=分割字符(数组,逗号);

数组1[0] //为地址
数组1[1] //为指令
数组1[2] //是直接运行还是等待运行
然后再根据指令下载。

上述是下载者在下载前对字符的处理,接下来介绍OllyDbg的下断方式。

首先说明一下中文帮助文档。OllyDbg支持数种不同类型的断点。

(1)一般断点(Ordinary breakpoint)。将想中断的命令的第一个字节,用一个特殊命令INT3(调试器陷阱)来替代。在反汇编窗口中,选中要设断点的指令行并按F2键,就可以设定一个此类型的断点,也可以在快捷菜单中设置。再次按下F2键时,断点将被删除。注意,程序将在设置断点指令被执行之前中断。

INT3断点的设置数量是没有限制的,当关闭被调试程序或者调试器的时候,OllyDbg将自动把这些断点保存到硬盘中。永远不要试图在数据段或者指令的中间设置这种断点,如果在代码段以外设置断点,OllyDbg将会发出警告。在安全选项(Security options)中关闭这个提示,调试器在某些情况下会插入自带的临时INT3断点。

(2)条件断点(Conditional breakpoint),其快捷键Shift+F2。条件断点是一个带有条件表达式的普通INT3断点。当调试器遇到这类断点时会计算表达式的值,如果结果非零或者表达式无效,将暂停被调试程序。当然,由条件为假的断点引起的开销是非常高的(主要归因于操作系统的反应时间)。在Windows NT、奔腾Ⅱ/450处理器环境下,OllyDbg每秒最多处理2500个条件为假的断点。条件断点的一个典型使用情况就是在Windows消息上(比如WM_PAINT)设置断点。为此,可以将伪变量MSG同适当的参数说明联合使用,如果窗口被激活,可参考后面的消息断点描述。

(3)条件记录断点(Conditional logging breakpoint),其快捷键为Shift+F4。条件记录断点是一种条件断点,每当遇到此类断点或者满足条件时,它将记录已知函数表达式或参数的值。例如,可以在一些窗口过程函数上,设置记录断点并列出对该函数的所有调用。要么只对接收到的WM_COMMAND消息标识符设断点,要么对创建文件的函数(CreateFile)设断点,并且记录以只读方式打开的文件名等,记录断点和条件断点的速度相当,从记录窗口中浏览上百条消息要比按上百次F9键轻松得多,您可以为表达式选择一个预先定义好的解释说明。

设置通过次数。每次符合暂停条件时,计数器就会减1。如果通过次数在减1前,不等于0,OllyDbg就会继续执行。如果一个循环执行100次(十进制),在循环体内设置一个断点并设置通过次数为99(十进制),OllyDbg将会在最后一次执行循环体时暂停。

条件记录断点允许传递一个或多个命令给插件(plugins)。例如,您需要使用命令行插件改变一个寄存器的内容,然后继续执行程序。

(4)消息断点(Message breakpoint)。消息断点和条件记录断点基本相同,OllyDbg会自动产生一个条件,这个条件允许在窗口过程的入口处设置某些消息(比如WM_PSINT)断点,可以在Windows窗口中设置它。

(5)跟踪断点(Trace breakpoint)。跟踪断点是在每个选中命令上设置的一种特殊的INT3断点。如果设置了Hit跟踪(hit trace),断点会在命令执行后移除并在该地址处做一个标记;如果使用的是Run跟踪(run trace),OllyDbg会添加跟踪数据记录并且仍然保持断点的激活状态。

(6)内存断点(Memory breakpoint)。OllyDbg每一时刻只允许有一个内存断点。在反汇编窗口、CPU窗口、数据窗口中选择一部分内存,然后使用快捷菜单可以设置内存断点,此时如果有以前的内存断点,将被自动删除。要么在内存访问(读、写、执行)时中断,要么在内存写入时中断。设置此类断点时,OllyDbg将会改变所选部分的内存块属性。在与80×86兼容的处理器上,将会有4096Byte的内存被分配并保护起来,即使仅仅选择了1个字节,OllyDbg也会将整个内存块都保护起来,这将会引起大量的错误警告,请小心使用此类断点。某些系统函数在访问受保护的内存时,不但不会产生调试事件反而会造成被调试程序的崩溃。

(7)硬件断点(Hardware breakpoint)在80×86兼容的处理器上,允许设置4个硬件断点,硬件断点和内存断点不同,它并不会降低执行速度,但是最多只能覆盖4个字节。在单步执行或者跟踪代码时,OllyDbg能够使用硬件断点代替INT3断点。

(8)内存访问一次性断点(Single-shot break on memory access)对整个内存块设置该类断点可以通过内存窗口的快捷菜单(或按F2键)来完成。若想捕捉调用或返回到某个模块时,该类断点就显得特别有用。中断发生以后,断点将被删除。

(9)暂停Run跟踪(Run trace pause)其快捷键为Ctrl+T。暂停Run跟踪是在每一步Run跟踪(run trace)时都要检查的一个条件集,它可以在EIP进入某个范围或超出某个范围时暂停、某个条件为真时暂停、命令与指定的模式匹配时暂停、当命令可疑的时候暂停。注意,这一选择会极大地(高达20%)降低Run跟踪的速度。

OllyDbg也可以在一些调试事件(debugging events)上暂停程序执行,比如加载或卸载DLL、启动或终止线程或者程序发出调试字符串的时候暂停。

CPU操作的对象是寄存器,寄存器又有诸多分类,其中有一类就是调试和测试寄存器。

调试寄存器被称为DRn(n为下角标)。DR调试寄存器总共有8个,从DR到DR7。

每个寄存器的作用如下。


图5 为调试寄存器

DR4~DR5:保留。

DR6:调试寄存器组状态寄存器。

DR7:调试寄存器组控制寄存器。


在OllyDbg中的调试寄存器窗口可以查看寄存器的值,如图5所示。

这里下一个硬件访问断点(见图6),可以看到DR已经保存了断点1指向的地址。而DR6、DR7也出现了数据。


图6 硬件断点

设置访问断点之后,如果是在API上下断会直接定位到API地址。关于标题栏的指向技巧,如图7所示。


图7 OllyICE标题栏比较

在图7上方显示“模块-MSVBVM60”字样,如果显示的并非是调试程序的名称,说明已经离开了用户代码,如果这个时候去脱壳,脱的并不是调试程序的壳。

常用下断点的方式是bp API函数(见图8),例如bp DeleteFileA(区分大小写)。程序直接运行后,遇到DeleteFile函数调用就会断下来,这里也有人会下bp DeleteFileW断点。需要说明一下,此方式涉及字符处理的API都有两个版本, ANSI("A")和("W")的Unicode版本。


图8 bp API函数

通过这个断点可以知道这个样本自运行后会删除什么。如果确定这个位置就是病毒的主要代码,那么可以执行到返回,然后在领空(push ebp)处直接下断,重载程序并运行到断点,然后逐个分析每一个语句。

二、“敲竹杠”病毒分析

首先打开样本,此时会弹出来一个对话框窗口,可以看到鼠标被限制在窗口区域内,并出现桌面图标消失等状况,如图9所示。


图9“敲竹杠”病毒

使用OllyDbg载入这个程序,执行到入口处(见图10)。


图10  入口点位置

前面都是一些加载易语言库文件的操作,很容易找到入口。

然后,按F7键跟进这个call [call eax]语句。由于只有一个按钮,所以跟踪到这个按钮事件。

此时可以一目了然地看到密码,如图11所示。


图11“敲竹杠”密码获得过程

这里如果中毒的话,可以直接再运行这个程序,然后输入wozhenxiangzisha就可以了。

三、重要辅助工具

当然,要想更加透彻地分析病毒,仅仅使用OD是不够的,下面将介绍一些常用的病毒分析工具,便于大家从行为、部分源码上理解病毒的原理,以及杀毒软件是如何有效防治病毒入侵计算机的原理。

1、PEiD——查壳工具

PEiD(见图12)是一款非常优秀的针对PE文件的查壳软件,病毒通过加壳一方面为了达到免杀的目的,另一方面也是为了保护代码不被轻易地破解分析。加壳的病毒程序通常需要脱壳以后才能从真正的入口点函数开始分析。


图12  PEiD使用界面

图13所示的是一个加壳的文件。很显然,程序代码通过加密以后,第一行显示出来的并不是程序入口点函数所代表的基地址,同时也无法从代码中读出有用的信息。


图13  加壳文件

进行脱壳处理之后,就能很明显地看到代码的真实面目,代码第一行也恢复为正常的入口点函数,同时从注释中可以看到该程序调用了MessageBoxA函数,如图14所示。


图14  文件脱壳后

简单地说。PE文件就是Windows平台上的可执行程序,包括exe、dll、com、sys、ocx等各种文件。

既然作用于计算机的病毒都是PE文件格式,那么也可以使用专门分析PE文件的软件来分析病毒,如Stud_PE。

2、Stud_PE

在Stud_PE(见图15)的“在16进位编辑器中视图文件头树”中的“数据_目录”段后,可以清楚地看到数据是被加了UPX壳(见图16)。


图15  Stud_PE功能图


图16  UPX壳

打开Stud_PE主界面的函数选项,可以清楚地了解到该程序调用的函数类型(见图17)。


图17  函数分析界面

其中KERNEL32.DLL装载的是程序的winmain入口函数,USER32.DLL装载则是MessageBoxA,这可是在加壳的情况下就能看到的,因此一款分析PE文件的软件在加壳程序函数的分析上是优于调试器的,类似的软件还有PEview。

当然,也有专门分析程序调用函数的武器——Dependency Walker(见图18)。


图18  Dependency Walker界面

接下来将介绍两种强大的进程监控软件—Process Monitor和Process Explorer。

3、Process Monitor,进程监控器(简称procmon)

procmon的主界面如图19所示,图上详细列举了每一个程序所使用的函数、路径等详细信息。


图19  procmon主界面

选中其中的一个程序双击,打开Event Properties对话框,可以看到这个程序所加载的DLL库连接、创建与运行时间,如图20所示。在Process选项卡中可以看到程序所在的安装地址以及最下方整个程序所加载的DLL库及其位置,如图21所示。而最后一个Stack选项卡,描述了程序以及加载的DLL库函数在内存中的位置。


图20  Event选项



图21  Process选项

该程序最有用的则是Process Monitor Filter功能,如图22所示。

图22  Filter过滤

您可以在Filter中选择想监控的函数,这样可以使繁杂的界面变得简单,同时也可以有针对性地对病毒的某种功能进行详细监控。

其实看程序最方便的还是看进程树,虽然这款软件中也有Process Tree(Tools选项下第2个)功能,但是却无法与接下来将要介绍的软件相比。

4、Process Explorer

Process Explorer可以使进程、PID、CPU占用率等一目了然,其中标记蓝色的进程为用户进程,红色为系统进程,绿色为新运行的进程,如图23所示。为了查看系统文件是否被修改,可以打开“选项”菜单下的“验证映像数字签名(Microsoft验证)”命令。


图23  procexp进程树

5、Regshot

Regshot是一款注册表快照软件。单击“快照(A)”按钮运行目标文件,单击“快照(B)”后就会出现检测注册表是否修改,以及伴随着注册表修改而产生的文件报告,如图24所示。


图24  Regshot

四、虚拟环境搭建


图25  Vmware Workstation软件

使用虚拟机搭建虚拟环境在分析病毒时是不可或缺的,模拟出一个真实的系统并且构造出一个虚拟的网络来隔绝病毒在互联网传播。

VMware Workstation软件如图25所示。

VMware Workstation的安装过程以及基本使用方式与VBox类似,这里主要介绍一下虚拟网络的搭建过程。

ApateDNS软件界面如图26所示。


图26  ApateDNS主界面

打开ApateDNS,在主界面的CaptureWindow选项卡下会显示访问Internet的网络活动,在下方的DNS Reply IP处填写将要访问的虚拟IP地址,在Selected Interface处选择虚拟机当前使用的网络,单击Start Server按钮,结果如图27所示。


图27  运行后结果

随后在虚拟机中打开任意一个网站,地址解析将会跳转到输入的虚拟IP地址,同时ApateDNS下也会显示将要访问的网站。如果想要详细地了解病毒的网络动态,请使用Wireshark进行抓包。

当然,有些病毒具有反虚拟机技术,即在虚拟机环境下病毒不会运行,这时便需要使用沙箱/沙盒(见图28)代替虚拟机环境。


图28  沙箱

将程序拖入沙箱,就可以在一个安全环境下运行了。

接下来将用以上提到的工具对病毒进行行为上的分析以及代码上的揭秘。

五、病毒实例分析

请务必于虚拟机或者沙箱内运行。

33.exe(见图29)是一个典型的PE文件,先使用杀毒软件对其进行手动查杀。


图29  33.exe

(1)先对虚拟机进行一次快照,作为系统还原备份,并且记录当前运行环境信息作为备注,如图30所示。


图30  拍摄还原点

(2)搭建虚拟网络,阻止病毒与网络的接触,将网络配置选择为虚拟机当前网络,如图31所示。


图31  设置虚拟网络

有了以上两点准备以后,就可以先尝试从行为上对其进行分析。

(3)打开Regshot软件,对其进行两次快照,快照时应选择全部注册表并且备注信息,如图32所示。


图32  注册表快照

快照(A)完毕后运行33.exe程序,同时使用Process Explorer进行进程监控。随后运行33.exe,然后进行快照(B),快照完毕后会弹出一个分析报告网页,从中可以看出注册表项的变化(见图33~图36)。


图33  注册表变化(1)

从图33中,可以看出新添加的注册表键值,其中的RDPTcp和TDTCP均为启动“3389”,也就是远程桌面控制的注册键值PortNumber的值修改为3389。


图34  注册表变化(2)


图35  注册表变化(3)


图36  注册表变化(4)

从图36中,也就是HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\Windows\load, Load ="C:\\WINDOWS\\system32\\33.exe",可以看出33.exe将其自身复制进系统目录,并且修改load键值,实现程序随注册表自启动。

运行完以后,33.exe从桌面上“消失”,工具procexp捕捉到cmd.exe短暂的运行(见图37)过程,自此可以推断其利用CMD命令进行自我删除。


图37  工具procexp捕捉

从桌面右下角出现防火墙关闭气泡提示,很明显地看出该病毒也关闭了系统防火墙,如图38所示。


图38  防火墙关闭提示

检测ApateDNS,发现并没有对Internet进行的访问,如图39所示,说明病毒没有悄悄在后台访问网站或下载其他病毒。


图39  ApateDNS截获

再次进入虚拟机,发现出现另外一个密码用户,则可以推断病毒利用CMD命令进行了一次用户添加操作,如图40所示。


图40  Windows登录界面

使用快照还原系统,接下来用调试工具对其进行源码上的分析。

首先使用PEiD进行查壳,如图41所示,查壳结果显示其并没有被加壳。


图41  查壳

接下来,可以选择Dependency Walker或者Stud_PE对其使用的函数进行分析,在KERNEL32.DLL截获的函数中,CopyFileA与CreateFileA函数(见图42)作为移动目标函数将“33.exe”文件移动至系统目录。


图42  KERNEL32.DLL(1)

LoadLibraryA用来装载DLL文件库,在本例中是装载KERNEL32.DLL,WinExec用来执行程序,WriteFile则是写入文件,如图43所示。


图43  KERNEL32.DLL(2)

ExitWindowsEx(见图44)是关机程序,目的是为了在Windows系统下修改注册表使其生效并重启计算机。


图44  USER32.DLL

如图45所示的最后三个修改注册表的函数则是启动“3389”的罪魁祸首。经过查看函数及行为分析,您大体已经明白了该病毒的功能是开启远程桌面,关闭防火墙,建立新用户,自启重生。


图45  ADVAPI32.DLL

而要从源码上更深层次地了解该病毒,就需要使用反汇编工具——IDA Pro/OD,本次我们利用IDA Pro(见图46)工具进行病毒源码的分析。


图46  IDA Pro

单击OK按钮进入主界面,出现如图47所示的反汇编文本界面。IDA Pro还支持图形模式,按“空格”键进行切换,如图48所示。


图47  文本模式


图48  图形模式

在文本模式下,旁白备注为蓝色字体,语句注释则用绿色字体,粉色字体表示调用的函数名称,灰色字体则专门对offset进行注释。

DA Pro图形模式类似程序的流程图。在图形模式中,红色箭头①表示没有发生该条件跳转,绿色箭头②表示该跳转已发生,而蓝色箭头则表示发生无条件跳转,向上的箭头则通常表示循环体。

仍然从函数入手,这里使用较为方便的图形模式进行源码分析。执行View→subviews→Imports命令,打开的Imports界面如图49所示。


图49  Imports界面

按Ctrl+F组合键,或者直接在Imports界面输入函数名称,就会自动搜索定位到该函数,先选中CreateFileA函数,然后双击它进入反汇编界面(见图50),在此就可以看出函数所调用的参数类型。


图50  函数的反汇编界面


图51  Xref界面

选择地址sub_401230+1B,进入其图形模式,通过图形模式可以清楚地看到该函数所附带的功能及运行的详细流程(见图52、图53)。


图52  CreateFileA(1)


图53  CreateFileA(2)

从图53中可以看出,首先映入眼帘的是备注offset CmdLine的“del.cmd”语句,结合后文的aNetStopShareda函数及其灰字备注,可以得出该自删除过程是先调用CreateFileA函数创建了一个CMD命令文件,返回一个实例句柄,同时通过WriteFile预先写好命令,实现关闭防火墙以及删除文件自身功能。然后,再通过最后的WinExec函数运行该“del.cmd”删除自身,并且设置其窗口为隐藏状态,这就是为什么之前我们用Process Monitor检测到了CMD在一瞬间被使用之后,“33.exe”神秘消失的原因,由此可见病毒并不神秘。

之后我们可以单击界面左上角Jump菜单下面的左箭头(见图54)回到刚才的CreateFileA函数界面,然后调出Xref选择第2个地址sub_4012C0+1B,并进入图形模式。


图54  箭头跳转

对比上例“del.cmd”“add.cmd”名称与之形成鲜明对比,该过程则是通过命令“net user admin”来添加新用户,由注释可知新添加的账户以及密码均为admin,如图55所示。


图55  CreateFileA(3)

最后,在C语言中来看看这些函数的真面目。


HANDLE CreateFile(
LPCTSTR lpFileName,               //指向文件名称的指针
DWORD dwDesiredAccess,             //访问权限
DWORD dwShareMode,               //共享方式
LPSECURITY_ATTRIBUTES lpSecurityAttributes,//安全属性
DWORD dwCreationDisposition,         //创建标志
DWORD dwFlagsAndAttributes,          //文件属性
HANDLE hTemplateFile              //模板文件句柄
);

首先来认识一下参数前的类型声明。

LPCSTR:指针字符串类型,指向一个常量字符串,并且以’\0’结尾,前缀标识为lp;

DWORD:双字(32位)的无符号长度单位,前缀标识为dw,与之相近的WORD类型,为双字节(16位),前缀w;

HANDLE:实例句柄,被用于Windows API表示对象;

HWND:窗口句柄,前缀标识为h;

BOOL:布尔值,前缀为b;

UNIT: unsigned int,无符号整数。

LPSECURITY_ATTRIBUTES则为一个结构体,具体代码如下。

typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;              //结构体大小
LPVOID lpSecurityDescriptor; //安全描述符
BOOL bInheritHandle ;        //判断句柄是否安全继承
} SECURITY_ATTRIBUTES;

下面再来谈谈参数值设定。

(1)lpFileName一般设置为文件路径或者文件的名称。

(2)dwDesiredAccess设置为GENERIC_READ | GENERIC_WRITE,即可以进行读写操作。

(3)dwShareMode可以为0。

(4)FILE_SHARE_DELETE,FILE_SHARE_READ,FILE_SHARE_WRITE中任意个数,分别为不可共享、可删除、可读、可写。

(5)LPSECURITY_ATTRIBUTES则通常设置为NULL。

(6)dwCreationDisposition,表示对文件存在与否所执行的行为,分别为:

CREATE_ALWAYS(创建文件,若之前该文件存在,则覆盖改写上一个文件)。

CREATE_NEW(创建文件,文件存在时报错)。

OPEN_ALWAYS(文件不存在就创建文件)。

OPEN_EXISTING(文件必须已经存在)。

TRUNCATE_EXISTING(文件长度清零)。

(7)dwFlagsAndAttributes,病毒一般会设置为FILE_ATTRIBUTE_HIDDEN,也就是文件处于隐藏状态。

(8)hTemplateFile,若无模板设置为NULL。

由此就可以推断“del.cmd”以及“add.cmd”的CreateFile调用方式为:声明一个句柄Handle ×××来接收CreateFile的返回值,即:

HANDLE ××× = CreateFile("del/add.cmd",GENERIC_WRITE, FILE_SHARE_WRITE, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_HIDDEN, NULL);

是不是很简单?完成了创建文件的操作,下一步就是对文件进行读写操作了,即使用WriteFile函数来实现。

BOOL WriteFile(
HANDLE hFile,                  //文件句柄
LPCVOID lpBuffer,              //写入数据缓存区的指针
DWORD nNumberOfBytesToWrite,    //写入字节数
LPDWORD lpNumberOfBytesWritten, //用于保存实际写入字节数的指针
LPOVERLAPPED lpOverlapped      //用于指向保存I/O异步信息的结构体
);

很显然,在本例中的WriteFile函数第1个参数hFile是之前调用CreateFile函数创建的×××句柄。第2个参数lpBuffer以及第3个参数nNumberOfBytesToWrite则是用来写入自删/创建用户的代码,第4个参数lpNumberOfBytesWritten则指向实际写入的代码,最后一个lpOverlapped通常设置为NULL。

之后执行的是CloseHandle函数,因为之前调用CreateFile时创建了一个×××句柄,在文件读写操作结束以后要关闭这个句柄,以防被其他函数误用并且释放系统内核资源(HANDLE句柄资源)。

BOOL CloseHandle(
HANDLE hObject //句柄名
);
CloseHandle(×××);//就可以关闭句柄了

接着,则使用WinExec函数运行CMD命令行程序,整个过程就结束了。WinExec函数包含两个参数,具体如下:

UINT WINAPI WinExec(
_In_  LPCSTR lpCmdLine, //命令行代码参数
_In_  UINT uCmdShow    //命令窗口
);

第1个参数lpCmdLine是之前生成的CMD程序,第2个参数uCmdShow根据分析来看则是隐藏窗口,也就是设置为SW_HIDE。

WinExec("del.cmd", SW_HIDE);//调入运行状态

通过查阅MSDN(微软提供的强大函数库查询工具),可以了解函数参数的详细信息,而函数后缀如CrateFileA的后缀A则表示编码方式为ASCII码,W则表示Unicode编码,Ex则表示最新发布。

接下来看看该病毒对于3389的开启过程(见图56)。


图56  注册表过程(1)

很明显从其使用的注册表函数RegCreateKeyExA、RegSetValueExA、RegCloseKey得知,该图形过程所代表程序的作用是修改之前使用Regshot快照得出的注册表键值,也就是:

HEKY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Terminal Server\RDPTcp
HEKY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\RDPWD\Enum
HEKY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\TDTCP\Enum
HEKY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server\RDPTcp
HEKY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\RDPWD\Enum
HEKY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TDTCP\Enum

分析一下流程,这个程序使用RegCreateKeyExA来打开/创建以上的注册表,并且返回创建一个实例句柄。这里出现一个条件跳转语句,如果函数调用失败则返回error并且退出程序;若打开/创建程序成功便调用RegSetValueExA对该键值进行修改。随后继续出现一个条件跳转语句,也就是对于键值是否修改成功进行判断,如果修改失败则返回error然后退出程序;如果修改成功则调用RegCloseKey关闭句柄,如图57所示。


图57  注册表过程(2)
查阅MSDN,从源码上分析这三个函数。
LONG WINAPI RegCreateKeyEx(
HKEY hKey,                //HKEY:注册表根键
LPCTSTR lpSubKey,        //打开或创建键值
DWORD dwReserved,        //参数必须设为0
LPTSTR lpClass,          //用户定义的类,可以设为NULL
DWORD dwOptions,          //功能选择
REGSAM samDesired,        //访问权限
CONST LPSECURITY_ATTRIBUTES lpSecurityAttributes, //安全属性
PHKEY phkResult,          //用来接收打开/创建键值的句柄
LPDWORD phkResult        //装载变量
);

第1个参数hKey的值往往是注册表根键其中之一,注册表根键为以下5个。

HKEY_USERS:保存计算机所有用户的信息,包括登录计算机账号及密码。

HEKY_CLASSES_ROOT:保存文件类型的信息。

HEKY_CURRENT_USER:保存当前系统的用户信息。

HEKY_CURRENT_CONFIG:保存当前用户的系统配置信息。

HEKY_LOCAL_MACHINE:保存计算机硬件信息,包括远程计算机访问的键值。

由开启远程桌面“3389”功能确定:

第1个参数设置肯定为排列在末尾的HEKY_LOCAL_MACHINE。

第2个参数lpSubKey则设置为之前Regshot快照中被修改的注册表键值的地址。

第3个参数dwReserved设置为0。

第4个参数lpClass若用户没有定义类,则设NULL。

第5个参数dWOptions设置为REG_OPTION_BACKUP_RESTORE、REG_OPTION_CREATE_LINK、REG_OPTION_NON_VOLATILE、REG_OPTION_VOLATILE之一,其中常用的为REG_OPTION_NON_VOLATILE:信息保留在文件中,重启之后仍然被保存。REG_OPTION_VOLATILE信息保留在内存中,重启之后失去效果。

第6个参数samDesired,一般均为KEY_ALL_ACCESS,也就是允许所有操作。

第7个参数lpSecurityAttributes指向的句柄无继承则设置为NULL。

第8个参数phkResult可以指向定义任意一个根键的地址,用于被其他注册表函数使用,如定义HKEY hKEY,则参数设置为&hKey。

最后一个参数为REG_CREATED_NEW_KEY(若子键存在则打开,不存在则创建)或者REG_OPENED_EXISTING_KEY(当且仅当子键存在时打开)。

当RegCreateKeyEx创建或者打开一个子键值后,接下来就是调用RegSetValueEx对其键值进行修改。

LONG RegSetValueEx(
HKEY hKey,            //根键
LPCWSTR lpValueName,  //将要修改的键值名称
DWORD Reserved,      //设置为0
DWORD dwType,        //键值的数据类型
CONST BYTE* lpData,  //指向读写键值的缓冲区
DWORD cbData          //缓存区大小
);

本例中的参数一hKey就被设置为RegCreateKeyEx中的phkResult的参数HKEY。参数一得到了将要修改键值的详细地址,因此参数二lpValueName只用设置为键值的键名。参数四dwType通常设置为REG_DWORD(用于修改双字类型)或者REG_SZ(用于修改字符串类型,SZ(string zero)表示null结尾的字符串)。参数五lpData设置为指向包含数据缓冲区(修改键值的目标值)的指针。参数六cbData可以通过strlen函数/size of函数分别得出类型为字符串/双字的缓冲区的大小。

有关注册表的最后一个函数RegCloseKey则非常简单。

LONG RegCloseKey(
HKEY hKey
//设置为之前由RegCreateKeyEx返回的参数phkResult,这里设置为hKey
);

该病毒最后一个功能就是将自身复制进入系统目录,通过修改注册表HKEY_CURRENT_USER\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows,修改load键值进行开机自启。通过修改注册表进行自启的方法还有以下几种:

HKEY_LOCAL_MACHINE\software\Microsoft\WindowsNT\CurrentVersion\Winlogon\Userinit键值下通常是userinit.exe,(逗号间隔)目标程序。
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\
Run
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer
\Run
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunServicesOnce
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce\Setup
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnce\Setup
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnce
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnceEx
(Windows XP)
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run
HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\Winlogon
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon

单击左下角的“菜单”按钮,在运行对话框中输入regedit,找到HEKY_CURRENT_USER\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows可知“33.exe”被复制到系统目录system32下(见图58),并且通过修改load键值,进行自启,如图59所示。


图58  regedit


图59  注册表自启(1)

接下来通过IDA Pro图形模式分析该过程。

进入Imports界面定位到CopyFile,随后使用Xref(Ctrl+X组合键)跳转到如图60所示的图形界面。


图60  注册表自启(2)

首先,程序调用GetSystemDirectoryA函数,通过该函数得出系统目录,也就是system32的位置所在(系统盘不一定都在C盘)。

其次,使用GetModuleFileNameA函数,该函数的作用是得到文件的完整路径,联系下文紧接着调用CopyFileA函数,可以得知,GetModuleFileNameA函数的目的是得到“33.exe”的具体所在位置,然后通过CopyFileA函数复制进系统目录。

最后,通过调用注册表函数修改load键值,添加“33.exe”进行注册表自启,如图60所示。

同样通过源码进行分析GetSystemDirectoryA函数。

UINT WINAPI GetSystemDirectory(
_out LPTSTR lpBuffer, //装载系统目录的缓冲区
_in UINT uSize            //缓冲区大小
);

使用起来则很简单,也就是先建立一个数组用来充当缓冲区(char ×××[256]),随后使用GetSystemDirectory(×××, 256),就得到了系统目录的路径。

DWORD WINAPI GetModuleFileName(
_In_opt_ HMODULE hModule,    //模板句柄,设置为NULL时返回该程序完整路径
_Out_  LPTSTR lpFilename,   //保存文件路径缓冲区
_In_  DWORD nSize  //缓冲区大小
);

为了得到自身的完整路径,第1个参数需要设置为NULL,后两个参数仍然可以通过建立一个数组存放路径,如char sss[256],进而使用GetModuleFileName(NULL, sss, 256)。

得到了系统目录和自身路径之后,调用CopyFile函数将自身复制进系统目录。

BOOL CopyFile(
LPCTSTR lpExistingFileName,  //文件自身路径
LPCTSTR lpNewFileName,      //文件目的路径
BOOL bFailIfExists          //对相同名称文件操作选项
);

第3个参数bFailIfExists,布尔值。设定为TRUE,表示若文件已经在目的路径存在,则函数失效;设定为FALSE,则覆盖目标文件。

由此,推断出CopyFile(sss(病毒自身路径),×××(系统目录路径),FLASE)。

最后,调用注册表函数修改键值。

整个病毒的分析到此结束,病毒看起来无非也就是各种函数的“连招组合”罢了。病毒并不神秘,只是需要人们拥有一颗敢于探索、无畏困难的心!

六、使用WinDbg进行蓝屏dmp文件分析

在Windows平台下,WinDbg是一款强大的用户态和内核态的调试工具,图61是32位WinDbg的主界面。


图61  32位(x86)WinDbg主界面

安装完成后,执行File→Symbol File Path命令,在Symbol File Path对话框中设置symbol Path为SRV*c:\symbol* http://msdl.microsoft.com/download/symbols,如图62、图63所示,其目的就是下载WinDbg所需的符号表到C盘的sybol文件夹下。


图62  符表设置(1)


图63  符号表设置(2)

从File文件选项(见图64)中可以看到,WinDbg可以调试可执行文件、进程、DMP文件,接下来将使用WinDbg在用户态模式调试蓝屏DMP文件,进而分析造成蓝屏的原因,用到的命令将会相应地做出解释。


图64  File菜单

使用组合键Ctrl+D或者执行File→Open Crash Dump命令,添加蓝屏dmp文件(也可以直接拖入)。弹出如图65所示对话框。


图65  工作空间

这里选择No,不保留工作空间,避免与其他将分析的文件冲突。

我们先来认识图9-66中信息,首先:

Microsoft (R) Windows Debugger Version 6.12.0002.633 X86
Copyright (c) Microsoft Corporation. All rights reserved.

这两句表明了使用的WinDbg版本与版权信息。

Executable search path is: Windows 7 Kernel Version 7601 (Service
Pack 1) MP (4 procs) Free x64
Product: WinNt, suite: TerminalServer SingleUserTS
Built by: 7601.17944.amd64fre.win7sp1_gdr.120830-0333
Machine Name:


图66  DMP文件基础信息(1)

以上代码为蓝屏系统的基本信息,从中可以得到系统的版本为64位的Windows 7系统,处理器核数为4(4 procs)。

Kernel base为内核地址,PsLoadedModuleList为Windows加载的所有内核模块构成的链表的表头。

从Debug session time: Fri Nov 23 13:44:08.182 2012 (UTC + 8:00)信息得知系统崩溃发生的具体时间为System Uptime:0 days 1:40:29.623,则标明系统在蓝屏溃前的运行时长。

如图67所示,从Probably caused by :igdpmd64.sys ( igdpmd64+15aa18 )得知,系统崩溃的原因也就是igdpmd64.sys——AMD显卡驱动文件导致的。


图67  DMP文件基础信息(2)

Unable to load image \SystemRoot\system32\DRIVERS\igdpmd64.sys, Win32
error 0n2*** WARNING: Unable to verify timestamp for igdpmd64.sys
*** ERROR: Module load completed but symbols could not be loaded for
igdpmd64.sys

表示无法找到加载igdpmd64.sys模块的符号表。

输入!analyze-v或者单击下画线标记的命令可以获得具体细节,如图68所示。


图68  Bugcheck Analysis(1)
PAGE_FAULT_IN_NONPAGED_AREA (50)
Invalid system memory was referenced.  This cannot be protected by try-except,
it must be protected by a Probe.  Typically the address is just plain bad or it
is pointing at freed memory.

这段代码为WinDbg分析蓝屏的原因:无效系统内存引用,这种错误不能被try-except保护,只能通过Probe(硬件侦测手段)来保护,最典型的原因是地址引用错误,或者指向一个已释放的内存空间。

Arguments:                                      //蓝屏代码
Arg1:fffff8812bc5cb60,memory referenced.        //错误内存引用
Arg2:0000000000000000,value 0 = read operation,1 = write operation.//读操作引发问题
Arg3: fffff8800739fa18,If non-zero, the instruction address which
referenced the bad memory address.        //非0标明引用错误地址
Arg4:0000000000000005,(reserved)  //预留信息

软件Bug产生的细节如图69所示。


图69  Bugcheck Analysis(2)

下面解释了蓝屏代码的具体原因:

Could not read faulting driver name  //无法读出驱动名称
READ_ADDRESS: GetPointerFromAddress: unable to read from fffff80004ac9100
fffff8812bc5cb60                        //内存读写错误,对应Arg1
FAULTING_IP:
igdpmd64+15aa18
fffff880`0739fa18 8b0408  mov  eax, dword ptr [rax+rcx] //导致蓝屏的指令
PROCESS_NAME: csrss.exe        //引发崩溃的用户层程序

图70存放的是在发生崩溃时寄存器存放的全部信息。


图70  寄存器

图71、图72存放的则是STACK_TEXT,即栈信息,从其中可以得引发崩溃的函数。


图71  Stck节选(1)


图72  Stack节选(2)

随后则显示了引发蓝屏的驱动信息,包括驱动文件名称、时间戳等,如图73所示。


图73  驱动信息(1)

单击图73中下画线标记的igdpmd64或者输入lmvm igdpmd64,可以得到更为详细的驱动信息(见图74)。

图74  驱动信息(2)

图74中列出了驱动文件加载的地址、路径、编译运行的时间戳及大小等信息。

至此,本次使用WinDbg蓝屏分析就结束了。除软件冲突以外,有些病毒也会引起系统崩溃,造成蓝屏现象。在C:\Windows\Minidump文件夹下可以找到相应DMP文件,或者找到C:\Windows\Memory.dmp路径下的文件,随后使用WinDbg进行分析,找到病毒相关程序。病毒引起系统崩溃的原因,可能是其对内核进行了修改操作,也就是Rootkit(加载驱动、修改内核、隐藏程序)。

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

推荐阅读更多精彩内容