前言

  1. 010 Editor 是很好的逆向学习软件,通过编写注册机对提高逆向能力有很大帮助,
  2. 由于本文写的比较早,发布时已经出9.0版本,特添加9.0版本破解补丁。

一、工具及软件介绍

  • 逆向工具:x64 dbg
  • 平台:Win_1803 x64 虚拟机
  • 破解软件:010 Editor v8.0.1 x64

1533889781547

二、逆向分析

2.1、找到提示错误注册弹窗

  接上一篇,通过对CreateWindowExA、CreateWindowExW下断点,并且利用堆栈调用找到关键代码处

1533890617070

  找到弹窗中的提示字符串,选中这一行,x64dbg会自动分析出该处是从何处跳转过来的,跟踪并分析

2.2、分析跳转处代码

1533891790788

  到达跳转处代码,我们会发现有多组比较跳转,其中一个就是弹出无效的用户名和注册码信息处的跳转。

  通过分析推测,这几组比较是判断错误类型的。

  此时,我们选中最上方的比较处,会发现该处比较是从哪里跳转过来的,继续跟踪分析。

1533893096178

  来到这里我们发现ebxDB比较,结合分析得出,如果不相等就进入错误判断分支,相等则弹出注册成功弹窗。

  我们选中比较处代码,会发现跳转来源,继续跟踪分析。

1534059108233

  通过分析该处代码可知,E7来源于第一个CALL中,第二个CALL的返回值是用于判断注册码是否正确的,即返回结果为DB,接着往下看,当esi等于E7,即第一个CALL的返回结果为E7或者rcx + 3c等于0时都会跳转到下边注册码正确判断的流程中去。

  此时,我们得出的结论是,当第二个CALL的返回值是DB时,注册就会成功,这里我们将第二个CALL函数打上标签为=DB,方便后续分析。

1534060777709

  我们进入=DB这个函数分析一下

1534061038996

  我们已经知道,要使注册成功,这个函数就必须返回DB,通个观察这个函数,当7FF715213D46这个函数的返回值为2D时会将这个函数的返回值赋值为DB,同时,我们发现这个函数还会返回E7这个值,这时我们想到在分析=DB这个函数之前也有一个函数可能会返回为E7,我们对比一下这2个函数看看。

1534061931049

  我们发现这2个是同一函数,此时根据前后关系分析,当该函数的返回值为E7时会跳转到注册码正确验证处,但这个函数的返回值是E7时,=DB这个函数的返回值却不是DB

  通过这2个条件,我们得出这个函数的返回值在等于2D时,注册会成功。

  我们将该函数打上=2D标签,方便分析,同时更改之前的分析注释。

1534062636600

2.3、=2D 函数分析

  通过上方分析已知,该函数需要等于2D,才会注册成功。

  此时,我们先找到返回值为2D操作的代码。

1534063186520

  找到2D赋值处后,我们就需要跟踪调试这个函数了。

  通过单步跟踪发现00007FF71704597E之前是获取注册码和一些检测工作

2.3.1、获取注册码处分析

  • 使用的测试注册码为:0011-2233-4455-6677-8899

1535016937124

  • 注册码与局部变量的对应关系
    • KEY[0] = 00 KEY[1] = 11 KEY[2] = 22 KEY[3] = 33 KEY[4] = 44
    • KEY[5] = 55 KEY[6] = 66 KEY[7] = 77 KEY[8] = 88 KEY[9] = 99
  • 内存中的存储状态【RSP + 28】

1535017576253

  • 通过00007FF71704597E及之后的汇编代码确定数据的存储位置
    • KEY[0] = [RSP + 28]
    • KEY[1] = [RSP + 29]
    • KEY[2] = [RSP + 2A]
    • KEY[3] = [RSP + 2B]
    • KEY[4] = [RSP + 2C]
    • KEY[5] = [RSP + 2D]
    • KEY[6] = [RSP + 2E]
    • KEY[7] = [RSP + 2F]
    • KEY[8] = [RSP + 30]
    • KEY[9] = [RSP + 31]
  • 00007FF71704597E之后的代码分析

1535024493581

1535024772731

  • 通过分析发现,当取出KEY[3]的值后,有3处比较跳转,分别为9C/FC/AC,对应的C代码如下
