GIVE_A_TRY.exe 逆向(NCK逆向初级第9,10,11课作业)

这个程序好像是一道CTF的题,对我这样的新手来说难度很大,解题过程中遇到了不少坑,还学到了新的反调试技巧。下面我将记录下我逆向这个程序的过程。

在这里插入图片描述

一、去花指令,过反调试,分析TLS回调函数功能

这个程序用到了TLS反调试技术,很遗憾,我用的编程达人OD和X64DBG都有反反调试插件,直接把TLS过了,所以我刚开始甚至没意识到反调试的存在。
然而,如果您使用无插件的OD,那么您运行该程序可能会崩溃,又或者即使输入了正确密钥,程序也会提示密钥错误,这是因为,程序会检测当前是否正在被调试,然后会根据这个判断结果修改随机数种子。如果正在被调试,那么生成的种子也是错的。
关于TLS反调试,我也写了一篇博客记录。https://blog.csdn.net/Kwansy/article/details/108570075

要逆向这个程序,首先必须了解TLS的原理,TLS回调函数会在主函数调用前由操作系统调用,作者把一些关键操作放在TLS回调里做了,所以我们必须跟进TLS函数,看看作者干了些啥。

打开StrongOD插件选项,勾上“在TLS断下”

在这里插入图片描述

这样,就能跟进第一个TLS回调函数。

在这里插入图片描述
关于TLS函数的位置,我们可以打开PE工具查看
在这里插入图片描述在这里插入图片描述
然后我们会发现作者弄了很多花指令,模板有两种,第一种作者喜欢放到函数开头附近,402006 的call就是一个花指令,是通过一系列的 CALL, ADD [ESP] 和 RET 来实现的,分析清楚他的模板之后,可以NOP掉了。
在这里插入图片描述
后面还有很多花指令,基本都是CALL的模板,全部NOP掉,分析第一个TLS回调,我在OD和IDA都分析过了,直接说结论,第一个TLS的功能是反调试,和检测调试状态,最后,动态修复第二个TLS回调函数的地址。

void __stdcall TlsCallback_0(int a1, int a2, int a3)
{
  void (__stdcall *TlsCallback_1)(int, int, int); // edi
  unsigned int v4; // et0
  void *hThread; // eax
  void *hProcess; // eax
  unsigned int v7; // [esp+0h] [ebp-10h]

  if ( a2 == 1 )
  {
    TlsCallback_1 = ::TlsCallback_1;
    v4 = __readeflags();
    v7 = v4;
    if ( v4 & 0x100 )
      TlsCallback_1 = 0;
    __writeeflags(v7);
    if ( *(_DWORD *)&NtCurrentPeb()->InheritedAddressSpace & 0x10000 )
      TlsCallback_1 = 0;
    hThread = (void *)GetCurrentThread();
    NtSetInformationThread(hThread, ThreadHideFromDebugger, 0, 0);
    hProcess = (void *)GetCurrentProcess();
    NtQueryInformationProcess(hProcess, ProcessDebugPort, &isdebug, 4u, 0);
    if ( isdebug )
      TlsCallback_1 = 0;
    dword_404036 = (int)TlsCallback_1; // 动态patch第二个TLS回调
  }
}

只要我们把反调试过掉,第二个TLS回调就有了,同样的道理,跟进TLS2,去掉花指令,分析代码。

void __stdcall TlsCallback_1(int a1, int a2, int a3)
{
  void *hProcess; // eax

  if ( a2 == 1 )
  {
    hProcess = (void *)GetCurrentProcess();
    NtQueryInformationProcess(hProcess, ProcessDebugPort, &isdebug, 4u, &isdebug);
    isdebug ^= 0x31333337u;
    dword_40403A = 0;
    isdebug ^= *(unsigned __int8 *)start;
  }
}

第二个TLS回调函数的功能就是修改了isdebug这个全局变量的值,然后给TLS数组的第三个位置填0,所以就没有第三个TLS回调了。
到这里为止,我们完成了去花指令,反反调试的工作,下一步就是分析算法。

二、分析算法

怎么找到密钥检验算法就略过了,用IDA反编译函数,根据功能分析函数,重命名一些变量后,得到如下代码:

