├── .gitattributes
├── .github
└── FUNDING.yml
├── .gitignore
├── App.config
├── BeaconEye.cs
├── BeaconEye.csproj
├── BeaconEye.png
├── BeaconEye.sln
├── BeaconProcess.cs
├── BeaconProgram.cs
├── Config
└── ConfigItem.cs
├── Options.cs
├── Properties
└── AssemblyInfo.cs
├── README.md
├── Reader
├── IProcessEnumerator.cs
├── MiniDumpProcessEnumerator.cs
├── MiniDumpReader.cs
├── NtProcessReader.cs
├── ProcessReader.cs
└── RunningProcessEnumerator.cs
└── StringUtils.cs
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: CCob
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Oo]ut/
33 | [Ll]og/
34 | [Ll]ogs/
35 |
36 | # Visual Studio 2015/2017 cache/options directory
37 | .vs/
38 | # Uncomment if you have tasks that create the project's static files in wwwroot
39 | #wwwroot/
40 |
41 | # Visual Studio 2017 auto generated files
42 | Generated\ Files/
43 |
44 | # MSTest test Results
45 | [Tt]est[Rr]esult*/
46 | [Bb]uild[Ll]og.*
47 |
48 | # NUnit
49 | *.VisualState.xml
50 | TestResult.xml
51 | nunit-*.xml
52 |
53 | # Build Results of an ATL Project
54 | [Dd]ebugPS/
55 | [Rr]eleasePS/
56 | dlldata.c
57 |
58 | # Benchmark Results
59 | BenchmarkDotNet.Artifacts/
60 |
61 | # .NET Core
62 | project.lock.json
63 | project.fragment.lock.json
64 | artifacts/
65 |
66 | # ASP.NET Scaffolding
67 | ScaffoldingReadMe.txt
68 |
69 | # StyleCop
70 | StyleCopReport.xml
71 |
72 | # Files built by Visual Studio
73 | *_i.c
74 | *_p.c
75 | *_h.h
76 | *.ilk
77 | *.meta
78 | *.obj
79 | *.iobj
80 | *.pch
81 | *.pdb
82 | *.ipdb
83 | *.pgc
84 | *.pgd
85 | *.rsp
86 | *.sbr
87 | *.tlb
88 | *.tli
89 | *.tlh
90 | *.tmp
91 | *.tmp_proj
92 | *_wpftmp.csproj
93 | *.log
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio LightSwitch build output
298 | **/*.HTMLClient/GeneratedArtifacts
299 | **/*.DesktopClient/GeneratedArtifacts
300 | **/*.DesktopClient/ModelManifest.xml
301 | **/*.Server/GeneratedArtifacts
302 | **/*.Server/ModelManifest.xml
303 | _Pvt_Extensions
304 |
305 | # Paket dependency manager
306 | .paket/paket.exe
307 | paket-files/
308 |
309 | # FAKE - F# Make
310 | .fake/
311 |
312 | # CodeRush personal settings
313 | .cr/personal
314 |
315 | # Python Tools for Visual Studio (PTVS)
316 | __pycache__/
317 | *.pyc
318 |
319 | # Cake - Uncomment if you are using it
320 | # tools/**
321 | # !tools/packages.config
322 |
323 | # Tabs Studio
324 | *.tss
325 |
326 | # Telerik's JustMock configuration file
327 | *.jmconfig
328 |
329 | # BizTalk build output
330 | *.btp.cs
331 | *.btm.cs
332 | *.odx.cs
333 | *.xsd.cs
334 |
335 | # OpenCover UI analysis results
336 | OpenCover/
337 |
338 | # Azure Stream Analytics local run output
339 | ASALocalRun/
340 |
341 | # MSBuild Binary and Structured Log
342 | *.binlog
343 |
344 | # NVidia Nsight GPU debugger configuration file
345 | *.nvuser
346 |
347 | # MFractors (Xamarin productivity tool) working folder
348 | .mfractor/
349 |
350 | # Local History for Visual Studio
351 | .localhistory/
352 |
353 | # BeatPulse healthcheck temp database
354 | healthchecksdb
355 |
356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
357 | MigrationBackup/
358 |
359 | # Ionide (cross platform F# VS Code tools) working folder
360 | .ionide/
361 |
362 | # Fody - auto-generated XML schema
363 | FodyWeavers.xsd
--------------------------------------------------------------------------------
/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/BeaconEye.cs:
--------------------------------------------------------------------------------
1 | using BeaconEye.Config;
2 | using BeaconEye.Reader;
3 | using libyaraNET;
4 | using Mono.Options;
5 | using NtApiDotNet;
6 | using SharpDisasm;
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Diagnostics;
10 | using System.IO;
11 | using System.Linq;
12 | using System.Threading;
13 |
14 | namespace BeaconEye {
15 | class BeaconEye {
16 |
17 | enum ScanState{
18 | NotFound,
19 | Found,
20 | FoundNoKeys,
21 | HeapEnumFailed
22 | }
23 |
24 | class FetchHeapsException : Exception {
25 |
26 | }
27 |
28 | class ScanResult {
29 | public ScanState State { get; set; }
30 | public long ConfigAddress { get; set; }
31 | public bool CrossArch { get; set; }
32 | public Configuration Configuration { get; set; }
33 | }
34 |
35 | public static string cobaltStrikeRule64 = "rule CobaltStrike { " +
36 | "strings: " +
37 | "$sdec = { " +
38 | " 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " +
39 | " 01 00 00 00 00 00 00 00 (00|01|02|04|08|10) 00 00 00 00 00 00 00 " +
40 | " 01 00 00 00 00 00 00 00 ?? ?? 00 00 00 00 00 00 " +
41 | " 02 00 00 00 00 00 00 00 ?? ?? ?? ?? 00 00 00 00 " +
42 | " 02 00 00 00 00 00 00 00 ?? ?? ?? ?? 00 00 00 00 " +
43 | " 01 00 00 00 00 00 00 00 ?? ?? 00 00 00 00 00 00 " +
44 | "} " +
45 | "condition: " +
46 | "any of them" +
47 | "}";
48 |
49 | public static string cobaltStrikeRule32 = "rule CobaltStrike { " +
50 | "strings: " +
51 | "$sdec = { " +
52 | " 00 00 00 00 00 00 00 00 " +
53 | " 01 00 00 00 (00|01|02|04|08|10) 00 00 00" +
54 | " 01 00 00 00 ?? ?? 00 00 " +
55 | " 02 00 00 00 ?? ?? ?? ?? " +
56 | " 02 00 00 00 ?? ?? ?? ?? " +
57 | " 01 00 00 00 ?? ?? 00 00 " +
58 | "} " +
59 | "condition: " +
60 | "any of them" +
61 | "}";
62 |
63 | static ManualResetEvent finishedEvent = new ManualResetEvent(false);
64 | static List beaconMonitorThreads = new List();
65 |
66 | static Rules CompileRules(bool x64) {
67 | using (Compiler compiler = new Compiler()) {
68 | compiler.AddRuleString(x64 ? cobaltStrikeRule64 : cobaltStrikeRule32);
69 | return compiler.GetRules();
70 | }
71 | }
72 |
73 | public static List IndexOfSequence(byte[] buffer, byte[] pattern, int startIndex) {
74 | List positions = new List();
75 | int i = Array.IndexOf(buffer, pattern[0], startIndex);
76 | while (i >= 0 && i <= buffer.Length - pattern.Length) {
77 | byte[] segment = new byte[pattern.Length];
78 | Buffer.BlockCopy(buffer, i, segment, 0, pattern.Length);
79 | if (segment.SequenceEqual(pattern))
80 | positions.Add(i);
81 | i = Array.IndexOf(buffer, pattern[0], i + 1);
82 | }
83 | return positions;
84 | }
85 |
86 | static Configuration ProcessHasConfig(ProcessReader process) {
87 |
88 | var heaps = process.Heaps;
89 |
90 | using (var ctx = new YaraContext()) {
91 | var rules = CompileRules(process.Is64Bit);
92 | Scanner scanner = new Scanner();
93 |
94 | foreach (var heap in heaps) {
95 |
96 | var memoryInfo = process.QueryMemoryInfo((ulong)heap);
97 |
98 | if (memoryInfo.NoAccess)
99 | continue;
100 |
101 | var memory = process.ReadMemory(memoryInfo.BaseAddress, (int)memoryInfo.RegionSize);
102 | var results = scanner.ScanMemory(memory, rules);
103 |
104 | if (results.Count > 0) {
105 | foreach (KeyValuePair> item in results[0].Matches) {
106 | var configStart = memoryInfo.BaseAddress + item.Value[0].Offset;
107 | var configBytes = process.ReadMemory(configStart, process.Is64Bit ? 0x800 : 0x400);
108 | return new Configuration((long)configStart, new BinaryReader(new MemoryStream(configBytes)), process);
109 | }
110 | }
111 | }
112 | }
113 |
114 | return null;
115 | }
116 |
117 | static void MonitorThread(object arg) {
118 | if(arg is BeaconProcess bp) {
119 | bp.MonitorTraffic();
120 | }
121 | }
122 |
123 | static Tuple GetKeyIVAddress(ProcessReader.MemoryInfo blockInfo, ProcessReader process) {
124 |
125 | if (process.Is64Bit) {
126 |
127 | var codeBlock = process.ReadMemory(blockInfo.BaseAddress, (int)blockInfo.RegionSize);
128 | var offsets = IndexOfSequence(codeBlock, new byte[] { 0x0F, 0x10, 0x05 }, 0);
129 |
130 | foreach (var offset in offsets) {
131 |
132 | byte[] instructions = process.ReadMemory(blockInfo.BaseAddress + (ulong)offset, 15);
133 | Disassembler disasm = new Disassembler(instructions, ArchitectureMode.x86_64, (ulong)blockInfo.BaseAddress + (ulong)offset);
134 |
135 | var movupsIns = disasm.NextInstruction();
136 | var movdquIns = disasm.NextInstruction();
137 |
138 | if (movdquIns.Mnemonic != SharpDisasm.Udis86.ud_mnemonic_code.UD_Imovdqu ||
139 | movdquIns.Operands[0].Base != SharpDisasm.Udis86.ud_type.UD_R_RIP ||
140 | movupsIns.Operands[0].Base != movdquIns.Operands[1].Base) {
141 | return null;
142 | } else {
143 |
144 | var iv_address = (long)movupsIns.PC + movupsIns.Operands[1].LvalSDWord;
145 | var keys_address = (long)movdquIns.PC + movdquIns.Operands[0].LvalSDWord - 32;
146 |
147 | return new Tuple(keys_address, iv_address);
148 | }
149 | }
150 |
151 | } else {
152 |
153 | var codeBlock = process.ReadMemory(blockInfo.BaseAddress, (int)blockInfo.RegionSize);
154 | var offsets = IndexOfSequence(codeBlock, new byte[] { 0xa5, 0xa5, 0xa5, 0xa5, 0xe8 }, 0);
155 |
156 | foreach (var offset in offsets) {
157 |
158 | byte[] instructions = process.ReadMemory(blockInfo.BaseAddress + (ulong)offset - 5, 24);
159 | Disassembler disasm = new Disassembler(instructions, ArchitectureMode.x86_32, (ulong)blockInfo.BaseAddress + (ulong)offset);
160 |
161 | var movEDIMem = disasm.NextInstruction();
162 |
163 | if(movEDIMem.Mnemonic != SharpDisasm.Udis86.ud_mnemonic_code.UD_Imov ||
164 | movEDIMem.Operands[0].Base != SharpDisasm.Udis86.ud_type.UD_R_EDI ||
165 | movEDIMem.Operands[1].Base != SharpDisasm.Udis86.ud_type.UD_NONE
166 | ) {
167 | continue;
168 | }
169 |
170 | var iv_address = movEDIMem.Operands[1].LvalUDWord;
171 | long key_address = 0;
172 |
173 | for(int idx = offset; idx > offset - 50; idx--) {
174 |
175 | var keysOffsets = IndexOfSequence(codeBlock, new byte[] { 0x53, 0x56, 0x57, 0xbb }, idx);
176 |
177 | if(keysOffsets.Count > 0 && keysOffsets[0] < offset) {
178 |
179 | instructions = codeBlock.Skip(idx+3).Take(8).ToArray();
180 | disasm = new Disassembler(instructions, ArchitectureMode.x86_32, (ulong)blockInfo.BaseAddress + (ulong)idx+3);
181 |
182 | var movEBX = disasm.NextInstruction();
183 | key_address = movEBX.Operands[1].LvalUDWord;
184 | break;
185 | }
186 | }
187 |
188 | if (key_address != 0)
189 | return new Tuple(key_address, iv_address);
190 | }
191 | }
192 |
193 | return null;
194 | }
195 |
196 | static ScanResult IsBeaconProcess(ProcessReader process, bool monitor) {
197 |
198 | try {
199 |
200 | var beaconConfig = ProcessHasConfig(process);
201 | if (beaconConfig == null) {
202 | return new ScanResult();
203 | }
204 |
205 | var memoryInfo = process.QueryAllMemoryInfo();
206 |
207 | foreach (var blockInfo in memoryInfo) {
208 |
209 | if(!blockInfo.IsExecutable) {
210 | continue;
211 | }
212 |
213 | BeaconProcess beaconProcess = null;
214 | Tuple keyIV;
215 | if((keyIV = GetKeyIVAddress(blockInfo, process)) == null) {
216 | continue;
217 | }
218 |
219 | bool crossArch = true;
220 |
221 | if (process.Is64Bit == NtProcess.Current.Is64Bit) {
222 | if (monitor) {
223 | beaconProcess = new BeaconProcess(process, beaconConfig, keyIV.Item2, keyIV.Item1, ref finishedEvent);
224 | var beaconMonitorThread = new Thread(MonitorThread);
225 | beaconMonitorThreads.Add(beaconMonitorThread);
226 | beaconMonitorThread.Start(beaconProcess);
227 | }
228 | crossArch = false;
229 | }
230 |
231 | return new ScanResult() {
232 | State = ScanState.Found,
233 | ConfigAddress = beaconConfig.Address,
234 | CrossArch = crossArch,
235 | Configuration = beaconConfig
236 | };
237 | }
238 |
239 | return new ScanResult() {
240 | State = ScanState.FoundNoKeys,
241 | ConfigAddress = beaconConfig.Address,
242 | Configuration = beaconConfig
243 | };
244 |
245 | } catch (FetchHeapsException) {
246 | return new ScanResult() {
247 | State = ScanState.HeapEnumFailed
248 | };
249 | }
250 | }
251 |
252 | static void Main(string[] args) {
253 |
254 | bool monitor = false;
255 | string processFilter = null;
256 | bool showHelp = false;
257 | bool verbose = false;
258 | string dump = null;
259 | IProcessEnumerator procEnum;
260 |
261 | Console.WriteLine(
262 | "BeconEye by @_EthicalChaos_\n" +
263 | $" CobaltStrike beacon hunter and command monitoring tool { (IntPtr.Size == 8 ? "x86_64" : "x86")} \n"
264 | );
265 |
266 | OptionSet option_set = new OptionSet()
267 | .Add("v|verbose", "Display more verbose output instead of just information on beacons found", v => verbose = true)
268 | .Add("m|monitor", "Attach to and monitor beacons found when scanning live processes", v => monitor = true)
269 | .Add("f=|filter=", "Filter process list with names starting with x (live mode only)", v => processFilter = v)
270 | .Add("d=|dump=", "A folder to use for MiniDump mode to scan for beacons (files with *.dmp or *.mdmp)", v => dump = v)
271 | .Add("h|help", "Display this help", v => showHelp = v != null);
272 |
273 | try {
274 |
275 | option_set.Parse(args);
276 |
277 | if (showHelp) {
278 | option_set.WriteOptionDescriptions(Console.Out);
279 | return;
280 | }
281 |
282 | } catch (Exception e) {
283 | Console.WriteLine("[!] Failed to parse arguments: {0}", e.Message);
284 | option_set.WriteOptionDescriptions(Console.Out);
285 | return;
286 | }
287 |
288 | var timer = new Stopwatch();
289 | timer.Start();
290 | Console.WriteLine($"[+] Scanning for beacon processess...");
291 | if(processFilter != null) {
292 | Console.WriteLine($"[=] Using process filter {processFilter}*");
293 | }
294 |
295 | if (!string.IsNullOrEmpty(dump)) {
296 | procEnum = new MiniDumpProcessEnumerator(dump, verbose);
297 | } else {
298 | procEnum = new RunningProcessEnumerator(processFilter);
299 | }
300 |
301 | var originalColor = Console.ForegroundColor;
302 | var beaconsFound = 0;
303 | var processesScanned = 0;
304 |
305 | foreach (var process in procEnum.GetProcesses()) {
306 |
307 | ScanResult sr;
308 |
309 | try
310 | {
311 | if ((sr = IsBeaconProcess(process, monitor)).State == ScanState.Found || sr.State == ScanState.FoundNoKeys) {
312 | beaconsFound++;
313 | Console.ForegroundColor = ConsoleColor.Red;
314 | Console.WriteLine($" {process.Name} ({process.ProcessId}), Keys Found:{sr.State == ScanState.Found}, Configuration Address: 0x{sr.ConfigAddress} {(sr.CrossArch ? $"(Please use the {(process.Is64Bit ? "x64" : "x86")} version of BeaconEye to monitor)" : "")}");
315 | sr.Configuration.PrintConfiguration(Console.Out, 1);
316 | } else if(sr.State == ScanState.NotFound && verbose) {
317 | Console.ForegroundColor = ConsoleColor.Green;
318 | Console.WriteLine($" {process.Name} ({process.ProcessId})");
319 | } else if(sr.State == ScanState.HeapEnumFailed) {
320 | Console.ForegroundColor = ConsoleColor.Yellow;
321 | Console.WriteLine($" {process.Name} ({process.ProcessId}) Failed to fetch heap info");
322 | }
323 | }
324 | catch (NtException e)
325 | {
326 | Console.Error.WriteLine($"[!] NtException \"{e.Status}\" for process {process.ProcessId} ({process.Name}).");
327 | }
328 |
329 | processesScanned++;
330 | }
331 |
332 | timer.Stop();
333 | Console.ForegroundColor = originalColor;
334 | Console.WriteLine($"[+] Scanned {processesScanned} processes in {timer.Elapsed}");
335 |
336 | if (beaconsFound > 0) {
337 |
338 | Console.WriteLine($"[+] Found {beaconsFound} beacon processes");
339 |
340 | if (beaconMonitorThreads.Count > 0 && monitor) {
341 | Console.WriteLine($"[+] Monitoring {beaconMonitorThreads.Count} beacon processes, press enter to stop monitoring");
342 | Console.ReadLine();
343 | Console.WriteLine($"[+] Exit triggered, detaching from beacon processes...");
344 |
345 | finishedEvent.Set();
346 | foreach (var bt in beaconMonitorThreads) {
347 | bt.Join();
348 | }
349 | }
350 |
351 | } else {
352 | Console.WriteLine($"[=] No beacon processes found");
353 | }
354 | }
355 | }
356 | }
357 |
--------------------------------------------------------------------------------
/BeaconEye.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {7C30A97D-E557-40C4-9B3F-5EE56599C858}
8 | Exe
9 | BeaconEye
10 | BeaconEye
11 | v4.8
12 | 512
13 | true
14 | true
15 |
16 |
17 |
18 |
19 |
20 | AnyCPU
21 | true
22 | full
23 | false
24 | bin\Debug\
25 | DEBUG;TRACE
26 | prompt
27 | 4
28 |
29 |
30 | AnyCPU
31 | pdbonly
32 | true
33 | bin\Release\
34 | TRACE
35 | prompt
36 | 4
37 |
38 |
39 | true
40 | bin\x64\Debug\
41 | DEBUG;TRACE
42 | full
43 | x64
44 | 7.3
45 | prompt
46 | true
47 |
48 |
49 | bin\x64\Release\
50 |
51 |
52 | true
53 | none
54 | x64
55 | 7.3
56 | prompt
57 | true
58 |
59 |
60 | true
61 | bin\x86\Debug\
62 | DEBUG;TRACE
63 | full
64 | x86
65 | 7.3
66 | prompt
67 | true
68 |
69 |
70 | bin\x86\Release\
71 | TRACE
72 | true
73 | none
74 | x86
75 | 7.3
76 | prompt
77 | true
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | 3.5.2
100 |
101 |
102 | 1.1.31
103 |
104 |
105 | 1.1.11
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/BeaconEye.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CCob/BeaconEye/ab2622d505d5773bcf69dcee9b2ae1d7d461ae5e/BeaconEye.png
--------------------------------------------------------------------------------
/BeaconEye.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.31025.194
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BeaconEye", "BeaconEye.csproj", "{7C30A97D-E557-40C4-9B3F-5EE56599C858}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Debug|x64 = Debug|x64
12 | Debug|x86 = Debug|x86
13 | Release|Any CPU = Release|Any CPU
14 | Release|x64 = Release|x64
15 | Release|x86 = Release|x86
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {7C30A97D-E557-40C4-9B3F-5EE56599C858}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {7C30A97D-E557-40C4-9B3F-5EE56599C858}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {7C30A97D-E557-40C4-9B3F-5EE56599C858}.Debug|x64.ActiveCfg = Debug|x64
21 | {7C30A97D-E557-40C4-9B3F-5EE56599C858}.Debug|x64.Build.0 = Debug|x64
22 | {7C30A97D-E557-40C4-9B3F-5EE56599C858}.Debug|x86.ActiveCfg = Debug|x86
23 | {7C30A97D-E557-40C4-9B3F-5EE56599C858}.Debug|x86.Build.0 = Debug|x86
24 | {7C30A97D-E557-40C4-9B3F-5EE56599C858}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {7C30A97D-E557-40C4-9B3F-5EE56599C858}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {7C30A97D-E557-40C4-9B3F-5EE56599C858}.Release|x64.ActiveCfg = Release|x64
27 | {7C30A97D-E557-40C4-9B3F-5EE56599C858}.Release|x64.Build.0 = Release|x64
28 | {7C30A97D-E557-40C4-9B3F-5EE56599C858}.Release|x86.ActiveCfg = Release|x86
29 | {7C30A97D-E557-40C4-9B3F-5EE56599C858}.Release|x86.Build.0 = Release|x86
30 | EndGlobalSection
31 | GlobalSection(SolutionProperties) = preSolution
32 | HideSolutionNode = FALSE
33 | EndGlobalSection
34 | GlobalSection(ExtensibilityGlobals) = postSolution
35 | SolutionGuid = {A0F41E69-9ECF-4F0A-A1ED-2AC52B1D2A4A}
36 | EndGlobalSection
37 | EndGlobal
38 |
--------------------------------------------------------------------------------
/BeaconProcess.cs:
--------------------------------------------------------------------------------
1 | using BeaconEye.Config;
2 | using NtApiDotNet;
3 | using NtApiDotNet.Win32;
4 | using System;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Net;
8 | using System.Security.Cryptography;
9 | using System.Text;
10 | using System.Threading;
11 |
12 | namespace BeaconEye {
13 | class BeaconProcess {
14 |
15 | public enum OutputTypes : int {
16 | CALLBACK_OUTPUT = 0,
17 | CALLBACK_KEYSTROKES = 1,
18 | CALLBACK_FILE = 2,
19 | CALLBACK_SCREENSHOT = 3,
20 | CALLBACK_CLOSE = 4,
21 | CALLBACK_READ = 5,
22 | CALLBACK_CONNECT = 6,
23 | CALLBACK_PING = 7,
24 | CALLBACK_FILE_WRITE = 8,
25 | CALLBACK_FILE_CLOSE = 9,
26 | CALLBACK_PIPE_OPEN = 10,
27 | CALLBACK_PIPE_CLOSE = 11,
28 | CALLBACK_PIPE_READ = 12,
29 | CALLBACK_POST_ERROR = 13,
30 | CALLBACK_PIPE_PING = 14,
31 | CALLBACK_TOKEN_STOLEN = 15,
32 | CALLBACK_TOKEN_GETUID = 16,
33 | CALLBACK_PROCESS_LIST = 17,
34 | CALLBACK_POST_REPLAY_ERROR = 18,
35 | CALLBACK_PWD = 19,
36 | CALLBACK_JOBS = 20,
37 | CALLBACK_HASHDUMP = 21,
38 | CALLBACK_PENDING = 22,
39 | CALLBACK_ACCEPT = 23,
40 | CALLBACK_NETVIEW = 24,
41 | CALLBACK_PORTSCAN = 25,
42 | CALLBACK_DEAD = 26,
43 | CALLBACK_SSH_STATUS = 27,
44 | CALLBACK_CHUNK_ALLOCATE = 28,
45 | CALLBACK_CHUNK_SEND = 29,
46 | CALLBACK_OUTPUT_OEM = 30,
47 | CALLBACK_ERROR = 31,
48 | CALLBACK_OUTPUT_UTF8 = 32
49 | }
50 |
51 | public NtProcess Process { get; private set; }
52 | public Configuration BeaconConfig { get; private set; }
53 |
54 | ManualResetEvent finishedEvent;
55 | StreamWriter logFile;
56 | long iv_address;
57 | long keys_address;
58 | string folderName;
59 |
60 | public BeaconProcess(ProcessReader process, Configuration beaconConfig, long iv_address, long keys_address, ref ManualResetEvent finishedEvent) {
61 |
62 | if(process is NtProcessReader ntpr) {
63 | Process = ntpr.Process;
64 | } else {
65 | throw new ArgumentException("Only live processes can be monitored");
66 | }
67 |
68 | BeaconConfig = beaconConfig;
69 | this.iv_address = iv_address;
70 | this.keys_address = keys_address;
71 | this.finishedEvent = finishedEvent;
72 |
73 | folderName = $"{process.Name}_{process.ProcessId}_{Process.User.Name.Replace('\\','_')}";
74 |
75 | Directory.CreateDirectory(folderName);
76 | logFile = new StreamWriter(new FileStream(Path.Combine(folderName, "activity.log"), FileMode.Create, FileAccess.ReadWrite));
77 |
78 | LogMessage("Configuration:");
79 | foreach (var config in beaconConfig.Items) {
80 | logFile.WriteLine($"\tValue {config.Value}");
81 | }
82 |
83 | logFile.Flush();
84 | }
85 |
86 | byte[] EnableBreakpoint(long address) {
87 | var oldBPInst = Process.ReadMemory(address, 1);
88 | var oldProtect = Process.ProtectMemory(address, 1, MemoryAllocationProtect.ExecuteReadWrite);
89 | Process.WriteMemory(address, new byte[] { 0xCC });
90 | Process.ProtectMemory(address, 1, oldProtect);
91 | Process.FlushInstructionCache(address, 16);
92 | return oldBPInst;
93 | }
94 |
95 | void DisableBreakpoint(NtProcess process, long address, byte[] oldInst) {
96 | var oldProtect = process.ProtectMemory(address, 1, MemoryAllocationProtect.ExecuteReadWrite);
97 | process.WriteMemory(address, oldInst);
98 | process.ProtectMemory(address, 1, oldProtect);
99 | process.FlushInstructionCache(address, 16);
100 | }
101 |
102 | void LogMessage(string message) {
103 | logFile.WriteLine($"{DateTime.Now} - {message}");
104 | logFile.Flush();
105 | }
106 |
107 | void EnableSingleStep(NtThread thread, long updateRip) {
108 |
109 | var ctx = thread.GetContext(ContextFlags.All);
110 |
111 | if (ctx is ContextAmd64 ctx64) {
112 |
113 | ctx64.Dr0 = ctx64.Dr6 = ctx64.Dr7 = 0;
114 | ctx64.EFlags |= 0x100;
115 | if (updateRip != 0)
116 | ctx64.Rip = (ulong)updateRip;
117 |
118 | }else if(ctx is ContextX86 ctx32) {
119 |
120 | ctx32.Dr0 = ctx32.Dr6 = ctx32.Dr7 = 0;
121 | ctx32.EFlags |= 0x100;
122 | if (updateRip != 0)
123 | ctx32.Eip = (uint)updateRip;
124 | }
125 |
126 | thread.SetContext(ctx);
127 | }
128 |
129 | static void DisableSingleStep(NtThread thread) {
130 |
131 | var ctx = thread.GetContext(ContextFlags.DebugRegisters);
132 |
133 | if (ctx is ContextAmd64 ctx64) {
134 |
135 | ctx64.Dr0 = ctx64.Dr6 = ctx64.Dr7 = 0;
136 | ctx64.EFlags = 0;
137 |
138 | } else if (ctx is ContextX86 ctx32) {
139 |
140 | ctx32.Dr0 = ctx32.Dr6 = ctx32.Dr7 = 0;
141 | ctx32.EFlags = 0;
142 | }
143 |
144 | thread.SetContext(ctx);
145 | }
146 |
147 | // Based on MSDN example: https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.rijndaelmanaged?redirectedfrom=MSDN&view=net-5.0#Y2262
148 | static byte[] DecryptStringFromBytes_Aes(byte[] cipherText, byte[] Key, byte[] IV) {
149 |
150 | if (cipherText == null || cipherText.Length <= 0)
151 | throw new ArgumentNullException("cipherText");
152 | if (Key == null || Key.Length <= 0)
153 | throw new ArgumentNullException("Key");
154 | if (IV == null || IV.Length <= 0)
155 | throw new ArgumentNullException("IV");
156 |
157 | using (Aes aesAlg = Aes.Create()) {
158 | aesAlg.Key = Key;
159 | aesAlg.IV = IV;
160 | aesAlg.Mode = CipherMode.CBC;
161 | aesAlg.Padding = PaddingMode.None;
162 |
163 | ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
164 |
165 | using (MemoryStream msDecrypt = new MemoryStream(cipherText)) {
166 | using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) {
167 | using (MemoryStream msPlain = new MemoryStream()) {
168 | csDecrypt.CopyTo(msPlain);
169 | return msPlain.ToArray();
170 | }
171 | }
172 | }
173 | }
174 | }
175 |
176 | void SaveScreenshot(BinaryReader br) {
177 | var jpgLen = br.ReadUInt32();
178 | var fileName = Path.Combine(folderName, $"{DateTime.Now.ToString("yyyyMMddHHmmss")}_Screenshot.jpg");
179 |
180 | //older beacons contain just the JPG data, newer versions contain other stuff too
181 | if (jpgLen != 0xE0FFD8FF) {
182 | var jpgData = br.ReadBytes((int)jpgLen);
183 | File.WriteAllBytes(fileName, jpgData);
184 | } else {
185 | var jpgData = br.ReadBytes((int)(br.BaseStream.Length - br.BaseStream.Position));
186 | jpgData = new byte[4] { 0xFF, 0xD8, 0xFF, 0xE0 }.Concat(jpgData).ToArray();
187 | File.WriteAllBytes(fileName, jpgData);
188 | }
189 | }
190 |
191 | void DecryptCallback(byte[] body, byte[] key) {
192 |
193 | var beaconProgram = (ConfigProgramItem)BeaconConfig.Items["HTTP_Post_Program"];
194 | byte[] decoded = beaconProgram.Value.RecoverOutput(body);
195 |
196 | var dataLen = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(decoded, 0));
197 | var encryptedData = decoded.Skip(4).Take(dataLen).ToArray();
198 | var decryptedData = DecryptStringFromBytes_Aes(encryptedData, key, Encoding.ASCII.GetBytes("abcdefghijklmnop"));
199 |
200 | BinaryReader br = new BinaryReader(new MemoryStream(decryptedData));
201 |
202 | var sequenceNumber = IPAddress.NetworkToHostOrder(br.ReadInt32());
203 | var callbackDataLen = IPAddress.NetworkToHostOrder(br.ReadInt32());
204 | var callbackId = (OutputTypes)IPAddress.NetworkToHostOrder(br.ReadInt32());
205 | callbackDataLen -= 4;
206 | string output = "";
207 |
208 | switch (callbackId) {
209 | case OutputTypes.CALLBACK_OUTPUT:
210 | case OutputTypes.CALLBACK_OUTPUT_OEM:
211 | output = Encoding.ASCII.GetString(br.ReadBytes(callbackDataLen));
212 | break;
213 | case OutputTypes.CALLBACK_OUTPUT_UTF8:
214 | case OutputTypes.CALLBACK_HASHDUMP:
215 | case OutputTypes.CALLBACK_KEYSTROKES:
216 | output = Encoding.UTF8.GetString(br.ReadBytes(callbackDataLen));
217 | break;
218 | case OutputTypes.CALLBACK_PENDING:
219 | var pendingRequest = IPAddress.NetworkToHostOrder(br.ReadInt32());
220 | output = Encoding.ASCII.GetString(br.ReadBytes(callbackDataLen - 4));
221 | break;
222 | case OutputTypes.CALLBACK_SCREENSHOT:
223 | SaveScreenshot(br);
224 | break;
225 | }
226 |
227 | LogMessage($"Callback {sequenceNumber} sent with type {callbackId}\n{output}");
228 | }
229 |
230 | long ReadArg(IContext ctx, int argNum) {
231 |
232 | if(ctx is ContextAmd64 ctx64) {
233 |
234 | switch (argNum) {
235 | case 0:
236 | return (long)ctx64.Rcx;
237 | case 1:
238 | return (long)ctx64.Rdx;
239 | case 2:
240 | return (long)ctx64.R8;
241 | case 3:
242 | return (long)ctx64.R9;
243 | default:
244 | long parameterAddress = (long)ctx64.Rsp + 0x28 + ((argNum - 4) * 8);
245 | return Process.ReadMemory(parameterAddress);
246 | }
247 |
248 | } else if(ctx is ContextX86 ctx32) {
249 |
250 | long parameterAddress = ctx32.Esp + 4 + (argNum * 4);
251 | return Process.ReadMemory(parameterAddress);
252 |
253 | } else {
254 | throw new NotImplementedException("Only x86 or AMD64 processes supported");
255 | }
256 | }
257 |
258 | public void MonitorTraffic() {
259 |
260 | NtDebug debugObject = NtDebug.Create();
261 | var debugging = true;
262 | debugObject.SetKillOnClose(false);
263 | debugObject.Attach(Process);
264 | byte[] oldBPInst = null;
265 | byte[] keys = null;
266 | long httpSendRequestAddress = 0;
267 | int singleStepThreadId = 0;
268 |
269 | while (debugging) {
270 |
271 | var status = NtStatus.DBG_CONTINUE;
272 | DebugEvent debugEvent = debugObject.WaitForDebugEvent(100);
273 |
274 | if (debugEvent is UnknownDebugEvent && finishedEvent.WaitOne(400)) {
275 |
276 | if (httpSendRequestAddress != 0 && oldBPInst != null) {
277 | DisableBreakpoint(Process, httpSendRequestAddress, oldBPInst);
278 | }
279 |
280 | if (singleStepThreadId != 0) {
281 | using (var requestThread = NtThread.Open(singleStepThreadId, ThreadAccessRights.MaximumAllowed)) {
282 | DisableSingleStep(requestThread);
283 | }
284 | }
285 |
286 | debugging = false;
287 | continue;
288 | }
289 |
290 | if (debugEvent is LoadDllDebugEvent loadDllDebugEvent) {
291 |
292 | if (Path.GetFileName(loadDllDebugEvent.File.FileName) == "wininet.dll") {
293 |
294 | var wininetLib = SafeLoadLibraryHandle.LoadLibrary("wininet.dll");
295 | httpSendRequestAddress = wininetLib.Exports
296 | .Where(e => e.Name == "HttpSendRequestA")
297 | .Select(e => e.Address)
298 | .First();
299 |
300 | oldBPInst = EnableBreakpoint(httpSendRequestAddress);
301 | }
302 | } else if (debugEvent is ExceptionDebugEvent exceptionDebugEvent) {
303 |
304 | if (exceptionDebugEvent.Code == NtStatus.STATUS_BREAKPOINT && exceptionDebugEvent.Address == httpSendRequestAddress) {
305 |
306 | if (keys == null) {
307 | string iv = Encoding.ASCII.GetString(Process.ReadMemory(iv_address, 16));
308 | if (iv == "abcdefghijklmnop") {
309 | keys = Process.ReadMemory(keys_address, 32);
310 | LogMessage($"Static IV found at 0x{iv_address:x}");
311 | LogMessage($"AES Key: {StringUtils.ByteArrayToString(keys.Take(16).ToArray())}");
312 | LogMessage($"HMAC Key: {StringUtils.ByteArrayToString(keys.Skip(16).Take(16).ToArray())}");
313 | }
314 | }
315 |
316 | DisableBreakpoint(Process, httpSendRequestAddress, oldBPInst);
317 | using (var requestThread = NtThread.Open(debugEvent.ThreadId, ThreadAccessRights.GetContext | ThreadAccessRights.SetContext)) {
318 |
319 | var ctx = requestThread.GetContext(ContextFlags.All);
320 | string httpHeaders = Encoding.ASCII.GetString(Process.ReadMemory(ReadArg(ctx,1), (int)ReadArg(ctx,2)));
321 | byte[] body = null;
322 | var body_len = ReadArg(ctx, 4);
323 | long body_ptr;
324 |
325 | if (body_len > 0 && (body_ptr = ReadArg(ctx, 3)) != 0) {
326 | body = Process.ReadMemory(body_ptr, (int)body_len);
327 | }
328 |
329 | if (body != null) {
330 | DecryptCallback(body, keys.Take(16).ToArray());
331 | }
332 |
333 | EnableSingleStep(requestThread, httpSendRequestAddress);
334 | singleStepThreadId = debugEvent.ThreadId;
335 | }
336 |
337 | } else if (exceptionDebugEvent.Code == NtStatus.STATUS_SINGLE_STEP) {
338 |
339 | using (var requestThread = NtThread.Open(debugEvent.ThreadId, ThreadAccessRights.MaximumAllowed)) {
340 | DisableSingleStep(requestThread);
341 | singleStepThreadId = 0;
342 | }
343 |
344 | EnableBreakpoint(httpSendRequestAddress);
345 |
346 | } else {
347 | status = NtStatus.DBG_EXCEPTION_NOT_HANDLED;
348 | }
349 | }
350 |
351 | if (!(debugEvent is UnknownDebugEvent))
352 | debugObject.Continue(debugEvent.ProcessId, debugEvent.ThreadId, status);
353 |
354 | }
355 |
356 | debugObject.Detach(Process);
357 | LogMessage($"Disconnected from beacon process");
358 | logFile.Close();
359 | }
360 | }
361 | }
362 |
--------------------------------------------------------------------------------
/BeaconProgram.cs:
--------------------------------------------------------------------------------
1 | using NtApiDotNet;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Net;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace BeaconEye {
11 |
12 | public enum Action : int {
13 | NONE,
14 | append,
15 | prepend,
16 | base64,
17 | print,
18 | parameter,
19 | header,
20 | BUILD,
21 | netbios,
22 | _PARAMETER,
23 | _HEADER,
24 | netbiosu,
25 | uri_append,
26 | base64url,
27 | strrep,
28 | mask,
29 | hostheader,
30 | }
31 |
32 | public class Statement {
33 | public Statement(Action action, byte[] argument) {
34 | Action = action;
35 | Argument = argument;
36 | }
37 |
38 | public Action Action { get; set; }
39 |
40 | public byte[] Argument { get; set; }
41 |
42 | public override string ToString() {
43 |
44 | string action = Action.ToString();
45 | if(Action == Action._HEADER) {
46 | action = "header";
47 | }else if(Action == Action._PARAMETER) {
48 | action = "parameter";
49 | }
50 |
51 | return $"{action} {Encoding.ASCII.GetString(Argument)};";
52 | }
53 | }
54 |
55 | public class BeaconProgram {
56 |
57 | public List Statements { get; set; } = new List();
58 |
59 | byte[] NetBIOSDecode(byte[] source, bool upper) {
60 |
61 | byte baseChar = (byte)(upper ? 0x41 : 0x61);
62 | byte[] result = new byte[source.Length / 2];
63 |
64 | for (int idx = 0; idx < source.Length; idx += 2){
65 | result[idx/2] = ((byte)(((source[idx] - baseChar) << 4) | (source[idx + 1] - baseChar) & 0xf));
66 | }
67 |
68 | return result;
69 | }
70 |
71 | byte[] MaskDecode(byte[] source) {
72 |
73 | var xorKey = source.Take(4).ToArray();
74 | var result = source.Skip(4).ToArray();
75 |
76 | for(int idx = 0; idx< result.Length; ++idx) {
77 | result[idx] ^= xorKey[idx % 4];
78 | }
79 |
80 | return result;
81 | }
82 |
83 | public byte[] RecoverOutput(byte[] source) {
84 |
85 | bool decode = false;
86 | byte[] decoded = source;
87 | bool done = false;
88 | var outputStatements = new List();
89 |
90 | foreach (var statement in Statements) {
91 | switch (statement.Action) {
92 | case Action.BUILD:
93 | int buildType = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(statement.Argument, 0));
94 | if (buildType == 1) {
95 | decode = true;
96 | }
97 | break;
98 | case Action.print:
99 | if (decode) {
100 | done = true;
101 | outputStatements.Reverse();
102 | }
103 | break;
104 | default:
105 | if (decode) {
106 | outputStatements.Add(statement);
107 | }
108 | break;
109 | }
110 |
111 | if (done)
112 | break;
113 | }
114 |
115 | foreach(var statement in outputStatements) {
116 | switch (statement.Action) {
117 | case Action.base64url:
118 | decoded = Convert.FromBase64String(Uri.UnescapeDataString(Encoding.ASCII.GetString(decoded)));
119 | break;
120 | case Action.base64:
121 | decoded = Convert.FromBase64String(Encoding.ASCII.GetString(decoded));
122 | break;
123 | case Action.prepend:
124 | decoded = decoded.Skip(statement.Argument.Length).ToArray();
125 | break;
126 | case Action.append:
127 | decoded = decoded.Take(decoded.Length - statement.Argument.Length).ToArray();
128 | break;
129 | case Action.netbiosu:
130 | decoded = NetBIOSDecode(decoded, true);
131 | break;
132 | case Action.netbios:
133 | decoded = NetBIOSDecode(decoded, false);
134 | break;
135 | case Action.mask:
136 | decoded = MaskDecode(decoded);
137 | break;
138 | default:
139 | throw new NotImplementedException($"Statment with action {statement.Action} currently not supported, please open an issue on GitHub");
140 | }
141 | }
142 |
143 | return decoded;
144 | }
145 |
146 | internal static BeaconProgram Parse(long address, ProcessReader process) {
147 | ;
148 | var result = new BeaconProgram();
149 | bool done = false;
150 |
151 | while (!done) {
152 | var action = (Action)IPAddress.NetworkToHostOrder(process.ReadMemory((ulong)address));
153 | address += 4;
154 |
155 | var actionParameter = new byte[0];
156 | switch (action) {
157 | case Action.NONE:
158 | done = true;
159 | break;
160 |
161 | case Action.append:
162 | case Action.prepend:
163 | case Action._HEADER:
164 | case Action.header:
165 | case Action._PARAMETER:
166 | case Action.parameter:
167 | case Action.hostheader:
168 | case Action.uri_append:
169 | case Action.base64url:
170 | int actionParamLen = IPAddress.NetworkToHostOrder(process.ReadMemory((ulong)address));
171 | address += 4;
172 | actionParameter = process.ReadMemory((ulong)address, actionParamLen);
173 | address += actionParamLen;
174 | break;
175 |
176 | case Action.BUILD:
177 | actionParameter = process.ReadMemory((ulong)address, 4);
178 | address += 4;
179 | break;
180 |
181 | case Action.base64:
182 | case Action.print:
183 | case Action.netbios:
184 | case Action.netbiosu:
185 | case Action.strrep:
186 | case Action.mask:
187 | break;
188 | }
189 |
190 | if (action != Action.NONE) {
191 | result.Statements.Add(new Statement(action, actionParameter));
192 | }
193 | }
194 |
195 | return result;
196 | }
197 | }
198 |
199 | }
200 |
--------------------------------------------------------------------------------
/Config/ConfigItem.cs:
--------------------------------------------------------------------------------
1 | using NtApiDotNet;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Net;
7 | using System.Reflection;
8 | using System.Text;
9 | using System.Threading.Tasks;
10 |
11 | namespace BeaconEye.Config {
12 |
13 | public enum Type {
14 | Unconfigured,
15 | Short,
16 | Integer,
17 | Bytes,
18 | String
19 | }
20 |
21 | /*
22 | { 0x01, new List { "BeaconType:", "beaconType" }},
23 | { 0x02, new List { "Port:", "short" }},
24 | { 0x03, new List { "Polling(ms):", "int" }},
25 | { 0x04, new List { "MaxGetSize:", "int" }},
26 | { 0x05, new List { "Jitter:", "short" }},
27 | { 0x06, new List { "Maxdns:", "short" }},
28 | //{ 0x07, new List { "PublicKey:", "bytes" }},
29 | { 0x08, new List { "C2Server:", "string" }},
30 | { 0x09, new List { "UserAgent:", "string" }},
31 | { 0x0a, new List { "HTTP_Post_URI:", "string" }},
32 | { 0x0b, new List { "HTTPGetServerOutput:", "program" }},
33 | { 0x0c, new List { "HTTP_Get_Program:", "program" }},
34 | { 0x0d, new List { "HTTP_Post_Program:", "program" }},
35 | { 0x0e, new List { "Injection_Process:", "string" }},
36 | { 0x0f, new List { "PipeName:", "string" }},
37 | // Options 0x10-0x12 are deprecated in 3.4
38 | { 0x10, new List { "Year:", "int" }},
39 | { 0x11, new List { "Month:", "int" }},
40 | { 0x12, new List { "Day:", "int" }},
41 | { 0x13, new List { "DNS_idle:", "int" }},
42 | { 0x14, new List { "DNS_sleep(ms):", "int" }},
43 | { 0x1a, new List { "HTTP_Method1:", "string" }},
44 | { 0x1b, new List { "HTTP_ Method2:", "string" }},
45 | { 0x1c, new List { "HttpPostChunk:", "int" }},
46 | { 0x1d, new List { "Spawnto_x86:", "string" }},
47 | { 0x1e, new List { "Spawnto_x64:", "string" }},
48 | { 0x1f, new List { "CryptoScheme:", "short" }},
49 | { 0x20, new List { "Proxy_HostName:", "string" }},
50 | { 0x21, new List { "Proxy_UserName:", "string" }},
51 | { 0x22, new List { "Proxy_Password:", "string" }},
52 | { 0x23, new List { "Proxy_AccessType:", "accessType" }},
53 | // Deprecated { 0x24, new List { "create_remote_thread:", "" }},
54 | { 0x25, new List { "Watermark:", "int" }},
55 | { 0x26, new List { "StageCleanup:", "bool" }},
56 | { 0x27, new List { "CfgCaution:", "bool" }},
57 | { 0x28, new List { "KillDate:", "int" }},
58 | // Not useful { 0x29, new List { "TextSectionEnd:", "" }},
59 | //{ 0x2a, new List { "ObfuscationSectionsInfo:", "" }},
60 | { 0x2b, new List { "ProcInject_StartRWX:", "bool" }},
61 | { 0x2c, new List { "ProcInject_UseRWX:", "bool" }},
62 | { 0x2d, new List { "ProcInject_MinAllocSize:", "int" }},
63 | { 0x2e, new List { "ProcInject_PrependAppend_x86:", "string" }},
64 | { 0x2f, new List { "ProcInject_PrependAppend_x64:", "string" }},
65 | { 0x32, new List { "UsesCookies:", "bool" }},
66 | { 0x33, new List { "ProcInject_Execute:", "executeType" }},
67 | { 0x34, new List { "ProcInject_AllocationMethod:", "allocationFunction" }},
68 | //{ 0x35, new List { "ProcInject_Stub:", "string" }},
69 | { 0x36, new List { "HostHeader:", "string" }},
70 | { 0x44, new List { "RotateStrategy:", "int" }},
71 | { 0x45, new List { "FailoverCount:", "int" }},
72 | { 0x46, new List { "FailoverTime:", "int" }},
73 | */
74 |
75 |
76 | [AttributeUsage(AttributeTargets.Class)]
77 | public class ConfigPropertyAttribute : Attribute {
78 | public Type ConfigType { get; set; }
79 | public int Index { get; set; }
80 |
81 | }
82 |
83 | public class Configuration {
84 |
85 | public class ConfigAttrbute{
86 | public int Index { get; private set; }
87 | public string Name { get; private set; }
88 | public System.Type Type { get; private set; }
89 |
90 | public ConfigAttrbute(int index, string name, System.Type objectType) {
91 | Index = index;
92 | Name = name;
93 | Type = objectType;
94 | }
95 | }
96 |
97 | static Dictionary configTypes = new Dictionary();
98 | public Dictionary Items { get; private set; } = new Dictionary();
99 | public long Address { get; private set; }
100 |
101 | int configEntrySize;
102 |
103 | static Configuration() {
104 | configTypes.Add(1, new ConfigAttrbute(1, "BeaconType", typeof(ConfigShortItem)));
105 | configTypes.Add(2, new ConfigAttrbute(2, "Port", typeof(ConfigShortItem)));
106 | configTypes.Add(3, new ConfigAttrbute(3, "Sleep", typeof(ConfigIntegerItem)));
107 | configTypes.Add(4, new ConfigAttrbute(4, "MaxGetSize",typeof(ConfigIntegerItem)));
108 | configTypes.Add(5, new ConfigAttrbute(5, "Jitter", typeof(ConfigShortItem)));
109 | configTypes.Add(6, new ConfigAttrbute(6, "MaxDNS", typeof(ConfigShortItem)));
110 | configTypes.Add(8, new ConfigAttrbute(8, "C2Server", typeof(ConfigStringItem)));
111 | configTypes.Add(9, new ConfigAttrbute(9, "UserAgent", typeof(ConfigStringItem)));
112 | configTypes.Add(10, new ConfigAttrbute(10, "HTTP_Post_URI", typeof(ConfigStringItem)));
113 | configTypes.Add(11, new ConfigAttrbute(11, "HTTPGetServerOutput", typeof(ConfigProgramItem)));
114 | configTypes.Add(12, new ConfigAttrbute(12, "HTTP_Get_Program", typeof(ConfigProgramItem)));
115 | configTypes.Add(13, new ConfigAttrbute(13, "HTTP_Post_Program", typeof(ConfigProgramItem)));
116 | configTypes.Add(14, new ConfigAttrbute(14, "Inject_Process", typeof(ConfigStringItem)));
117 | configTypes.Add(15, new ConfigAttrbute(15, "PipeName", typeof(ConfigStringItem)));
118 | configTypes.Add(19, new ConfigAttrbute(19, "DNS_idle", typeof(ConfigIntegerItem)));
119 | configTypes.Add(20, new ConfigAttrbute(20, "DNS_sleep", typeof(ConfigIntegerItem)));
120 | configTypes.Add(26, new ConfigAttrbute(26, "HTTP_Method1", typeof(ConfigStringItem)));
121 | configTypes.Add(27, new ConfigAttrbute(27, "HTTP_Method2", typeof(ConfigStringItem)));
122 | configTypes.Add(28, new ConfigAttrbute(28, "HttpPostChunk", typeof(ConfigIntegerItem)));
123 | configTypes.Add(29, new ConfigAttrbute(29, "Spawnto_x86", typeof(ConfigStringItem)));
124 | configTypes.Add(30, new ConfigAttrbute(30, "Spawnto_x64", typeof(ConfigStringItem)));
125 | configTypes.Add(32, new ConfigAttrbute(32, "Proxy_Host", typeof(ConfigStringItem)));
126 | configTypes.Add(33, new ConfigAttrbute(33, "Proxy_Username", typeof(ConfigStringItem)));
127 | configTypes.Add(34, new ConfigAttrbute(34, "Proxy_Password", typeof(ConfigStringItem)));
128 | configTypes.Add(37, new ConfigAttrbute(37, "Watermark", typeof(ConfigIntegerItem)));
129 | configTypes.Add(38, new ConfigAttrbute(38, "StageCleanup", typeof(ConfigShortItem)));
130 | configTypes.Add(39, new ConfigAttrbute(39, "CfgCaution", typeof(ConfigShortItem)));
131 | configTypes.Add(40, new ConfigAttrbute(40, "KillDate", typeof(ConfigIntegerItem)));
132 | configTypes.Add(54, new ConfigAttrbute(54, "Host_Header", typeof(ConfigStringItem)));
133 | }
134 |
135 |
136 | public Configuration(long configAddress, BinaryReader configReader, ProcessReader process) {
137 |
138 | Address = configAddress;
139 | configEntrySize = process.Is64Bit ? 16 : 8;
140 | configReader.ReadBytes(configEntrySize);
141 | int index = 1;
142 |
143 | while (configReader.BaseStream.Position < configReader.BaseStream.Length) {
144 |
145 | Type type;
146 |
147 | if (configEntrySize == 16)
148 | type = (Type)configReader.ReadInt64();
149 | else
150 | type = (Type)configReader.ReadInt32();
151 |
152 | if (!configTypes.ContainsKey(index) || type == Type.Unconfigured) {
153 | configReader.ReadBytes(configEntrySize/2);
154 | index++;
155 | continue;
156 | }
157 |
158 | var configType = configTypes[index];
159 | ConfigItem configItem = (ConfigItem)Activator.CreateInstance(configType.Type, new object[] { configType.Name });
160 |
161 | if(configItem.ExpectedType != type) {
162 | throw new FormatException("Serialized config format does not match configuration type");
163 | }
164 |
165 | configItem.Parse(configReader, process);
166 |
167 | if(configReader.BaseStream.Position % configEntrySize != 0)
168 | configReader.ReadBytes(configEntrySize - (int)configReader.BaseStream.Position % configEntrySize);
169 |
170 | if(configItem != null)
171 | Items.Add(configItem.Name, configItem);
172 |
173 | index++;
174 | }
175 | }
176 |
177 | public void PrintConfiguration(TextWriter writer, int numTabs) {
178 | foreach (var config in Items) {
179 | if (!string.IsNullOrWhiteSpace(config.Value.ToString())) {
180 | writer.Write(new string('\t', numTabs));
181 | writer.WriteLine(config.Value);
182 | }
183 | }
184 | }
185 | }
186 |
187 | public abstract class ConfigItem {
188 | public string Name { get; protected set; }
189 |
190 | public abstract Type ExpectedType { get; }
191 |
192 | public ConfigItem(string name) {
193 | Name = name;
194 | }
195 |
196 | public abstract void Parse(BinaryReader br, ProcessReader process);
197 | }
198 |
199 | public class ConfigShortItem : ConfigItem {
200 |
201 | public short Value { get; private set; }
202 | public override Type ExpectedType => Type.Short;
203 |
204 | public ConfigShortItem(string name) : base(name) {
205 | }
206 |
207 | public override string ToString() {
208 | return $"{Name}: {Value}";
209 | }
210 |
211 | public override void Parse(BinaryReader br, ProcessReader process) {
212 | Value = br.ReadInt16();
213 | }
214 | }
215 |
216 | public class ConfigIntegerItem : ConfigItem {
217 |
218 | public int Value { get; private set; }
219 | public override Type ExpectedType => Type.Integer;
220 |
221 | public ConfigIntegerItem(string name) : base(name) {
222 |
223 | }
224 |
225 | public override string ToString() {
226 | return $"{Name}: {Value}";
227 | }
228 |
229 | public override void Parse(BinaryReader br, ProcessReader process) {
230 | Value = br.ReadInt32();
231 | }
232 | }
233 |
234 | public class ConfigStringItem : ConfigItem {
235 |
236 | public override Type ExpectedType => Type.Bytes;
237 |
238 | public string Value { get; private set; }
239 |
240 | public ConfigStringItem(string name) : base(name) {
241 | }
242 |
243 | public override string ToString() {
244 | return $"{Name}: {Value}";
245 | }
246 |
247 | public override void Parse(BinaryReader br, ProcessReader process) {
248 | Value = ReadNullString(process, process.Is64Bit ? br.ReadInt64() : br.ReadInt32());
249 | }
250 |
251 | string ReadNullString(ProcessReader process, long address) {
252 |
253 | MemoryStream ms = new MemoryStream();
254 |
255 | while (true) {
256 | var strChar = process.ReadMemory((ulong)address++, 1);
257 | if (strChar[0] == '\0') {
258 | break;
259 | }
260 | ms.Write(strChar, 0, 1);
261 | }
262 |
263 | return Encoding.ASCII.GetString(ms.ToArray());
264 | }
265 | }
266 |
267 | public class ConfigProgramItem : ConfigItem {
268 |
269 | public override Type ExpectedType => Type.Bytes;
270 |
271 | public BeaconProgram Value { get; private set; }
272 |
273 | public ConfigProgramItem(string name) : base(name) {
274 | }
275 |
276 | public override void Parse(BinaryReader br, ProcessReader process) {
277 | Value = BeaconProgram.Parse(process.Is64Bit ? br.ReadInt64() : br.ReadInt32(), process);
278 | }
279 |
280 | public override string ToString() {
281 | var str = new StringBuilder();
282 | str.AppendLine();
283 |
284 | foreach(var statement in Value.Statements) {
285 |
286 | if(statement.Action == Action.NONE) {
287 | break;
288 | }else if(statement.Action == Action.BUILD) {
289 | int type = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(statement.Argument, 0));
290 | if(type == 1) {
291 | str.AppendLine("\t\toutput:");
292 | } else {
293 | str.AppendLine("\t\tid|meta:");
294 | }
295 | continue;
296 | }
297 |
298 | str.AppendLine($"\t\t{statement}");
299 | }
300 | return str.ToString();
301 | }
302 | }
303 | }
304 |
--------------------------------------------------------------------------------
/Options.cs:
--------------------------------------------------------------------------------
1 | //
2 | // Options.cs
3 | //
4 | // Authors:
5 | // Jonathan Pryor ,
6 | // Federico Di Gregorio
7 | // Rolf Bjarne Kvinge
8 | //
9 | // Copyright (C) 2008 Novell (http://www.novell.com)
10 | // Copyright (C) 2009 Federico Di Gregorio.
11 | // Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com)
12 | // Copyright (C) 2017 Microsoft Corporation (http://www.microsoft.com)
13 | //
14 | // Permission is hereby granted, free of charge, to any person obtaining
15 | // a copy of this software and associated documentation files (the
16 | // "Software"), to deal in the Software without restriction, including
17 | // without limitation the rights to use, copy, modify, merge, publish,
18 | // distribute, sublicense, and/or sell copies of the Software, and to
19 | // permit persons to whom the Software is furnished to do so, subject to
20 | // the following conditions:
21 | //
22 | // The above copyright notice and this permission notice shall be
23 | // included in all copies or substantial portions of the Software.
24 | //
25 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 | //
33 |
34 | // Compile With:
35 | // mcs -debug+ -r:System.Core Options.cs -o:Mono.Options.dll -t:library
36 | // mcs -debug+ -d:LINQ -r:System.Core Options.cs -o:Mono.Options.dll -t:library
37 | //
38 | // The LINQ version just changes the implementation of
39 | // OptionSet.Parse(IEnumerable), and confers no semantic changes.
40 |
41 | //
42 | // A Getopt::Long-inspired option parsing library for C#.
43 | //
44 | // Mono.Options.OptionSet is built upon a key/value table, where the
45 | // key is a option format string and the value is a delegate that is
46 | // invoked when the format string is matched.
47 | //
48 | // Option format strings:
49 | // Regex-like BNF Grammar:
50 | // name: .+
51 | // type: [=:]
52 | // sep: ( [^{}]+ | '{' .+ '}' )?
53 | // aliases: ( name type sep ) ( '|' name type sep )*
54 | //
55 | // Each '|'-delimited name is an alias for the associated action. If the
56 | // format string ends in a '=', it has a required value. If the format
57 | // string ends in a ':', it has an optional value. If neither '=' or ':'
58 | // is present, no value is supported. `=' or `:' need only be defined on one
59 | // alias, but if they are provided on more than one they must be consistent.
60 | //
61 | // Each alias portion may also end with a "key/value separator", which is used
62 | // to split option values if the option accepts > 1 value. If not specified,
63 | // it defaults to '=' and ':'. If specified, it can be any character except
64 | // '{' and '}' OR the *string* between '{' and '}'. If no separator should be
65 | // used (i.e. the separate values should be distinct arguments), then "{}"
66 | // should be used as the separator.
67 | //
68 | // Options are extracted either from the current option by looking for
69 | // the option name followed by an '=' or ':', or is taken from the
70 | // following option IFF:
71 | // - The current option does not contain a '=' or a ':'
72 | // - The current option requires a value (i.e. not a Option type of ':')
73 | //
74 | // The `name' used in the option format string does NOT include any leading
75 | // option indicator, such as '-', '--', or '/'. All three of these are
76 | // permitted/required on any named option.
77 | //
78 | // Option bundling is permitted so long as:
79 | // - '-' is used to start the option group
80 | // - all of the bundled options are a single character
81 | // - at most one of the bundled options accepts a value, and the value
82 | // provided starts from the next character to the end of the string.
83 | //
84 | // This allows specifying '-a -b -c' as '-abc', and specifying '-D name=value'
85 | // as '-Dname=value'.
86 | //
87 | // Option processing is disabled by specifying "--". All options after "--"
88 | // are returned by OptionSet.Parse() unchanged and unprocessed.
89 | //
90 | // Unprocessed options are returned from OptionSet.Parse().
91 | //
92 | // Examples:
93 | // int verbose = 0;
94 | // OptionSet p = new OptionSet ()
95 | // .Add ("v", v => ++verbose)
96 | // .Add ("name=|value=", v => Console.WriteLine (v));
97 | // p.Parse (new string[]{"-v", "--v", "/v", "-name=A", "/name", "B", "extra"});
98 | //
99 | // The above would parse the argument string array, and would invoke the
100 | // lambda expression three times, setting `verbose' to 3 when complete.
101 | // It would also print out "A" and "B" to standard output.
102 | // The returned array would contain the string "extra".
103 | //
104 | // C# 3.0 collection initializers are supported and encouraged:
105 | // var p = new OptionSet () {
106 | // { "h|?|help", v => ShowHelp () },
107 | // };
108 | //
109 | // System.ComponentModel.TypeConverter is also supported, allowing the use of
110 | // custom data types in the callback type; TypeConverter.ConvertFromString()
111 | // is used to convert the value option to an instance of the specified
112 | // type:
113 | //
114 | // var p = new OptionSet () {
115 | // { "foo=", (Foo f) => Console.WriteLine (f.ToString ()) },
116 | // };
117 | //
118 | // Random other tidbits:
119 | // - Boolean options (those w/o '=' or ':' in the option format string)
120 | // are explicitly enabled if they are followed with '+', and explicitly
121 | // disabled if they are followed with '-':
122 | // string a = null;
123 | // var p = new OptionSet () {
124 | // { "a", s => a = s },
125 | // };
126 | // p.Parse (new string[]{"-a"}); // sets v != null
127 | // p.Parse (new string[]{"-a+"}); // sets v != null
128 | // p.Parse (new string[]{"-a-"}); // sets v == null
129 | //
130 |
131 | //
132 | // Mono.Options.CommandSet allows easily having separate commands and
133 | // associated command options, allowing creation of a *suite* along the
134 | // lines of **git**(1), **svn**(1), etc.
135 | //
136 | // CommandSet allows intermixing plain text strings for `--help` output,
137 | // Option values -- as supported by OptionSet -- and Command instances,
138 | // which have a name, optional help text, and an optional OptionSet.
139 | //
140 | // var suite = new CommandSet ("suite-name") {
141 | // // Use strings and option values, as with OptionSet
142 | // "usage: suite-name COMMAND [OPTIONS]+",
143 | // { "v:", "verbosity", (int? v) => Verbosity = v.HasValue ? v.Value : Verbosity+1 },
144 | // // Commands may also be specified
145 | // new Command ("command-name", "command help") {
146 | // Options = new OptionSet {/*...*/},
147 | // Run = args => { /*...*/},
148 | // },
149 | // new MyCommandSubclass (),
150 | // };
151 | // return suite.Run (new string[]{...});
152 | //
153 | // CommandSet provides a `help` command, and forwards `help COMMAND`
154 | // to the registered Command instance by invoking Command.Invoke()
155 | // with `--help` as an option.
156 | //
157 |
158 | using System;
159 | using System.Collections;
160 | using System.Collections.Generic;
161 | using System.Collections.ObjectModel;
162 | using System.ComponentModel;
163 | using System.Globalization;
164 | using System.IO;
165 | #if PCL
166 | using System.Reflection;
167 | #else
168 | using System.Runtime.Serialization;
169 | using System.Security.Permissions;
170 | #endif
171 | using System.Text;
172 | using System.Text.RegularExpressions;
173 |
174 | #if LINQ
175 | using System.Linq;
176 | #endif
177 |
178 | #if TEST
179 | using NDesk.Options;
180 | #endif
181 |
182 | #if PCL
183 | using MessageLocalizerConverter = System.Func;
184 | #else
185 | using MessageLocalizerConverter = System.Converter;
186 | #endif
187 |
188 | #if NDESK_OPTIONS
189 | namespace NDesk.Options
190 | #else
191 | namespace Mono.Options
192 | #endif
193 | {
194 | static class StringCoda {
195 |
196 | public static IEnumerable WrappedLines(string self, params int[] widths) {
197 | IEnumerable w = widths;
198 | return WrappedLines(self, w);
199 | }
200 |
201 | public static IEnumerable WrappedLines(string self, IEnumerable widths) {
202 | if (widths == null)
203 | throw new ArgumentNullException("widths");
204 | return CreateWrappedLinesIterator(self, widths);
205 | }
206 |
207 | private static IEnumerable CreateWrappedLinesIterator(string self, IEnumerable widths) {
208 | if (string.IsNullOrEmpty(self)) {
209 | yield return string.Empty;
210 | yield break;
211 | }
212 | using (IEnumerator ewidths = widths.GetEnumerator()) {
213 | bool? hw = null;
214 | int width = GetNextWidth(ewidths, int.MaxValue, ref hw);
215 | int start = 0, end;
216 | do {
217 | end = GetLineEnd(start, width, self);
218 | // endCorrection is 1 if the line end is '\n', and might be 2 if the line end is '\r\n'.
219 | int endCorrection = 1;
220 | if (end >= 2 && self.Substring(end - 2, 2).Equals("\r\n"))
221 | endCorrection = 2;
222 | char c = self[end - endCorrection];
223 | if (char.IsWhiteSpace(c))
224 | end -= endCorrection;
225 | bool needContinuation = end != self.Length && !IsEolChar(c);
226 | string continuation = "";
227 | if (needContinuation) {
228 | --end;
229 | continuation = "-";
230 | }
231 | string line = self.Substring(start, end - start) + continuation;
232 | yield return line;
233 | start = end;
234 | if (char.IsWhiteSpace(c))
235 | start += endCorrection;
236 | width = GetNextWidth(ewidths, width, ref hw);
237 | } while (start < self.Length);
238 | }
239 | }
240 |
241 | private static int GetNextWidth(IEnumerator ewidths, int curWidth, ref bool? eValid) {
242 | if (!eValid.HasValue || (eValid.HasValue && eValid.Value)) {
243 | curWidth = (eValid = ewidths.MoveNext()).Value ? ewidths.Current : curWidth;
244 | // '.' is any character, - is for a continuation
245 | const string minWidth = ".-";
246 | if (curWidth < minWidth.Length)
247 | throw new ArgumentOutOfRangeException("widths",
248 | string.Format("Element must be >= {0}, was {1}.", minWidth.Length, curWidth));
249 | return curWidth;
250 | }
251 | // no more elements, use the last element.
252 | return curWidth;
253 | }
254 |
255 | private static bool IsEolChar(char c) {
256 | return !char.IsLetterOrDigit(c);
257 | }
258 |
259 | private static int GetLineEnd(int start, int length, string description) {
260 | int end = System.Math.Min(start + length, description.Length);
261 | int sep = -1;
262 | for (int i = start; i < end; ++i) {
263 | if (i + 2 <= description.Length && description.Substring(i, 2).Equals("\r\n"))
264 | return i + 2;
265 | if (description[i] == '\n')
266 | return i + 1;
267 | if (IsEolChar(description[i]))
268 | sep = i + 1;
269 | }
270 | if (sep == -1 || end == description.Length)
271 | return end;
272 | return sep;
273 | }
274 | }
275 |
276 | public class OptionValueCollection : IList, IList {
277 |
278 | List values = new List();
279 | OptionContext c;
280 |
281 | internal OptionValueCollection(OptionContext c) {
282 | this.c = c;
283 | }
284 |
285 | #region ICollection
286 | void ICollection.CopyTo(Array array, int index) { (values as ICollection).CopyTo(array, index); }
287 | bool ICollection.IsSynchronized { get { return (values as ICollection).IsSynchronized; } }
288 | object ICollection.SyncRoot { get { return (values as ICollection).SyncRoot; } }
289 | #endregion
290 |
291 | #region ICollection
292 | public void Add(string item) { values.Add(item); }
293 | public void Clear() { values.Clear(); }
294 | public bool Contains(string item) { return values.Contains(item); }
295 | public void CopyTo(string[] array, int arrayIndex) { values.CopyTo(array, arrayIndex); }
296 | public bool Remove(string item) { return values.Remove(item); }
297 | public int Count { get { return values.Count; } }
298 | public bool IsReadOnly { get { return false; } }
299 | #endregion
300 |
301 | #region IEnumerable
302 | IEnumerator IEnumerable.GetEnumerator() { return values.GetEnumerator(); }
303 | #endregion
304 |
305 | #region IEnumerable
306 | public IEnumerator GetEnumerator() { return values.GetEnumerator(); }
307 | #endregion
308 |
309 | #region IList
310 | int IList.Add(object value) { return (values as IList).Add(value); }
311 | bool IList.Contains(object value) { return (values as IList).Contains(value); }
312 | int IList.IndexOf(object value) { return (values as IList).IndexOf(value); }
313 | void IList.Insert(int index, object value) { (values as IList).Insert(index, value); }
314 | void IList.Remove(object value) { (values as IList).Remove(value); }
315 | void IList.RemoveAt(int index) { (values as IList).RemoveAt(index); }
316 | bool IList.IsFixedSize { get { return false; } }
317 | object IList.this[int index] { get { return this[index]; } set { (values as IList)[index] = value; } }
318 | #endregion
319 |
320 | #region IList
321 | public int IndexOf(string item) { return values.IndexOf(item); }
322 | public void Insert(int index, string item) { values.Insert(index, item); }
323 | public void RemoveAt(int index) { values.RemoveAt(index); }
324 |
325 | private void AssertValid(int index) {
326 | if (c.Option == null)
327 | throw new InvalidOperationException("OptionContext.Option is null.");
328 | if (index >= c.Option.MaxValueCount)
329 | throw new ArgumentOutOfRangeException("index");
330 | if (c.Option.OptionValueType == OptionValueType.Required &&
331 | index >= values.Count)
332 | throw new OptionException(string.Format(
333 | c.OptionSet.MessageLocalizer("Missing required value for option '{0}'."), c.OptionName),
334 | c.OptionName);
335 | }
336 |
337 | public string this[int index] {
338 | get {
339 | AssertValid(index);
340 | return index >= values.Count ? null : values[index];
341 | }
342 | set {
343 | values[index] = value;
344 | }
345 | }
346 | #endregion
347 |
348 | public List ToList() {
349 | return new List(values);
350 | }
351 |
352 | public string[] ToArray() {
353 | return values.ToArray();
354 | }
355 |
356 | public override string ToString() {
357 | return string.Join(", ", values.ToArray());
358 | }
359 | }
360 |
361 | public class OptionContext {
362 | private Option option;
363 | private string name;
364 | private int index;
365 | private OptionSet set;
366 | private OptionValueCollection c;
367 |
368 | public OptionContext(OptionSet set) {
369 | this.set = set;
370 | this.c = new OptionValueCollection(this);
371 | }
372 |
373 | public Option Option {
374 | get { return option; }
375 | set { option = value; }
376 | }
377 |
378 | public string OptionName {
379 | get { return name; }
380 | set { name = value; }
381 | }
382 |
383 | public int OptionIndex {
384 | get { return index; }
385 | set { index = value; }
386 | }
387 |
388 | public OptionSet OptionSet {
389 | get { return set; }
390 | }
391 |
392 | public OptionValueCollection OptionValues {
393 | get { return c; }
394 | }
395 | }
396 |
397 | public enum OptionValueType {
398 | None,
399 | Optional,
400 | Required,
401 | }
402 |
403 | public abstract class Option {
404 | string prototype, description;
405 | string[] names;
406 | OptionValueType type;
407 | int count;
408 | string[] separators;
409 | bool hidden;
410 |
411 | protected Option(string prototype, string description)
412 | : this(prototype, description, 1, false) {
413 | }
414 |
415 | protected Option(string prototype, string description, int maxValueCount)
416 | : this(prototype, description, maxValueCount, false) {
417 | }
418 |
419 | protected Option(string prototype, string description, int maxValueCount, bool hidden) {
420 | if (prototype == null)
421 | throw new ArgumentNullException("prototype");
422 | if (prototype.Length == 0)
423 | throw new ArgumentException("Cannot be the empty string.", "prototype");
424 | if (maxValueCount < 0)
425 | throw new ArgumentOutOfRangeException("maxValueCount");
426 |
427 | this.prototype = prototype;
428 | this.description = description;
429 | this.count = maxValueCount;
430 | this.names = (this is OptionSet.Category)
431 | // append GetHashCode() so that "duplicate" categories have distinct
432 | // names, e.g. adding multiple "" categories should be valid.
433 | ? new[] { prototype + this.GetHashCode() }
434 | : prototype.Split('|');
435 |
436 | if (this is OptionSet.Category || this is CommandOption)
437 | return;
438 |
439 | this.type = ParsePrototype();
440 | this.hidden = hidden;
441 |
442 | if (this.count == 0 && type != OptionValueType.None)
443 | throw new ArgumentException(
444 | "Cannot provide maxValueCount of 0 for OptionValueType.Required or " +
445 | "OptionValueType.Optional.",
446 | "maxValueCount");
447 | if (this.type == OptionValueType.None && maxValueCount > 1)
448 | throw new ArgumentException(
449 | string.Format("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount),
450 | "maxValueCount");
451 | if (Array.IndexOf(names, "<>") >= 0 &&
452 | ((names.Length == 1 && this.type != OptionValueType.None) ||
453 | (names.Length > 1 && this.MaxValueCount > 1)))
454 | throw new ArgumentException(
455 | "The default option handler '<>' cannot require values.",
456 | "prototype");
457 | }
458 |
459 | public string Prototype { get { return prototype; } }
460 | public string Description { get { return description; } }
461 | public OptionValueType OptionValueType { get { return type; } }
462 | public int MaxValueCount { get { return count; } }
463 | public bool Hidden { get { return hidden; } }
464 |
465 | public string[] GetNames() {
466 | return (string[])names.Clone();
467 | }
468 |
469 | public string[] GetValueSeparators() {
470 | if (separators == null)
471 | return new string[0];
472 | return (string[])separators.Clone();
473 | }
474 |
475 | protected static T Parse(string value, OptionContext c) {
476 | Type tt = typeof(T);
477 | #if PCL
478 | TypeInfo ti = tt.GetTypeInfo ();
479 | #else
480 | Type ti = tt;
481 | #endif
482 | bool nullable =
483 | ti.IsValueType &&
484 | ti.IsGenericType &&
485 | !ti.IsGenericTypeDefinition &&
486 | ti.GetGenericTypeDefinition() == typeof(Nullable<>);
487 | #if PCL
488 | Type targetType = nullable ? tt.GenericTypeArguments [0] : tt;
489 | #else
490 | Type targetType = nullable ? tt.GetGenericArguments()[0] : tt;
491 | #endif
492 | T t = default(T);
493 | try {
494 | if (value != null) {
495 | #if PCL
496 | if (targetType.GetTypeInfo ().IsEnum)
497 | t = (T) Enum.Parse (targetType, value, true);
498 | else
499 | t = (T) Convert.ChangeType (value, targetType);
500 | #else
501 | TypeConverter conv = TypeDescriptor.GetConverter(targetType);
502 | t = (T)conv.ConvertFromString(value);
503 | #endif
504 | }
505 | } catch (Exception e) {
506 | throw new OptionException(
507 | string.Format(
508 | c.OptionSet.MessageLocalizer("Could not convert string `{0}' to type {1} for option `{2}'."),
509 | value, targetType.Name, c.OptionName),
510 | c.OptionName, e);
511 | }
512 | return t;
513 | }
514 |
515 | internal string[] Names { get { return names; } }
516 | internal string[] ValueSeparators { get { return separators; } }
517 |
518 | static readonly char[] NameTerminator = new char[] { '=', ':' };
519 |
520 | private OptionValueType ParsePrototype() {
521 | char type = '\0';
522 | List seps = new List();
523 | for (int i = 0; i < names.Length; ++i) {
524 | string name = names[i];
525 | if (name.Length == 0)
526 | throw new ArgumentException("Empty option names are not supported.", "prototype");
527 |
528 | int end = name.IndexOfAny(NameTerminator);
529 | if (end == -1)
530 | continue;
531 | names[i] = name.Substring(0, end);
532 | if (type == '\0' || type == name[end])
533 | type = name[end];
534 | else
535 | throw new ArgumentException(
536 | string.Format("Conflicting option types: '{0}' vs. '{1}'.", type, name[end]),
537 | "prototype");
538 | AddSeparators(name, end, seps);
539 | }
540 |
541 | if (type == '\0')
542 | return OptionValueType.None;
543 |
544 | if (count <= 1 && seps.Count != 0)
545 | throw new ArgumentException(
546 | string.Format("Cannot provide key/value separators for Options taking {0} value(s).", count),
547 | "prototype");
548 | if (count > 1) {
549 | if (seps.Count == 0)
550 | this.separators = new string[] { ":", "=" };
551 | else if (seps.Count == 1 && seps[0].Length == 0)
552 | this.separators = null;
553 | else
554 | this.separators = seps.ToArray();
555 | }
556 |
557 | return type == '=' ? OptionValueType.Required : OptionValueType.Optional;
558 | }
559 |
560 | private static void AddSeparators(string name, int end, ICollection seps) {
561 | int start = -1;
562 | for (int i = end + 1; i < name.Length; ++i) {
563 | switch (name[i]) {
564 | case '{':
565 | if (start != -1)
566 | throw new ArgumentException(
567 | string.Format("Ill-formed name/value separator found in \"{0}\".", name),
568 | "prototype");
569 | start = i + 1;
570 | break;
571 | case '}':
572 | if (start == -1)
573 | throw new ArgumentException(
574 | string.Format("Ill-formed name/value separator found in \"{0}\".", name),
575 | "prototype");
576 | seps.Add(name.Substring(start, i - start));
577 | start = -1;
578 | break;
579 | default:
580 | if (start == -1)
581 | seps.Add(name[i].ToString());
582 | break;
583 | }
584 | }
585 | if (start != -1)
586 | throw new ArgumentException(
587 | string.Format("Ill-formed name/value separator found in \"{0}\".", name),
588 | "prototype");
589 | }
590 |
591 | public void Invoke(OptionContext c) {
592 | OnParseComplete(c);
593 | c.OptionName = null;
594 | c.Option = null;
595 | c.OptionValues.Clear();
596 | }
597 |
598 | protected abstract void OnParseComplete(OptionContext c);
599 |
600 | internal void InvokeOnParseComplete(OptionContext c) {
601 | OnParseComplete(c);
602 | }
603 |
604 | public override string ToString() {
605 | return Prototype;
606 | }
607 | }
608 |
609 | public abstract class ArgumentSource {
610 |
611 | protected ArgumentSource() {
612 | }
613 |
614 | public abstract string[] GetNames();
615 | public abstract string Description { get; }
616 | public abstract bool GetArguments(string value, out IEnumerable replacement);
617 |
618 | #if !PCL || NETSTANDARD1_3
619 | public static IEnumerable GetArgumentsFromFile(string file) {
620 | return GetArguments(File.OpenText(file), true);
621 | }
622 | #endif
623 |
624 | public static IEnumerable GetArguments(TextReader reader) {
625 | return GetArguments(reader, false);
626 | }
627 |
628 | // Cribbed from mcs/driver.cs:LoadArgs(string)
629 | static IEnumerable GetArguments(TextReader reader, bool close) {
630 | try {
631 | StringBuilder arg = new StringBuilder();
632 |
633 | string line;
634 | while ((line = reader.ReadLine()) != null) {
635 | int t = line.Length;
636 |
637 | for (int i = 0; i < t; i++) {
638 | char c = line[i];
639 |
640 | if (c == '"' || c == '\'') {
641 | char end = c;
642 |
643 | for (i++; i < t; i++) {
644 | c = line[i];
645 |
646 | if (c == end)
647 | break;
648 | arg.Append(c);
649 | }
650 | } else if (c == ' ') {
651 | if (arg.Length > 0) {
652 | yield return arg.ToString();
653 | arg.Length = 0;
654 | }
655 | } else
656 | arg.Append(c);
657 | }
658 | if (arg.Length > 0) {
659 | yield return arg.ToString();
660 | arg.Length = 0;
661 | }
662 | }
663 | } finally {
664 | if (close)
665 | reader.Dispose();
666 | }
667 | }
668 | }
669 |
670 | #if !PCL || NETSTANDARD1_3
671 | internal class ResponseFileSource : ArgumentSource {
672 |
673 | public override string[] GetNames() {
674 | return new string[] { "@file" };
675 | }
676 |
677 | public override string Description {
678 | get { return "Read response file for more options."; }
679 | }
680 |
681 | public override bool GetArguments(string value, out IEnumerable replacement) {
682 | if (string.IsNullOrEmpty(value) || !value.StartsWith("@")) {
683 | replacement = null;
684 | return false;
685 | }
686 | replacement = ArgumentSource.GetArgumentsFromFile(value.Substring(1));
687 | return true;
688 | }
689 | }
690 | #endif
691 |
692 | #if !PCL
693 | [Serializable]
694 | #endif
695 | internal class OptionException : Exception {
696 | private string option;
697 |
698 | public OptionException() {
699 | }
700 |
701 | public OptionException(string message, string optionName)
702 | : base(message) {
703 | this.option = optionName;
704 | }
705 |
706 | public OptionException(string message, string optionName, Exception innerException)
707 | : base(message, innerException) {
708 | this.option = optionName;
709 | }
710 |
711 | #if !PCL
712 | protected OptionException(SerializationInfo info, StreamingContext context)
713 | : base(info, context) {
714 | this.option = info.GetString("OptionName");
715 | }
716 | #endif
717 |
718 | public string OptionName {
719 | get { return this.option; }
720 | }
721 |
722 | #if !PCL
723 | #pragma warning disable 618 // SecurityPermissionAttribute is obsolete
724 | [SecurityPermission(SecurityAction.LinkDemand, SerializationFormatter = true)]
725 | #pragma warning restore 618
726 | public override void GetObjectData(SerializationInfo info, StreamingContext context) {
727 | base.GetObjectData(info, context);
728 | info.AddValue("OptionName", option);
729 | }
730 | #endif
731 | }
732 |
733 | public delegate void OptionAction(TKey key, TValue value);
734 |
735 | public class OptionSet : KeyedCollection {
736 | public OptionSet()
737 | : this(null) {
738 | }
739 |
740 | public OptionSet(MessageLocalizerConverter localizer) {
741 | this.roSources = new ReadOnlyCollection(sources);
742 | this.localizer = localizer;
743 | if (this.localizer == null) {
744 | this.localizer = delegate (string f) {
745 | return f;
746 | };
747 | }
748 | }
749 |
750 | MessageLocalizerConverter localizer;
751 |
752 | public MessageLocalizerConverter MessageLocalizer {
753 | get { return localizer; }
754 | internal set { localizer = value; }
755 | }
756 |
757 | List sources = new List();
758 | ReadOnlyCollection roSources;
759 |
760 | public ReadOnlyCollection ArgumentSources {
761 | get { return roSources; }
762 | }
763 |
764 |
765 | protected override string GetKeyForItem(Option item) {
766 | if (item == null)
767 | throw new ArgumentNullException("option");
768 | if (item.Names != null && item.Names.Length > 0)
769 | return item.Names[0];
770 | // This should never happen, as it's invalid for Option to be
771 | // constructed w/o any names.
772 | throw new InvalidOperationException("Option has no names!");
773 | }
774 |
775 | [Obsolete("Use KeyedCollection.this[string]")]
776 | protected Option GetOptionForName(string option) {
777 | if (option == null)
778 | throw new ArgumentNullException("option");
779 | try {
780 | return base[option];
781 | } catch (KeyNotFoundException) {
782 | return null;
783 | }
784 | }
785 |
786 | protected override void InsertItem(int index, Option item) {
787 | base.InsertItem(index, item);
788 | AddImpl(item);
789 | }
790 |
791 | protected override void RemoveItem(int index) {
792 | Option p = Items[index];
793 | base.RemoveItem(index);
794 | // KeyedCollection.RemoveItem() handles the 0th item
795 | for (int i = 1; i < p.Names.Length; ++i) {
796 | Dictionary.Remove(p.Names[i]);
797 | }
798 | }
799 |
800 | protected override void SetItem(int index, Option item) {
801 | base.SetItem(index, item);
802 | AddImpl(item);
803 | }
804 |
805 | private void AddImpl(Option option) {
806 | if (option == null)
807 | throw new ArgumentNullException("option");
808 | List added = new List(option.Names.Length);
809 | try {
810 | // KeyedCollection.InsertItem/SetItem handle the 0th name.
811 | for (int i = 1; i < option.Names.Length; ++i) {
812 | Dictionary.Add(option.Names[i], option);
813 | added.Add(option.Names[i]);
814 | }
815 | } catch (Exception) {
816 | foreach (string name in added)
817 | Dictionary.Remove(name);
818 | throw;
819 | }
820 | }
821 |
822 | public OptionSet Add(string header) {
823 | if (header == null)
824 | throw new ArgumentNullException("header");
825 | Add(new Category(header));
826 | return this;
827 | }
828 |
829 | internal sealed class Category : Option {
830 |
831 | // Prototype starts with '=' because this is an invalid prototype
832 | // (see Option.ParsePrototype(), and thus it'll prevent Category
833 | // instances from being accidentally used as normal options.
834 | public Category(string description)
835 | : base("=:Category:= " + description, description) {
836 | }
837 |
838 | protected override void OnParseComplete(OptionContext c) {
839 | throw new NotSupportedException("Category.OnParseComplete should not be invoked.");
840 | }
841 | }
842 |
843 |
844 | public new OptionSet Add(Option option) {
845 | base.Add(option);
846 | return this;
847 | }
848 |
849 | sealed class ActionOption : Option {
850 | Action action;
851 |
852 | public ActionOption(string prototype, string description, int count, Action action)
853 | : this(prototype, description, count, action, false) {
854 | }
855 |
856 | public ActionOption(string prototype, string description, int count, Action action, bool hidden)
857 | : base(prototype, description, count, hidden) {
858 | if (action == null)
859 | throw new ArgumentNullException("action");
860 | this.action = action;
861 | }
862 |
863 | protected override void OnParseComplete(OptionContext c) {
864 | action(c.OptionValues);
865 | }
866 | }
867 |
868 | public OptionSet Add(string prototype, Action action) {
869 | return Add(prototype, null, action);
870 | }
871 |
872 | public OptionSet Add(string prototype, string description, Action action) {
873 | return Add(prototype, description, action, false);
874 | }
875 |
876 | public OptionSet Add(string prototype, string description, Action action, bool hidden) {
877 | if (action == null)
878 | throw new ArgumentNullException("action");
879 | Option p = new ActionOption(prototype, description, 1,
880 | delegate (OptionValueCollection v) { action(v[0]); }, hidden);
881 | base.Add(p);
882 | return this;
883 | }
884 |
885 | public OptionSet Add(string prototype, OptionAction action) {
886 | return Add(prototype, null, action);
887 | }
888 |
889 | public OptionSet Add(string prototype, string description, OptionAction action) {
890 | return Add(prototype, description, action, false);
891 | }
892 |
893 | public OptionSet Add(string prototype, string description, OptionAction action, bool hidden) {
894 | if (action == null)
895 | throw new ArgumentNullException("action");
896 | Option p = new ActionOption(prototype, description, 2,
897 | delegate (OptionValueCollection v) { action(v[0], v[1]); }, hidden);
898 | base.Add(p);
899 | return this;
900 | }
901 |
902 | sealed class ActionOption : Option {
903 | Action action;
904 |
905 | public ActionOption(string prototype, string description, Action action)
906 | : base(prototype, description, 1) {
907 | if (action == null)
908 | throw new ArgumentNullException("action");
909 | this.action = action;
910 | }
911 |
912 | protected override void OnParseComplete(OptionContext c) {
913 | action(Parse(c.OptionValues[0], c));
914 | }
915 | }
916 |
917 | sealed class ActionOption : Option {
918 | OptionAction action;
919 |
920 | public ActionOption(string prototype, string description, OptionAction action)
921 | : base(prototype, description, 2) {
922 | if (action == null)
923 | throw new ArgumentNullException("action");
924 | this.action = action;
925 | }
926 |
927 | protected override void OnParseComplete(OptionContext c) {
928 | action(
929 | Parse(c.OptionValues[0], c),
930 | Parse(c.OptionValues[1], c));
931 | }
932 | }
933 |
934 | public OptionSet Add(string prototype, Action action) {
935 | return Add(prototype, null, action);
936 | }
937 |
938 | public OptionSet Add(string prototype, string description, Action action) {
939 | return Add(new ActionOption(prototype, description, action));
940 | }
941 |
942 | public OptionSet Add(string prototype, OptionAction action) {
943 | return Add(prototype, null, action);
944 | }
945 |
946 | public OptionSet Add(string prototype, string description, OptionAction action) {
947 | return Add(new ActionOption(prototype, description, action));
948 | }
949 |
950 | public OptionSet Add(ArgumentSource source) {
951 | if (source == null)
952 | throw new ArgumentNullException("source");
953 | sources.Add(source);
954 | return this;
955 | }
956 |
957 | protected virtual OptionContext CreateOptionContext() {
958 | return new OptionContext(this);
959 | }
960 |
961 | public List Parse(IEnumerable arguments) {
962 | if (arguments == null)
963 | throw new ArgumentNullException("arguments");
964 | OptionContext c = CreateOptionContext();
965 | c.OptionIndex = -1;
966 | bool process = true;
967 | List unprocessed = new List();
968 | Option def = Contains("<>") ? this["<>"] : null;
969 | ArgumentEnumerator ae = new ArgumentEnumerator(arguments);
970 | foreach (string argument in ae) {
971 | ++c.OptionIndex;
972 | if (argument == "--") {
973 | process = false;
974 | continue;
975 | }
976 | if (!process) {
977 | Unprocessed(unprocessed, def, c, argument);
978 | continue;
979 | }
980 | if (AddSource(ae, argument))
981 | continue;
982 | if (!Parse(argument, c))
983 | Unprocessed(unprocessed, def, c, argument);
984 | }
985 | if (c.Option != null)
986 | c.Option.Invoke(c);
987 | return unprocessed;
988 | }
989 |
990 | class ArgumentEnumerator : IEnumerable {
991 | List> sources = new List>();
992 |
993 | public ArgumentEnumerator(IEnumerable arguments) {
994 | sources.Add(arguments.GetEnumerator());
995 | }
996 |
997 | public void Add(IEnumerable arguments) {
998 | sources.Add(arguments.GetEnumerator());
999 | }
1000 |
1001 | public IEnumerator GetEnumerator() {
1002 | do {
1003 | IEnumerator c = sources[sources.Count - 1];
1004 | if (c.MoveNext())
1005 | yield return c.Current;
1006 | else {
1007 | c.Dispose();
1008 | sources.RemoveAt(sources.Count - 1);
1009 | }
1010 | } while (sources.Count > 0);
1011 | }
1012 |
1013 | IEnumerator IEnumerable.GetEnumerator() {
1014 | return GetEnumerator();
1015 | }
1016 | }
1017 |
1018 | bool AddSource(ArgumentEnumerator ae, string argument) {
1019 | foreach (ArgumentSource source in sources) {
1020 | IEnumerable replacement;
1021 | if (!source.GetArguments(argument, out replacement))
1022 | continue;
1023 | ae.Add(replacement);
1024 | return true;
1025 | }
1026 | return false;
1027 | }
1028 |
1029 | private static bool Unprocessed(ICollection extra, Option def, OptionContext c, string argument) {
1030 | if (def == null) {
1031 | extra.Add(argument);
1032 | return false;
1033 | }
1034 | c.OptionValues.Add(argument);
1035 | c.Option = def;
1036 | c.Option.Invoke(c);
1037 | return false;
1038 | }
1039 |
1040 | private readonly Regex ValueOption = new Regex(
1041 | @"^(?--|-|/)(?[^:=]+)((?[:=])(?.*))?$");
1042 |
1043 | protected bool GetOptionParts(string argument, out string flag, out string name, out string sep, out string value) {
1044 | if (argument == null)
1045 | throw new ArgumentNullException("argument");
1046 |
1047 | flag = name = sep = value = null;
1048 | Match m = ValueOption.Match(argument);
1049 | if (!m.Success) {
1050 | return false;
1051 | }
1052 | flag = m.Groups["flag"].Value;
1053 | name = m.Groups["name"].Value;
1054 | if (m.Groups["sep"].Success && m.Groups["value"].Success) {
1055 | sep = m.Groups["sep"].Value;
1056 | value = m.Groups["value"].Value;
1057 | }
1058 | return true;
1059 | }
1060 |
1061 | protected virtual bool Parse(string argument, OptionContext c) {
1062 | if (c.Option != null) {
1063 | ParseValue(argument, c);
1064 | return true;
1065 | }
1066 |
1067 | string f, n, s, v;
1068 | if (!GetOptionParts(argument, out f, out n, out s, out v))
1069 | return false;
1070 |
1071 | Option p;
1072 | if (Contains(n)) {
1073 | p = this[n];
1074 | c.OptionName = f + n;
1075 | c.Option = p;
1076 | switch (p.OptionValueType) {
1077 | case OptionValueType.None:
1078 | c.OptionValues.Add(n);
1079 | c.Option.Invoke(c);
1080 | break;
1081 | case OptionValueType.Optional:
1082 | case OptionValueType.Required:
1083 | ParseValue(v, c);
1084 | break;
1085 | }
1086 | return true;
1087 | }
1088 | // no match; is it a bool option?
1089 | if (ParseBool(argument, n, c))
1090 | return true;
1091 | // is it a bundled option?
1092 | if (ParseBundledValue(f, string.Concat(n + s + v), c))
1093 | return true;
1094 |
1095 | return false;
1096 | }
1097 |
1098 | private void ParseValue(string option, OptionContext c) {
1099 | if (option != null)
1100 | foreach (string o in c.Option.ValueSeparators != null
1101 | ? option.Split(c.Option.ValueSeparators, c.Option.MaxValueCount - c.OptionValues.Count, StringSplitOptions.None)
1102 | : new string[] { option }) {
1103 | c.OptionValues.Add(o);
1104 | }
1105 | if (c.OptionValues.Count == c.Option.MaxValueCount ||
1106 | c.Option.OptionValueType == OptionValueType.Optional)
1107 | c.Option.Invoke(c);
1108 | else if (c.OptionValues.Count > c.Option.MaxValueCount) {
1109 | throw new OptionException(localizer(string.Format(
1110 | "Error: Found {0} option values when expecting {1}.",
1111 | c.OptionValues.Count, c.Option.MaxValueCount)),
1112 | c.OptionName);
1113 | }
1114 | }
1115 |
1116 | private bool ParseBool(string option, string n, OptionContext c) {
1117 | Option p;
1118 | string rn;
1119 | if (n.Length >= 1 && (n[n.Length - 1] == '+' || n[n.Length - 1] == '-') &&
1120 | Contains((rn = n.Substring(0, n.Length - 1)))) {
1121 | p = this[rn];
1122 | string v = n[n.Length - 1] == '+' ? option : null;
1123 | c.OptionName = option;
1124 | c.Option = p;
1125 | c.OptionValues.Add(v);
1126 | p.Invoke(c);
1127 | return true;
1128 | }
1129 | return false;
1130 | }
1131 |
1132 | private bool ParseBundledValue(string f, string n, OptionContext c) {
1133 | if (f != "-")
1134 | return false;
1135 | for (int i = 0; i < n.Length; ++i) {
1136 | Option p;
1137 | string opt = f + n[i].ToString();
1138 | string rn = n[i].ToString();
1139 | if (!Contains(rn)) {
1140 | if (i == 0)
1141 | return false;
1142 | throw new OptionException(string.Format(localizer(
1143 | "Cannot use unregistered option '{0}' in bundle '{1}'."), rn, f + n), null);
1144 | }
1145 | p = this[rn];
1146 | switch (p.OptionValueType) {
1147 | case OptionValueType.None:
1148 | Invoke(c, opt, n, p);
1149 | break;
1150 | case OptionValueType.Optional:
1151 | case OptionValueType.Required: {
1152 | string v = n.Substring(i + 1);
1153 | c.Option = p;
1154 | c.OptionName = opt;
1155 | ParseValue(v.Length != 0 ? v : null, c);
1156 | return true;
1157 | }
1158 | default:
1159 | throw new InvalidOperationException("Unknown OptionValueType: " + p.OptionValueType);
1160 | }
1161 | }
1162 | return true;
1163 | }
1164 |
1165 | private static void Invoke(OptionContext c, string name, string value, Option option) {
1166 | c.OptionName = name;
1167 | c.Option = option;
1168 | c.OptionValues.Add(value);
1169 | option.Invoke(c);
1170 | }
1171 |
1172 | private const int OptionWidth = 29;
1173 | private const int Description_FirstWidth = 80 - OptionWidth;
1174 | private const int Description_RemWidth = 80 - OptionWidth - 2;
1175 |
1176 | static readonly string CommandHelpIndentStart = new string(' ', OptionWidth);
1177 | static readonly string CommandHelpIndentRemaining = new string(' ', OptionWidth + 2);
1178 |
1179 | public void WriteOptionDescriptions(TextWriter o) {
1180 | foreach (Option p in this) {
1181 | int written = 0;
1182 |
1183 | if (p.Hidden)
1184 | continue;
1185 |
1186 | Category c = p as Category;
1187 | if (c != null) {
1188 | WriteDescription(o, p.Description, "", 80, 80);
1189 | continue;
1190 | }
1191 | CommandOption co = p as CommandOption;
1192 | if (co != null) {
1193 | WriteCommandDescription(o, co.Command, co.CommandName);
1194 | continue;
1195 | }
1196 |
1197 | if (!WriteOptionPrototype(o, p, ref written))
1198 | continue;
1199 |
1200 | if (written < OptionWidth)
1201 | o.Write(new string(' ', OptionWidth - written));
1202 | else {
1203 | o.WriteLine();
1204 | o.Write(new string(' ', OptionWidth));
1205 | }
1206 |
1207 | WriteDescription(o, p.Description, new string(' ', OptionWidth + 2),
1208 | Description_FirstWidth, Description_RemWidth);
1209 | }
1210 |
1211 | foreach (ArgumentSource s in sources) {
1212 | string[] names = s.GetNames();
1213 | if (names == null || names.Length == 0)
1214 | continue;
1215 |
1216 | int written = 0;
1217 |
1218 | Write(o, ref written, " ");
1219 | Write(o, ref written, names[0]);
1220 | for (int i = 1; i < names.Length; ++i) {
1221 | Write(o, ref written, ", ");
1222 | Write(o, ref written, names[i]);
1223 | }
1224 |
1225 | if (written < OptionWidth)
1226 | o.Write(new string(' ', OptionWidth - written));
1227 | else {
1228 | o.WriteLine();
1229 | o.Write(new string(' ', OptionWidth));
1230 | }
1231 |
1232 | WriteDescription(o, s.Description, new string(' ', OptionWidth + 2),
1233 | Description_FirstWidth, Description_RemWidth);
1234 | }
1235 | }
1236 |
1237 | internal void WriteCommandDescription(TextWriter o, Command c, string commandName) {
1238 | var name = new string(' ', 8) + (commandName ?? c.Name);
1239 | if (name.Length < OptionWidth - 1) {
1240 | WriteDescription(o, name + new string(' ', OptionWidth - name.Length) + c.Help, CommandHelpIndentRemaining, 80, Description_RemWidth);
1241 | } else {
1242 | WriteDescription(o, name, "", 80, 80);
1243 | WriteDescription(o, CommandHelpIndentStart + c.Help, CommandHelpIndentRemaining, 80, Description_RemWidth);
1244 | }
1245 | }
1246 |
1247 | void WriteDescription(TextWriter o, string value, string prefix, int firstWidth, int remWidth) {
1248 | bool indent = false;
1249 | foreach (string line in GetLines(localizer(GetDescription(value)), firstWidth, remWidth)) {
1250 | if (indent)
1251 | o.Write(prefix);
1252 | o.WriteLine(line);
1253 | indent = true;
1254 | }
1255 | }
1256 |
1257 | bool WriteOptionPrototype(TextWriter o, Option p, ref int written) {
1258 | string[] names = p.Names;
1259 |
1260 | int i = GetNextOptionIndex(names, 0);
1261 | if (i == names.Length)
1262 | return false;
1263 |
1264 | if (names[i].Length == 1) {
1265 | Write(o, ref written, " -");
1266 | Write(o, ref written, names[0]);
1267 | } else {
1268 | Write(o, ref written, " --");
1269 | Write(o, ref written, names[0]);
1270 | }
1271 |
1272 | for (i = GetNextOptionIndex(names, i + 1);
1273 | i < names.Length; i = GetNextOptionIndex(names, i + 1)) {
1274 | Write(o, ref written, ", ");
1275 | Write(o, ref written, names[i].Length == 1 ? "-" : "--");
1276 | Write(o, ref written, names[i]);
1277 | }
1278 |
1279 | if (p.OptionValueType == OptionValueType.Optional ||
1280 | p.OptionValueType == OptionValueType.Required) {
1281 | if (p.OptionValueType == OptionValueType.Optional) {
1282 | Write(o, ref written, localizer("["));
1283 | }
1284 | Write(o, ref written, localizer("=" + GetArgumentName(0, p.MaxValueCount, p.Description)));
1285 | string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0
1286 | ? p.ValueSeparators[0]
1287 | : " ";
1288 | for (int c = 1; c < p.MaxValueCount; ++c) {
1289 | Write(o, ref written, localizer(sep + GetArgumentName(c, p.MaxValueCount, p.Description)));
1290 | }
1291 | if (p.OptionValueType == OptionValueType.Optional) {
1292 | Write(o, ref written, localizer("]"));
1293 | }
1294 | }
1295 | return true;
1296 | }
1297 |
1298 | static int GetNextOptionIndex(string[] names, int i) {
1299 | while (i < names.Length && names[i] == "<>") {
1300 | ++i;
1301 | }
1302 | return i;
1303 | }
1304 |
1305 | static void Write(TextWriter o, ref int n, string s) {
1306 | n += s.Length;
1307 | o.Write(s);
1308 | }
1309 |
1310 | static string GetArgumentName(int index, int maxIndex, string description) {
1311 | var matches = Regex.Matches(description ?? "", @"(?<=(? 1
1320 | if (maxIndex > 1 && parts.Length == 2 &&
1321 | parts[0] == index.ToString(CultureInfo.InvariantCulture)) {
1322 | argName = parts[1];
1323 | }
1324 | }
1325 |
1326 | if (string.IsNullOrEmpty(argName)) {
1327 | argName = maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1);
1328 | }
1329 | return argName;
1330 | }
1331 |
1332 | private static string GetDescription(string description) {
1333 | if (description == null)
1334 | return string.Empty;
1335 | StringBuilder sb = new StringBuilder(description.Length);
1336 | int start = -1;
1337 | for (int i = 0; i < description.Length; ++i) {
1338 | switch (description[i]) {
1339 | case '{':
1340 | if (i == start) {
1341 | sb.Append('{');
1342 | start = -1;
1343 | } else if (start < 0)
1344 | start = i + 1;
1345 | break;
1346 | case '}':
1347 | if (start < 0) {
1348 | if ((i + 1) == description.Length || description[i + 1] != '}')
1349 | throw new InvalidOperationException("Invalid option description: " + description);
1350 | ++i;
1351 | sb.Append("}");
1352 | } else {
1353 | sb.Append(description.Substring(start, i - start));
1354 | start = -1;
1355 | }
1356 | break;
1357 | case ':':
1358 | if (start < 0)
1359 | goto default;
1360 | start = i + 1;
1361 | break;
1362 | default:
1363 | if (start < 0)
1364 | sb.Append(description[i]);
1365 | break;
1366 | }
1367 | }
1368 | return sb.ToString();
1369 | }
1370 |
1371 | private static IEnumerable GetLines(string description, int firstWidth, int remWidth) {
1372 | return StringCoda.WrappedLines(description, firstWidth, remWidth);
1373 | }
1374 | }
1375 |
1376 | internal class Command {
1377 | public string Name { get; }
1378 | public string Help { get; }
1379 |
1380 | public OptionSet Options { get; set; }
1381 | public Action> Run { get; set; }
1382 |
1383 | public CommandSet CommandSet { get; internal set; }
1384 |
1385 | public Command(string name, string help = null) {
1386 | if (string.IsNullOrEmpty(name))
1387 | throw new ArgumentNullException(nameof(name));
1388 |
1389 | Name = NormalizeCommandName(name);
1390 | Help = help;
1391 | }
1392 |
1393 | static string NormalizeCommandName(string name) {
1394 | var value = new StringBuilder(name.Length);
1395 | var space = false;
1396 | for (int i = 0; i < name.Length; ++i) {
1397 | if (!char.IsWhiteSpace(name, i)) {
1398 | space = false;
1399 | value.Append(name[i]);
1400 | } else if (!space) {
1401 | space = true;
1402 | value.Append(' ');
1403 | }
1404 | }
1405 | return value.ToString();
1406 | }
1407 |
1408 | public virtual int Invoke(IEnumerable arguments) {
1409 | var rest = Options?.Parse(arguments) ?? arguments;
1410 | Run?.Invoke(rest);
1411 | return 0;
1412 | }
1413 | }
1414 |
1415 | class CommandOption : Option {
1416 | public Command Command { get; }
1417 | public string CommandName { get; }
1418 |
1419 | // Prototype starts with '=' because this is an invalid prototype
1420 | // (see Option.ParsePrototype(), and thus it'll prevent Category
1421 | // instances from being accidentally used as normal options.
1422 | public CommandOption(Command command, string commandName = null, bool hidden = false)
1423 | : base("=:Command:= " + (commandName ?? command?.Name), (commandName ?? command?.Name), maxValueCount: 0, hidden: hidden) {
1424 | if (command == null)
1425 | throw new ArgumentNullException(nameof(command));
1426 | Command = command;
1427 | CommandName = commandName ?? command.Name;
1428 | }
1429 |
1430 | protected override void OnParseComplete(OptionContext c) {
1431 | throw new NotSupportedException("CommandOption.OnParseComplete should not be invoked.");
1432 | }
1433 | }
1434 |
1435 | class HelpOption : Option {
1436 | Option option;
1437 | CommandSet commands;
1438 |
1439 | public HelpOption(CommandSet commands, Option d)
1440 | : base(d.Prototype, d.Description, d.MaxValueCount, d.Hidden) {
1441 | this.commands = commands;
1442 | this.option = d;
1443 | }
1444 |
1445 | protected override void OnParseComplete(OptionContext c) {
1446 | commands.showHelp = true;
1447 |
1448 | option?.InvokeOnParseComplete(c);
1449 | }
1450 | }
1451 |
1452 | class CommandOptionSet : OptionSet {
1453 | CommandSet commands;
1454 |
1455 | public CommandOptionSet(CommandSet commands, MessageLocalizerConverter localizer)
1456 | : base(localizer) {
1457 | this.commands = commands;
1458 | }
1459 |
1460 | protected override void SetItem(int index, Option item) {
1461 | if (ShouldWrapOption(item)) {
1462 | base.SetItem(index, new HelpOption(commands, item));
1463 | return;
1464 | }
1465 | base.SetItem(index, item);
1466 | }
1467 |
1468 | bool ShouldWrapOption(Option item) {
1469 | if (item == null)
1470 | return false;
1471 | var help = item as HelpOption;
1472 | if (help != null)
1473 | return false;
1474 | foreach (var n in item.Names) {
1475 | if (n == "help")
1476 | return true;
1477 | }
1478 | return false;
1479 | }
1480 |
1481 | protected override void InsertItem(int index, Option item) {
1482 | if (ShouldWrapOption(item)) {
1483 | base.InsertItem(index, new HelpOption(commands, item));
1484 | return;
1485 | }
1486 | base.InsertItem(index, item);
1487 | }
1488 | }
1489 |
1490 | internal class CommandSet : KeyedCollection {
1491 | readonly string suite;
1492 |
1493 | OptionSet options;
1494 | TextWriter outWriter;
1495 | TextWriter errorWriter;
1496 |
1497 | internal List NestedCommandSets;
1498 |
1499 | internal HelpCommand help;
1500 |
1501 | internal bool showHelp;
1502 |
1503 | internal OptionSet Options => options;
1504 |
1505 | #if !PCL || NETSTANDARD1_3
1506 | public CommandSet(string suite, MessageLocalizerConverter localizer = null)
1507 | : this(suite, Console.Out, Console.Error, localizer) {
1508 | }
1509 | #endif
1510 |
1511 | public CommandSet(string suite, TextWriter output, TextWriter error, MessageLocalizerConverter localizer = null) {
1512 | if (suite == null)
1513 | throw new ArgumentNullException(nameof(suite));
1514 | if (output == null)
1515 | throw new ArgumentNullException(nameof(output));
1516 | if (error == null)
1517 | throw new ArgumentNullException(nameof(error));
1518 |
1519 | this.suite = suite;
1520 | options = new CommandOptionSet(this, localizer);
1521 | outWriter = output;
1522 | errorWriter = error;
1523 | }
1524 |
1525 | public string Suite => suite;
1526 | public TextWriter Out => outWriter;
1527 | public TextWriter Error => errorWriter;
1528 | public MessageLocalizerConverter MessageLocalizer => options.MessageLocalizer;
1529 |
1530 | protected override string GetKeyForItem(Command item) {
1531 | return item?.Name;
1532 | }
1533 |
1534 | public new CommandSet Add(Command value) {
1535 | if (value == null)
1536 | throw new ArgumentNullException(nameof(value));
1537 | AddCommand(value);
1538 | options.Add(new CommandOption(value));
1539 | return this;
1540 | }
1541 |
1542 | void AddCommand(Command value) {
1543 | if (value.CommandSet != null && value.CommandSet != this) {
1544 | throw new ArgumentException("Command instances can only be added to a single CommandSet.", nameof(value));
1545 | }
1546 | value.CommandSet = this;
1547 | if (value.Options != null) {
1548 | value.Options.MessageLocalizer = options.MessageLocalizer;
1549 | }
1550 |
1551 | base.Add(value);
1552 |
1553 | help = help ?? value as HelpCommand;
1554 | }
1555 |
1556 | public CommandSet Add(string header) {
1557 | options.Add(header);
1558 | return this;
1559 | }
1560 |
1561 | public CommandSet Add(Option option) {
1562 | options.Add(option);
1563 | return this;
1564 | }
1565 |
1566 | public CommandSet Add(string prototype, Action action) {
1567 | options.Add(prototype, action);
1568 | return this;
1569 | }
1570 |
1571 | public CommandSet Add(string prototype, string description, Action action) {
1572 | options.Add(prototype, description, action);
1573 | return this;
1574 | }
1575 |
1576 | public CommandSet Add(string prototype, string description, Action action, bool hidden) {
1577 | options.Add(prototype, description, action, hidden);
1578 | return this;
1579 | }
1580 |
1581 | public CommandSet Add(string prototype, OptionAction action) {
1582 | options.Add(prototype, action);
1583 | return this;
1584 | }
1585 |
1586 | public CommandSet Add(string prototype, string description, OptionAction action) {
1587 | options.Add(prototype, description, action);
1588 | return this;
1589 | }
1590 |
1591 | public CommandSet Add(string prototype, string description, OptionAction action, bool hidden) {
1592 | options.Add(prototype, description, action, hidden);
1593 | return this;
1594 | }
1595 |
1596 | public CommandSet Add(string prototype, Action action) {
1597 | options.Add(prototype, null, action);
1598 | return this;
1599 | }
1600 |
1601 | public CommandSet Add(string prototype, string description, Action action) {
1602 | options.Add(prototype, description, action);
1603 | return this;
1604 | }
1605 |
1606 | public CommandSet Add(string prototype, OptionAction action) {
1607 | options.Add(prototype, action);
1608 | return this;
1609 | }
1610 |
1611 | public CommandSet Add(string prototype, string description, OptionAction action) {
1612 | options.Add(prototype, description, action);
1613 | return this;
1614 | }
1615 |
1616 | public CommandSet Add(ArgumentSource source) {
1617 | options.Add(source);
1618 | return this;
1619 | }
1620 |
1621 | public CommandSet Add(CommandSet nestedCommands) {
1622 | if (nestedCommands == null)
1623 | throw new ArgumentNullException(nameof(nestedCommands));
1624 |
1625 | if (NestedCommandSets == null) {
1626 | NestedCommandSets = new List();
1627 | }
1628 |
1629 | if (!AlreadyAdded(nestedCommands)) {
1630 | NestedCommandSets.Add(nestedCommands);
1631 | foreach (var o in nestedCommands.options) {
1632 | if (o is CommandOption c) {
1633 | options.Add(new CommandOption(c.Command, $"{nestedCommands.Suite} {c.CommandName}"));
1634 | } else {
1635 | options.Add(o);
1636 | }
1637 | }
1638 | }
1639 |
1640 | nestedCommands.options = this.options;
1641 | nestedCommands.outWriter = this.outWriter;
1642 | nestedCommands.errorWriter = this.errorWriter;
1643 |
1644 | return this;
1645 | }
1646 |
1647 | bool AlreadyAdded(CommandSet value) {
1648 | if (value == this)
1649 | return true;
1650 | if (NestedCommandSets == null)
1651 | return false;
1652 | foreach (var nc in NestedCommandSets) {
1653 | if (nc.AlreadyAdded(value))
1654 | return true;
1655 | }
1656 | return false;
1657 | }
1658 |
1659 | public IEnumerable GetCompletions(string prefix = null) {
1660 | string rest;
1661 | ExtractToken(ref prefix, out rest);
1662 |
1663 | foreach (var command in this) {
1664 | if (command.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) {
1665 | yield return command.Name;
1666 | }
1667 | }
1668 |
1669 | if (NestedCommandSets == null)
1670 | yield break;
1671 |
1672 | foreach (var subset in NestedCommandSets) {
1673 | if (subset.Suite.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) {
1674 | foreach (var c in subset.GetCompletions(rest)) {
1675 | yield return $"{subset.Suite} {c}";
1676 | }
1677 | }
1678 | }
1679 | }
1680 |
1681 | static void ExtractToken(ref string input, out string rest) {
1682 | rest = "";
1683 | input = input ?? "";
1684 |
1685 | int top = input.Length;
1686 | for (int i = 0; i < top; i++) {
1687 | if (char.IsWhiteSpace(input[i]))
1688 | continue;
1689 |
1690 | for (int j = i; j < top; j++) {
1691 | if (char.IsWhiteSpace(input[j])) {
1692 | rest = input.Substring(j).Trim();
1693 | input = input.Substring(i, j).Trim();
1694 | return;
1695 | }
1696 | }
1697 | rest = "";
1698 | if (i != 0)
1699 | input = input.Substring(i).Trim();
1700 | return;
1701 | }
1702 | }
1703 |
1704 | public int Run(IEnumerable arguments) {
1705 | if (arguments == null)
1706 | throw new ArgumentNullException(nameof(arguments));
1707 |
1708 | this.showHelp = false;
1709 | if (help == null) {
1710 | help = new HelpCommand();
1711 | AddCommand(help);
1712 | }
1713 | Action setHelp = v => showHelp = v != null;
1714 | if (!options.Contains("help")) {
1715 | options.Add("help", "", setHelp, hidden: true);
1716 | }
1717 | if (!options.Contains("?")) {
1718 | options.Add("?", "", setHelp, hidden: true);
1719 | }
1720 | var extra = options.Parse(arguments);
1721 | if (extra.Count == 0) {
1722 | if (showHelp) {
1723 | return help.Invoke(extra);
1724 | }
1725 | Out.WriteLine(options.MessageLocalizer($"Use `{Suite} help` for usage."));
1726 | return 1;
1727 | }
1728 | var command = GetCommand(extra);
1729 | if (command == null) {
1730 | help.WriteUnknownCommand(extra[0]);
1731 | return 1;
1732 | }
1733 | if (showHelp) {
1734 | if (command.Options?.Contains("help") ?? true) {
1735 | extra.Add("--help");
1736 | return command.Invoke(extra);
1737 | }
1738 | command.Options.WriteOptionDescriptions(Out);
1739 | return 0;
1740 | }
1741 | return command.Invoke(extra);
1742 | }
1743 |
1744 | internal Command GetCommand(List extra) {
1745 | return TryGetLocalCommand(extra) ?? TryGetNestedCommand(extra);
1746 | }
1747 |
1748 | Command TryGetLocalCommand(List extra) {
1749 | var name = extra[0];
1750 | if (Contains(name)) {
1751 | extra.RemoveAt(0);
1752 | return this[name];
1753 | }
1754 | for (int i = 1; i < extra.Count; ++i) {
1755 | name = name + " " + extra[i];
1756 | if (!Contains(name))
1757 | continue;
1758 | extra.RemoveRange(0, i + 1);
1759 | return this[name];
1760 | }
1761 | return null;
1762 | }
1763 |
1764 | Command TryGetNestedCommand(List extra) {
1765 | if (NestedCommandSets == null)
1766 | return null;
1767 |
1768 | var nestedCommands = NestedCommandSets.Find(c => c.Suite == extra[0]);
1769 | if (nestedCommands == null)
1770 | return null;
1771 |
1772 | var extraCopy = new List(extra);
1773 | extraCopy.RemoveAt(0);
1774 | if (extraCopy.Count == 0)
1775 | return null;
1776 |
1777 | var command = nestedCommands.GetCommand(extraCopy);
1778 | if (command != null) {
1779 | extra.Clear();
1780 | extra.AddRange(extraCopy);
1781 | return command;
1782 | }
1783 | return null;
1784 | }
1785 | }
1786 |
1787 | internal class HelpCommand : Command {
1788 | public HelpCommand()
1789 | : base("help", help: "Show this message and exit") {
1790 | }
1791 |
1792 | public override int Invoke(IEnumerable arguments) {
1793 | var extra = new List(arguments ?? new string[0]);
1794 | var _ = CommandSet.Options.MessageLocalizer;
1795 | if (extra.Count == 0) {
1796 | CommandSet.Options.WriteOptionDescriptions(CommandSet.Out);
1797 | return 0;
1798 | }
1799 | var command = CommandSet.GetCommand(extra);
1800 | if (command == this || extra.Contains("--help")) {
1801 | CommandSet.Out.WriteLine(_($"Usage: {CommandSet.Suite} COMMAND [OPTIONS]"));
1802 | CommandSet.Out.WriteLine(_($"Use `{CommandSet.Suite} help COMMAND` for help on a specific command."));
1803 | CommandSet.Out.WriteLine();
1804 | CommandSet.Out.WriteLine(_($"Available commands:"));
1805 | CommandSet.Out.WriteLine();
1806 | var commands = GetCommands();
1807 | commands.Sort((x, y) => string.Compare(x.Key, y.Key, StringComparison.OrdinalIgnoreCase));
1808 | foreach (var c in commands) {
1809 | if (c.Key == "help") {
1810 | continue;
1811 | }
1812 | CommandSet.Options.WriteCommandDescription(CommandSet.Out, c.Value, c.Key);
1813 | }
1814 | CommandSet.Options.WriteCommandDescription(CommandSet.Out, CommandSet.help, "help");
1815 | return 0;
1816 | }
1817 | if (command == null) {
1818 | WriteUnknownCommand(extra[0]);
1819 | return 1;
1820 | }
1821 | if (command.Options != null) {
1822 | command.Options.WriteOptionDescriptions(CommandSet.Out);
1823 | return 0;
1824 | }
1825 | return command.Invoke(new[] { "--help" });
1826 | }
1827 |
1828 | List> GetCommands() {
1829 | var commands = new List>();
1830 |
1831 | foreach (var c in CommandSet) {
1832 | commands.Add(new KeyValuePair(c.Name, c));
1833 | }
1834 |
1835 | if (CommandSet.NestedCommandSets == null)
1836 | return commands;
1837 |
1838 | foreach (var nc in CommandSet.NestedCommandSets) {
1839 | AddNestedCommands(commands, "", nc);
1840 | }
1841 |
1842 | return commands;
1843 | }
1844 |
1845 | void AddNestedCommands(List> commands, string outer, CommandSet value) {
1846 | foreach (var v in value) {
1847 | commands.Add(new KeyValuePair($"{outer}{value.Suite} {v.Name}", v));
1848 | }
1849 | if (value.NestedCommandSets == null)
1850 | return;
1851 | foreach (var nc in value.NestedCommandSets) {
1852 | AddNestedCommands(commands, $"{outer}{value.Suite} ", nc);
1853 | }
1854 | }
1855 |
1856 | internal void WriteUnknownCommand(string unknownCommand) {
1857 | CommandSet.Error.WriteLine(CommandSet.Options.MessageLocalizer($"{CommandSet.Suite}: Unknown command: {unknownCommand}"));
1858 | CommandSet.Error.WriteLine(CommandSet.Options.MessageLocalizer($"{CommandSet.Suite}: Use `{CommandSet.Suite} help` for usage."));
1859 | }
1860 | }
1861 | }
1862 |
--------------------------------------------------------------------------------
/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("BeaconEye")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("BeaconEye")]
13 | [assembly: AssemblyCopyright("Copyright © 2021")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("7c30a97d-e557-40c4-9b3f-5ee56599c858")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BeaconEye
2 |
3 | ## Introduction
4 |
5 | BeaconEye scans running processes for active CobaltStrike beacons. When processes are found to be running beacon, BeaconEye will monitor each process for C2 activity.
6 |
7 | 
8 |
9 | ## How it works
10 |
11 | BeaconEye will scan live processes or MiniDump files for suspected CobaltStrike beacons. In live process mode, BeaconEye optionally attaches itself as a debugger and will begin monitoring beacon activity for C2 traffic (HTTP/HTTPS beacons supported currently).
12 |
13 | The AES keys used for encrypting C2 data and mallable profile are decoded on the fly, which enables BeaconEye to extract and decrypt beacon's output when commands are sent via the operator.
14 |
15 | A log folder of activity is created per process relative to the current directory where BeaconEye is executed from.
16 |
17 | ## Usage
18 |
19 | ```shell
20 | BeconEye by @_EthicalChaos_
21 | CobaltStrike beacon hunter and command monitoring tool x86_64
22 |
23 | -v, --verbose Display more verbose output instead of just
24 | information on beacons found
25 | -m, --monitor Attach to and monitor beacons found when scanning
26 | live processes
27 | -f, --filter=VALUE Filter process list with names starting with x (
28 | live mode only)
29 | -d, --dump=VALUE A folder to use for MiniDump mode to scan for
30 | beacons (files with *.dmp or *.mdmp)
31 | -h, --help Display this help
32 | ```
33 |
34 | ## Features
35 |
36 | * A per process log folder
37 | * Dumps beacon config
38 | * Displays output from most beacon commands
39 | * Saves screenshots
40 | * Detects standalone and injected beacons
41 | * Detects beacons masked with built in `sleep_mask`
42 | * Scan running processes or Minidumps offline
43 |
44 | ## Caveats
45 |
46 | BeaconEye can detect all beacon types but only monitor HTTP/HTTPS beacons. At present, only command output is decoded and not command requests. See TODO list below for a full list of intended features.
47 |
48 | BeaconEye should be considered **ALPHA**, I'm keen to get feedback on 4.x beacons that cannot be detected or where the malleable C2 profile has not been parsed correctly resulting in incorrect decoding of output.
49 |
50 | ## TODO
51 |
52 | * ~~Implement 32bit beacon monitoring~~
53 | * Add support for monitoring named pipe beacons
54 | * Add support for monitoring TCP beacons
55 | * Add support for CobaltStrike 3.x
56 | * ~~Add command line argument for targeting specific processes~~
57 | * Add command line argument to specify output logging location
58 | * Add support for extracting operator commands
59 | * ~~Support scanning MiniDump files~~
60 |
61 |
62 | ## References and Thanks
63 |
64 | * BeaconEye's initial beacon process detection is heavily based on @Apr4h's [CobaltStrikeScan](https://github.com/Apr4h/CobaltStrikeScan).
65 | * James Forshaw's NtApiDotNet library, which makes process deubgging and interaction a breeze from C#.
66 | * @cube0x0 for his port of a pure managed C# MiniDump reader which was used as a reference.
67 |
--------------------------------------------------------------------------------
/Reader/IProcessEnumerator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace BeaconEye.Reader {
8 | public interface IProcessEnumerator {
9 | IEnumerable GetProcesses();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Reader/MiniDumpProcessEnumerator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 |
6 | namespace BeaconEye.Reader {
7 | class MiniDumpProcessEnumerator : IProcessEnumerator {
8 |
9 | public bool Verbose { get; private set; }
10 |
11 | readonly string searchPath;
12 |
13 | public MiniDumpProcessEnumerator(string searchPath, bool verbose) {
14 | this.searchPath = searchPath;
15 | this.Verbose = verbose;
16 | }
17 |
18 | public IEnumerable GetProcesses() {
19 |
20 | var minidumpReaders = new List();
21 | var minidumpFiles = Directory.EnumerateFiles(searchPath)
22 | .Where(file => file.ToLower().EndsWith("mdmp") || file.ToLower().EndsWith("dmp"));
23 |
24 | foreach(var minidumpFile in minidumpFiles) {
25 | try {
26 | minidumpReaders.Add(new MiniDumpReader(minidumpFile));
27 | }catch(FormatException fe) {
28 | if(Verbose)
29 | Console.WriteLine($"[=] Failed to open minidump {Path.GetFileName(minidumpFile)} with error: {fe.Message}");
30 | }
31 | }
32 |
33 | return minidumpReaders;
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Reader/MiniDumpReader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Runtime.InteropServices;
5 | using System.Linq;
6 | using System.Text;
7 |
8 | namespace BeaconEye {
9 | public class MiniDumpReader : ProcessReader {
10 |
11 | public enum StreamType : uint {
12 | UnusedStream,
13 | ReservedStream0,
14 | ReservedStream1,
15 | ThreadListStream,
16 | ModuleListStream,
17 | MemoryListStream,
18 | ExceptionStream,
19 | SystemInfoStream,
20 | ThreadExListStream,
21 | Memory64ListStream,
22 | CommentStreamA,
23 | CommentStreamW,
24 | HandleDataStream,
25 | FunctionTableStream,
26 | UnloadedModuleListStream,
27 | MiscInfoStream,
28 | MemoryInfoListStream,
29 | ThreadInfoListStream,
30 | HandleOperationListStream,
31 | TokenStream,
32 | JavaScriptDataStream,
33 | SystemMemoryInfoStream,
34 | ProcessVmCountersStream,
35 | IptTraceStream,
36 | ThreadNamesStream,
37 | ceStreamNull,
38 | ceStreamSystemInfo,
39 | ceStreamException,
40 | ceStreamModuleList,
41 | ceStreamProcessList,
42 | ceStreamThreadList,
43 | ceStreamThreadContextList,
44 | ceStreamThreadCallStackList,
45 | ceStreamMemoryVirtualList,
46 | ceStreamMemoryPhysicalList,
47 | ceStreamBucketParameters,
48 | ceStreamProcessModuleMap,
49 | ceStreamDiagnosisList,
50 | LastReservedStream
51 | }
52 |
53 | [Flags]
54 | enum MinidumpType : ulong {
55 | MiniDumpNormal,
56 | MiniDumpWithDataSegs,
57 | MiniDumpWithFullMemory,
58 | MiniDumpWithHandleData,
59 | MiniDumpFilterMemory,
60 | MiniDumpScanMemory,
61 | MiniDumpWithUnloadedModules,
62 | MiniDumpWithIndirectlyReferencedMemory,
63 | MiniDumpFilterModulePaths,
64 | MiniDumpWithProcessThreadData,
65 | MiniDumpWithPrivateReadWriteMemory,
66 | MiniDumpWithoutOptionalData,
67 | MiniDumpWithFullMemoryInfo,
68 | MiniDumpWithThreadInfo,
69 | MiniDumpWithCodeSegs,
70 | MiniDumpWithoutAuxiliaryState,
71 | MiniDumpWithFullAuxiliaryState,
72 | MiniDumpWithPrivateWriteCopyMemory,
73 | MiniDumpIgnoreInaccessibleMemory,
74 | MiniDumpWithTokenInformation,
75 | MiniDumpWithModuleHeaders,
76 | MiniDumpFilterTriage,
77 | MiniDumpWithAvxXStateContext,
78 | MiniDumpWithIptTrace,
79 | MiniDumpScanInaccessiblePartialPages,
80 | MiniDumpFilterWriteCombinedMemory,
81 | MiniDumpValidTypeFlags
82 | }
83 |
84 |
85 | [StructLayout(LayoutKind.Sequential, Pack = 1)]
86 | struct Header {
87 | public uint Signature;
88 | public uint Version;
89 | public uint NumberOfStreams;
90 | public uint StreamsDirectoryRva;
91 | public uint Checksum;
92 | public uint Timestamp;
93 | public MinidumpType Flags;
94 | }
95 |
96 | [StructLayout(LayoutKind.Sequential, Pack = 1)]
97 | struct Directory {
98 | public StreamType StreamType;
99 | public uint Length;
100 | public uint Offset;
101 |
102 | public override string ToString() {
103 | return $"{StreamType}: Length={Length}, Offset={Offset}";
104 | }
105 | }
106 |
107 | [StructLayout(LayoutKind.Sequential, Pack = 1)]
108 | struct LocationDescriptor {
109 | public uint Length;
110 | public uint Offset;
111 | }
112 |
113 | [StructLayout(LayoutKind.Sequential, Pack = 1)]
114 | struct MemoryDescriptor {
115 | public ulong AddressMemoryRange;
116 | public LocationDescriptor Memory;
117 | }
118 |
119 | [StructLayout(LayoutKind.Sequential, Pack = 1)]
120 | struct MemoryDescriptorFull {
121 | public ulong StartOfMemoryRange;
122 | public ulong DataSize;
123 | public override string ToString() {
124 | return $"Start=0x{StartOfMemoryRange:x}: DataSize=0x{DataSize:x}";
125 | }
126 | }
127 |
128 | [StructLayout(LayoutKind.Sequential, Pack = 1)]
129 | struct MiniDumpThread {
130 | public uint ThreadId;
131 | public uint SuspendCount;
132 | public uint PriorityClass;
133 | public uint Priority;
134 | public ulong Teb;
135 | public MemoryDescriptor Stack;
136 | public LocationDescriptor ThreadContext;
137 | }
138 |
139 | [StructLayout(LayoutKind.Sequential, Pack = 1)]
140 | struct Module {
141 | public ulong BaseOfImage;
142 | public uint SizeOfImage;
143 | public uint CheckSum;
144 | public uint TimeDateStamp;
145 | public uint ModuleNameRva;
146 | public FileInfo VersionInfo;
147 | public LocationDescriptor CvRecord;
148 | public LocationDescriptor MiscRecord;
149 | ulong Reserved0;
150 | ulong Reserved1;
151 | }
152 |
153 | [StructLayout(LayoutKind.Sequential, Pack = 1)]
154 | struct FileInfo {
155 | public uint Signature;
156 | public uint StrucVersion;
157 | public uint FileVersionMS;
158 | public uint FileVersionLS;
159 | public uint ProductVersionMS;
160 | public uint ProductVersionLS;
161 | public uint FileFlagsMask;
162 | public uint FileFlags;
163 | public uint FileOS;
164 | public uint FileType;
165 | public uint FileSubtype;
166 | public uint FileDateMS;
167 | public uint FileDateLS;
168 | }
169 |
170 | [StructLayout(LayoutKind.Sequential, Pack = 1)]
171 | struct SystemInfo {
172 | public ushort ProcessorArchitecture;
173 | public ushort ProcessorLevel;
174 | public ushort ProcessorRevision;
175 | public byte NumberOfProcessors;
176 | public byte ProductType;
177 | public uint MajorVersion;
178 | public uint MinorVersion;
179 | public uint BuildNumber;
180 | public uint PlatformId;
181 | public uint CDSVersionRva;
182 | }
183 |
184 | [StructLayout(LayoutKind.Sequential, Pack = 1)]
185 | struct PartialTeb64 {
186 | public ulong SehFrame;
187 | public ulong StackBase;
188 | public ulong StackLimit;
189 | public ulong SubSystemTib;
190 | public ulong FibreData;
191 | public ulong DataSlot;
192 | public ulong TebAddress;
193 | public ulong EnvironmentPointer;
194 | public ulong ProcessId;
195 | public ulong ThreadId;
196 | public ulong ActiveRPCHandle;
197 | public ulong ThreadLocalStorageAddr;
198 | public ulong PebAddress;
199 | }
200 |
201 | [StructLayout(LayoutKind.Sequential, Pack = 1)]
202 | struct PartialTeb32 {
203 | public uint SehFrame;
204 | public uint StackBase;
205 | public uint StackLimit;
206 | public uint SubSystemTib;
207 | public uint FibreData;
208 | public uint DataSlot;
209 | public uint TebAddress;
210 | public uint EnvironmentPointer;
211 | public uint ProcessId;
212 | public uint ThreadId;
213 | public uint ActiveRPCHandle;
214 | public uint ThreadLocalStorageAddr;
215 | public uint PebAddress;
216 | }
217 |
218 | Stream miniDumpStream;
219 | BinaryReader miniDumpReader;
220 | List threads = new List();
221 | List modules = new List();
222 | List memoryInfoFull = new List();
223 | SystemInfo systemInfo;
224 | ulong memoryFullRVA;
225 | ulong pebAddress;
226 | string processName;
227 | int processId;
228 | bool is64 = true;
229 |
230 |
231 | //TODO: get process name from dump
232 | public override string Name => processName;
233 |
234 |
235 | //TODO: determine 32/64 bit dumps
236 | public override bool Is64Bit => is64;
237 |
238 | public override ulong PebAddress => pebAddress;
239 | //TODO: extract PID
240 | public override int ProcessId => processId;
241 |
242 | public MiniDumpReader(string fileName) : this(new FileStream(fileName, FileMode.Open, FileAccess.Read)) {
243 | }
244 |
245 | public MiniDumpReader(Stream source) {
246 |
247 | miniDumpStream = source;
248 |
249 | if (!source.CanSeek) {
250 | throw new ArgumentException("Only seekable streams supported");
251 | }
252 |
253 | miniDumpReader = new BinaryReader(miniDumpStream);
254 | source.Seek(0, SeekOrigin.Begin);
255 |
256 | var hdr = ReadStruct();
257 |
258 | if(hdr.Signature != 0x504d444d) {
259 | throw new FormatException("Input stream doesn't appear to be a Minidump");
260 | }
261 |
262 | if( ((ulong)hdr.Flags | (ulong)MinidumpType.MiniDumpWithFullMemoryInfo) == 0) {
263 | throw new FormatException("Only full Minidump types supported");
264 | }
265 |
266 | var directories = new List();
267 | source.Seek(hdr.StreamsDirectoryRva, SeekOrigin.Begin);
268 |
269 | for (int idx = 0; idx < hdr.NumberOfStreams; ++idx) {
270 | directories.Add(ReadStruct());
271 | }
272 |
273 | foreach (var dir in directories) {
274 |
275 | source.Seek(dir.Offset, SeekOrigin.Begin);
276 |
277 | if (dir.StreamType == StreamType.ThreadListStream) {
278 |
279 | var threadCount = miniDumpReader.ReadInt32();
280 | for (int idx = 0; idx < threadCount; ++idx) {
281 | threads.Add(ReadStruct());
282 | }
283 |
284 | } else if (dir.StreamType == StreamType.Memory64ListStream) {
285 |
286 | var memoryRangeCount = miniDumpReader.ReadUInt64();
287 | memoryFullRVA = miniDumpReader.ReadUInt64();
288 | for (uint idx = 0; idx < memoryRangeCount; ++idx) {
289 | memoryInfoFull.Add(ReadStruct());
290 | }
291 |
292 | } else if (dir.StreamType == StreamType.ModuleListStream) {
293 |
294 | var moduleCount = miniDumpReader.ReadInt32();
295 | while (moduleCount-- > 0) {
296 | modules.Add(ReadStruct());
297 | }
298 | } else if (dir.StreamType == StreamType.SystemInfoStream) {
299 | systemInfo = ReadStruct();
300 | }
301 | }
302 |
303 | processName = Path.GetFileName(ReadMinidumpString(modules[0].ModuleNameRva));
304 | is64 = systemInfo.ProcessorArchitecture == 9;
305 |
306 | if (is64) {
307 | var teb = ReadMemory(threads[0].Teb);
308 | processId = (int)teb.ProcessId;
309 | pebAddress = teb.PebAddress;
310 | } else {
311 | var teb = ReadMemory(threads[0].Teb);
312 | processId = (int)teb.ProcessId;
313 | pebAddress = teb.PebAddress;
314 | }
315 | }
316 |
317 | string ReadMinidumpString(long rva) {
318 |
319 | long oldPosition = miniDumpStream.Position;
320 | miniDumpStream.Seek(rva, SeekOrigin.Begin);
321 | var strLen = ReadStruct();
322 | string result = Encoding.Unicode.GetString(miniDumpReader.ReadBytes(strLen));
323 | miniDumpStream.Seek(oldPosition, SeekOrigin.Begin);
324 | return result;
325 | }
326 |
327 | T ReadStruct() {
328 |
329 | var structData = new byte[Marshal.SizeOf(typeof(T))];
330 | miniDumpStream.Read(structData, 0, structData.Length);
331 |
332 | var handle = GCHandle.Alloc(structData, GCHandleType.Pinned);
333 | var theStructure = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
334 | handle.Free();
335 |
336 | return theStructure;
337 | }
338 |
339 | public override T ReadMemory(ulong address) {
340 |
341 | var structData = ReadMemory(address, Marshal.SizeOf(typeof(T)));
342 |
343 | var handle = GCHandle.Alloc(structData, GCHandleType.Pinned);
344 | var theStructure = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
345 | handle.Free();
346 |
347 | return theStructure;
348 | }
349 |
350 | public override byte[] ReadMemory(ulong address, int len) {
351 |
352 | ulong fileAddress = memoryFullRVA;
353 |
354 | foreach (var descriptor in memoryInfoFull) {
355 | if (address >= descriptor.StartOfMemoryRange && address < descriptor.StartOfMemoryRange + descriptor.DataSize) {
356 |
357 | ulong offsetInRage = address - descriptor.StartOfMemoryRange;
358 | miniDumpStream.Seek((long)(fileAddress + offsetInRage), SeekOrigin.Begin);
359 | byte[] data = new byte[len];
360 | miniDumpStream.Read(data, 0, data.Length);
361 | return data;
362 | }
363 | fileAddress += descriptor.DataSize;
364 | }
365 |
366 | throw new ArgumentOutOfRangeException();
367 | }
368 |
369 | public override MemoryInfo QueryMemoryInfo(ulong address) {
370 |
371 | ulong fileAddress = memoryFullRVA;
372 |
373 | foreach (var descriptor in memoryInfoFull) {
374 | if (address >= descriptor.StartOfMemoryRange && address < descriptor.StartOfMemoryRange + descriptor.DataSize) {
375 | return new MemoryInfo(descriptor.StartOfMemoryRange, descriptor.StartOfMemoryRange, descriptor.DataSize, false, false);
376 | }
377 | fileAddress += descriptor.DataSize;
378 | }
379 |
380 | throw new ArgumentOutOfRangeException($"Memory address 0x{address:x} not mapped inside Minidump");
381 | }
382 |
383 | public override IEnumerable QueryAllMemoryInfo() {
384 | return memoryInfoFull.Select(mi => new MemoryInfo(mi.StartOfMemoryRange, mi.StartOfMemoryRange, mi.DataSize, false, false));
385 | }
386 | }
387 | }
388 |
--------------------------------------------------------------------------------
/Reader/NtProcessReader.cs:
--------------------------------------------------------------------------------
1 | using NtApiDotNet;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace BeaconEye {
6 | public class NtProcessReader : ProcessReader {
7 |
8 | NtProcess process;
9 |
10 | public NtProcess Process => process;
11 | public override string Name => process.Name;
12 |
13 | public override bool Is64Bit => process.Is64Bit;
14 |
15 | public override ulong PebAddress => (ulong)(Is64Bit ? process.PebAddress : process.PebAddress32);
16 |
17 | public override int ProcessId => process.ProcessId;
18 |
19 | public NtProcessReader(NtProcess process) {
20 | this.process = process;
21 | }
22 |
23 | public override byte[] ReadMemory(ulong address, int len) {
24 | return process.ReadMemory((long)address, len);
25 | }
26 |
27 | public override T ReadMemory(ulong address) {
28 | return process.ReadMemory((long)address);
29 | }
30 |
31 | public override MemoryInfo QueryMemoryInfo(ulong address) {
32 |
33 | var info = process.QueryMemoryInformation((long)address);
34 |
35 | return new MemoryInfo((ulong)info.BaseAddress, (ulong)info.AllocationBase, (ulong)info.RegionSize,
36 | info.Protect == MemoryAllocationProtect.ExecuteRead || info.Protect == MemoryAllocationProtect.ExecuteReadWrite, ((int)info.Protect & (int)MemoryAllocationProtect.NoAccess) == (int)MemoryAllocationProtect.NoAccess);
37 | }
38 |
39 | public override IEnumerable QueryAllMemoryInfo() {
40 | return process.QueryAllMemoryInformation().Select(info => new MemoryInfo((ulong)info.BaseAddress, (ulong)info.AllocationBase, (ulong)info.RegionSize,
41 | info.Protect == MemoryAllocationProtect.ExecuteRead || info.Protect == MemoryAllocationProtect.ExecuteReadWrite, ((int)info.Protect & (int)MemoryAllocationProtect.NoAccess) == (int)MemoryAllocationProtect.NoAccess));
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Reader/ProcessReader.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace BeaconEye {
4 | public abstract class ProcessReader {
5 |
6 | static readonly uint SegmentHeapSignature = 0xffeeffee;
7 |
8 | public struct MemoryInfo {
9 | public ulong BaseAddress { get; }
10 | public ulong AllocationBase { get; }
11 | public ulong RegionSize { get; }
12 | public bool IsExecutable { get; }
13 | public bool NoAccess { get; }
14 |
15 | public MemoryInfo(ulong baseAddress, ulong allocationBase, ulong regionSize, bool isExecutable, bool noAccess) {
16 | BaseAddress = baseAddress;
17 | AllocationBase = allocationBase;
18 | RegionSize = regionSize;
19 | IsExecutable = isExecutable;
20 | NoAccess = noAccess;
21 | }
22 | }
23 |
24 | public abstract int ProcessId { get; }
25 | public abstract ulong PebAddress { get; }
26 | public abstract string Name { get; }
27 | public abstract bool Is64Bit { get; }
28 | public abstract T ReadMemory(ulong address) where T : new();
29 | public abstract byte[] ReadMemory(ulong address, int len);
30 | public abstract MemoryInfo QueryMemoryInfo(ulong address);
31 | public abstract IEnumerable QueryAllMemoryInfo();
32 |
33 | public long ReadPointer(ulong address) {
34 | if (Is64Bit) {
35 | return ReadMemory(address);
36 | } else {
37 | return ReadMemory(address);
38 | }
39 | }
40 |
41 | bool IsSegmentHeap(long heapBase) {
42 | return ReadMemory((ulong)heapBase+0x10) == SegmentHeapSignature;
43 | }
44 |
45 | int PointerSize() {
46 | return (Is64Bit ? 8 : 4);
47 | }
48 |
49 | public List Heaps { get {
50 |
51 | int numHeaps;
52 | long heapArray;
53 |
54 | if (Is64Bit) {
55 | numHeaps = ReadMemory(PebAddress + 0xE8);
56 | heapArray = ReadPointer(PebAddress + 0xF0);
57 | } else {
58 | numHeaps = ReadMemory(PebAddress + 0x88);
59 | heapArray = ReadPointer(PebAddress + 0x90);
60 | }
61 |
62 | var heaps = new List();
63 | for (int idx = 0; idx < numHeaps; ++idx) {
64 | var heap = ReadPointer((ulong)(heapArray + (idx * PointerSize())));
65 |
66 | if (IsSegmentHeap(heap)) {
67 | var segmentListEntryForward = ReadPointer((ulong)heap + 0x18);
68 | var segmentBase = ReadPointer((ulong)heap + 0x30);
69 |
70 | while (!heaps.Contains(segmentBase)) {
71 | heaps.Add(segmentBase);
72 | segmentListEntryForward = ReadPointer((ulong)segmentListEntryForward + (ulong)PointerSize());
73 | segmentBase = ReadPointer((ulong)segmentListEntryForward + 0x30);
74 | }
75 |
76 | } else {
77 |
78 | //TODO: Handle Windows 10 Segment Heap
79 | heaps.Add(heap);
80 | }
81 | }
82 |
83 | return heaps;
84 | }
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Reader/RunningProcessEnumerator.cs:
--------------------------------------------------------------------------------
1 | using NtApiDotNet;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace BeaconEye.Reader {
6 | class RunningProcessEnumerator : IProcessEnumerator {
7 |
8 | string filter;
9 |
10 | public RunningProcessEnumerator(string filter) {
11 | this.filter = filter;
12 | }
13 |
14 | public IEnumerable GetProcesses() {
15 | return NtProcess.GetProcesses(ProcessAccessRights.AllAccess)
16 | .Where(p => p.ExitNtStatus == NtStatus.STATUS_PENDING && (string.IsNullOrEmpty(filter) ? p.Name.Length > 0 : p.Name.ToLower().StartsWith(filter.ToLower())) )
17 | .Select(p => new NtProcessReader(p));
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/StringUtils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace BeaconEye {
8 | public class StringUtils {
9 | public static string ByteArrayToString(byte[] ba) {
10 | StringBuilder hex = new StringBuilder(ba.Length * 2);
11 | foreach (byte b in ba)
12 | hex.AppendFormat("{0:x2}", b);
13 | return hex.ToString();
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------