switch ( KEY[3] )
{
    case 0x9C:
        break;
    case 0xFC:
        break;
    case 0xAC:
        break;
}
  • 接下来就可以单独分析每一个分支了

2.3.2、3处分支分析

2.3.2.1、9C情况

1535189040758

  • 得到的结果为
    • EBX = ((( KEY[0] ^ KEY[6] ) ^ 0x18 ) + 0x3D ) ^ 0xA7
    • EAX = (((((( KEY[1] ^ KEY[7] ) * 0x100) + ( KEY[2] ^ KEY[5] )) ^ 0x7892 ) + 0x4d30 ) ^ 0x3421) / 0xB 或 0
  • 对应的C代码
case 0x9C:
    DWORD EAX = ( ( ( ( ( ( KEY[ 1 ] ^ KEY[ 7 ] ) * 0x100 ) +
        ( KEY[ 2 ] ^ KEY[ 5 ] ) ) ^ 0x7892 ) + 0x4d30 ) ^ 0x3421 );
    DWORD EBX = ( ( ( KEY[ 0 ] ^ KEY[ 6 ] ) ^ 0x18 ) + 0x3D ) ^ 0xA7;
    if (EAX % 0xB != 0)
    {
        EAX = 0; // error
    }
    if (EAX == 0 || EAX > 0x3E8)
    {
        return; // error
    }
    if (EBX == 0)
    {
        return; // error
    }
    // 如果EBX >= 0x2 EBX = 0 否则不变
    // EBX 做为下面用户名处理函数的参数
    if ( EBX >= 0x2 )
    {
    EBX = 0;
    }
    // EAX 做为下面用户名处理函数的参数
    EAX = EAX / 0xB;
    break;

2.3.2.2、FC情况

1535193304737

  • 得到的结果为
    • EBX = FF
    • EAX = 0x1
  • 由于后面的判断,如果是0xFC,是不会返回0x2D的,这里略

2.3.2.3、AC情况

1535204979591

  • 得到的结果为
    • EBX = EAX
    • DWORD PTR DS:[RDI+0x30] = (((((( KEY[1] ^ KEY[7] ) * 0x100) + ( KEY[2] ^ KEY[5] )) ^ 0x7892 ) + 0x4d30 ) ^ 0x3421) / 0xB 或 0
  • 对应的C代码
case 0xAC:
    DWORD EAX = ( ( ( ( ( ( KEY[ 1 ] ^ KEY[ 7 ] ) * 0x100 ) +
        ( KEY[ 2 ] ^ KEY[ 5 ] ) ) ^ 0x7892 ) + 0x4d30 ) ^ 0x3421 );
    if ( EAX % 0xB != 0 )
    {
        EAX = 0; // error
    }
    if ( EAX == 0 || EAX > 0x3E8 )
    {
        return; // error
    }
    DWORD ECX = ( ( ( ( ( ( ( KEY[ 4 ] ^ KEY[ 8 ] ) << 0x8 ) + ( ( KEY[ 9 ] ^ KEY[ 5 ] ) << 0x10 ) + ( KEY[ 0 ] ^ KEY[ 6 ] ) ) ^ 0x5B8C27 ) ^ 0x22C078 ) - 0x2C175 ) ^ 0xFFE53167 ) & 0xFFFFFF;

    EAX = 0xF0F0F0F1;
    DWORD EDX = 0;
    // 伪代码:EDX : EAX = EAX * ECX
    EDX = EDX >> 0x4;
    EAX = EDX * 0x11;
    ECX = ECX - EAX;
    if (ECX == 0) // ECX = EAX
    {
        EAX = EDX; // Right
    }
    else
    {
        EAX = 0;
    }
    DWORD RDI0x30 = EAX / 0xB;
    EBX = EAX;
    break;

2.3.3、用户名处理函数

2.3.3.1、函数调用处分析

1535421915838

  • 上面已经准备好了 EBXRDI + 30的值
  • 结合IDA分析得出,用户名处理函数的参数如下:
    • 参数一:用户名字符串首地址
    • 参数二:如果类型是0xFC就是0,否则是1,用于函数内部判断类型
    • 参数三:准备好的EBX
    • 参数四:准备好的RDI + 30

2.3.3.2、函数内部分析

1535703799261

1535703844526