int __stdcall CheckKey(char *key)
{
  int result; // eax
  int v2; // edi
  unsigned __int8 v3; // al
  char *v4; // esi
  unsigned int i; // ebx
  unsigned int v6; // eax
  unsigned int v7; // edx
  unsigned int v8; // edx
  unsigned int v9; // edx
  unsigned int v10; // edx
  unsigned int v11; // edx
  unsigned int v12; // edx
  unsigned int v13; // edx
  unsigned int v14; // edx
  unsigned int v15; // edx
  unsigned int v16; // edx
  unsigned int v17; // edx
  unsigned int v18; // edx
  unsigned int v19; // edx
  unsigned int v20; // edx
  unsigned int v21; // edx

  if ( strlen(key) != 42 )
    return MessageBoxA(0, aThinkAgain, 0, 0);
  v2 = 0;
  v3 = *key;
  v4 = key + 1;
  while ( v3 )
  {
    v2 += v3;
    v3 = *v4++;
  }
  srand(isdebug ^ v2);
  for ( i = 0; i != 42; ++i )
  {
    v6 = (unsigned __int8)key[i] * rand();
    v7 = v6 * (unsigned __int64)v6 % 0xFAC96621;
    v8 = v7 * (unsigned __int64)v7 % 0xFAC96621;
    v9 = v8 * (unsigned __int64)v8 % 0xFAC96621;
    v10 = v9 * (unsigned __int64)v9 % 0xFAC96621;
    v11 = v10 * (unsigned __int64)v10 % 0xFAC96621;
    v12 = v11 * (unsigned __int64)v11 % 0xFAC96621;
    v13 = v12 * (unsigned __int64)v12 % 0xFAC96621;
    v14 = v13 * (unsigned __int64)v13 % 0xFAC96621;
    v15 = v14 * (unsigned __int64)v14 % 0xFAC96621;
    v16 = v15 * (unsigned __int64)v15 % 0xFAC96621;
    v17 = v16 * (unsigned __int64)v16 % 0xFAC96621;
    v18 = v17 * (unsigned __int64)v17 % 0xFAC96621;
    v19 = v18 * (unsigned __int64)v18 % 0xFAC96621;
    v20 = v19 * (unsigned __int64)v19 % 0xFAC96621;
    v21 = v20 * (unsigned __int64)v20 % 0xFAC96621;
    if ( v6 % 0xFAC96621 * (unsigned __int64)v21 % 0xFAC96621 != dword_4030B4[i] )
      break;
  }
  if ( i >= 0x2A )
    result = MessageBoxA(0, aCorrect, aCongrats, 0);
  else
    result = MessageBoxA(0, aIncorrect, 0, 0);
  return result;
}

要注意IDA F5反汇编的代码有错误,我已经在下文的代码注释中标明了。
如果你不明白上面这句话的含义,你可以拿IDA生成的代码和汇编对照,就能明白了,
.text:004011D8 mul edx
.text:004011DA div ecx
.text:004011DC mov eax, edx
IDA F5插件无视了这三行,莫名其妙。。。

校验函数用到了随机数,我第一反应是懵逼的,甚至有点害怕。实际上,当srand的种子确定后,rand的结果也是国定的,可以看到,种子是 isdebug ^ keySum 计算得来。isdebug是在第二个TLS回调函数里设置的,固定就是 0x31333359,而这个 keySum 是密钥每一位累加得来,也就是说,每次输入不同的密钥,keySum 都不同。
我到这一步就不会做了,下面是其他牛人的解题思路:
首先,我们知道密钥是以"flag{"开头的(程序里有字符串提示),那么前5次循环的key[i]是确定的,我们希望 if ( v6 % 0xFAC96621 * (unsigned __int64)v21 % 0xFAC96621 != dword_4030B4[i] ) 这个判断是相等的,v21是通过v6计算得来,所以唯一不确定的是v6,而v6有key[i]和rand()共同确定,前5次key[i]已知,不确定rand(),前五次rand()由srand种子决定,所以只需要暴力枚举种子,让前5个密钥字符满足比较条件即可。种子的暴力枚举不需要从0到0xFFFFFFFF,只需要枚举 42个字符的累加和这个范围就行了。直接上代码:

// 如果知道 key 的累加和,就可以得到随机数种子
// 要想计算得到 key 的累加和,只能通过 v6 = (unsigned __int8)key[i] * rand(); 反推,反推的方法是暴力枚举srand种子
// 已知key以"flag"开头,则key[0] - key[3] 都是确定的,那么只要找到一个种子,使前4个字符计算后和dword_4030B4[i]相等
void CalcKeySum()
{
	char key[5] = "flag";
	// 爆破keySum
	for (unsigned int keySum = 0; keySum < 255*42; keySum++)
	{
		srand(keySum ^ isdebug);
		int i;
		for (i = 0; i < 4; i++)
		{
			unsigned int v6 = (unsigned __int8)key[i] * rand();
			unsigned int v7 = v6 * (unsigned __int64)v6 % 0xFAC96621;
			unsigned int v8 = v7 * (unsigned __int64)v7 % 0xFAC96621;
			unsigned int v9 = v8 * (unsigned __int64)v8 % 0xFAC96621;
			unsigned int v10 = v9 * (unsigned __int64)v9 % 0xFAC96621;
			unsigned int v11 = v10 * (unsigned __int64)v10 % 0xFAC96621;
			unsigned int v12 = v11 * (unsigned __int64)v11 % 0xFAC96621;
			unsigned int v13 = v12 * (unsigned __int64)v12 % 0xFAC96621;
			unsigned int v14 = v13 * (unsigned __int64)v13 % 0xFAC96621;
			unsigned int v15 = v14 * (unsigned __int64)v14 % 0xFAC96621;
			unsigned int v16 = v15 * (unsigned __int64)v15 % 0xFAC96621;
			unsigned int v17 = v16 * (unsigned __int64)v16 % 0xFAC96621;
			unsigned int v18 = v17 * (unsigned __int64)v17 % 0xFAC96621;
			unsigned int v19 = v18 * (unsigned __int64)v18 % 0xFAC96621;
			unsigned int v20 = v19 * (unsigned __int64)v19 % 0xFAC96621;
			unsigned int v21 = v20 * (unsigned __int64)v20 % 0xFAC96621;
			unsigned int v22 = v21 * (unsigned __int64)v21 % 0xFAC96621; // 注意,IDA的F5插件漏了这行,大坑
			// 还有一个坑,(unsigned __int64)v6 不要省略强转,IDA怎么生成就怎么抄
			// 因为在vs里,64位的求余根本没用div指令,不强转就算不出来了
			if ((unsigned __int64)v6 % 0xFAC96621 * (unsigned int)v22 % 0xFAC96621 != dword_4030B4[i])
			{
				break;
			}			
		}
		if (i == 4)
		{
			printf("keySum = %X\n", keySum);
			seed = keySum ^ isdebug;
			break;
		}		
	}
}

爆破得到keySum后,马上就能算出前42次随机数的值

void SetRand()
{
	srand(seed); // 爆破得到的种子
	for (int i = 0; i < 42; i++)
	{
		Rand[i] = rand();
	}
}

三、爆破密码

// lession09.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <windows.h>
#include <stdio.h>
using namespace std;

DWORD dword_4030B4[42] = {
	0x63B25AF1, 0x0C5659BA5, 0x4C7A3C33, 0x0E4E4267, 0x0B611769B,
	0x3DE6438C, 0x84DBA61F, 0x0A97497E6, 0x650F0FB3, 0x84EB507C,
	0x0D38CD24C, 0x0E7B912E0, 0x7976CD4F, 0x84100010, 0x7FD66745,
	0x711D4DBF, 0x5402A7E5, 0x0A3334351, 0x1EE41BF8, 0x22822EBE,
	0x0DF5CEE48, 0x0A8180D59, 0x1576DEDC, 0x0F0D62B3B, 0x32AC1F6E,
	0x9364A640, 0x0C282DD35, 0x14C5FC2E, 0x0A765E438, 0x7FCF345A,
	0x59032BAD, 0x9A5600BE, 0x5F472DC5, 0x5DDE0D84, 0x8DF94ED5,
	0x0BDF826A6, 0x515A737A, 0x4248589E, 0x38A96C20, 0x0CC7F61D9,
	0x2638C417, 0x0D9BEB996
};

DWORD isdebug = 0x31333359; // 过掉反调试后得出的值,用于计算种子
DWORD seed; // 随机种子
DWORD Rand[42]; // 伪随机数组,因为种子已经爆破得到,所以42个随机数已经确定

