内容简介:操作挑战
操作挑战 – 便利性
自一个多月前发布 SharpSploit 以来,有两个开源项目已经尝试解决便利性这个操作挑战。 SharpSploitConsole 和 SharpAttack 都试图以类似但不同的方式解决这个问题。它们都可以作为一个独特的控制台应用程序,并可以与 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; } }
在这种情况下,使用 SharpSploitConsole 或 SharpAttack ,你可能必须运行 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; } }
如果使用 SharpSploitConsole 或 SharpAttack ,我不确定这是否可行,因为自定义的 C #代码需要编译并作为程序集加载。
使用这些方法需要注意一点,这个问题实际上是 SharpSploit 本身的错误,就是它们不能与 Cobalt Strike 的 execute-assembly 一起使用。你必须在使用之前去掉嵌入的 Mimikatz PE 二进制文件才可以与 execute-assembly 一起使用,因为 Cobalt Strike 对程序集的文件大小限制为最大 1MB 。如果不能在 Cobalt Strike 中通过 SharpSploit 使用 Mimikatz ,那将是一件非常糟糕的事。幸运的是,我已经在 SharpSploit v1.1 中做了一些改变,解决了这个问题,我们将在后面讨论细节。
通过这些方法,我们就可以以可定制的方式将 SharpSploit 作为库来使用从而获得便利性。接下来,我们将介绍操作挑战中的灵活性。
操作挑战 – 灵活性
使用像 C #这样的编译语言与使用像 PowerShell 这样的解释型语言之间存在着关键的操作差异。为了实现这一改变我们失去了相当多的灵活性。使用脚本语言,我们可以快速的编辑脚本,而无需担心编译所需的额外步骤。如果没有 PowerShell ,我们将无法使用 Pipline 的功能,也不能使用内置的 cmdlet 组合 工具 集和快速过滤文本以及格式化输出,如 Select-Object 或 Format-List 。
如果使用 C #,则没有一种本地方法可以将一个工具的输出通过管道作为另一个工具的输入。也没有一种本地方法可以对已编译的可执行文件进行编辑。为了帮助恢复这种灵活性,我编写了一个名为 SharpGen 的工具,该工具将在本文的剩余章节中进行描述。
SharpGen
为了尝试应对灵活性这个操作挑战,我创建了一个名为 SharpGen 的 .NET Core 控制台应用程序。 SharpGen 利用了 Roslyn C# 编译器从而可以快速完成 .NET 框架控制台应用程序或库的交叉编译。 .NET Core 使得 SharpGen 实现了跨平台,因此,渗透测试人员可以从他们所喜欢的任何操作系统中使用 SharpGen 。
请记住,操作挑战的便利性问题的产生是由于必须在 Visual Studio 中不断创建新的控制台应用程序,然后添加引用,之后使用 Costura 或 ILMerge 嵌入引用。针对灵活性这个操作挑战, SharpGen 是通过像执行单个命令那样一键创建新的控制台应用程序来快速的解决了这一挑战,并带来了一些额外的好处。
基本用法
SharpGen 最基本的用法是你需要为 SharpGen 提供一个输出文件名和一行你想要执行的 C# 代码。 SharpGen 将生成一个能够执行单行代码的 .NET 框架控制台应用程序。例如:
<a href="/cdn-cgi/l/email-protection" data-cfemail="1a79757878685a777b79">[email protected]</a>:~/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 命令行参数从文件中读取你要执行的代码。
<a href="/cdn-cgi/l/email-protection" data-cfemail="0f6c606d6d7d4f626e6c">[email protected]</a>:~/SharpGen > cat example.txt string whoami = Shell.ShellExecute("whoami"); if (whoami == "SomeUser") { Console.WriteLine(Mimikatz.LogonPasswords()); } <a href="/cdn-cgi/l/email-protection" data-cfemail="e5868a878797a5888486">[email protected]</a>:~/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 函数完成执行:
<a href="/cdn-cgi/l/email-protection" data-cfemail="62010d000010220f0301">[email protected]</a>:~/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; } } <a href="/cdn-cgi/l/email-protection" data-cfemail="1a79757878685a777b79">[email protected]</a>:~/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
这些都是有关于这个工具的基础知识。下面是完整的命令行用法信息:
<a href="/cdn-cgi/l/email-protection" data-cfemail="86e5e9e4e4f4c6ebe7e5">[email protected]</a>:~/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
你需要特别关注 Source , References 和 Resources 目录,因为这些驱动了 SharpGen 的核心功能。放置在 Source 文件夹下的所有源代码将作为源编译到单个程序集中。因为它是作为源编译的,所以无需担心组合程序集或将它们嵌入为资源时出现问题。默认情况下, SharpSploit 的源代码包含在 Source 目录中,便于编译 SharpSploit 。然而, SharpGen 构建的方式,使得任何代码库都可以放置到该文件夹中并被包括在程序内。
例如,我们可以将 GhostPack 的 SharpWMI 源代码(稍作了修改)放入 Source 文件夹中并对其进行编译:
<a href="/cdn-cgi/l/email-protection" data-cfemail="7d1e121f1f0f3d101c1e">[email protected]</a>:~/SharpGen > cp -r ~/GhostPack/SharpWMI/SharpWMI ./Source <a href="/cdn-cgi/l/email-protection" data-cfemail="bbd8d4d9d9c9fbd6dad8">[email protected]</a>:~/SharpGen > cat example.txt SharpWMI.Program.LocalWMIQuery("select * from win32_service"); Console.WriteLine(Host.GetProcessList()); <a href="/cdn-cgi/l/email-protection" data-cfemail="92f1fdf0f0e0d2fff3f1">[email protected]</a>:~/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
这个特性能够使我们从单个程序集中调用 SharpWMI 和 SharpSploit 的方法!能够放入其他库并快速编译出组合后的库文件可能是 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 方法,你需要确认你真的需要 Mimikatz 的 x86 和 x64 副本吗?如果不需要,那么你只需嵌入你所需要的平台资源即可。有两种方法可以做到这一点:
2a. 你可以通过命令行参数根据平台过滤资源。这应该会让应用程序的大小小于 1MB :
<a href="/cdn-cgi/l/email-protection" data-cfemail="25464a47475765484446">[email protected]</a>:~/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 压缩,这个库的压缩效率不是很高,不能将你要嵌入的 x64 和 x86 资源压缩到小于 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 并嵌入 x64 和 x86 资源,同时将文件大小保持在 1MB 的限制之下有个更为强大的方法是使用 ConfuserEx 资源保护,它使用了更高效的压缩算法。我们将在 “ 高级用法 ” 部分对此进行讨论。
高级用法
SharpGen 支持使用 ConfuserEx ,它是 .NET 应用程序的一个开源保护程序。我所熟悉的原始的 ConfuserEx 可以 在这里找到 。我发现 ConfuserEx 项目已被创建了新的分支并维护在一个新的位置,因为最初的项目已经废弃。当我意识到新的 ConfuserEx 支持 .NET Core 时,我更加兴奋了!这使得我们可以从 SharpGen 中执行自动保护并且混淆我们的二进制文件,从而为 .NET 框架程序执行交叉编译!
SharpGen 包含了默认的 ConfuserEx 项目文件,可以使用 confuse.cr 或者使用其他的 ConfuserEx 规则进行自定义,并可以通过下面的命令行参数使用:
<a href="/cdn-cgi/l/email-protection" data-cfemail="55363a37372715383436">[email protected]</a>:~/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 命令行参数禁用优化(请注意,这可能会增加你的二进制文件的大小!):
<a href="/cdn-cgi/l/email-protection" data-cfemail="771418151505371a1614">[email protected]</a>:~/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 是我对这种平衡所提出的解决方案,我希望其他人也能认为它很有用。但其他可行的解决方案,如 SharpSploitConsole , SharpAttack 和 SharpCompile 也确实有效,我相信将来也会出现其他的解决方案。我鼓励读者多多思考这些操作挑战,并为正确的用例挑选合适的工具。
致谢
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 - SharpAttack 是 SharpSploit 的开源控制台应用程序前端,利用 ILMerge 实现,由 Jared Haight 编写。
· SharpSploitConsole - SharpSploitConsole 是 SharpSploit 的一个开源控制台应用程序前端,它利用 Costura 并由 anthemtotheego 和 g0ldengunsec 编写 。
· SharpCompile - 一个自动化实现 .NET 编译的解决方案,它利用了攻击者脚本以及一个利用了 csc.exe 的 HTTP 服务器。
· Costura - Costura 将引用程序集作为资源嵌入程序。
· ILMerge - ILMerge 是一个 .NET 程序集的静态链接器,由 Mike Barnett 编写并由微软维护。
最后,我建议读者参考一些额外的资源:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 论C#作为攻击语言的操作挑战(上)
- 论C#作为攻击语言的操作挑战(下)
- 调查 | 黑客将Python作为攻击编码语言的首选
- 使用Elasticsearch作为主数据存储
- 如何把MongoDB作为循环队列
- 使用 utterances 作为博客评论组件
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Dive Into Python 3
Mark Pilgrim / Apress / 2009-11-6 / USD 44.99
Mark Pilgrim's Dive Into Python 3 is a hands-on guide to Python 3 (the latest version of the Python language) and its differences from Python 2. As in the original book, Dive Into Python, each chapter......一起来看看 《Dive Into Python 3》 这本书的介绍吧!