void function( char* name )
{
    DWORD KEY[ 4 ] = { 0x9C };
    DWORD EBX = 1;
    DWORD RDI30 = 0;
    DWORD LOCAL1 = 0;
    DWORD LOCAL2 = ( EBX/*参数3*/ * 0x11 ) & 0xFF;
    DWORD LOCAL3 = 0;
    DWORD LOCAL4 = 0;
    DWORD ESI = RDI30/*参数4*/ * 0xF;
    int NameLen = strlen( name );
    char EAX[ 4 ] = { 0 }; // 函数返回值
    DWORD PW[ 256 ] = {
        0x39CB44B8, 0x23754F67, 0x5F017211, 0x3EBB24DA, 0x351707C6, 0x63F9774B, 0x17827288, 0x0FE74821,
        …………
        0x5CFF0261, 0x33AE061E, 0x3BB6345F, 0x5D814A75, 0x257B5DF4, 0x0A5C2C5B, 0x16A45527, 0x16F23945 };
    for ( int i = 0; i < NameLen; i++ )
    {
        char nameChar = toupper( name[ i ] );
        if ( KEY[ 3 ] != 0xFC )
        {
            LOCAL1 = ( PW[ nameChar ] ^ PW[ nameChar + 0xD ] ) * PW[ nameChar + 0x2F ]
                + PW[ LOCAL2 ]
                + PW[ ESI ]
                + PW[ LOCAL3 ];
        }
        else
        {
            LOCAL1 = ( PW[ nameChar ] ^ PW[ nameChar + 0x3F ] ) + PW[ nameChar + 0x17 ]
                + PW[ LOCAL2 ]
                + PW[ ESI ]
                + PW[ LOCAL4 ];
        }
        LOCAL3 += 0x13;
        LOCAL2 += 0x9;
        ESI += 0xD;
        LOCAL4 += 0x7;
    }
    *( DWORD* ) EAX = LOCAL1;
}

2.3.4、用户名处理函数返回值及剩余代码分析

1535531336000

  • 通过分析发现,当KEY[3] = 0xFC时,不会跳转到正确的返回值赋值处
  • 所以,,当KEY[3] = 0xFC时,之后的代码都不用分析
  • 对应C代码如下
void function1()
{
    DWORD KEY[ 10 ] = { 0 };
    DWORD EAX[ 4 ] = { 0 };
    if ( KEY[ 4 ] == EAX[ 0 ] &&
         KEY[ 5 ] == EAX[ 1 ] &&
         KEY[ 6 ] == EAX[ 2 ] &&
         KEY[ 7 ] == EAX[ 3 ] )
    {
        switch ( KEY[3] )
        {
            case 0x9C:
                /*
                  正确
                */
                break;
            case 0xFC:
                /*
                  错误
                */
                break;
            case 0xAC:
                /*
                  正确
                */
                break;
        }
    }
}

三、注册机编写

  • 本次使用MFC编写注册机
  • 全局数组密码表(内存中提取)
