zoukankan      html  css  js  c++  java
  • 恶意代码分析——静态分析高级技术

    三、静态分析高级技术

    ***********************************************************************************************************************************************************************
    知识补充——汇编基础:




                    ************
                    *基础知识:*
                    ************

    本节内容需要一定的反汇编基础,需要学习一定的汇编知识

    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    1、计算机系统被分为六个抽象层次:


    硬件层(熟悉《计算机科学导论》的相关内容)

    微指令层(即固件层,恶意代码分析时同常不予关注)

    机器码层(由高级语言编写的计算机程序编译而来)

    低级语言层(主要是汇编语言,恶意代码分析主要关注该层次)

    高级语言层(如c/c++等)

    解释性语言层(位于最高层,jave,c#等,独立于操作系统)

    2、掌握逆向工程

    运用反汇编器"disassembler、IDA Pro"将恶意代码的二进制文件翻译为汇编语言。尤其对x86和x64等语言的学习需要有一定了解,可以参考《汇编语言的艺术-Randall Hyde》(清华大学出版社)

    3、x86体系结构

    (1)三种硬件组成:
    中央处理器(CPU):执行代码
    内存(RAM):存储所有的数据和代码
    输入/输出系统(I/O):对外部设备提供接口
    (2)一个程序内存的节

    通常由低地址向高地址依次为:

    栈->堆->代码->数据

    数据:程序初始值,全局值和静态值

    代码:交付CPU执行,决定了程序是做什么的

    堆:程序执行期间所需的动态内存空间

    栈:程序函数的局部变量及参数存放位置,同时控制程序执行流

    *注意:这个顺序也不是一定的,四个主要的内存节可以分布在内存的任意位置
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    具备编译器使用约定的知识基础

    ********************************************************************



                    ************
                    *主干知识:*
                    ************

    x86汇编语言

    1、指令:

    (1)助记符:表示要执行的指令

    mov:移动数据


    (2)目标操作数:说明指令要使用的信息,如:寄存器或数据

    ecx:指向寄存器

    (3)源操作数:

    2、操作码和字节序:
    操作码:cpu所执行的指令
    字节序:一个大数据项中,最高位还是最低位被排在第一位(即排在最低的地址上)

    注意一:x86采用小端字节序,如:0x42在x86架构上的表示为0x42000000而不是0x00000042
    注意二:一些恶意代码在网络通信时必须改变字节序,因为网络数据使用大端字节序,而x86程序使用小端字节序。(如127.0.0.0网络通信中表示为0x7F000001,而在本地内存的小端字节序下,表示为0x0100007F)

    3、操作数:说明指令要使用的数据

    分为三种类型:

    立即数操作数:是一个固定值,如:0x42

    寄存器操作数:指向寄存器,如:ecx

    内存地址操作数:指向感兴趣的值所在的内存地址,一般由方括号内包含值、寄存器或方程式组成,如:[eax]

    4、寄存器:是可以被CPU使用的少数数据存储器

    所有的通用寄存器都是32位,可以再汇编代码中以32位或16位引用,如:EDX指向完整的32位寄存器,而DX指向EDX寄存器的低16位

    x86下有四类常用寄存器:

    (1)通用寄存器:CPU在执行期间使用

    EAX(AX,AH,AL)
    EBX(BX,BH,BL)
    ECX(CX,CH,CL)
    EDX(DX,DH,DL)
    EBP(BP)
    ESP(SP)
    ESI(SI)

    一些x86指令只能使用特定的寄存器,如乘法和除法指令只能使用EAX和EDX

    EAX通常储存了一个函数调用的返回值,如果看到一个函数调用后马上用到了EAX寄存器,可能就是在操作返回值

    (2)段寄存器:用于定位内存节

    CS
    SS
    DS
    ES
    FS
    GS

    (3)状态标志:用于做出决定

    EFIAGS
    这是一个标志寄存器,x86中是32位的,每一位只一个标志,执行期间,每一位要么是置位(值为1),要么是清除(值为0),并由这些值来控制CPU的运算,或者给出某些CPU运算的值,对恶意代码分析,重要的标志如下:

    ZF:当一个运算的结果等于0时,ZF被置位,否则被清除

    CF:当一个运算的结果相对于目标操作数太大或太小时,CF被置位,否则被清除

    SF:当一个运算的结果为负数,SF被置位;若结果为正数,SF被清除。对算术运算,当运算结果的最高位值为1时,SF也会被置位

    TF:用于调试,当它被置位时,x86处理器每次只执行一条指令

    (4)指令指针:

    EIP

    EIP寄存器又称为(指令指针或程序计数器,保存程序将要执行的下一条指令在内存中的地址,唯一作用就是告诉处理器接下来要做什么

    5、简单指令(采用intel汇编语法)

    (1)mov指令:用于将数据从一个位置移动到另一个位置,对内存数据进行读写操作

    格式:mov destination(目标地址),source(源数据/地址)

    mov eax,ebx                     将EBX中的内容复制至EAX寄存器
    mov eax,0x42                    将立即数0x42复制至EAX寄存器
    mov eax,[0x4037C4]              将内存地址0x4037C4的4个字节复制到EAX寄存器
    mov eax,[ebx]                   将EBX寄存器指向的内存地址处4个字节复制到EAX寄存器
    mov eax,[ebx+esi*4]             将ebx+esi*4等式结果指向的内存地址处4个字节复制到EAX

    (2)lea指令:用来将一个内存地址付给目的操作数

    格式:lea destination,source

    lea eax,[ebx+8]                 将EBX+8的值给EAX

    *与mov指令对比:mov是寻址方式复制,lea是直接赋值。如:lea eax,[ebx+8]是将EBX+8的值给EAX,而mov eax,[ebx+8]是加载内存中地址为EBX+8处的数据。从功能上来说lea eax,[ebx+8]和mov eax,ebx+8是等效的,但是这种寻址形式的mov指令时无效的

    (3)inc指令:目标操作数+1

    格式:inc destination

    inc eax                          将EAX寄存器的值递增1

    (4)dec指令:目标操作数-1

    格式:dec destination

    dec eax                          将eax寄存器的值递减1

    (5)move指令:(DST)<-(SRC) 将原操作数(字节或字)传送到目的地址。是数据的传送,即拷贝的功能(数据类型不变)。

    格式:move destination,source

    move cx,5                        把5放入ecx寄存器中

    指令支持的寻址方式:目的操作数和源操作数不能同时用存储器寻址方式,这个限制适用于所有指令。指令的执行对标志位的影响:不影响标志位。

    指令的特殊要求:目的操作数DST和源操作数SRC不允许同时为段寄存器;目的操作数DST不能是CS,也不能用立即数方式。

    (6)mul指令:乘法运算指令

    格式:mul source

    mul ecx                          将ecx的值与eax相乘

    如果SRC是字节操作数,则把AL中的无符号数与SRC相乘得到16位结果送AX中,即:AX←(AL)*(SRC)。如果SRC是字操作数,则把AX中的无符号数与SRC相乘得到32位结果送DX和AX中,DX存高16位,AX存低16位,即:AX←(AL)*(SRC)。受影响的标志位有:CF和OF(AF、PF、SF和ZF无定义)。如果乘积结果的高半部分等于零,则CF=OF=0,否则CF=OF=1

    mul source指令总是将eax乘上source,乘法的结果以64位的形式分开储存在两个寄存器:EDX和EAX中,其中EDX存储了高的32位,EAX存储低的32位。

    (7)div指令:除法运算指令

    格式:div source

    div ecx                          将eax的值除以ecx的值

    与mul运算方向相反,将edx和eax合起来储存的64位值除以source,除法的商存储到eax,余数存储在edx中
    (7)add指令:加法运算指令

    格式:add destination,value

    add eax,ebx                      将ebx的值加上eax并将结果保存至eax

    (8)sub指令:减法运算指令

    格式 sub destination,value       

    sub eax,0x10                     eax寄存器的值减去0x10

    (9)or,and,xor指令:分别对应“或”“与”“非”,用法与add和sub相同

    xor eax,eax                      将eax寄存器清零
    or eax,0x7575                    对eax进行与0x7575的or操作

    *有一种快速将寄存器置零的用法,用该指令实现:xor eax,eax,这种用法只需要2个字节。

    (10)shr/shl指令:用于对寄存器做移位操作:右移/左移

    格式:shr destination,count

    mov eax,0xA                     将eax寄存器左移两位,这个指令将导致eax=0x28,
    shl eax                         因为1010(0xA的二进制表示)左移两位之后为101000(即0x28)

    *移位运算可以作为乘法的优化算法:“一般的,左移n位,相当于乘以2的n次方,右移n位相当于除以2的n次方”

    (11)ror/rol指令:循环移位指令,但移出的那一位会被填到另一端空出来的位上(即:右循环ror会将最低位循环移到最高位,左循环rol会将最低位循环移到最高位

    格式:与shr/shl相同

    mov bl,0xA                       将bl寄存器循环移位两位,这两条指令将导致bl=10000010,因为1010向右循环移动2位为10000010
    ror bl,2

    (12)nop指令:无运算指令,直接执行下一条指令

    *注:nop实际上是xchg eax,eax的伪名字,这条指令的opcode(操作码)是0x90。

    *在缓冲区溢出攻击中,当攻击者无法完美地控制利用代码,就经常使用nop滑板,它起到了填充代码的作用,以降低shellcode可能在中间部分开始执行所造成的风险。

    6、栈

    采用压和弹得操作来刻画的数据结构,是一种后入先出的结构,由高地址到低地址依次使用,例如依次压入数字1、2、3,则第一个被弹出的为3,最后一个被弹出的为1。

    x86架构中用ESP和EBP两个寄存器来支持栈:

    ESP是栈指针,包含了指向栈顶的内存地址,数据被压入或弹出栈的时候,该寄存器的值会改变。

    EBP是栈基址寄存器,在一个函数中会保持不变,,程序会把它当成定位器,用来确定局部变量和参数的位置。

    栈操作指令:
    push

    pop

    call

    leave

    enter

    ret

    popa

    popad

    条件指令:
    test

    cmp

    分支指令:
    jmp

    jcc(条件跳转语句总称)
    *************************************
    重复指令:

    x可替换为b,w,d,分别表示字节,字,双子

    movsx

    cmpsx

    stosx

    scasx

    rep

    repe,repz

    repne,repnz

    *************************************



    *函数调用*
    *栈的布局*
    *条件指令*
    *分支指令*-*跳转指令*
    *重复指令*
    *************************************

    7、c语言主函数和偏移

    p77页实例

    8、关于x86的其他指令,可参考x86完整架构手册
    www.intel.com/products/processor/manuals/index.htm

    主要内容:

    第1卷:基本架构
    第2A卷:指令集参考A-M;第2B卷:指令集参考N-Z(这两卷很重要)
    第3A卷:系统编程指南(第一部分);第3B卷:系统编程指南(第二部分)
    优化参考手册

    汇编知识基础补充(完)


    ***********************************************************************************************************************************************************************


    静态高级分析实操部分:

    (一)IDA Pro的使用

    参考书籍:《The IDA Pro Book:The Unofficial Guide to the World's Most Popular Disassembler,2nd Edition》(No Starch出版社)


    (二)汇编中的c代码结构

    1、全局变量通过内存地址引用,而局部变量通过栈地址引用

    2、对if语句的判别

    (1)、如果在cmp比较指令后直接接有jnz(条件跳转)等跳转指令,表示,如果比较结果为不相等(为假)时则跳转,否则继续执行,如:

    cmp eax,[ebp+var_8]
    jnz short loc_40102B

    表示如果eax储蓄器的值与eax+var_8不相等,则跳转至40102B处

    (2)、if语句必定有一个条件跳转指令,但注意不是所有的跳转指令都对应if语句。

    (3)、注意两个跳转指令的区别:

    jz为条件跳转指令:结果为零或相等时跳转

    jnz为条件跳转指令,结果不为零或不相等时跳转

    jmp代码跳转指令,当跳转执行完毕或都满足条件时,跳过条件语句,直接执行后面的代码



    3、反汇编算术指令由编译器决定

    4、对循环的识别

    (1)对for循环的识别

    for循环总是包含4个组件——初始化,比较,执行指令,递增或递减

    补充汇编条件指令:(跳转指令前一般都会有test、cmp等比较指令)

    jge指令:当大于或等于时进行跳转;

    jae指令:当高于等于,或者进位标志转移清零时跳转;

    (2)对while循环的识别

    与for循环的结构很相似,区别在于少一个递增或递减的组件。

    有一个条件跳转和一个无条件跳转

    5、理解函数的调用约定

    (1)cdecl调用约定

    参数从右向左依次压入栈,当函数完成调用时,由调用者自行清理栈,并将返回值保存在EAX中。

    ***********************************实例:

    实际c代码:

    int test(int x,int y,int z);
    int a,b,c,ret;

    ret=test(a,b,c);

    —————————————————反汇编代码:

    1、push c
    2、push b
    3、push a
    4、call test
    5、add esp,12
    6、mov ret,eax

    注:第5行的操作为调用者清理栈

    ***********************************

    (2)stdcall调用约定

    该约定是Windows API的标准约定,除了在函数完成时不需要调用者清理栈外,与cdecl约定的反汇编代码一样。

    *调用windows api的代码都不需要清理栈,清理栈的工作由实现api函数代码的dll程序承担,所以在该约定下,上述实例的代码中会少掉第五行的内容

    (3)fastcall调用约定

    *约定细节会随编译器的不同而有所不同,但工作方式大体相同:

    前一些参数被传到寄存器中,(在微软fastcall约定中还会有两个备用寄存器:EDX和ECX),如果需要的话,剩下的参数再以从左向右的次序加载到栈上

    *该约定的优点:使用该约定更高效,因为代码不涉及很多的栈操作
    ***********************************
    【压栈与移动】

    不同的编译器在选择压栈还是移动时调用的约定不同,如visual studio多半会选择压栈(push),而gun多半会选择移动(mov)。
    ***********************************

    6、分析switch语句

    【注】:switch有两种样式编译:if样式和使用跳转表

    (1)if样式:

    该样式下的switch反汇编与if反汇编代码十分类似,但:

    switch的反汇编代码往往有连续的好几组同等级别的cmp和jcc指令。而if的反汇编虽然有可能存在连续,但不会是同等级别的一组指令。

    总体格式是:先依次比较,满足条件后再执行case分支操作。

    (2)跳转表

    case后的值先减一,然后把减一后的值乘以字节数跳转到跳转表所在的基地址位置,再在该基地址处予以跳转,到代码执行处。

    【比较】:if样式通常是分多次判断跳转,而跳转表通常是先减一,然后依次判断,两次跳转

    7、反汇编数组【 !】

    数组是通过一个基地址作为起始点访问的,往往由一个ecx寄存器所谓索引使用,ecx乘以数据类型的字节数来指明元素的大小,结果值与数组的基地址相加,来访问正确的数组元素

    8、识别结构体【 !】

    结构体通过一个起始指针的基地址来访问

    9、分析链表遍历

    需要识别出包含指针的对象,并且这些指针指向同一类型的对象。还要识别其递归结构

    (三)分析恶意windows程序

    1、Windows API

    windows API采用匈牙利表达法,即通过前缀表示数据类型。

    (1)类型:

    WORD(w):表示16位的无符号数值

    DWORD(dw):表示双字节、32位的无符号数值

    Handles(H):对象索引,句柄中存储的信息并没有文档化,且只被api操作

    Long Pointer(L):指向另一类型的指针,如:LPByte指向字节的指针、LPCSTR指向字符串的指针,字符串通常由LP作为前缀

    Callback:表示一个将会被windows api调用的函数

    (2)句柄

    是在操作系统中被打开或创建的项,比如窗口、进程、模块、菜单、文件等

    句柄可以引用对象或内存位置,很像指针,但与指针不同的是:不能进行数学运算,且并不总是表示对象地址

    (3)文件系统函数

    CreatFile:用来创建和打开文件,可以打开文件、管道、流、I/O设备

    ReadFile和WriteFile:用来对文件进行读写操作,都作为流进行

    CreatFileMapping和MapViewOfFile:前者负责从磁盘上加载一个文件到内存中;后者返回一个指向映射的基地址指针,被用来访问内存中的文件。程序可以使用后者返回的指针,在文件中任意位置进行读写

    (4)特殊文件

    有些特殊文件的访问方式和普通文件不一样,不能通过盘符与文件夹进行访问,但这些特殊文件可以提供对系统硬件和内部数据更强的访问能力

    【共享文件】

    是以\serverNameshare或
    \?serverNameshare开头命名的特殊文件

    用来访问共享目录中的目录或文件,\?前缀告诉操作系统禁用所有的字符串解析,并允许访问长文件名

    【通过名字空间访问的文件】

    名字空间可以被认为是固定数目的文件夹,灭个文件夹中保存不同类型的对象。

    底层的名字空间是NT名字空间,以前缀开始,这个名字空间可以访问所有设备一级所有在自其中存在的其他名字空间

    以前缀\.开始的Win32设备名字空间,可以直接访问物理设备,并像文件一样读写操作,从而绕过文件系统。恶意代码可以通过这个方式读写数据而无需创建文件,以避开安全程序检测

    【备用数据流(ADS)】

    这个特性允许附加数据被添加到一个已存在的NTFS文件中,相当于添加一个文件到另外一文件中。额外数据在列一个目录时不会被显示出来,并且当显示文件内容是也不显示,只有访问流时才可见

    恶意代码可以用其来隐藏数据

    2、Windows注册表

    注册表用来保存操作系统与程序的配置信息,比如设置和选项,恶意代码功能也可由注册表分析出来

    (1)注册表根键

    HKEY_LOCAL_MACHINE(HKLM):保存对本地机器全局设置

    HKEY_CURRENT_USER(HKCU):保存当前用户特定的设置

    HKEY_CLASSES_ROOT:保存定义的类型信息

    HKEY_CURRENT_CONFIG:保存关于当前硬件配置的设置,特别是与当前和标准配置之间不同的部分

    HKEY_USERS:定义默认用户、新用户和当前用户

    (2)IDA PRO一些对注册表的注释

    samDesired:指示了安全访问请求的类型

    ulOptions:是一个表示调用选项的无符号长整数

    hkey:被访问的根键的句柄

    (3)注册表脚本

    注册表版本号

    [要修改的子键](有中括号)

    "要修改的项目名"="键值"(有引号)

    【例】:
    **************************************
    Windows Registry Editor Version 5.00

    [HKLMSOFTWAREMicrosoftWindowsCurrentVersionRun]
    "MaliciousValue"="C:Windowsevil.exe"

    **************************************

    3、网络API

    (1)伯克利兼容套接字

    在windows中有Winsock库实现,在ws2_32.dll中,其中以下几个是常用的:

    socket:创建套接字

    bind:将套接字绑定到特定端口,在accept之前调用

    listen:预示一个套接字将进入监听,等待入站连接

    accept:向一个远程套接字打开一个连接,并接受连接

    connect:向一个远程套接字打开一个连接,远程套接字必须在等待连接

    recv:从远程套接字接收数据

    send:发送数据到远程套接字

    【注】:WSAStartup函数必须在其他网络函数之前调用,用来为网络库分配资源。在调试代码查找网络连接入口是,在WSAStartup函数中设置断点,一般网络入口应该跟在后面不远处

    (2)服务端和客户端

    恶意代码既可以是服务端也可能是客户端

    在连接一个远程套接字的客户端中,会依次有socket调用,connect调用,如果需要的话,会跟着send和recv调用

    一个监听入站连接的服务端,顺序则依次是socket函数创建套接字、bind函数将这个套接字附加到一个端口、listen调用将这个套接字设置为监听状态、然后accept调用挂起,等待远程套接字的连接,如果需要还可能有send和recv调用

    (3)WinINet API

    该函数保存在wininet.dll中,是一个高一级的API,实现应用层协议,如http等,恶意代码可以使用其连接服务器,获得指令

    InternetOpen:初始化一个到互联网的连接

    InternetOpenUrl:访问一个URL

    InternetReadFile和ReadFile:允许程序从一个来自互联网的下载文件中读取数据

    4、跟踪恶意代码的运行

    (1)DLL(动态链接库)

    【恶意代码使用DLL的方式】

    保存恶意代码:通过被其他程序调用公共DLL来将自己加载到其他进程上

    通过使用Windows DLL:可以达到和操作系统交互的目的

    通过使用第三方DLL:和其他程序交互,或扩充恶意代码本身功能

    【基本DLL结构】
    和exe文件一样使用PE格式,只有一个标志,有很多到处函数,导入函数较少。

    主函数是DLLMain,被指定为文件的入口点

    (2)进程

    访问某内存地址的恶意程序,只会影响包含恶意代码的那个进程在这个位置上保存的东西,系统中其他使用这个地址的程序则不会受到影响

    恶意代码通常会使用CreatProcess函数来创建新进程

    【注】:

    通过CreatProcess创建的远程shell,要找到这台远程主机,需要判断其使用的套接字在哪里被初始化。要发现哪个程序将被运行,需要通过被执行的命令行地址来查找字符串

    (3)线程

    通过CreatThread函数来创建线程,函数调用者指定一个起始地址,被称为start函数。当分析调用CreatThread的代码时,除了分析这个start函数外,还需要分析调用CreatThread的剩下的代码

    【恶意代码使用CreatThread的方式】:

    使用CreatThread加载一个新的恶意库文件到进程中,通过调用CreatThread时将起始地址设置为LoadLibrary的地址;

    可以为输入和输出创建两个线程,一个用来在套接字或管道上监听,并输出到一个进程的标准输入里;一个用来从标准输出读取数据,并发送到套接字或管道上。发送所有信息到单一套接字或管道,来和运行的程序进行无缝通信;

    (4)使用互斥量的进程间协作

    互斥量用于控制共享资源的访问。一次只允许一个进程访问某共享资源

    互斥量通常使用硬编码的名字,将它们作为基于主机的感染迹象是很好的选择,如果一个互斥量被两个不使用其他通信方式通信的进程使用时,它的名字必须相互一致

    线程通过一个对WaitForSingleObject的调用获取对互斥量的访问。完成互斥量的使用后使用ReleaseMutex函数

    可以通过CreateMutex函数创建,进程可以通过OpenMutex调用来获取另一个进程中互斥量的句柄

    (5)服务

    作为服务安装也是恶意代码执行附加代码的另一种方式

    通过一些API来安装和操作:

    OpenSCManager:返回一个服务控制管理器的句柄,被用来进行所有后续与服务相关的函数调用。和服务交互的代码会调用

    CreateService:添加一个新服务到服务控制管理器,并允许调用者指定服务是否在引导时自动启动或手动启动

    StartService:启动服务,并且在服务被设置成手动启动时使用

    (6)组件对象模型(COM)

    这是一个标准接口,使得不同软件组件在不知道其他组件代码的接口规范时,相互之间可以进行调用

    【注】:

    每个使用COM的线程,必须在调用任何其他COM库函数之前,至少调用一次OleInitialize或CoInitializeEx函数,可以搜索这些调用来分析是否使用了COM功能,然后找到一些正在被使用对象的标识符继续分析

    【COM服务器恶意代码】:

    这类恶意代码很容易检测,有如下几个函数必须由COM服务器软件导出:DllCanUnloadNow、DllGetClassObject、DllInstall、DllRegisterServer、DllUnregisterServer

    5、内核中的恶意代码简介

    当反汇编中出现SYSENTER、SYSCALL或者INT 0x2E时,指明一个调用被使用进入到内核

    6、原生API

    原生API是用来和windows进行交互的底层API,恶意代码通过调用原生API函数可以绕过普通的windows API

    主要用到的是IDA Pro从反汇编代码分析恶意代码的行为与调用,从而进一步得出恶意代码危害行为报告,要求熟练掌握IDA Pro的使用及Windows API的基本知识(掌握其调用所需参数及功能即可,不需深入了解其实现过程)。

    还要注意的是,在分析过程中,由于对系统库函数及API的不熟悉,导致分析进入歧途,如会去分析库函数的实现过程,从而使分析进入死胡同。

  • 相关阅读:
    编译openwrt时报错build_dir/hostpkg/libubox-2018-07-25-c83a84af/blobmsg_json.c:21:19: fatal error: json.h: No such file or directory
    git add时遇到类似fatal: Path 'XXX' is in submodule 'XXX'错误提示如何解决?
    select下拉框多选取值
    JavaScript substr() 方法
    select2多选设置select多选,select2取值和赋值
    bootstrap select 多选的用法,取值和赋值(取消默认选择第一个的对勾)
    Bootstrap select 多选并获取选中的值
    Bootstrap select多选下拉框实现代码
    Bootstrap selectpicker 下拉框多选获取选中value和多选获取文本值
    Mysql中EXISTS关键字用法、总结
  • 原文地址:https://www.cnblogs.com/zlgxzswjy/p/4839844.html
Copyright © 2011-2022 走看看