BOOL __stdcall CheckKey(char *key)
{
	int keySum; // edi
	unsigned __int8 c; // al
	char *ptr; // esi
	unsigned int i; // ebx
	unsigned int v6; // eax
	unsigned int v7; // edx
	unsigned int v8; // edx
	unsigned int v9; // edx
	unsigned int v10; // edx
	unsigned int v11; // edx
	unsigned int v12; // edx
	unsigned int v13; // edx
	unsigned int v14; // edx
	unsigned int v15; // edx
	unsigned int v16; // edx
	unsigned int v17; // edx
	unsigned int v18; // edx
	unsigned int v19; // edx
	unsigned int v20; // edx
	unsigned int v21; // edx
	unsigned int v22; // IDA 漏了这句
	if (strlen(key) != 42)
	{
		MessageBoxA(0, "密码长度!=42", "", MB_OK);
		return FALSE;
	}
	keySum = 0;
	c = *key;
	ptr = key + 1;
	while (c)
	{
		keySum += c;
		c = *ptr++;
	}
	srand(isdebug ^ keySum);                      // isdebug == 0x31333359
	
	for (i = 0; i != 42; ++i)
	{
		v6 = (unsigned __int8)key[i] * rand();
		v7 = v6 * (unsigned __int64)v6 % 0xFAC96621;
		v8 = v7 * (unsigned __int64)v7 % 0xFAC96621;
		v9 = v8 * (unsigned __int64)v8 % 0xFAC96621;
		v10 = v9 * (unsigned __int64)v9 % 0xFAC96621;
		v11 = v10 * (unsigned __int64)v10 % 0xFAC96621;
		v12 = v11 * (unsigned __int64)v11 % 0xFAC96621;
		v13 = v12 * (unsigned __int64)v12 % 0xFAC96621;
		v14 = v13 * (unsigned __int64)v13 % 0xFAC96621;
		v15 = v14 * (unsigned __int64)v14 % 0xFAC96621;
		v16 = v15 * (unsigned __int64)v15 % 0xFAC96621;
		v17 = v16 * (unsigned __int64)v16 % 0xFAC96621;
		v18 = v17 * (unsigned __int64)v17 % 0xFAC96621;
		v19 = v18 * (unsigned __int64)v18 % 0xFAC96621;
		v20 = v19 * (unsigned __int64)v19 % 0xFAC96621;
		v21 = v20 * (unsigned __int64)v20 % 0xFAC96621;
		v22 = v21 * (unsigned __int64)v21 % 0xFAC96621; // IDA漏了这句
		if (v6 % 0xFAC96621 * (unsigned __int64)v22 % 0xFAC96621 != dword_4030B4[i])
			break;
	}
	if (i >= 0x2A)
		return TRUE;
	else
		return FALSE;
}

// 如果知道 key 的累加和,就可以得到随机数种子
// 要想计算得到 key 的累加和,只能通过 v6 = (unsigned __int8)key[i] * rand(); 反推,反推的方法是暴力枚举srand种子
// 已知key以"flag"开头,则key[0] - key[3] 都是确定的,那么只要找到一个种子,使前4个字符计算后和dword_4030B4[i]相等
void CalcKeySum()
{
	char key[5] = "flag";
	// 爆破keySum
	for (unsigned int keySum = 0; keySum < 255*42; keySum++)
	{
		srand(keySum ^ isdebug);
		int i;
		for (i = 0; i < 4; i++)
		{
			unsigned int v6 = (unsigned __int8)key[i] * rand();
			unsigned int v7 = v6 * (unsigned __int64)v6 % 0xFAC96621;
			unsigned int v8 = v7 * (unsigned __int64)v7 % 0xFAC96621;
			unsigned int v9 = v8 * (unsigned __int64)v8 % 0xFAC96621;
			unsigned int v10 = v9 * (unsigned __int64)v9 % 0xFAC96621;
			unsigned int v11 = v10 * (unsigned __int64)v10 % 0xFAC96621;
			unsigned int v12 = v11 * (unsigned __int64)v11 % 0xFAC96621;
			unsigned int v13 = v12 * (unsigned __int64)v12 % 0xFAC96621;
			unsigned int v14 = v13 * (unsigned __int64)v13 % 0xFAC96621;
			unsigned int v15 = v14 * (unsigned __int64)v14 % 0xFAC96621;
			unsigned int v16 = v15 * (unsigned __int64)v15 % 0xFAC96621;
			unsigned int v17 = v16 * (unsigned __int64)v16 % 0xFAC96621;
			unsigned int v18 = v17 * (unsigned __int64)v17 % 0xFAC96621;
			unsigned int v19 = v18 * (unsigned __int64)v18 % 0xFAC96621;
			unsigned int v20 = v19 * (unsigned __int64)v19 % 0xFAC96621;
			unsigned int v21 = v20 * (unsigned __int64)v20 % 0xFAC96621;
			unsigned int v22 = v21 * (unsigned __int64)v21 % 0xFAC96621; // 注意,IDA的F5插件漏了这行,大坑
			// 还有一个坑,(unsigned __int64)v6 不要省略强转,IDA怎么生成就怎么抄
			// 因为在vs里,64位的求余根本没用div指令,不强转就算不出来了
			if ((unsigned __int64)v6 % 0xFAC96621 * (unsigned int)v22 % 0xFAC96621 != dword_4030B4[i])
			{
				break;
			}			
		}
		if (i == 4)
		{
			printf("keySum = %X\n", keySum);
			seed = keySum ^ isdebug;
			break;
		}		
	}
}