DWORD PW[ 256 ] = {
    0x39CB44B8, 0x23754F67, 0x5F017211, 0x3EBB24DA, 0x351707C6, 0x63F9774B, 0x17827288, 0x0FE74821,
    0x5B5F670F, 0x48315AE8, 0x785B7769, 0x2B7A1547, 0x38D11292, 0x42A11B32, 0x35332244, 0x77437B60,
    0x1EAB3B10, 0x53810000, 0x1D0212AE, 0x6F0377A8, 0x43C03092, 0x2D3C0A8E, 0x62950CBF, 0x30F06FFA,
    0x34F710E0, 0x28F417FB, 0x350D2F95, 0x5A361D5A, 0x15CC060B, 0x0AFD13CC, 0x28603BCF, 0x3371066B,
    0x30CD14E4, 0x175D3A67, 0x6DD66A13, 0x2D3409F9, 0x581E7B82, 0x76526B99, 0x5C8D5188, 0x2C857971,
    0x15F51FC0, 0x68CC0D11, 0x49F55E5C, 0x275E4364, 0x2D1E0DBC, 0x4CEE7CE3, 0x32555840, 0x112E2E08,
    0x6978065A, 0x72921406, 0x314578E7, 0x175621B7, 0x40771DBF, 0x3FC238D6, 0x4A31128A, 0x2DAD036E,
    0x41A069D6, 0x25400192, 0x00DD4667, 0x6AFC1F4F, 0x571040CE, 0x62FE66DF, 0x41DB4B3E, 0x3582231F,
    0x55F6079A, 0x1CA70644, 0x1B1643D2, 0x3F7228C9, 0x5F141070, 0x3E1474AB, 0x444B256E, 0x537050D9,
    0x0F42094B, 0x2FD820E6, 0x778B2E5E, 0x71176D02, 0x7FEA7A69, 0x5BB54628, 0x19BA6C71, 0x39763A99,
    0x178D54CD, 0x01246E88, 0x3313537E, 0x2B8E2D17, 0x2A3D10BE, 0x59D10582, 0x37A163DB, 0x30D6489A,
    0x6A215C46, 0x0E1C7A76, 0x1FC760E7, 0x79B80C65, 0x27F459B4, 0x799A7326, 0x50BA1782, 0x2A116D5C,
    0x63866E1B, 0x3F920E3C, 0x55023490, 0x55B56089, 0x2C391FD1, 0x2F8035C2, 0x64FD2B7A, 0x4CE8759A,
    0x518504F0, 0x799501A8, 0x3F5B2CAD, 0x38E60160, 0x637641D8, 0x33352A42, 0x51A22C19, 0x085C5851,
    0x032917AB, 0x2B770AC7, 0x30AC77B3, 0x2BEC1907, 0x035202D0, 0x0FA933D3, 0x61255DF3, 0x22AD06BF,
    0x58B86971, 0x5FCA0DE5, 0x700D6456, 0x56A973DB, 0x5AB759FD, 0x330E0BE2, 0x5B3C0DDD, 0x495D3C60,
    0x53BD59A6, 0x4C5E6D91, 0x49D9318D, 0x103D5079, 0x61CE42E3, 0x7ED5121D, 0x14E160ED, 0x212D4EF2,
    0x270133F0, 0x62435A96, 0x1FA75E8B, 0x6F092FBE, 0x4A000D49, 0x57AE1C70, 0x004E2477, 0x561E7E72,
    0x468C0033, 0x5DCC2402, 0x78507AC6, 0x58AF24C7, 0x0DF62D34, 0x358A4708, 0x3CFB1E11, 0x2B71451C,
    0x77A75295, 0x56890721, 0x0FEF75F3, 0x120F24F1, 0x01990AE7, 0x339C4452, 0x27A15B8E, 0x0BA7276D,
    0x60DC1B7B, 0x4F4B7F82, 0x67DB7007, 0x4F4A57D9, 0x621252E8, 0x20532CFC, 0x6A390306, 0x18800423,
    0x19F3778A, 0x462316F0, 0x56AE0937, 0x43C2675C, 0x65CA45FD, 0x0D604FF2, 0x0BFD22CB, 0x3AFE643B,
    0x3BF67FA6, 0x44623579, 0x184031F8, 0x32174F97, 0x4C6A092A, 0x5FB50261, 0x01650174, 0x33634AF1,
    0x712D18F4, 0x6E997169, 0x5DAB7AFE, 0x7C2B2EE8, 0x6EDB75B4, 0x5F836FB6, 0x3C2A6DD6, 0x292D05C2,
    0x052244DB, 0x149A5F4F, 0x5D486540, 0x331D15EA, 0x4F456920, 0x483A699F, 0x3B450F05, 0x3B207C6C,
    0x749D70FE, 0x417461F6, 0x62B031F1, 0x2750577B, 0x29131533, 0x588C3808, 0x1AEF3456, 0x0F3C00EC,
    0x7DA74742, 0x4B797A6C, 0x5EBB3287, 0x786558B8, 0x00ED4FF2, 0x6269691E, 0x24A2255F, 0x62C11F7E,
    0x2F8A7DCD, 0x643B17FE, 0x778318B8, 0x253B60FE, 0x34BB63A3, 0x5B03214F, 0x5F1571F4, 0x1A316E9F,
    0x7ACF2704, 0x28896838, 0x18614677, 0x1BF569EB, 0x0BA85EC9, 0x6ACA6B46, 0x1E43422A, 0x514D5F0E,
    0x413E018C, 0x307626E9, 0x01ED1DFA, 0x49F46F5A, 0x461B642B, 0x7D7007F2, 0x13652657, 0x6B160BC5,
    0x65E04849, 0x1F526E1C, 0x5A0251B6, 0x2BD73F69, 0x2DBF7ACD, 0x51E63E80, 0x5CF2670F, 0x21CD0A03,
    0x5CFF0261, 0x33AE061E, 0x3BB6345F, 0x5D814A75, 0x257B5DF4, 0x0A5C2C5B, 0x16A45527, 0x16F23945
};
  • 获取随机数小于0x3E8的函数(获取注册人数)
