├── C2Protocol.md ├── HTTP.md ├── README.md ├── img ├── HTTP │ └── WebService_SubType.PNG ├── fake_beacon.PNG ├── result_parser.PNG ├── script_parse_tasks.PNG ├── sendresult.PNG └── zheng.jpg ├── scripts ├── define.py ├── sendresult.py └── utils.py ├── src ├── beacon │ ├── BeaconC2.md │ ├── BeaconData.md │ └── BeaconSetup.md ├── common │ └── BeaconEntry.md └── dns │ └── BaseSecurity.md ├── tasks_flow.md └── tree.md /C2Protocol.md: -------------------------------------------------------------------------------- 1 | # Beacon C2 Protocol 2 | 3 | ## Overview 4 | 5 | 该文档分为以下几个部分: 6 | 7 | * overview:简单介绍 8 | * Key Exchange:描述beacon通信过程中的key exchange过程 9 | * metadata:描述beacon metadata的相关信息 10 | * Command:描述beacon的task/command如何下发,C2数据包如何构造,command定义如何 11 | * Result:描述beacon的result数据包格式,如何解析,各种结果的定义 12 | 13 | 由于不知道怎么表示二进制数据结构,随便编了一个: 14 | 15 | ``` 16 | task { 17 | 4 command = big_endian int; // <长度,单位字节> <字段名> = [字节序] 18 | 4 length = big_endian int; 19 | }; 20 | ``` 21 | 22 | 23 | 24 | ## Key Exchange 25 | 26 | 每个beacon生成自己的session key,放入metadata,使用RSA/PKCS1PADDING加密metadata,公钥长度为1024bit,然后发送给服务端 27 | 28 | tasks和results使用AES128/CBC模式加密,padding为简单填充至16倍数,密文末尾使用HMACSHA256对密文做消息验证,但是验证码只取前16字节。 29 | 30 | RSA公私钥存放文件为`.cobaltstrike.beacon_keys`,为java序列化对象的二进制格式,想导出public key暂时想到两种方法: 31 | 32 | * java readObject,但是要用cobaltstrike sleep库 33 | * 通过external c2导出stage,然后直接导出public key,这也是我脚本里用到的,不用编译java,没什么依赖 34 | 35 | ## metadata 36 | 37 | metadata主要包括以下字段: 38 | 39 | | 名称 | 含义 | 偏移 | 40 | | -------------- | ------------------------------------------------------------ | -------- | 41 | | session key | 生成AES128加密密钥和HMACSHA256 hash密钥的密码 | [0, 16) | 42 | | ANSI code page | windows gui所用的代码页 | [16, 18) | 43 | | OEM code page | windows console所用的代码页 | [18, 20) | 44 | | id | beacon id | 0 | 45 | | pid | beacon进程的pid | 1 | 46 | | ver | 用来表示系统版本和ssh版本 | 2 | 47 | | intz | 内网地址 | 3 | 48 | | comp | computer name | 4 | 49 | | user | 创建beacon进程的用户,如果末尾加上了`[空格]*`表示是管理员并且完整性级别最高 | 5 | 50 | | is64 | beacon是否在x64系统上 | 6 | 51 | | barch | beacon进程是否是64位进程 | 7 | 52 | | port | ssh port | 8 | 53 | 54 | 上面表格表示的偏移形如`[0, 16)`,表示metadata二进制数据起始偏移和终止偏移 55 | 56 | 上面表格表示的偏移形如`0`指metadata根据`\t`分割字符串后的数组索引 57 | 58 | metadata的各类字段以上面偏移为说明,如果偏移5的字段不存在(分割字符串后数组索引为5),那么之后的字段也就不会读取了。 59 | 60 | 61 | 62 | ## Command 63 | 64 | 单个task是以以下格式 65 | 66 | ```c 67 | task { 68 | 4 command = big_endian int; 69 | 4 length = big_endian int; 70 | length content = byte[]; 71 | }; 72 | ``` 73 | 74 | 可以多个task一起发送,beacon会根据长度来分割这些task。 75 | 76 | ```c 77 | tasks { 78 | task; 79 | task; 80 | task; 81 | ... // 多个 82 | }; 83 | ``` 84 | 85 | 加密前还会给tasks加上点别的信息,用来防重放: 86 | 87 | padding为`'A' * padding_len`,不过其实用其他字符也可以,因为解密的时候不会检查这个 88 | 89 | ```c 90 | tasks_before_encrypt { 91 | 4 current_time = big_endian int; 92 | 4 tasks_len = big_endian int; 93 | tasks_len tasks = byte[]; 94 | padding_len padding = byte[]; // padding_len = (16 - ((4 + 4 + tasks_len) % 16)) % 16 95 | }; 96 | ``` 97 | 98 | 加密后的密文会附上hmac 99 | 100 | ```c 101 | tasks_encrypted { 102 | tasks_before_encrypt_len ciphertext = byte[]; 103 | 16 hmac = byte[]; 104 | }; 105 | ``` 106 | 107 | 最后发送给beacon 108 | 109 | beacon支持的c2命令有,通过强大的idea搜索加`script/utils.py`脚本调试得出,但是似乎仍有缺少的部分: 110 | 111 | ```java 112 | 'COMMAND_SPAWN' : 1, 113 | 'COMMAND_SHELL' : 2, 114 | 'COMMAND_DIE' : 3, 115 | 'COMMAND_SLEEP' : 4, 116 | 'COMMAND_CD' : 5, 117 | 'COMMAND_KEYLOG_START' : 6, 118 | 'COMMAND_KEYLOG_STOP' : 7, 119 | 'COMMAND_CHECKIN': 8, 120 | 'COMMAND_INJECT_PID' : 9, 121 | 'COMMAND_UPLOAD' : 10, 122 | 'COMMAND_DOWNLOAD': 11, 123 | 'COMMAND_EXECUTE': 12, 124 | 'COMMAND_SPAWN_PROC_X86' : 13, 125 | 'COMMAND_INJECT_PING' : 18, 126 | 'COMMAND_DOWNLOAD_CANCEL': 19, 127 | 'COMMAND_FORWARD_PIPE_DATA': 22, 128 | 'COMMAND_UNLINK': 23, 129 | 'COMMAND_PIPE_PONG': 24, 130 | 'COMMAND_GET_SYSTEM': 25, 131 | 'COMMAND_GETUID': 27, 132 | 'COMMAND_REV2SELF': 28, 133 | 'COMMAND_TIMESTOMP': 29, 134 | 'COMMAND_STEALTOKEN': 31, 135 | 'COMMAND_PS': 32, 136 | 'COMMAND_KILL': 33, 137 | 'COMMAND_KerberosTicketUse': 34, 138 | 'COMMAND_Kerberos_Ticket_Purge': 35, 139 | 'COMMAND_POWERSHELL_IMPORT': 37, 140 | 'COMMAND_RUNAS': 38, 141 | 'COMMAND_PWD': 39, 142 | 'COMMAND_JOB_REGISTER' : 40, 143 | 'COMMAND_JOBS': 41, 144 | 'COMMAND_JOB_KILL': 42, 145 | 'COMMAND_INJECTX64_PID' : 43, 146 | 'COMMAND_SPAWNX64' : 44, 147 | 'COMMAND_VNC_INJECT': 45, 148 | 'COMMAND_VNC_INJECT_X64': 46, 149 | 'COMMAND_PAUSE': 47, 150 | 'COMMAND_IPCONFIG': 48, 151 | 'COMMAND_MAKE_TOKEN': 49, 152 | 'COMMAND_PORT_FORWARD': 50, 153 | 'COMMAND_PORT_FORWARD_STOP': 51, 154 | 'COMMAND_BIND_STAGE': 52, 155 | 'COMMAND_LS': 53, 156 | 'COMMAND_MKDIR': 54, 157 | 'COMMAND_DRIVERS': 55, 158 | 'COMMAND_RM': 56, 159 | 'COMMAND_STAGE_REMOTE_SMB': 57, 160 | 'COMMAND_START_SERVICE': 58, # not sure 161 | 'COMMAND_HTTPHOSTSTRING': 59, 162 | 'COMMAND_OPEN_PIPE': 60, 163 | 'COMMAND_CLOSE_PIPE': 61, 164 | 'COMMAND_JOB_REGISTER_IMPERSONATE' : 62, 165 | 'COMMAND_SPAWN_POWERSHELLX86' : 63, 166 | 'COMMAND_SPAWN_POWERSHELLX64' : 64, 167 | 'COMMAND_INJECT_POWERSHELLX86_PID' : 65, 168 | 'COMMAND_INJECT_POWERSHELLX64_PID' : 66, 169 | 'COMMAND_UPLOAD_CONTINUE' : 67, 170 | 'COMMAND_PIPE_OPEN_EXPLICIT' : 68, 171 | 'COMMAND_SPAWN_PROC_X64' : 69, 172 | 'COMMAND_JOB_SPAWN_X86' : 70, 173 | 'COMMAND_JOB_SPAWN_X64' : 71, 174 | 'COMMAND_SETENV' : 72, 175 | 'COMMAND_FILE_COPY' : 73, 176 | 'COMMAND_FILE_MOVE' : 74, 177 | 'COMMAND_PPID' : 75, 178 | 'COMMAND_RUN_UNDER_PID' : 76, 179 | 'COMMAND_GETPRIVS' : 77, 180 | 'COMMAND_EXECUTE_JOB' : 78, 181 | 'COMMAND_PSH_HOST_TCP' : 79, 182 | 'COMMAND_DLL_LOAD' : 80, 183 | 'COMMAND_REG_QUERY' : 81, 184 | 'COMMAND_LSOCKET_TCPPIVOT' : 82, 185 | 'COMMAND_ARGUE_ADD' : 83, 186 | 'COMMAND_ARGUE_REMOVE' : 84, 187 | 'COMMAND_ARGUE_LIST' : 85, 188 | 'COMMAND_TCP_CONNECT' : 86, 189 | 'COMMAND_JOB_SPAWN_TOKEN_X86' : 87, 190 | 'COMMAND_JOB_SPAWN_TOKEN_X64' : 88, 191 | 'COMMAND_SPAWN_TOKEN_X86' : 89, 192 | 'COMMAND_SPAWN_TOKEN_X64' : 90, 193 | 'COMMAND_INJECTX64_PING' : 91, 194 | 'COMMAND_BLOCKDLLS' : 92, 195 | ``` 196 | 197 | 198 | 199 | 200 | 201 | ## Result 202 | 203 | 返回结果为多个job的result: 204 | 205 | ```c 206 | results { 207 | result_encrypted; 208 | result_encrypted; 209 | ... 210 | }; 211 | ``` 212 | 213 | 单个加密的result是包含长度和密文还有hmac的 214 | 215 | ```c 216 | result_encrypted { 217 | 4 cipher_hmac_len = big_endian int; 218 | cipher_len cipher_text = byte[]; 219 | 16 hmac = byte[]; 220 | }; 221 | ``` 222 | 223 | 与task一样,result包含时间用来防重放 224 | 225 | ```c 226 | result_decrypt { 227 | 4 timestamp = big_endian int; 228 | 4 length = big_endian int; 229 | length result = byte[]; 230 | }; 231 | ``` 232 | 233 | 最后的结构为: 234 | 235 | ```c 236 | result { 237 | 4 job_type = big_endian int; 238 | (length-4) result_data = byte[]; 239 | }; 240 | ``` 241 | 242 | 243 | 244 | 结果类型的定义有: 245 | 246 | ```java 247 | 'CALLBACK_OUTPUT' : 0, 248 | 'CALLBACK_KEYSTROKES' : 1, 249 | 'CALLBACK_FILE' : 2, 250 | 'CALLBACK_SCREENSHOT' : 3, 251 | 'CALLBACK_CLOSE' : 4, 252 | 'CALLBACK_READ' : 5, 253 | 'CALLBACK_CONNECT' : 6, 254 | 'CALLBACK_PING' : 7, 255 | 'CALLBACK_FILE_WRITE' : 8, 256 | 'CALLBACK_FILE_CLOSE' : 9, 257 | 'CALLBACK_PIPE_OPEN' : 10, 258 | 'CALLBACK_PIPE_CLOSE' : 11, 259 | 'CALLBACK_PIPE_READ' : 12, 260 | 'CALLBACK_POST_ERROR' : 13, 261 | 'CALLBACK_PIPE_PING' : 14, 262 | 'CALLBACK_TOKEN_STOLEN' : 15, 263 | 'CALLBACK_TOKEN_GETUID' : 16, 264 | 'CALLBACK_PROCESS_LIST' : 17, 265 | 'CALLBACK_POST_REPLAY_ERROR' : 18, 266 | 'CALLBACK_PWD' : 19, 267 | 'CALLBACK_JOBS' : 20, 268 | 'CALLBACK_HASHDUMP' : 21, 269 | 'CALLBACK_PENDING' : 22, 270 | 'CALLBACK_ACCEPT' : 23, 271 | 'CALLBACK_NETVIEW' : 24, 272 | 'CALLBACK_PORTSCAN' : 25, 273 | 'CALLBACK_DEAD' : 26, 274 | 'CALLBACK_SSH_STATUS' : 27, 275 | 'CALLBACK_CHUNK_ALLOCATE' : 28, 276 | 'CALLBACK_CHUNK_SEND' : 29, 277 | 'CALLBACK_OUTPUT_OEM' : 30, 278 | 'CALLBACK_ERROR' : 31, 279 | 'CALLBACK_OUTPUT_UTF8' : 32 280 | ``` 281 | 282 | 283 | 284 | -------------------------------------------------------------------------------- /HTTP.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | # overview 4 | 5 | `WebService`的实现定义了Cobalt strike的具体uri所做的事情,相当于MVC里的Controller。 6 | 7 | ![](./img/HTTP/WebService_SubType.PNG) 8 | 9 | `WebService`的子类如上图,说明了Cobaltstrike的http server实现的功能 10 | 11 | `WebServer.WebListener`是一个接收已处理请求的接口 12 | 13 | `WebCalls`实现了ServerHook, WebServer.WebListener,其中WebListener的实现为添加一个weblog,然后广播给所有用户 14 | 15 | `beacon.BeaconHTTP`有两个内部类,`beacon.BeaconHTTP.PostHandler`和`beacon.BeaconHTTP.GetHandler`继承于`MalleableHook`,并且满足: 16 | 17 | - post负责接收结果,不返回任何数据 18 | - get负责发送任务,并使用对称加密task数据 19 | 20 | # 分析 21 | 22 | ## HTTP 23 | 24 | ### NanoHTTPD 25 | 26 | `cloudstrike.NanoHTTPD`是修改了部分代码的`NanoHTTPD` 27 | 28 | 主要逻辑很简单,新连接新建`HTTPSession`: 29 | 30 | `cloudstrike.NanoHTTPD#listen` 31 | 32 | ```java 33 | //... 34 | while (NanoHTTPD.this.alive) { 35 | try { 36 | final Socket temp = NanoHTTPD.this.ss.accept(); 37 | if (temp == null) { 38 | continue; 39 | } 40 | new HTTPSession(temp); 41 | //... 42 | ``` 43 | 44 | `HTTPSession`实现了`Runnable`,主要逻辑在`run`内: 45 | 46 | `cloudstrike.NanoHTTPD.HTTPSession#run` 47 | 48 | ```java 49 | // 空白字符分割第一段http报文 50 | final String request = this.readLine(in); 51 | final StringTokenizer st = new StringTokenizer((request == null) ? "" : request); 52 | // ... 53 | final String method = st.nextToken(); 54 | // ... 55 | String uri = st.nextToken(); 56 | final Properties header = new Properties(); 57 | final Properties parms = new Properties(); 58 | final int qmi = uri.indexOf(63); // 63 == '?' 59 | if (qmi >= 0) { 60 | header.put("QUERY_STRING", uri.substring(qmi + 1)); 61 | this.decodeParms(uri.substring(qmi + 1), parms); 62 | uri = this.decodePercent(uri.substring(0, qmi)); 63 | } 64 | else { 65 | uri = this.decodePercent(uri); 66 | } 67 | if (st.hasMoreTokens()) { 68 | // 这一段就是parse headers的过程,根据':'分割接下来的每行,放入header中去 69 | } 70 | if (method.equalsIgnoreCase("POST")) { // 如果是post额外进行下面的步骤 71 | long size = 0L; 72 | final String contentLength = header.getProperty("Content-Length"); 73 | // 根据content-length获取body的大小 74 | if (contentLength != null) { 75 | try { 76 | size = Integer.parseInt(contentLength); 77 | } 78 | catch (NumberFormatException ex) {} 79 | } 80 | if (size > 10485760L) { 81 | this.sendError("413 Entity Too Large", "BAD REQUEST: Request Entity Too Large"); 82 | } 83 | if (size > 0L) { 84 | // 根据上面获取的大小读取http报文剩下的内容 85 | final byte[] all = new byte[(int)size]; 86 | in.readFully(all, 0, (int)size); 87 | // 当Content-Type为application/octet-stream或者是事先注册的只接收原始字节流的uri 88 | if ("application/octet-stream".equals(header.getProperty("Content-Type")) || NanoHTTPD.this.alwaysRaw(uri)) { 89 | // 二进制数据两个键值:length和input 90 | final ByteArrayInputStream bis = new ByteArrayInputStream(all); 91 | parms.put("length", new Long(all.length)); 92 | parms.put("input", bis); 93 | } 94 | else { 95 | // 非二进制数据则假设是application/x-www-form-urlencoded,跟get query string一样解码放入parms 96 | this.decodeParms(new String(all), parms); 97 | } 98 | } 99 | } 100 | 101 | header.put("REMOTE_ADDRESS", this.mySocket.getInetAddress().toString()); 102 | final Response r = NanoHTTPD.this.serve(uri, method, header, parms); // 最后调用外部类的serve方法 103 | 104 | // ... 105 | ``` 106 | 107 | 上面一些方法的用途: 108 | 109 | * `cloudstrike.NanoHTTPD.HTTPSession#decodePercent`:就是urldecode 110 | * `cloudstrike.NanoHTTPD.HTTPSession#decodeParms`: 解析get query string,存入parms里,parms是Properties对象实际上就是一个HashTable 111 | 112 | `run`方法剩下的部分还有一点: 113 | 114 | ```java 115 | // 如果定义了过滤器,会根据调用过滤器 116 | if (NanoHTTPD.this.filter != null) { 117 | NanoHTTPD.this.filter.filterResponse(r); 118 | } 119 | ``` 120 | 121 | 122 | 123 | ### WebServer 124 | 125 | `cloudstrike.WebServer`继承了`NanoHTTPD` 126 | 127 | 字段的说明: 128 | 129 | ```java 130 | // Map 相当于路由中用来存放uri对应controller的映射 131 | // @key: uri 132 | // @val: 实现了特定功能WebService,比如ServeFile、StaticContent 133 | protected Map hooks; 134 | protected Map hooksSecondary; 135 | // Map 这里面存放着不需要parse body里的post参数的url,即raw url 136 | // @key: uri 137 | // @val: 始终是true 138 | protected Map always; 139 | // Map 140 | // @key: uri 141 | // @val: host 142 | protected Map hosts; 143 | // 每受到一个请求都会向该链表里的WebListener发该请求的结果,由于只有WebCalls实现了,因此可以理解为每个WebListener都会 144 | // 对该次请求产生weblog 145 | protected List weblisteners; 146 | ``` 147 | 148 | 149 | 150 | 上面分析到`run`方法最终会调用外部类的`serve`方法: 151 | 152 | ```java 153 | public Response serve(final String uri, final String method, final Properties header, final Properties param) { 154 | return this.handleRanges(method, header, this._serve(uri, method, header, param)); 155 | } 156 | ``` 157 | 158 | `serve`方法调用`_serve`方法 159 | 160 | 这个方法实际上是一个路由,根据不同的uri获取`WebService`,然后调用真正的`WebService` 161 | 162 | `cloudstrike.WebServer#_serve`做了下列的判断: 163 | 164 | * 符合这个判断直接返回404:`useragent.startsWith("lynx") || useragent.startsWith("curl") || useragent.startsWith("wget")` 165 | * `OPTIONS`请求返回`OPTIONS,GET,HEAD,POST` 166 | 167 | 然后进入路由判断: 168 | 169 | * `this.hooks`中有该uri,取出`WebService`,调用 170 | * `this.hooksSecondary`中有该uri,取出`WebService`,调用 171 | * `this.hooks`中有该`uri + '/'`,取出`WebService`,调用 172 | * `this.hooksSecondary`中有该`uri + '/'`,取出`WebService`,调用 173 | * uri开始是`http://`,如果`this.hooks`中有`proxy`,则调用,否则返回404 174 | 175 | 如果上面的都不满足,则进入判断默认的`stager uri`的流程。 176 | 177 | 先看它是如何判断是stager的uri的: 178 | 179 | `cloudstrike.WebServer` 180 | 181 | ```java 182 | 183 | public static long checksum8(String text) { 184 | if (text.length() < 4) { 185 | return 0L; 186 | } 187 | text = text.replace("/", ""); 188 | long sum = 0L; 189 | for (int x = 0; x < text.length(); ++x) { 190 | sum += text.charAt(x); 191 | } 192 | return sum % 256L; 193 | } 194 | 195 | public static boolean isStager(final String uri) { 196 | return checksum8(uri) == 92L; 197 | } 198 | 199 | public static boolean isStagerX64(final String uri) { 200 | return checksum8(uri) == 93L && uri.matches("/[A-Za-z0-9]{4}"); 201 | } 202 | 203 | public static boolean isStagerStrict(final String uri) { 204 | return isStager(uri) && uri.length() == 5; 205 | } 206 | 207 | public static boolean isStagerX64Strict(final String uri) { 208 | return isStagerX64(uri) && uri.length() == 5; 209 | } 210 | ``` 211 | 212 | 也就是我们常常看到默认设置下请求`stage`请求的uri是像这种`/AbCd`,就会进入到这里了。 213 | 214 | 然后就会根据上面这些方法来使用对应的`WebService`,如果不存在则返回404。 215 | 216 | 中间还会遍历`this.hooksSecondary`: 217 | 218 | `cloudstrike.WebServer#_serve` 219 | 220 | ```java 221 | // ... 222 | for (final Map.Entry e : this.hooksSecondary.entrySet()) { 223 | final WebService svc = e.getValue(); 224 | final String hook = e.getKey() + ""; 225 | // 如果uri开始部分是hooksSecondary的键,并且该WebService是模糊匹配 226 | if (uri.startsWith(hook) && svc.isFuzzy()) { 227 | return this.processResponse(uri, method, header, param, false, svc, svc.serve(uri.substring(hook.length()), method, header, param)); 228 | } 229 | } 230 | // ... 231 | ``` 232 | 233 | 当产生响应对象后会进入: 234 | 235 | ```java 236 | protected Response processResponse(final String uri, final String method, final Properties header, final Properties param, final boolean primary, final WebService service, final Response r) { 237 | String desc; 238 | if (service == null) { 239 | desc = null; 240 | } 241 | else { 242 | desc = service.getType() + " " + service.toString(); 243 | } 244 | final String resp = r.status; 245 | final long size = r.size; 246 | if (service == null || !service.suppressEvent(uri)) { 247 | // 如果service不为null并且会产生事件 248 | this.fireWebListener(uri, method, header, param, desc, primary, resp, size); 249 | } 250 | return r; 251 | } 252 | ``` 253 | 254 | `cloudstrike.WebServer#fireWebListener` 255 | 256 | ```java 257 | final Iterator i = this.weblisteners.iterator(); 258 | while (i.hasNext()) { 259 | i.next().receivedClient(uri, method, header, param, desc, primary, response, size); 260 | } 261 | ``` 262 | 263 | 遍历所有`WebListener`调用`receivedClient`方法 264 | 265 | 实践上`WebListener`的实现只有`WebCalls`,`WebCalls`的`receivedClient`就是记录weblog的过程。 266 | 267 | `processResponse`结束后response对象继续进入`handleRanges`方法,当条件符合`header.containsKey("Range") && "GET".equals(method) && original.size > 0L && original.data != null && "200 OK".equals(original.status)`,original就是Response对象,也就是会根据http协议规范里的定义,对返回结果进行处理,只返回相应范围的数据。 268 | 269 | ### Malleable Hook 270 | 271 | `c2profile.MalleableHook`是实现了`cloudstrike.WebService`的类,负责实现可自定义的c2 http的通信。 272 | 273 | `serve`方法是主要逻辑: 274 | 275 | ```java 276 | public Response serve(final String uri, final String method, final Properties headers, final Properties param) { 277 | try { 278 | final Response response = new Response("200 OK", null, (InputStream)null); 279 | this.profile.apply(this.key + ".server", response, this.hook.serve(uri, method, headers, param)); // 调用MyHook接口的serve方法,然后使用c2profile将response转换为c2profile里定义的格式 280 | return response; 281 | } 282 | catch (Exception ex) { 283 | ex.printStackTrace(); 284 | return new Response("500 Internal Server Error", "text/plain", "Oops... something went wrong"); 285 | } 286 | } 287 | ``` 288 | 289 | ### BeaconHTTP 290 | 291 | `beacon.BeaconHTTP`实现了MyHook接口,提供了Get和Post Handler。 292 | 293 | `beacon.BeaconHTTP.PostHandler` 294 | 295 | ```java 296 | private class PostHandler implements MalleableHook.MyHook 297 | { 298 | @Override 299 | public byte[] serve(final String uri, final String method, final Properties headers, final Properties param) { 300 | try { 301 | // 如果定义了trust_x_forwarded_for并且存在X-Forwarded-For,则从X-Forwarded-For中取得远程地址 302 | final String remoteAddress = ServerUtils.getRemoteAddress(BeaconHTTP.this.c2profile, headers); 303 | // 获取param中input键的流,实际上就是未parse的body,然后转换成string 304 | final String postedData = BeaconHTTP.this.getPostedData(param); 305 | // 获取beacon id 306 | final String s = new String(BeaconHTTP.this.c2profile.recover(".http-post.client.id", headers, param, postedData, uri)); 307 | if (s.length() == 0) { 308 | CommonUtils.print_error("HTTP " + method + " to " + uri + " from " + remoteAddress + " has no session ID! This could be an error (or mid-engagement change) in your c2 profile"); 309 | MudgeSanity.debugRequest(".http-post.client.id", headers, param, postedData, uri, remoteAddress); 310 | } 311 | else { 312 | // 根据c2profile的定义转换接受的数据 313 | final byte[] bytes = CommonUtils.toBytes(BeaconHTTP.this.c2profile.recover(".http-post.client.output", headers, param, postedData, uri)); 314 | // 进入真正处理beacon请求的方法,即beacon.BeaconC2#process_beacon_data 315 | if (bytes.length == 0 || !BeaconHTTP.this.controller.process_beacon_data(s, bytes)) { 316 | MudgeSanity.debugRequest(".http-post.client.output", headers, param, postedData, uri, remoteAddress); 317 | } 318 | } 319 | } 320 | catch (Exception ex) { 321 | MudgeSanity.logException("beacon post handler", ex, false); 322 | } 323 | return new byte[0]; // 只接受不返回任何数据 324 | } 325 | } 326 | ``` 327 | 328 | `beacon.BeaconHTTP.GetHandler` 329 | 330 | ```java 331 | public byte[] serve(final String s, final String s2, final Properties properties, final Properties properties2) { 332 | // 如果定义了trust_x_forwarded_for并且存在X-Forwarded-For,则从X-Forwarded-For中取得远程地址 333 | final String remoteAddress = ServerUtils.getRemoteAddress(BeaconHTTP.this.c2profile, properties); 334 | // 根据C2Profile转换metadata 335 | final String recover = BeaconHTTP.this.c2profile.recover(".http-get.client.metadata", properties, properties2, BeaconHTTP.this.getPostedData(properties2), s); 336 | if (recover.length() == 0 || recover.length() != 128) { 337 | CommonUtils.print_error("Invalid session id"); 338 | MudgeSanity.debugRequest(".http-get.client.metadata", properties, properties2, "", s, remoteAddress); 339 | return new byte[0]; 340 | } 341 | 342 | // parse metadata,然后获取对应的BeaconEntry 343 | final BeaconEntry process_beacon_metadata = BeaconHTTP.this.controller.process_beacon_metadata(remoteAddress, CommonUtils.toBytes(recover), null, 0); 344 | if (process_beacon_metadata == null) { 345 | MudgeSanity.debugRequest(".http-get.client.metadata", properties, properties2, "", s, remoteAddress); 346 | return new byte[0]; 347 | } 348 | 349 | // 获取该beacon队列中的task 350 | final byte[] dump = BeaconHTTP.this.controller.dump(process_beacon_metadata.getId(), 921600, 1048576); 351 | if (dump.length > 0) { 352 | // 对称加密task返回 353 | return BeaconHTTP.this.controller.getSymmetricCrypto().encrypt(process_beacon_metadata.getId(), dump); 354 | } 355 | return new byte[0]; 356 | } 357 | ``` 358 | 359 | 360 | 361 | 详细协议分析参考C2Protocol.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cobaltstrike homework 2 | 3 | 这些东西都是根据cobalt strike3.14分析而来,现在应该有点过时了(毕竟4.0出了) 4 | 5 | 项目内容仅限学习用途,对,我就是为了学malware分析才搞的 6 | 7 | ## 项目说明 8 | 9 | 该repo包含了这几天和@caijiji 聚聚分析cobaltstrike的相关记录,项目文件说明如下: 10 | 11 | ``` 12 | │ C2Profile.md 13 | │ C2Protocol.md c2协议的格式 14 | │ DNS.md 15 | │ HTTP.md 分析cobaltstrike http c2代码 16 | │ README.md 17 | │ tasks_flow.md 分析一个task从beacon console到task队列的过程 18 | │ tree.md cobaltstrike项目代码一些说明 19 | │ 20 | ├─img 21 | │ │ fake_beacon.PNG 22 | │ │ result_parser.PNG 23 | │ │ script_parse_tasks.PNG 24 | │ │ sendresult.PNG 25 | │ │ zheng.jpg 26 | │ │ 27 | │ └─HTTP 28 | │ WebService_SubType.PNG 29 | │ 30 | ├─scripts 随便写的代码 31 | │ define.py 定义了c2的command和result类型 32 | │ sendresult.py 会发送给teamserver一个’kiss my ass‘ result 33 | │ utils.py 会连接external c2,然后会输出从teamserver得到的task类型 34 | │ 35 | └─src 关于一些源码的分析 36 | ├─beacon 37 | │ BeaconC2.md 38 | │ BeaconData.md 39 | │ BeaconSetup.md 40 | │ CommandBuilder.md 41 | │ 42 | ├─common 43 | │ BeaconEntry.md 44 | │ 45 | ├─dns 46 | │ BaseSecurity.md 47 | │ 48 | └─server 49 | Resources.md 50 | ``` 51 | 52 | 53 | 54 | ## 脚本 55 | 56 | * define.py: 定义了发现的c2 command和result类型 57 | * utils.py: 连上cobaltstrike,自动导出public key,可以parse来自teamserver的task类型,稍微修改下就可以用来分析task组成结构,实现自定义beacon 58 | 59 | ![](img/fake_beacon.PNG) 60 | 61 | ![](img/script_parse_tasks.PNG) 62 | 63 | * sendresult.py: 发送个简单的消息 64 | 65 | ![](img/sendresult.PNG) 66 | 67 | 这些脚本都是连接external c2的,非dns、http 68 | 69 | ## 其他 70 | 71 | 听说大家都在搞cobaltstrike? 72 | 73 | ![](./img/zheng.jpg) 74 | 75 | ## 其他其他 76 | 77 | 本来想把自定义命令和c2profile搞完,可是搞了三天发现好像没什么意思了,算了不搞了 -------------------------------------------------------------------------------- /img/HTTP/WebService_SubType.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verctor/Cobalt_Homework/7314e593e88d59cc6b665a880f6d457349b8100a/img/HTTP/WebService_SubType.PNG -------------------------------------------------------------------------------- /img/fake_beacon.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verctor/Cobalt_Homework/7314e593e88d59cc6b665a880f6d457349b8100a/img/fake_beacon.PNG -------------------------------------------------------------------------------- /img/result_parser.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verctor/Cobalt_Homework/7314e593e88d59cc6b665a880f6d457349b8100a/img/result_parser.PNG -------------------------------------------------------------------------------- /img/script_parse_tasks.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verctor/Cobalt_Homework/7314e593e88d59cc6b665a880f6d457349b8100a/img/script_parse_tasks.PNG -------------------------------------------------------------------------------- /img/sendresult.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verctor/Cobalt_Homework/7314e593e88d59cc6b665a880f6d457349b8100a/img/sendresult.PNG -------------------------------------------------------------------------------- /img/zheng.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verctor/Cobalt_Homework/7314e593e88d59cc6b665a880f6d457349b8100a/img/zheng.jpg -------------------------------------------------------------------------------- /scripts/define.py: -------------------------------------------------------------------------------- 1 | command = { 2 | 'COMMAND_SPAWN' : 1, 3 | 'COMMAND_SHELL' : 2, 4 | 'COMMAND_DIE' : 3, 5 | 'COMMAND_SLEEP' : 4, 6 | 'COMMAND_CD' : 5, 7 | 'COMMAND_KEYLOG_START' : 6, 8 | 'COMMAND_KEYLOG_STOP' : 7, 9 | 'COMMAND_CHECKIN': 8, 10 | 'COMMAND_INJECT_PID' : 9, 11 | 'COMMAND_UPLOAD' : 10, 12 | 'COMMAND_DOWNLOAD': 11, 13 | 'COMMAND_EXECUTE': 12, 14 | 'COMMAND_SPAWN_PROC_X86' : 13, 15 | 'COMMAND_INJECT_PING' : 18, 16 | 'COMMAND_DOWNLOAD_CANCEL': 19, 17 | 'COMMAND_FORWARD_PIPE_DATA': 22, 18 | 'COMMAND_UNLINK': 23, 19 | 'COMMAND_PIPE_PONG': 24, 20 | 'COMMAND_GET_SYSTEM': 25, 21 | 'COMMAND_GETUID': 27, 22 | 'COMMAND_REV2SELF': 28, 23 | 'COMMAND_TIMESTOMP': 29, 24 | 'COMMAND_STEALTOKEN': 31, 25 | 'COMMAND_PS': 32, 26 | 'COMMAND_KILL': 33, 27 | 'COMMAND_KerberosTicketUse': 34, 28 | 'COMMAND_Kerberos_Ticket_Purge': 35, 29 | 'COMMAND_POWERSHELL_IMPORT': 37, 30 | 'COMMAND_RUNAS': 38, 31 | 'COMMAND_PWD': 39, 32 | 'COMMAND_JOB_REGISTER' : 40, 33 | 'COMMAND_JOBS': 41, 34 | 'COMMAND_JOB_KILL': 42, 35 | 'COMMAND_INJECTX64_PID' : 43, 36 | 'COMMAND_SPAWNX64' : 44, 37 | 'COMMAND_VNC_INJECT': 45, 38 | 'COMMAND_VNC_INJECT_X64': 46, 39 | 'COMMAND_PAUSE': 47, 40 | 'COMMAND_IPCONFIG': 48, 41 | 'COMMAND_MAKE_TOKEN': 49, 42 | 'COMMAND_PORT_FORWARD': 50, 43 | 'COMMAND_PORT_FORWARD_STOP': 51, 44 | 'COMMAND_BIND_STAGE': 52, 45 | 'COMMAND_LS': 53, 46 | 'COMMAND_MKDIR': 54, 47 | 'COMMAND_DRIVERS': 55, 48 | 'COMMAND_RM': 56, 49 | 'COMMAND_STAGE_REMOTE_SMB': 57, 50 | 'COMMAND_START_SERVICE': 58, # not sure 51 | 'COMMAND_HTTPHOSTSTRING': 59, 52 | 'COMMAND_OPEN_PIPE': 60, 53 | 'COMMAND_CLOSE_PIPE': 61, 54 | 'COMMAND_JOB_REGISTER_IMPERSONATE' : 62, 55 | 'COMMAND_SPAWN_POWERSHELLX86' : 63, 56 | 'COMMAND_SPAWN_POWERSHELLX64' : 64, 57 | 'COMMAND_INJECT_POWERSHELLX86_PID' : 65, 58 | 'COMMAND_INJECT_POWERSHELLX64_PID' : 66, 59 | 'COMMAND_UPLOAD_CONTINUE' : 67, 60 | 'COMMAND_PIPE_OPEN_EXPLICIT' : 68, 61 | 'COMMAND_SPAWN_PROC_X64' : 69, 62 | 'COMMAND_JOB_SPAWN_X86' : 70, 63 | 'COMMAND_JOB_SPAWN_X64' : 71, 64 | 'COMMAND_SETENV' : 72, 65 | 'COMMAND_FILE_COPY' : 73, 66 | 'COMMAND_FILE_MOVE' : 74, 67 | 'COMMAND_PPID' : 75, 68 | 'COMMAND_RUN_UNDER_PID' : 76, 69 | 'COMMAND_GETPRIVS' : 77, 70 | 'COMMAND_EXECUTE_JOB' : 78, 71 | 'COMMAND_PSH_HOST_TCP' : 79, 72 | 'COMMAND_DLL_LOAD' : 80, 73 | 'COMMAND_REG_QUERY' : 81, 74 | 'COMMAND_LSOCKET_TCPPIVOT' : 82, 75 | 'COMMAND_ARGUE_ADD' : 83, 76 | 'COMMAND_ARGUE_REMOVE' : 84, 77 | 'COMMAND_ARGUE_LIST' : 85, 78 | 'COMMAND_TCP_CONNECT' : 86, 79 | 'COMMAND_JOB_SPAWN_TOKEN_X86' : 87, 80 | 'COMMAND_JOB_SPAWN_TOKEN_X64' : 88, 81 | 'COMMAND_SPAWN_TOKEN_X86' : 89, 82 | 'COMMAND_SPAWN_TOKEN_X64' : 90, 83 | 'COMMAND_INJECTX64_PING' : 91, 84 | 'COMMAND_BLOCKDLLS' : 92, 85 | } 86 | 87 | result = { 88 | 'CALLBACK_OUTPUT' : 0, 89 | 'CALLBACK_KEYSTROKES' : 1, 90 | 'CALLBACK_FILE' : 2, 91 | 'CALLBACK_SCREENSHOT' : 3, 92 | 'CALLBACK_CLOSE' : 4, 93 | 'CALLBACK_READ' : 5, 94 | 'CALLBACK_CONNECT' : 6, 95 | 'CALLBACK_PING' : 7, 96 | 'CALLBACK_FILE_WRITE' : 8, 97 | 'CALLBACK_FILE_CLOSE' : 9, 98 | 'CALLBACK_PIPE_OPEN' : 10, 99 | 'CALLBACK_PIPE_CLOSE' : 11, 100 | 'CALLBACK_PIPE_READ' : 12, 101 | 'CALLBACK_POST_ERROR' : 13, 102 | 'CALLBACK_PIPE_PING' : 14, 103 | 'CALLBACK_TOKEN_STOLEN' : 15, 104 | 'CALLBACK_TOKEN_GETUID' : 16, 105 | 'CALLBACK_PROCESS_LIST' : 17, 106 | 'CALLBACK_POST_REPLAY_ERROR' : 18, 107 | 'CALLBACK_PWD' : 19, 108 | 'CALLBACK_JOBS' : 20, 109 | 'CALLBACK_HASHDUMP' : 21, 110 | 'CALLBACK_PENDING' : 22, 111 | 'CALLBACK_ACCEPT' : 23, 112 | 'CALLBACK_NETVIEW' : 24, 113 | 'CALLBACK_PORTSCAN' : 25, 114 | 'CALLBACK_DEAD' : 26, 115 | 'CALLBACK_SSH_STATUS' : 27, 116 | 'CALLBACK_CHUNK_ALLOCATE' : 28, 117 | 'CALLBACK_CHUNK_SEND' : 29, 118 | 'CALLBACK_OUTPUT_OEM' : 30, 119 | 'CALLBACK_ERROR' : 31, 120 | 'CALLBACK_OUTPUT_UTF8' : 32 121 | } 122 | 123 | command_rev = {} 124 | for k, v in command.items(): 125 | command_rev[v] = k 126 | 127 | result_rev = {} 128 | for k, v in result.items(): 129 | result_rev[v] = k -------------------------------------------------------------------------------- /scripts/sendresult.py: -------------------------------------------------------------------------------- 1 | import utils 2 | import define 3 | 4 | def send_output_result(): 5 | s = utils.test_metadata() 6 | utils.recv_frame(s) 7 | 8 | result = utils.p32_b(define.result['CALLBACK_OUTPUT']) 9 | result += 'kIss My AsS' 10 | 11 | while raw_input('>') == 'y': 12 | print('Send result: ' + repr(result)) 13 | print('\n ---------------- \n') 14 | enc = utils.bs_encrypt(result) 15 | 16 | results = utils.p32_b(len(enc)) + enc 17 | utils.send_frame(s, results) 18 | utils.recv_frame(s) 19 | 20 | if __name__ == '__main__': 21 | send_output_result() -------------------------------------------------------------------------------- /scripts/utils.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import struct 3 | import base64 4 | import hashlib 5 | import time 6 | 7 | import define 8 | 9 | from datetime import datetime 10 | from Crypto.PublicKey import RSA 11 | from Crypto import Cipher 12 | from Crypto.Hash import SHA256 13 | from Crypto.Hash import HMAC 14 | 15 | 16 | iv = 'abcdefghijklmnop' 17 | sk = '1234567890abcdef' 18 | 19 | empty = '\x00' 20 | 21 | def sha256(s): 22 | sha = hashlib.sha256() 23 | sha.update(s) 24 | return sha.digest() 25 | 26 | session_key = sha256(sk)[:16] 27 | hash_key = sha256(sk)[16:] 28 | 29 | def p16(i): 30 | return struct.pack('I', i) 34 | 35 | def b32_b(p): 36 | return struct.unpack('>I', p)[0] 37 | 38 | def metadata(): 39 | md = sk # session key 40 | md += p16(936) # ansi code page 41 | md += p16(936) # oem code page 42 | md += '23333\t' # beacon id 43 | md += '6666\t' # pid 44 | md += '6.1\t' # version 45 | md += '233.233.233.233\t' # internal address 46 | md += 'FakeComp\t' # computer name 47 | md += 'FakeUserButAdmin *\t' # username 48 | md += '1\t' # os is x64 system 49 | md += '1\t' # beacon is x64 process 50 | 51 | return md 52 | 53 | def aes_enc(plain, k, iv): 54 | aes = Cipher.AES.new(k, Cipher.AES.MODE_CBC, iv) 55 | return aes.encrypt(plain) 56 | 57 | def aes_dec(ct, k, iv): 58 | aes = Cipher.AES.new(k, Cipher.AES.MODE_CBC, iv) 59 | return aes.decrypt(ct) 60 | 61 | def bs_encrypt(plain): 62 | p = p32_b(int(time.time())) 63 | p += p32_b(len(plain)) 64 | p += plain 65 | 66 | if len(p) % 16 != 0: 67 | p += 'a' * (16 - (len(p) % 16)) 68 | 69 | ct = aes_enc(p, session_key, iv) 70 | return ct + HMAC.new(hash_key, ct, digestmod=SHA256).digest()[:16] 71 | 72 | def bs_decrypt(ct): 73 | ct = ct[:-16] # ignore hmac 74 | dec = aes_dec(ct, session_key, iv) 75 | 76 | timestamp = b32_b(dec[:4]) 77 | data_len = b32_b(dec[4:8]) 78 | return timestamp, dec[8: 8 + data_len] 79 | 80 | def fake_rsa_pkcs1(rsa, plain): 81 | if len(plain) > 117: 82 | raise Exception('too big') 83 | 84 | pad_len = 117 - len(plain) 85 | p = '\x00\x02' + 'a' * 8 + 'a' * pad_len + '\x00' + plain 86 | return rsa.encrypt(p, 1)[0] 87 | 88 | def recv_frame(sock): 89 | try: 90 | chunk = sock.recv(4) 91 | except: 92 | return("") 93 | if len(chunk) < 4: 94 | return() 95 | slen = struct.unpack(' 117) { 91 | System.err.println("Length field check failed :( [RSA decrypt]"); 92 | return new byte[0]; 93 | } 94 | final byte[] b = new byte[int1]; 95 | // 根据长度读入剩下的字节 96 | dataInputStream.readFully(b, 0, int1); 97 | return b; 98 | } 99 | catch (Exception ex) { 100 | MudgeSanity.logException("RSA decrypt", ex, false); 101 | return new byte[0]; 102 | } 103 | } 104 | ``` 105 | 106 | 对称加密使用的是`dns.QuickSecurity`类,继承于`dns.BaseSecurity`,该类的字段定义如下: 107 | 108 | ```java 109 | // 即QuickSecurity的getCryptoScheme方法返回的值的定义,如果是trial版那么为1,表示不加密 110 | public static final short CRYPTO_LICENSED_PRODUCT = 0; 111 | public static final short CRYPTO_TRIAL_PRODUCT = 1; 112 | // iv 永远是abcdefghijklmnop 113 | protected IvParameterSpec ivspec; 114 | // "AES/CBC/NoPadding" 115 | protected Cipher in; 116 | // "AES/CBC/NoPadding" 117 | protected Cipher out; 118 | // mac instance,初始化为HMACSHA256 119 | protected Mac mac; 120 | // Map 121 | // @key: beacon id 122 | // @val: Session类,保存着session key,hash key和一个判断重放的counter 123 | protected static Map keymap; 124 | ``` 125 | 126 | 再看这个类`registerKey`的过程: 127 | 128 | `dns.BaseSecurity#registerKey` 129 | 130 | ```java 131 | public void registerKey(final String bid, final byte[] input) { 132 | if (BaseSecurity.keymap.containsKey(bid)) { 133 | return; 134 | } 135 | try { 136 | final byte[] digest = MessageDigest.getInstance("SHA-256").digest(input); 137 | final byte[] copyOfRange = Arrays.copyOfRange(digest, 0, 16); 138 | final byte[] copyOfRange2 = Arrays.copyOfRange(digest, 16, 32); 139 | final Session session = new Session(); 140 | session.key = new SecretKeySpec(copyOfRange, "AES"); 141 | session.hash_key = new SecretKeySpec(copyOfRange2, "HmacSHA256"); 142 | BaseSecurity.keymap.put(bid, session); 143 | } 144 | catch (Exception ex) { 145 | ex.printStackTrace(); 146 | } 147 | ``` 148 | 149 | 从metadata取得bid和用来生成session key的16字节初始密码后使用SHA256生成32字节的摘要,然后前16字节作为aes加密密钥,后16字节作为HMAC的hash密钥。 150 | 151 | 对称加密相关的分析参考`src/BaseSecurity.md` 152 | 153 | ## metadata process 154 | 155 | `beacon.BeaconC2#process_beacon_metadata` 156 | 157 | ```java 158 | public BeaconEntry process_beacon_metadata(final String externalAddr, final byte[] array, final String s2, final int n) { 159 | // RSA1024解密metadata 160 | final byte[] decrypt = this.getAsymmetricCrypto().decrypt(array); 161 | if (decrypt == null || decrypt.length == 0) { 162 | CommonUtils.print_error("decrypt of metadata failed"); 163 | return null; 164 | } 165 | final String bString = CommonUtils.bString(decrypt); 166 | // metadata开始的16字节是session key,之后用来生成正式的session key和hash key 167 | final String session_key = bString.substring(0, 16); 168 | // ansi代码页,windows gui使用的代码页 169 | final String name = WindowsCharsets.getName(CommonUtils.toShort(bString.substring(16, 18))); 170 | // oem代码页,windows console使用的代码页 171 | final String name2 = WindowsCharsets.getName(CommonUtils.toShort(bString.substring(18, 20))); 172 | // 根据matadata new一个BeaconEntry对象,这个代表一个有beacon存在的目标 173 | final BeaconEntry obj = new BeaconEntry(decrypt, name, externalAddr); 174 | // BeaconEntry会做metadata合法性校验,校验通过那么就继续 175 | if (!obj.sane()) { 176 | CommonUtils.print_error("Session " + obj + " metadata validation failed. Dropping"); 177 | return null; 178 | } 179 | // 注册字符集 180 | this.getCharsets().register(obj.getId(), name, name2); 181 | // s2表示parent bid的意思,代表这个beacon是某个beacon的子beacon,n代表link类型 182 | if (s2 != null) { 183 | obj.link(s2, n); 184 | } 185 | // 注册session key 186 | this.getSymmetricCrypto().registerKey(obj.getId(), CommonUtils.toBytes(session_key)); 187 | // 调用server.Beacons#checkin,如果是新的beacon回连,会做一系列初始化操作并写eventlog 188 | if (this.getCheckinListener() != null) { 189 | this.getCheckinListener().checkin(obj); 190 | } 191 | else { 192 | CommonUtils.print_stat("Checkin listener was NULL (this is good!)"); 193 | } 194 | return obj; 195 | ``` 196 | 197 | 关于`common.BeaconEntry`,这个类定义了metadata的相关格式,具体分析参考`src/BeaconEntry.md`。 198 | 199 | ## dump tasks 200 | 201 | 通过调用`beacon.BeaconC2#dump(java.lang.String, int, int)`方法可以获取要发送给该beacon的任务,即command 202 | 203 | ```java 204 | public byte[] dump(final String bid, final int socksSize, final int threshold) { 205 | return this.dump(bid, socksSize, threshold, new LinkedHashSet()); 206 | } 207 | ``` 208 | 209 | `beacon.BeaconC2#dump(java.lang.String, int, int, java.util.HashSet)` 210 | 211 | ```java 212 | public byte[] dump(final String bid, final int size, final int threshold, final HashSet set) { 213 | // 用来判断是否重复dump beacon的task,因为下面会有递归dump子节点的task的操作 214 | if (!AssertUtils.TestUnique(bid, set)) { 215 | return new byte[0]; 216 | } 217 | set.add(bid); 218 | 219 | // 取得该beacon的tasks,返回的byte数组大小小于threshold 220 | final byte[] tasks = this.data.dump(bid, threshold); 221 | final int length = tasks.length; 222 | // 该beacon如果运行了代理,还会从teamserver获取socks代理的数据 223 | final byte[] dump2 = this.socks.dump(bid, size - tasks.length); 224 | int total = length + dump2.length; 225 | try { 226 | final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(size); 227 | if (tasks.length > 0) { 228 | byteArrayOutputStream.write(tasks, 0, tasks.length); 229 | } 230 | if (dump2.length > 0) { 231 | byteArrayOutputStream.write(dump2, 0, dump2.length); 232 | } 233 | 234 | // 这一段这么长实际上就是看这个beacon是不是连接了smb beacon,如果有连接的,那么就获取子beacon 235 | // 的task,在不超过大小限制的情况下也获取相关socks代理的数据发送过去 236 | final Iterator iterator = (Iterator)this.pipes.children(bid).iterator(); 237 | while (iterator.hasNext()) { 238 | final String childbid = iterator.next() + ""; 239 | if (total < size && this.getSymmetricCrypto().isReady(childbid)) { 240 | final byte[] childDumpTask = this.dump(childbid, size - total, threshold - total, set); 241 | if (childDumpTask.length > 0) { 242 | final byte[] encrypt = this.getSymmetricCrypto().encrypt(childbid, childDumpTask); 243 | final CommandBuilder commandBuilder = new CommandBuilder(); 244 | commandBuilder.setCommand(22); 245 | commandBuilder.addInteger(Integer.parseInt(childbid)); 246 | commandBuilder.addString(encrypt); 247 | final byte[] build = commandBuilder.build(); 248 | byteArrayOutputStream.write(build, 0, build.length); 249 | total += build.length; 250 | } 251 | else { 252 | if (this.socks.isActive(childbid) || !this.downloads.isActive(childbid)) {} 253 | final CommandBuilder commandBuilder2 = new CommandBuilder(); 254 | commandBuilder2.setCommand(22); 255 | commandBuilder2.addInteger(Integer.parseInt(childbid)); 256 | final byte[] build2 = commandBuilder2.build(); 257 | byteArrayOutputStream.write(build2, 0, build2.length); 258 | total += build2.length; 259 | } 260 | } 261 | } 262 | byteArrayOutputStream.flush(); 263 | byteArrayOutputStream.close(); 264 | final byte[] byteArray = byteArrayOutputStream.toByteArray(); 265 | if (tasks.length > 0) { 266 | // 输出到beaconlog里 267 | this.getCheckinListener().output(BeaconOutput.Checkin(bid, "host called home, sent: " + byteArray.length + " bytes")); 268 | } 269 | return byteArray; 270 | // ... 271 | ``` 272 | 273 | dump出的tasks之后还会通过加密,最后发送给beacon。 274 | 275 | ## parse result 276 | 277 | 跟task一样,beacon也能同时返回多个result 278 | 279 | `beacon.BeaconC2#process_beacon_data` 280 | 281 | ```java 282 | public boolean process_beacon_data(final String bid, final byte[] result) { 283 | try { 284 | final DataInputStream dataInputStream = new DataInputStream(new ByteArrayInputStream(result)); 285 | while (dataInputStream.available() > 0) { 286 | // 读取一个result长度 287 | final int len = dataInputStream.readInt(); 288 | if (len > dataInputStream.available()) { 289 | CommonUtils.print_error("Beacon " + bid + " response length " + len + " exceeds " + dataInputStream.available() + " available bytes. [Received " + result.length + " bytes]"); 290 | return false; 291 | } 292 | if (len <= 0) { 293 | CommonUtils.print_error("Beacon " + bid + " response length " + len + " is invalid. [Received " + result.length + " bytes]"); 294 | return false; 295 | } 296 | final byte[] array2 = new byte[len]; 297 | // 读出改result 298 | dataInputStream.read(array2, 0, len); 299 | this.process_beacon_callback(bid, array2); 300 | } 301 | dataInputStream.close(); 302 | return true; 303 | 304 | ``` 305 | 306 | 可以看到每个result都是用`process_beacon_callback`方法处理的 307 | 308 | `beacon.BeaconC2#process_beacon_callback` 309 | 310 | ```java 311 | public void process_beacon_callback(final String s, final byte[] array) { 312 | this.process_beacon_callback_decrypted(s, this.getSymmetricCrypto().decrypt(s, array)); 313 | } 314 | ``` 315 | 316 | 实际就是个解密的过程,解密的分析参考`src/dns/BaseSecurity.md` 317 | 318 | `beacon.BeaconC2#process_beacon_callback_decrypted` 319 | 320 | ```java 321 | public void process_beacon_callback_decrypted(final String bid, final byte[] result) { 322 | //... 323 | try { 324 | final DataInputStream dataInputStream = new DataInputStream(new ByteArrayInputStream(result)); 325 | // 读取result的类型,cobaltstrike管这个叫job= = 326 | job_type = dataInputStream.readInt(); 327 | // 下面就是各个不同的Job类型的处理 328 | if (job_type == Job.CALLBACK_OUTPUT) { 329 | // 该类型就是直接输出到beacon console的窗口,输出前先根据metadata获取的ansi code page解码,这样就能适应各种语言的系统了 330 | final String process = this.getCharsets().process(bid, CommonUtils.readAll(dataInputStream)); 331 | this.getCheckinListener().output(BeaconOutput.Output(bid, "received output:\n" + process)); 332 | // 依次调用注册的parser去处理结果 333 | this.runParsers(process, bid, job_type); 334 | } 335 | // 下面是同样的各种分支 336 | // ... 337 | else { 338 | // 这里稍微有点不同 339 | // 检查这个beacon之前是否下发过task,如果没有,不允许处理下面这些类型的result 340 | if (this.data.isNewSession(bid)) { 341 | this.getCheckinListener().output(BeaconOutput.Error(bid, "Dropped responses from session. Didn't expect " + job_type + " prior to first task.")); 342 | CommonUtils.print_error("Dropped responses from session " + bid + " [type: " + job_type + "] (no interaction with this session yet)"); 343 | return; 344 | // 下发过task后才能处理的分支 345 | ``` 346 | 347 | 上面比较重要的是这一步`this.runParsers(process, bid, job_type);` 348 | 349 | cobaltstrike注册的parser有: 350 | 351 | ![](../../img/result_parser.PNG) 352 | 353 | 这些parser会处理相应的结果,比如`MimikatzCredentials`会提取hash然后存到credentials -------------------------------------------------------------------------------- /src/beacon/BeaconData.md: -------------------------------------------------------------------------------- 1 | # overview 2 | 3 | `beacon.BeaconData`实现了下发beacon任务进任务队列的相关逻辑 4 | 5 | 属性字段如下: 6 | 7 | ```java 8 | // beacon出口的通信类型 9 | public static final int MODE_HTTP = 0; 10 | public static final int MODE_DNS = 1; 11 | public static final int MODE_DNS_TXT = 2; 12 | public static final int MODE_DNS6 = 3; 13 | // Map 包含了每个beacon对应的task队列 14 | // @key: beacon id 15 | // @val: task队列,链表类型 16 | protected Map queues; 17 | // Map 包含了beacon对应的通信出口类型 18 | protected Map modes; 19 | // 存放有task的beacon的id 20 | protected Set tasked; 21 | // 用来遏制trial版使用的值 22 | protected boolean shouldPad; 23 | // 啥时候遏制trial版使用 24 | protected long when; 25 | ``` 26 | 27 | 由于该分析是反编译cobaltstrike3.14,所以现在看来未免有些落伍了,因为cobaltstrike 4.0已经取消了trial版,上述字段也应该会删掉这些无用的东西了= = 28 | 29 | # 分析 30 | 31 | ## 下发task 32 | 33 | `beacon.BeaconData#task`是用来将build好的二进制的task数据放入队列中,下一次beacon callback时,就会发送出去。 34 | 35 | ```java 36 | public void task(final String bid, final byte[] array) { 37 | synchronized (this) { 38 | // 获取对应队列 39 | final List queue = this.getQueue(bid); 40 | // 如果这是trial版/检测出crack版,并且当前时间大于teamserver启动时间+1800000ms,即30分钟 41 | if (this.shouldPad && System.currentTimeMillis() > this.when) { 42 | final CommandBuilder commandBuilder = new CommandBuilder(); 43 | // 3 == COMMAND_DIE,也就是结束beacon 44 | commandBuilder.setCommand(3); 45 | commandBuilder.addString(array); 46 | // 添加一个exit任务 47 | queue.add(commandBuilder.build()); 48 | } 49 | else { 50 | // 直接添加任务 51 | queue.add(array); 52 | } 53 | // 将bid添加进有task的beacon id的set中 54 | this.tasked.add(bid); 55 | } 56 | } 57 | ``` 58 | 59 | 60 | 61 | ## 导出task 62 | 63 | `beacon.BeaconData#dump` 64 | 65 | ```java 66 | public byte[] dump(final String bid, final int maxSize) { 67 | synchronized (this) { 68 | int total = 0; 69 | // 获取该beacon的task列表 70 | final List queue = this.getQueue(bid); 71 | if (queue.size() == 0) { 72 | return new byte[0]; 73 | } 74 | final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(8192); 75 | final Iterator iterator = queue.iterator(); 76 | // 循环获取列表里的task 77 | while (iterator.hasNext()) { 78 | final byte[] b = iterator.next(); 79 | // 加上这个task的大小不会超过最大大小 80 | if (total + b.length < maxSize) { 81 | // 写入流中 82 | byteArrayOutputStream.write(b, 0, b.length); 83 | // 从列表中去掉这个task 84 | iterator.remove(); 85 | total += b.length; 86 | } 87 | else { 88 | // 加上这个task大小大于等于maxSize时,并且该task大小不大于maxSize 89 | if (b.length < maxSize) { 90 | CommonUtils.print_warn("Chunking tasks for " + bid + "! " + b.length + " + " + total + " past threshold. " + queue.size() + " task(s) on hold until next checkin."); 91 | break; 92 | } 93 | // task长度过大直接删掉这个task 94 | CommonUtils.print_error("Woah! Task " + b.length + " for " + bid + " is beyond our limit. Dropping it"); 95 | iterator.remove(); 96 | } 97 | } 98 | return byteArrayOutputStream.toByteArray(); 99 | } 100 | } 101 | 102 | ``` 103 | 104 | 105 | 106 | ## 过时的trial分析 107 | 108 | 上面的`shouldPad`实际上就是下面的方法的判断 109 | 110 | `beacon.BeaconC2#isPaddingRequired` 111 | 112 | ```java 113 | protected boolean isPaddingRequired() { 114 | boolean b = false; 115 | try { 116 | final ZipFile zipFile = new ZipFile(this.appd); 117 | final Enumeration entries = zipFile.entries(); 118 | while (entries.hasMoreElements()) { 119 | final ZipEntry zipEntry = (ZipEntry)entries.nextElement(); 120 | final long checksum8 = CommonUtils.checksum8(zipEntry.getName()); 121 | final long n = zipEntry.getName().length(); 122 | // resources/authkey.pub 是否被修改 123 | if (checksum8 == 75L && n == 21L) { 124 | if (zipEntry.getCrc() == 1661186542L || zipEntry.getCrc() == 1309838793L) { 125 | continue; 126 | } 127 | b = true; 128 | } 129 | // 检测common/License.class是否被修改 130 | else if (checksum8 == 144L && n == 20L) { 131 | if (zipEntry.getCrc() == 1701567278L || zipEntry.getCrc() == 3030496089L) { 132 | continue; 133 | } 134 | b = true; 135 | } 136 | else { 137 | // 检测common/Authoriaztion.class是否被修改 138 | if (checksum8 != 62L || n != 26L || zipEntry.getCrc() == 2913634760L || zipEntry.getCrc() == 376142471L) { 139 | continue; 140 | } 141 | b = true; 142 | } 143 | } 144 | zipFile.close(); 145 | } 146 | catch (Throwable t) {} 147 | return b; 148 | } 149 | ``` 150 | 151 | 当判断了这三个文件被修改后,shouldPad就会为true了,进而引起了下发任务的逻辑变动。 -------------------------------------------------------------------------------- /src/beacon/BeaconSetup.md: -------------------------------------------------------------------------------- 1 | # overview 2 | 3 | package beacon.BeaconSetup拥有以下字段 4 | 5 | ```java 6 | protected WebCalls web; 7 | protected DNSServer dns; 8 | protected Profile c2profile; 9 | protected BeaconC2 handlers; 10 | protected BeaconData data; 11 | protected String error; 12 | protected Resources resources; 13 | protected Map c2info; 14 | protected MalleablePE pe; 15 | ``` 16 | 17 | 还有静态字段: 18 | 19 | ```java 20 | public static final int SETTING_PROTOCOL = 1; 21 | public static final int SETTING_PORT = 2; 22 | public static final int SETTING_SLEEPTIME = 3; 23 | public static final int SETTING_MAXGET = 4; 24 | public static final int SETTING_JITTER = 5; 25 | public static final int SETTING_MAXDNS = 6; 26 | public static final int SETTING_PUBKEY = 7; 27 | public static final int SETTING_DOMAINS = 8; 28 | public static final int SETTING_USERAGENT = 9; 29 | public static final int SETTING_SUBMITURI = 10; 30 | public static final int SETTING_C2_RECOVER = 11; 31 | public static final int SETTING_C2_REQUEST = 12; 32 | public static final int SETTING_C2_POSTREQ = 13; 33 | public static final int SETTING_SPAWNTO = 14; 34 | public static final int SETTING_PIPENAME = 15; 35 | public static final int DEPRECATED_SETTING_KILLDATE_YEAR = 16; 36 | public static final int DEPRECATED_SETTING_KILLDATE_MONTH = 17; 37 | public static final int DEPRECATED_SETTING_KILLDATE_DAY = 18; 38 | public static final int SETTING_DNS_IDLE = 19; 39 | public static final int SETTING_DNS_SLEEP = 20; 40 | public static final int SETTING_SSH_HOST = 21; 41 | public static final int SETTING_SSH_PORT = 22; 42 | public static final int SETTING_SSH_USERNAME = 23; 43 | public static final int SETTING_SSH_PASSWORD = 24; 44 | public static final int SETTING_SSH_KEY = 25; 45 | public static final int SETTING_C2_VERB_GET = 26; 46 | public static final int SETTING_C2_VERB_POST = 27; 47 | public static final int SETTING_C2_CHUNK_POST = 28; 48 | public static final int SETTING_SPAWNTO_X86 = 29; 49 | public static final int SETTING_SPAWNTO_X64 = 30; 50 | public static final int SETTING_CRYPTO_SCHEME = 31; 51 | public static final int SETTING_PROXY_CONFIG = 32; 52 | public static final int SETTING_PROXY_USER = 33; 53 | public static final int SETTING_PROXY_PASSWORD = 34; 54 | public static final int SETTING_PROXY_BEHAVIOR = 35; 55 | public static final int DEPRECATED_SETTING_INJECT_OPTIONS = 36; 56 | public static final int SETTING_WATERMARK = 37; 57 | public static final int SETTING_CLEANUP = 38; 58 | public static final int SETTING_CFG_CAUTION = 39; 59 | public static final int SETTING_KILLDATE = 40; 60 | public static final int SETTING_GARGLE_NOOK = 41; 61 | public static final int SETTING_GARGLE_SECTIONS = 42; 62 | public static final int SETTING_PROCINJ_PERMS_I = 43; 63 | public static final int SETTING_PROCINJ_PERMS = 44; 64 | public static final int SETTING_PROCINJ_MINALLOC = 45; 65 | public static final int SETTING_PROCINJ_TRANSFORM_X86 = 46; 66 | public static final int SETTING_PROCINJ_TRANSFORM_X64 = 47; 67 | public static final int DEPRECATED_SETTING_PROCINJ_ALLOWED = 48; 68 | public static final int SETTING_BINDHOST = 49; 69 | public static final int SETTING_HTTP_NO_COOKIES = 50; 70 | public static final int SETTING_PROCINJ_EXECUTE = 51; 71 | public static final int SETTING_PROCINJ_ALLOCATOR = 52; 72 | public static final int SETTING_PROCINJ_STUB = 53; 73 | ``` 74 | 75 | 静态字段定义了beacon的stage,即beacon核心逻辑Dll的配置。 76 | 77 | 初始化过程: 78 | 79 | ```java 80 | public BeaconSetup(final Resources resources) { 81 | this.web = null; 82 | this.dns = null; 83 | this.c2profile = null; 84 | this.handlers = null; 85 | this.data = new BeaconData(); 86 | this.error = ""; 87 | this.c2info = null; 88 | this.pe = null; 89 | this.resources = resources; 90 | this.web = ServerUtils.getWebCalls(resources); 91 | this.c2profile = ServerUtils.getProfile(resources); 92 | this.pe = new MalleablePE(this.c2profile); 93 | this.handlers = new BeaconC2(this.c2profile, this.data, resources); 94 | } 95 | ``` 96 | 97 | -------------------------------------------------------------------------------- /src/common/BeaconEntry.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | 定义了beacon的metadata和校验的逻辑 4 | 5 | metadata主要包括以下字段: 6 | 7 | | 名称 | 含义 | 偏移 | 8 | | -------------- | ------------------------------------------------------------ | -------- | 9 | | session key | 生成AES128加密密钥和HMACSHA256 hash密钥的密码 | [0, 16) | 10 | | ANSI code page | windows gui所用的代码页 | [16, 18) | 11 | | OEM code page | windows console所用的代码页 | [18, 20) | 12 | | id | beacon id | 0 | 13 | | pid | beacon进程的pid | 1 | 14 | | ver | 用来表示系统版本和ssh版本 | 2 | 15 | | intz | 内网地址 | 3 | 16 | | comp | computer name | 4 | 17 | | user | 创建beacon进程的用户,如果末尾加上了`[空格]*`表示是管理员并且完整性级别最高 | 5 | 18 | | is64 | beacon是否在x64系统上 | 6 | 19 | | barch | beacon进程是否是64位进程 | 7 | 20 | | port | ssh port | 8 | 21 | 22 | 上面表格表示的偏移形如`[0, 16)`,表示metadata二进制数据起始偏移和终止偏移 23 | 24 | 上面表格表示的偏移形如`0`指metadata根据`\t`分割字符串后的数组索引 25 | 26 | metadata的各类字段以上面偏移为说明,如果偏移5的字段不存在(分割字符串后数组索引为5),那么之后的字段也就不会读取了。 27 | 28 | # 分析 29 | 30 | ## 构造函数 31 | 32 | `common.BeaconEntry#BeaconEntry` 33 | 34 | ```java 35 | public BeaconEntry(final byte[] original, final String chst, final String ext) { 36 | this.id = ""; 37 | this.pid = ""; 38 | this.ver = ""; 39 | this.intz = ""; 40 | this.comp = ""; 41 | this.user = ""; 42 | this.is64 = "0"; 43 | this.ext = ""; 44 | this.last = System.currentTimeMillis(); 45 | this.diff = 0L; 46 | this.state = 0; 47 | this.hint = 0; 48 | this.pbid = ""; 49 | this.note = ""; 50 | this.barch = "x86"; 51 | this.alive = true; 52 | this.port = ""; 53 | this.sane = false; 54 | this.chst = null; 55 | // 跳过session key和代码页的字节,metadata以'\t'分隔 56 | final String[] split = CommonUtils.bString(Arrays.copyOfRange(original, 20, original.length), chst).split("\t"); 57 | if (split.length > 0) { 58 | this.id = split[0]; 59 | } 60 | if (split.length > 1) { 61 | this.pid = split[1]; 62 | } 63 | if (split.length > 2) { 64 | this.ver = split[2]; 65 | } 66 | if (split.length > 3) { 67 | this.intz = split[3]; 68 | } 69 | if (split.length > 4) { 70 | this.comp = split[4]; 71 | } 72 | if (split.length > 5) { 73 | this.user = split[5]; 74 | } 75 | if (split.length > 6) { 76 | this.is64 = split[6]; 77 | } 78 | if (split.length > 7) { 79 | this.barch = ("1".equals(split[7]) ? "x64" : "x86"); 80 | } 81 | if (split.length > 8) { 82 | this.port = split[8]; 83 | } 84 | this.ext = ext; 85 | this.chst = chst; 86 | // 检验metadata数据是否合法 87 | this.sane = this.sanity(); 88 | ``` 89 | 90 | 合法性校验: 91 | 92 | `common.BeaconEntry#_sanity(final LinkedList list)` 93 | 94 | ```java 95 | if (!CommonUtils.isNumber(this.id)) { 96 | list.add("id '" + this.id + "' is not a number"); 97 | this.id = "0"; 98 | } 99 | if (!"".equals(this.intz) && !CommonUtils.isIP(this.intz) && !CommonUtils.isIPv6(this.intz) && !"unknown".equals(this.intz)) { 100 | list.add("internal address '" + this.intz + "' is not an address"); 101 | this.intz = ""; 102 | } 103 | if (!this.checkExt(this.ext)) { 104 | list.add("external address '" + this.ext + "' is not an address"); 105 | this.ext = ""; 106 | } 107 | if (!"".equals(this.pid) && !CommonUtils.isNumber(this.pid)) { 108 | list.add("pid '" + this.pid + "' is not a number"); 109 | this.pid = "0"; 110 | } 111 | if (!"".equals(this.port) && !CommonUtils.isNumber(this.port)) { 112 | list.add("port '" + this.port + "' is not a number"); 113 | this.port = ""; 114 | } 115 | if (!"".equals(this.is64) && !CommonUtils.isNumber(this.is64)) { 116 | list.add("is64 '" + this.is64 + "' is not a number"); 117 | this.is64 = ""; 118 | } 119 | if (this.comp != null && this.comp.length() > 64) { 120 | list.add("comp '" + this.comp + "' is too long. Truncating"); 121 | this.comp = this.comp.substring(0, 63); 122 | } 123 | if (this.user != null && this.user.length() > 64) { 124 | list.add("user '" + this.user + "' is too long. Truncating"); 125 | this.user = this.user.substring(0, 63); 126 | } 127 | if (list.size() > 0) { 128 | final Iterator iterator = list.iterator(); 129 | CommonUtils.print_error("Beacon entry did not validate"); 130 | while (iterator.hasNext()) { 131 | System.out.println("\t" + iterator.next()); 132 | } 133 | return false; 134 | } 135 | return true; 136 | ``` 137 | 138 | -------------------------------------------------------------------------------- /src/dns/BaseSecurity.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | 对称加密使用的是`AES128`,加密模式为`CBC`,iv为固定的`abcdefghijklmnop`,padding为自己实现的padding。数据包校验使用`HMACSHA256`。 4 | 5 | 当一段明文要加密时,会先增加一些信息: 6 | 7 | ``` 8 | | current time | plaintext length | plaintext | 'a' * padding_len | 9 | ``` 10 | 11 | 其中current time是为了防止重放攻击,当一个密文被接收时,解密后会查询这个时间是否`<=`对应`Session`的计时器,如果小于等于,判断为重放,直接drop。 12 | 13 | 然后将最终的数据作为明文加密,然后在末尾加上HMAC: 14 | 15 | ``` 16 | | cipher text | MAC of cipher text | 17 | ``` 18 | 19 | 20 | 21 | 一些`BaseSecurity`相关的字段信息可以参考`src/BeaconC2.md`的`加密`部分。 22 | 23 | # 分析 24 | 25 | ## encrypt 26 | 27 | `dns.BaseSecurity#encrypt` 28 | 29 | ```java 30 | public byte[] encrypt(final String bid, final byte[] b) { 31 | try { 32 | // 之前是否注册了key 33 | if (!this.isReady(bid)) { 34 | CommonUtils.print_error("encrypt: No session for '" + bid + "'"); 35 | return new byte[0]; 36 | } 37 | final ByteArrayOutputStream out = new ByteArrayOutputStream(b.length + 1024); 38 | final DataOutputStream dataOutputStream = new DataOutputStream(out); 39 | final SecretKey key = this.getKey(bid); 40 | final SecretKey hashKey = this.getHashKey(bid); 41 | out.reset(); 42 | // 大端序先写入当前的时间,单位为秒 43 | dataOutputStream.writeInt((int)(System.currentTimeMillis() / 1000L)); 44 | // 大端序写入明文长度 45 | dataOutputStream.writeInt(b.length); 46 | // 写入明文 47 | dataOutputStream.write(b, 0, b.length); 48 | // padding至长度为16的整数,因为是AES128-CBC模式 49 | this.pad(out); 50 | byte[] do_encrypt = null; 51 | synchronized (this.in) { 52 | // 这个被QuickSecurity继承后实际上就是调用了Cipher.init和Cipher.doFinal加密 53 | do_encrypt = this.do_encrypt(key, out.toByteArray()); 54 | } 55 | byte[] doFinal = null; 56 | // 生成密文的消息验证码 57 | synchronized (this.mac) { 58 | this.mac.init(hashKey); 59 | doFinal = this.mac.doFinal(do_encrypt); 60 | } 61 | final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 62 | // 先写入密文 63 | byteArrayOutputStream.write(do_encrypt); 64 | // 再写入HMAC 65 | byteArrayOutputStream.write(doFinal, 0, 16); 66 | return byteArrayOutputStream.toByteArray(); 67 | // ... 68 | ``` 69 | 70 | ## decrypt 71 | 72 | `dns.BaseSecurity#decrypt` 73 | 74 | ```java 75 | public byte[] decrypt(final String s, final byte[] ciphertext) { 76 | try { 77 | // 之前是否注册过key 78 | if (!this.isReady(s)) { 79 | CommonUtils.print_error("decrypt: No session for '" + s + "'"); 80 | return new byte[0]; 81 | } 82 | final Session session = this.getSession(s); 83 | final SecretKey key = this.getKey(s); 84 | final SecretKey hashKey = this.getHashKey(s); 85 | // 获取HMAC 86 | final byte[] copyOfRange = Arrays.copyOfRange(ciphertext, 0, ciphertext.length - 16); 87 | // 获取密文 88 | final byte[] copyOfRange2 = Arrays.copyOfRange(ciphertext, ciphertext.length - 16, ciphertext.length); 89 | byte[] doFinal = null; 90 | synchronized (this.mac) { 91 | this.mac.init(hashKey); 92 | doFinal = this.mac.doFinal(copyOfRange); 93 | } 94 | // 判断HMAC是否正确 95 | if (!MessageDigest.isEqual(copyOfRange2, Arrays.copyOfRange(doFinal, 0, 16))) { 96 | CommonUtils.print_error("[Session Security] Bad HMAC on " + ciphertext.length + " byte message from Beacon " + s); 97 | return new byte[0]; 98 | } 99 | byte[] do_decrypt = null; 100 | synchronized (this.out) { 101 | // 解密 102 | do_decrypt = this.do_decrypt(key, copyOfRange); 103 | } 104 | final DataInputStream dataInputStream = new DataInputStream(new ByteArrayInputStream(do_decrypt)); 105 | // 获取这个包的创建时间 106 | final int int1 = dataInputStream.readInt(); 107 | // 判断重放 108 | if (int1 <= session.counter) { 109 | CommonUtils.print_error("[Session Security] Bad counter (replay attack?) " + int1 + " <= " + session.counter + " message from Beacon " + s); 110 | return new byte[0]; 111 | } 112 | // 获取数据长度 113 | final int int2 = dataInputStream.readInt(); 114 | if (int2 < 0 || int2 > ciphertext.length) { 115 | CommonUtils.print_error("[Session Security] Impossible message length: " + int2 + " from Beacon " + s); 116 | return new byte[0]; 117 | } 118 | final byte[] b = new byte[int2]; 119 | dataInputStream.readFully(b, 0, int2); 120 | // 更新判断重放的计时器 121 | session.counter = int1; 122 | return b; 123 | ``` 124 | 125 | -------------------------------------------------------------------------------- /tasks_flow.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | # overview 4 | 5 | 这个是关于cobaltstrike3.13的分析,之前做的,不是3.14哦 6 | 7 | # tasks flow分析 8 | 9 | --------- 10 | 11 | ## 从client到teamserver 12 | 13 | 在beacon的interactive console输入命令后会直接用下面方法进行处理 14 | 15 | aggressor/windows/BeaconConsole.java: 16 | 17 | ```java 18 | public void actionPerformed(final ActionEvent ev) { //分发具体命令 19 | final String text = ev.getActionCommand().trim(); 20 | ((JTextField)ev.getSource()).setText(""); 21 | final CommandParser parser = new CommandParser(text); 22 | //... 23 | this.master.input(text); //服务端输出到log文件 24 | if (parser.is("argue")) { //以argue为例 25 | if (!this.isVistaAndLater()) { 26 | parser.error("Target is not Windows Vista or later"); 27 | } 28 | //假设输入 argue powershell IEX "whomai && ls" 29 | else if (parser.verify("AZ") || parser.reset()) { //根据格式“AZ”进行命令的解析 30 | final String args = parser.popString(); //Z表示余下的所有字符串 31 | final String command2 = parser.popString(); //A表示一个不含空格的字符串,这里就是powershell 32 | this.master.SpoofArgsAdd(command2, args); //调用TaskBeacon的具体方法 33 | } 34 | else if (parser.verify("A") || parser.reset()) { 35 | final String command = parser.popString(); 36 | this.master.SpoofArgsRemove(command); 37 | } 38 | else { 39 | this.master.SpoofArgsList(); 40 | } 41 | } 42 | //... 43 | } 44 | ``` 45 | 46 | 首先要知道beacon c&c命令的定义 47 | 48 | ```java 49 | //beacon/Tasks.java: 50 | public class Tasks 51 | { 52 | public static final int COMMAND_SPAWN = 1; 53 | public static final int COMMAND_SHELL = 2; 54 | public static final int COMMAND_DIE = 3; 55 | public static final int COMMAND_SLEEP = 4; 56 | public static final int COMMAND_CD = 5; 57 | public static final int COMMAND_KEYLOG_START = 6; 58 | public static final int COMMAND_KEYLOG_STOP = 7; 59 | public static final int COMMAND_INJECT_PID = 9; 60 | public static final int COMMAND_INJECT_PING = 18; 61 | public static final int COMMAND_UPLOAD = 10; 62 | public static final int COMMAND_SPAWN_PROC_X86 = 13; 63 | public static final int COMMAND_JOB_REGISTER = 40; 64 | public static final int COMMAND_INJECTX64_PID = 43; 65 | public static final int COMMAND_SPAWNX64 = 44; 66 | public static final int COMMAND_JOB_REGISTER_IMPERSONATE = 62; 67 | public static final int COMMAND_SPAWN_POWERSHELLX86 = 63; 68 | public static final int COMMAND_SPAWN_POWERSHELLX64 = 64; 69 | public static final int COMMAND_INJECT_POWERSHELLX86_PID = 65; 70 | public static final int COMMAND_INJECT_POWERSHELLX64_PID = 66; 71 | public static final int COMMAND_UPLOAD_CONTINUE = 67; 72 | public static final int COMMAND_PIPE_OPEN_EXPLICIT = 68; 73 | public static final int COMMAND_SPAWN_PROC_X64 = 69; 74 | public static final int COMMAND_JOB_SPAWN_X86 = 70; 75 | public static final int COMMAND_JOB_SPAWN_X64 = 71; 76 | public static final int COMMAND_SETENV = 72; 77 | public static final int COMMAND_FILE_COPY = 73; 78 | public static final int COMMAND_FILE_MOVE = 74; 79 | public static final int COMMAND_PPID = 75; 80 | public static final int COMMAND_RUN_UNDER_PID = 76; 81 | public static final int COMMAND_GETPRIVS = 77; 82 | public static final int COMMAND_EXECUTE_JOB = 78; 83 | public static final int COMMAND_PSH_HOST_TCP = 79; 84 | public static final int COMMAND_DLL_LOAD = 80; 85 | public static final int COMMAND_REG_QUERY = 81; 86 | public static final int COMMAND_LSOCKET_TCPPIVOT = 82; 87 | public static final int COMMAND_ARGUE_ADD = 83; 88 | public static final int COMMAND_ARGUE_REMOVE = 84; 89 | public static final int COMMAND_ARGUE_LIST = 85; 90 | public static final int COMMAND_TCP_CONNECT = 86; 91 | public static final int COMMAND_JOB_SPAWN_TOKEN_X86 = 87; 92 | public static final int COMMAND_JOB_SPAWN_TOKEN_X64 = 88; 93 | public static final int COMMAND_SPAWN_TOKEN_X86 = 89; 94 | public static final int COMMAND_SPAWN_TOKEN_X64 = 90; 95 | public static final int COMMAND_INJECTX64_PING = 91; 96 | 97 | public static final long max() { 98 | return 1048576L; 99 | } 100 | } 101 | ``` 102 | 103 | beacon/TaskBeacon.java: 104 | 105 | 这里的this.builder是beacon/EncodedCommandBuilder.java里的类,但是这个类只是在`CommandBuilder`的基础上会对字符串根据目标上的字符集进行编码,其次java的字节序问题没搞懂= = 106 | 107 | ```java 108 | //... 109 | public void SpoofArgsAdd(final String command, final String fakeargs) { 110 | final String result = command + " " + fakeargs; 111 | this.builder.setCommand(83); //即COMMAND_ARGUE_ADD 112 | this.builder.addLengthAndString(command); //向buffer写入 4字节的command的长度,再写入command 113 | this.builder.addLengthAndString(command + " " + fakeargs); 114 | final byte[] task = this.builder.build(); //构造发送给目标的cc字节数组 115 | for (int x = 0; x < this.bids.length; ++x) { 116 | this.log_task(this.bids[x], "Tasked beacon to spoof '" + command + "' as '" + fakeargs + "'", "T1059, T1093, T1106"); 117 | this.conn.call("beacons.task", CommonUtils.args(this.bids[x], task)); //args方法将参数打包成object[] 118 | } 119 | } 120 | //... 121 | ``` 122 | 123 | CommandBuilder: 124 | 125 | ```java 126 | //... 127 | public byte[] build() { 128 | try { 129 | this.output.flush(); 130 | final byte[] args = this.backing.toByteArray(); //将builder的buffer转换成字节数组 131 | this.backing.reset(); 132 | this.output.writeInt(this.command); //写入4字节的cc命令,大端序 133 | this.output.writeInt(args.length); //写入args的长度,大端序 134 | if (args.length > 0) { 135 | this.output.write(args, 0, args.length); //写入args 136 | } 137 | this.output.flush(); 138 | final byte[] result = this.backing.toByteArray(); 139 | this.backing.reset(); 140 | return result; //返回上述组成的字节数组 141 | } 142 | catch (IOException ioex) { 143 | MudgeSanity.logException("command builder", ioex, false); 144 | return new byte[0]; 145 | } 146 | } 147 | ... 148 | ``` 149 | 150 | 之后client会向服务器发送请求,由服务器向beacon目标发送cc命令 151 | 152 | common/TeamQueue.java: 153 | 154 | ```java 155 | public void call(final String name, final Object[] args, final Callback c) { 156 | if (c == null) { 157 | final Request r = new Request(name, args, 0L); //new 一个Request对象 158 | this.writer.addRequest(r); //添加request对象到writer的队列里 159 | } 160 | else { 161 | synchronized (this.callbacks) { 162 | ++this.reqno; 163 | this.callbacks.put(new Long(this.reqno), c); 164 | final Request r2 = new Request(name, args, this.reqno); 165 | this.writer.addRequest(r2); 166 | } 167 | } 168 | } 169 | //... 170 | 171 | //TeamWriter 172 | //... 173 | @Override 174 | public void run() { 175 | while (TeamQueue.this.socket.isConnected()) { 176 | final Request next = this.grabRequest(); //pop 链表第一个节点 177 | if (next != null) { 178 | TeamQueue.this.socket.writeObject(next); //发送Request对象 179 | Thread.yield(); 180 | } 181 | else { 182 | try { 183 | Thread.sleep(25L); 184 | } 185 | catch (InterruptedException iex) { 186 | MudgeSanity.logException("teamwriter sleep", iex, false); 187 | } 188 | } 189 | } 190 | } 191 | ``` 192 | 193 | common/TeamSocket.java: 194 | 195 | ```java 196 | //... 197 | public void writeObject(final Object data) { 198 | if (!this.isConnected()) { 199 | return; 200 | } 201 | try { 202 | synchronized (this.client) { 203 | if (this.bout == null) { 204 | this.bout = new BufferedOutputStream(this.client.getOutputStream(), 262144); 205 | } 206 | final ObjectOutputStream out = new ObjectOutputStream(this.bout); 207 | out.writeUnshared(data); //实际上就是序列化Request对象,发送出去 208 | out.flush(); //清空缓存,发送数据 209 | } 210 | } 211 | catch (IOException ioex) { 212 | MudgeSanity.logException("client (" + this.from + ") write", ioex, true); 213 | this.close(); 214 | } 215 | catch (Exception ex) { 216 | MudgeSanity.logException("client (" + this.from + ") write", ex, false); 217 | this.close(); 218 | } 219 | } 220 | //... 221 | ``` 222 | 223 | ## 从teamserver到beacon 224 | 225 | teamserver负责控制与client交互的是ManageUser类 226 | 227 | server/TeamServer.java: 228 | 229 | ```java 230 | //... 231 | while (true) { 232 | server.acceptAndAuthenticate(this.pass, new PostAuthentication() { 233 | @Override 234 | public void clientAuthenticated(final Socket client) { 235 | try { 236 | client.setSoTimeout(0); 237 | final TeamSocket plebe = new TeamSocket(client); 238 | new Thread(new ManageUser(plebe, TeamServer.this.resources, TeamServer.this.calls), "Manage: unauth'd user").start(); 239 | } 240 | catch (Exception ex) { 241 | MudgeSanity.logException("Start client thread", ex, false); 242 | } 243 | } 244 | }); 245 | } 246 | //... 247 | ``` 248 | 249 | server/ManageUser.java: 250 | 251 | ```java 252 | //... 253 | public void process(final Request r) throws Exception { 254 | //检查Request的call是否是其他字段的if分支 255 | 256 | else if (this.calls.containsKey(r.getCall())) { //teamserver的call是否有request里的call字段对应的call,命令通过上面可以知道,这里的call是`beacons.task`,刚好来到这个分支 257 | final ServerHook callme = this.calls.get(r.getCall()); 258 | callme.call(r, this); //有就调用 259 | } 260 | //... 261 | @Override 262 | public void run() { 263 | try { 264 | this.mine = Thread.currentThread(); 265 | //当连接未关闭时,从client接受Request对象,传参给process对象 266 | while (this.client.isConnected()) { 267 | final Request r = (Request)this.client.readObject(); 268 | if (r != null) { 269 | this.process(r); 270 | } 271 | } 272 | } 273 | catch (Exception ex) { 274 | MudgeSanity.logException("manage user", ex, false); 275 | this.client.close(); 276 | } 277 | if (this.authenticated) { 278 | this.resources.deregister(this.nickname, this); 279 | this.resources.broadcast("eventlog", LoggedEvent.Quit(this.nickname)); 280 | } 281 | } 282 | //... 283 | ``` 284 | 285 | 再回到server/TeamServer.java: 286 | 287 | ```java 288 | //... 289 | public void go() { 290 | try { 291 | //将一系列的ServerHook对象注册到this.calls里 292 | new ProfileEdits(this.c2profile); 293 | this.c2profile.addParameter(".watermark", this.auth.getWatermark()); 294 | (this.resources = new Resources(this.calls)).put("c2profile", this.c2profile); 295 | this.resources.put("localip", this.host); 296 | this.resources.put("password", this.pass); 297 | new TestCall().register(this.calls); 298 | final WebCalls web = new WebCalls(this.resources); 299 | web.register(this.calls); 300 | this.resources.put("webcalls", web); 301 | new Listeners(this.resources).register(this.calls); 302 | new Beacons(this.resources).register(this.calls); 303 | new Phisher(this.resources).register(this.calls); 304 | new VPN(this.resources).register(this.calls); 305 | new BrowserPivotCalls(this.resources).register(this.calls); 306 | new DownloadCalls(this.resources).register(this.calls); 307 | final Iterator i = Keys.getDataModelIterator(); 308 | while (i.hasNext()) { 309 | new DataCalls(this.resources, i.next()).register(this.calls); 310 | } 311 | //... 312 | ``` 313 | 314 | 可以知道this.calls里beacons.task对应的是server/Beacons.java里的类实例 315 | 316 | server/Beacons.java: 317 | 318 | ```java 319 | public Beacons(final Resources r) { 320 | this.beacons = new HashMap(); 321 | this.data = null; 322 | this.socks = null; 323 | this.cmdlets = new HashMap(); 324 | this.setup = null; 325 | this.notes = new HashMap(); 326 | this.empty = new HashSet(); 327 | this.initial = new LinkedList(); 328 | this.resources = r; 329 | this.web = ServerUtils.getWebCalls(r); 330 | Timers.getTimers().every(1000L, "beacons", this); 331 | r.put("beacons", this); 332 | this.setup = new BeaconSetup(this.resources); 333 | this.setup.getHandlers().setCheckinListener(this); 334 | this.data = this.setup.getData(); //从BeaconSetup的实例中获取BeaconData实例 335 | this.socks = this.setup.getSocks(); 336 | this.resources.broadcast("cmdlets", new HashMap(), true); 337 | } 338 | //... 339 | @Override 340 | public void call(final Request r, final ManageUser client) { 341 | //... 342 | else if (r.is("beacons.task", 2)) { 343 | final String id = r.arg(0) + ""; //这个就是bid 344 | final byte[] task = (byte[])r.arg(1); //client build好的字节数组,表示了要发送的命令 345 | this.data.task(id, task); 346 | } 347 | //... 348 | ``` 349 | 350 | beacon/BeaconData.java: 351 | 352 | ```java 353 | //... 354 | public void task(final String bid, final byte[] data) { 355 | synchronized (this) { 356 | final List queue = this.getQueue(bid); //bid如果有对应的队列就直接返回该队列的引用,否则新建队列,并添加到实例的队列HashMap里 357 | queue.add(data); //队列append一个待发送的数据 358 | this.tasked.add(bid); //使用Set表示需要发送task的bid集合 359 | } 360 | } 361 | //... 362 | ``` 363 | 364 | 至此,要发送的task都被封装好了,就等其他的代码调用这个来发送它,因此为了搞清楚如何发送这个数据,需要分析其他部分的代码。 -------------------------------------------------------------------------------- /tree.md: -------------------------------------------------------------------------------- 1 | # src 2 | 3 | ## c2profile 4 | 5 | ``` 6 | c2profile/ 7 | ├── Checkers.java 8 | ├── Lint.java 9 | ├── LintURI.java 10 | ├── Loader.java 11 | ├── MalleableHook.java 负责调用Program转换和内部MyHook接口serve方法 12 | ├── MalleableStager.java 13 | ├── Preview.java 14 | ├── Profile.java 15 | ├── Program.java 具体的c2profile转换逻辑 16 | └── SmartBuffer.java 17 | 18 | 19 | ``` 20 | 21 | ## cloudstrike 22 | 23 | ``` 24 | cloudstrike/ 25 | ├── CaptureContent.java 26 | ├── Hook.java 27 | ├── Keylogger.java 28 | ├── NanoHTTPD.java 修改版NanoHTTPD 29 | ├── ResponseFilter.java 30 | ├── Response.java 31 | ├── ServeApplet.java 32 | ├── ServeFile.java 33 | ├── StaticContent.java 34 | ├── WebServer.java 继承NanoHTTPD,真正的http server逻辑在这里 35 | └── WebService.java 接口 36 | ``` 37 | 38 | ## server 39 | 40 | ``` 41 | server 42 | ├── Beacons.java 43 | ├── BrowserPivotCalls.java 44 | ├── DataCalls.java 45 | ├── DownloadCalls.java 46 | ├── KeyloggerHandler.java 47 | ├── Listeners.java 48 | ├── ManageUser.java 49 | ├── PendingRequest.java 50 | ├── PersistentData.java 51 | ├── Phisher.java 52 | ├── ProfileEdits.java 53 | ├── ProfileHandler.java 54 | ├── Resources.java 55 | ├── ServerBus.java 56 | ├── ServerHook.java 57 | ├── ServerUtils.java 58 | ├── TeamServer.java 59 | ├── TestCall.java 60 | ├── VPN.java 61 | ├── WebCalls.java 实现了cloudstrike.WebServer.WebListener 62 | └── WebsiteCloneTool.java 63 | ``` 64 | 65 | 66 | 67 | ## beacon 68 | 69 | ``` 70 | beacon/ 71 | ├── BeaconC2.java 负责处理与beacon交互的数据的编码解码工作,具体的c2协议实现在这里 72 | ├── BeaconCharsets.java 73 | ├── BeaconCommands.java 74 | ├── BeaconData.java 负责beacon的task队列相关逻辑 75 | ├── BeaconDNS.java 实现了beacon dns listener相关逻辑 76 | ├── BeaconDownloads.java 77 | ├── BeaconErrors.java 78 | ├── BeaconExploits.java 79 | ├── BeaconHTTP.java 实现了beacon http listener相关逻辑 80 | ├── BeaconParts.java 81 | ├── BeaconPipes.java 管理smb beacon的连接信息,parent-children关系 82 | ├── BeaconPivot.java 83 | ├── BeaconSetup.java 负责新的listener的初始化工作 84 | ├── BeaconSocks.java 85 | ├── BeaconTabCompletion.java 86 | ├── CheckinListener.java 87 | ├── CommandBuilder.java 负责构造c2数据包 88 | ├── dns 89 | │   ├── CacheManager.java 90 | │   ├── ConversationManager.java 91 | │   ├── RecvConversation.java 92 | │   ├── SendConversationAAAA.java 93 | │   ├── SendConversationA.java 94 | │   ├── SendConversation.java 95 | │   └── SendConversationTXT.java 96 | ├── EncodedCommandBuilder.java 97 | ├── exploits 98 | │   ├── BypassUAC.java 99 | │   ├── BypassUACToken.java 100 | │   └── cve_2014_4113.java 101 | ├── Job.java 定义了c2协议中返回结果的类型 102 | ├── jobs 103 | │   ├── BypassUACJob.java 104 | │   ├── BypassUACTokenJob.java 105 | │   ├── DllSpawnJob.java 106 | │   ├── ElevateJob.java 107 | │   ├── ExecuteAssemblyJob.java 108 | │   ├── HashdumpJob.java 109 | │   ├── KeyloggerJob.java 110 | │   ├── MimikatzJob.java 111 | │   ├── MimikatzJobSmall.java 112 | │   ├── NetViewJob.java 113 | │   ├── PortScannerJob.java 114 | │   ├── PowerShellJob.java 115 | │   └── ScreenshotJob.java 116 | ├── JobSimple.java 117 | ├── pivots 118 | │   ├── PortForwardPivot.java 119 | │   ├── ReversePortForwardPivot.java 120 | │   └── SOCKSPivot.java 121 | ├── PowerShellTasks.java 122 | ├── Registry.java 123 | ├── SecureShellCommands.java 124 | ├── SecureShellTabCompletion.java 125 | ├── Settings.java 126 | ├── setup 127 | │   ├── BrowserPivot.java 128 | │   ├── ProcessInject.java 129 | │   └── SSHAgent.java 130 | ├── TaskBeaconCallback.java 131 | ├── TaskBeacon.java 132 | ├── TaskBeaconStaging.java 133 | └── Tasks.java 定义了c2协议的command类型 134 | 135 | ``` 136 | 137 | --------------------------------------------------------------------------------