void SetRand()
{
	srand(seed); // 爆破得到的种子
	for (int i = 0; i < 42; i++)
	{
		Rand[i] = rand();
	}
}

int main()
{
	CalcKeySum(); // 爆破种子(0xE61)
	SetRand(); // 设置伪随机数组

	// 爆破密码
	for (int i = 0; i < 42; i++)
	{
		for (unsigned char ch = 0; ch < 0xFF; ch++)
		{
			unsigned int v6 = (unsigned __int8)ch * Rand[i];
			unsigned int v7 = v6 * (unsigned __int64)v6 % 0xFAC96621;
			unsigned int v8 = v7 * (unsigned __int64)v7 % 0xFAC96621;
			unsigned int v9 = v8 * (unsigned __int64)v8 % 0xFAC96621;
			unsigned int v10 = v9 * (unsigned __int64)v9 % 0xFAC96621;
			unsigned int v11 = v10 * (unsigned __int64)v10 % 0xFAC96621;
			unsigned int v12 = v11 * (unsigned __int64)v11 % 0xFAC96621;
			unsigned int v13 = v12 * (unsigned __int64)v12 % 0xFAC96621;
			unsigned int v14 = v13 * (unsigned __int64)v13 % 0xFAC96621;
			unsigned int v15 = v14 * (unsigned __int64)v14 % 0xFAC96621;
			unsigned int v16 = v15 * (unsigned __int64)v15 % 0xFAC96621;
			unsigned int v17 = v16 * (unsigned __int64)v16 % 0xFAC96621;
			unsigned int v18 = v17 * (unsigned __int64)v17 % 0xFAC96621;
			unsigned int v19 = v18 * (unsigned __int64)v18 % 0xFAC96621;
			unsigned int v20 = v19 * (unsigned __int64)v19 % 0xFAC96621;
			unsigned int v21 = v20 * (unsigned __int64)v20 % 0xFAC96621;
			unsigned int v22 = v21 * (unsigned __int64)v21 % 0xFAC96621; // IDA漏了这句
			if (v6 % 0xFAC96621 * (unsigned __int64)v22 % 0xFAC96621 == dword_4030B4[i])
				putchar((char)ch);
		}
	}

	//char key[] = "flag{wh3r3_th3r3_i5_@_w111-th3r3_i5_@_w4y}";
	//if (CheckKey(key))
	//{
	//    printf("密码正确\n");
	//}
	//else
	//{
	//    printf("密码错误\n");
	//}
	return 0;
}


在这里插入图片描述

四、去花脚本

手动去除花指令吃力不讨好,下面附上去除花指令的脚本。

find eip,#E80000000081042417000000C3576174636820757220737465702100#
cmp $RESULT,0
je exit
mov [$RESULT],#90909090909090909090909090909090909090909090909090909090#

find eip,#E80000000081042425000000C354686520666C616720626567696E7320776974682022666C61677B2200#
cmp $RESULT,0
je exit
mov [$RESULT],#909090909090909090909090909090909090909090909090909090909090909090909090909090909090#