DWORD GetRandNum() {
    DWORD Result;
    srand( ( unsigned ) time( NULL ) );
    return Result = rand() % 0x3E8;  // 用于获取随机区间,(0, 3E8],为注册人数 1 ~ 1000
}
  • 用户名处理函数
    • 参数1:用户名
    • 参数2:注册人数(随机函数获取)
    • 参数3:注册天数(当版本注册【0x9C】时 = 0,当时间注册时【0xAC】时=要注册的年月日 - 1970.1.1之间的天数)
void CKeyGenDlg::GetEncryStr( char * szName, DWORD Regmen, DWORD day )
{
    DWORD ESI = Regmen * 0xF;
    DWORD LOCAL1 = 0;
    DWORD LOCAL2 = ( day * 0x11 ) & 0xFF;
    DWORD LOCAL3 = 0;
    //DWORD LOCAL4 = 0;
    BYTE EAX[ 4 ] = { 0 };
    int NameLen = strlen( szName );
    for ( int i = 0; i < NameLen; i++ )
    {
        char nameChar = toupper( szName[ i ] );
        LOCAL1 =
            ( ( ( LOCAL1 + PW[ nameChar ] ) ^ PW[ ( nameChar + 0xD ) & 0xFF ] )*PW[ ( nameChar + 0x2F ) & 0xFF ] )
            + PW[ LOCAL2 ]
            + PW[ ESI & 0xFF ]
            + PW[ LOCAL3 ];

        ESI += 0xD;
        LOCAL2 += 0x9;
        LOCAL3 += 0x13;
    }
    *( DWORD* ) EAX = LOCAL1;
    KEY[ 4 ] = EAX[ 0 ];
    KEY[ 5 ] = EAX[ 1 ];
    KEY[ 6 ] = EAX[ 2 ];
    KEY[ 7 ] = EAX[ 3 ];
}
  • 注册码计算函数
    • 参数1:用户名
    • 参数2:注册天数
  • 通过该函数可以根据用户名计算出注册码,同时可计算版本注册及天数注册(按选择不同区分)
void CKeyGenDlg::CalcReg( char * szName, DWORD day )
{
    char destbuff[ 100 ] = { 0 };
    DWORD Regmen = GetRandNum();
    /*
    判断是否整除函数的结果为:Regmen
    对应公式为:Regmen = ((((( KEY[1] ^ KEY[7] ) * 0x100 + ( KEY[2] ^ KEY[5] )) ^ 0x7892 ) + 0x4D30 )  ^ 0x3421) / 0xB
    逆向计算得出:( KEY[1] ^ KEY[7] ) * 0x100 + ( KEY[2] ^ KEY[5] ) 的值,WORD
    */
    WORD tmp = ( ( ( Regmen * 0xB ) ^ 0x3421 ) - 0x4D30 ) ^ 0x7892;

    USES_CONVERSION;
    if ( status )
    {
        //MessageBox( L"版本", 0, 0 );
        KEY[ 3 ] = 0x9C;
        day = 0; // 当为版本注册时,天数必须为0
        GetEncryStr( szName, Regmen, day );
        KEY[ 1 ] = ( HIBYTE( tmp ) ) ^ KEY[ 7 ];
        KEY[ 2 ] = ( LOBYTE( tmp ) ) ^ KEY[ 5 ];
        /*
        版本号 9 < ((( KEY[0] ^ KEY[6] ) ^ 0x18 ) + 0x3D ) ^ 0xA7
        这里取 0x18
        */
        KEY[ 0 ] = ( ( ( 0x18 ^ 0xA7 ) - 0x3D ) ^ 0x18 ) ^ KEY[ 6 ];
        sprintf_s( destbuff, "%02X%02X-%02X%02X-%02X%02X-%02X%02X", KEY[ 0 ], KEY[ 1 ], KEY[ 2 ], KEY[ 3 ], KEY[ 4 ], KEY[ 5 ], KEY[ 6 ], KEY[ 7 ] );
        m_Reg = A2T( destbuff );
        UpdateData( FALSE );
    }
    else
    {
        //MessageBox( L"天数", 0, 0 );
        KEY[ 3 ] = 0xAC;
        GetEncryStr( szName, Regmen, day );
        KEY[ 1 ] = ( HIBYTE( tmp ) ) ^ KEY[ 7 ];
        KEY[ 2 ] = ( LOBYTE( tmp ) ) ^ KEY[ 5 ];
        // 通过天数逆向推导得出计算之前的数值
        /*
        day = ((((((( KEY[9] ^ KEY[5] ) << 0x10 ) + (( KEY[4] ^ KEY[8] ) << 0x8 ) + KEY[0] ^ KEY[6])xor 005B8C27)
        xor 0x22c078) - 0x2C175) xor 0xFFE53167) and 0xFFFFFF
        */
        DWORD tmp2 = ( ( ( ( day * 0x11 ) ^ 0xFFE53167 ) + 0x2C175 ) ^ 0x22C078 ) ^ 0x5B8C27;
        KEY[ 0 ] = ( tmp2 & 0xFF ) ^ KEY[ 6 ];
        KEY[ 8 ] = ( ( tmp2 >> 0x8 ) & 0xFF ) ^ KEY[ 4 ];
        KEY[ 9 ] = ( ( tmp2 >> 0x10 ) & 0xFF ) ^ KEY[ 5 ];
        sprintf_s( destbuff, "%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X", KEY[ 0 ], KEY[ 1 ], KEY[ 2 ], KEY[ 3 ], KEY[ 4 ], KEY[ 5 ], KEY[ 6 ], KEY[ 7 ], KEY[ 8 ], KEY[ 9 ] );
        m_Reg = A2T( destbuff );
        UpdateData( FALSE );
    }
}
  • 按钮响应函数
  • 该函数对运算逻辑及数据进行处理,当用户名不为空时,计算注册码
void CKeyGenDlg::OnBnClickedOk()
{
    // TODO: 在此添加控件通知处理程序代码
    // CDialogEx::OnOK();
    memset( KEY, 0, sizeof( KEY ) ); // 清空数组
    UpdateData( TRUE );
    if ( !m_User.IsEmpty() )
    {
        USES_CONVERSION;
        char * szName = T2A( m_User );
        DWORD day = 0x7224; // 1970.1.1 - 指定日期的天数
        CalcReg( szName, day );
    }
    else
    {
        MessageBox( _T( "用户名不能为空" ), 0, MB_ICONWARNING );
    }
}

3.1、注册机外观

由于使用了单选控件,于是在选择授权方式时对注册码编辑框进行处理

1535705857764

3.2、结果验证

3.2.1、版本授权

1535707084682

1535707110524

3.2.2、天数授权

1535707144675

1535707193537

四、网络验证

4.1、两处判断网络验证是否通过处

  • 第一处

1535707426448

  • 第二处

1535707466692

总结:当RCX + 3C处的值为0时,走正常流程,否则,使用注册机注册的将会提示注册码无效。

1535707814925

继续观察第一处,在网络验证处下方有一个函数,我们进去浏览会发现网址拼接,网址拼接中会发现有namepwd字样,此时怀疑该函数可能就是修改RCX + 3C处的值的函数。

4.2、确认是否为网络验证函数

  • 可以通过新建虚拟机或修改RCX + 3C处的值为0,通过修改ZF标志位强制执行该函数,验证结果如下

1535708629364

4.3、突破网络验证

  • 方法一:将两处网络验证比较处的跳转进行修改,即JEJMP,使其强制执行网络验证通过后的代码
  • 方法二:进入网络验证函数在其函数头直接RET将函数返回,不对RCX + 3C进行修改

总结:突破网络验证的方法有很多,还需要多学习。

五、附件

010 Editor 8.0.1  提取码:d6ds

010 Editor 9.0     提取码:0lfe

注册机

六、源码

说明:移植MFC部分代码,使用Qt重写,并为时间注册添加自选时间功能

GitHub:KenGen

posted on 2018-10-30 20:05  PhantomW  阅读(1675)  评论(0编辑  收藏  举报