loop:
	find eip,			#E801000000??????????C3#
	cmp $RESULT,0
	je exit
	mov [$RESULT],#9090909090909090909090#
jmp loop

exit:
MSG "bye\r\n"
ret
本资源为非常不错的一套王网传资源,是继之前上传的基础班的升级版,更加全面,资源过大,上传乃是下载链接,如失效请留言!!!资源远大于5积分,不多说,下面直接上目录: APC机制 I5 J$ i: U0 f1 r: O9 B( Q" b │ 01 APC的本质.mp4 │ 02 备用Apc队列.mp4: U8 p7 ]3 f" w$ b0 ?5 Z9 `0 H8 G* [ │ 03 APC挂入过程.mp48 g! H4 s1 V; ]+ b4 Y9 H0 L- B │ 04 内核APC执行过程.mp4 │ 05 用户APC执行过程.mp4 │ ├─事件等待' x% `" J' } ?& S: t' ]# I5 \5 G │ 01临界区.mp4- o( U$ W9 O+ ` ~0 u4 ~, @. \ │ 02 自旋锁.mp4) c3 ~. J& L, V& s. Q8 x/ [. w │ 03 线程等待与唤醒.mp4# b* ^" k$ d# O3 f8 t8 a3 k │ 04 WaitForSingleObject函数分析.mp4$ V7 L' C3 I( W │ 05 事件.mp4 │ 06 信号量.mp4 │ 07 互斥体.mp4 │ ├─保护模式- }! n! C$ O/ s" Q │ 014 中断门.mp4, B' i, r7 Y: B3 |! N( ^6 { l9 F │ 015 陷阱门.mp4 │ 017 任务段_下.mp4, |/ M# A: K3 T7 i* Q/ ? I& o& D; p │ 018 任务门.mp46 m. D+ f4 _/ V) ~9 S& B │ 019 10-10-12分页.mp4 │ 020 PDE_PTE.mp4 │ 021 PDE_PTE属性(P_RW).mp43 ~/ ]1 x5 {4 u: {$ I │ 022 PDE_PTE属性(US_PS_A_D).mp4 │ 023 页目录表基址.mp4 │ 024 页表基址.mp4$ A f' [+ g6 }5 F; e │ 025 2-9-9-12分页.mp4 │ 026 2-9-9-12分页(下).mp4- ~' ~9 i0 T5 f" p2 U$ j │ 027 TLB.mp4 │ 028 中断与异常.mp4 │ 029 控制寄存器.mp46 j2 l3 j) O# {% {4 w │ 030 PWT_PCD属性.mp4 │ 031 保护模式阶段测试.mp4 │ _001 保护模式.mp4, I; c5 X ~) t1 d1 }8 S# f3 i: b │ _002 段寄存器结构.mp48 n- |- i( H$ ^* f │ _003 段寄存器属性探测.mp4 │ _004 段描述符与段选择子.mp4 │ _005 段描述符属性_P位_G位.mp4 │ _006 段描述符属性_S位_TYPE域.mp4 │ _007 段描述符属性_DB位.mp4 │ _008 段权限检查.mp4 │ _009 代码跨段跳转流程.mp4& S# i9 i- \0 D" @1 U- P │ _010 代码跨段跳转实验.mp4" @* S2 Y- a- S6 n7 n: ~ │ _011 长调用与短调用.mp4 │ _012 调用门_上.mp4; [) _2 c8 A5 F% }! u% ]: ~. N │ _013 调用门_下.mp4 │ ├─内存管理 │ 01 线性地址的管理.mp4; ? |+ ^5 i& } │ 02 Private Memory.mp4* @3 B( Y6 ^ y- { │ 03 Mapped Memory.mp4 │ 04 物理内存的管理.mp4' [8 C6 q \1 H8 w" H2 ]0 Y │ 05 无处不在的缺页异常.mp4 │ ├─句柄表 │ 01 句柄表.mp4 │ 02 全局句柄表.mp4 │ 5 h" u" i& {+ G4 T+ E ├─异常 │ 01 CPU异常记录.mp4 │ 02 模拟异常记录.mp4: K0 J( d1
相关推荐
©️2020 CSDN 皮肤主题: 1024 设计师:白松林 返回首页