github.com/iDigitalFlame/xmt@v0.5.4/tools/strip_bin.py (about) 1 #!/usr/bin/python3 2 # Copyright (C) 2020 - 2023 iDigitalFlame 3 # 4 # This program is free software: you can redistribute it and/or modify 5 # it under the terms of the GNU General Public License as published by 6 # the Free Software Foundation, either version 3 of the License, or 7 # any later version. 8 # 9 # This program is distributed in the hope that it will be useful, 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 # GNU General Public License for more details. 13 # 14 # You should have received a copy of the GNU General Public License 15 # along with this program. If not, see <https://www.gnu.org/licenses/>. 16 # 17 # strip_bin.py <in> <out> 18 # Strips named symbols and obvious strings from Golang binaries. 19 # MUST have compiled with "-trimpath -ldflags='-w -s'" for this to work 100%. 20 # 21 # This file is superseded via the "strip_binary" function in ThunderStorm's 22 # "strip_binary" function. (I might backport it here). 23 # 24 # Keep this in check with JetStream's crypt.py file. 25 26 from os import argv 27 from sys import stderr 28 from re import compile 29 from random import choice 30 from string import ascii_letters 31 32 33 PACKAGES = [ 34 b"\x00bufio", 35 b"\x00bytes", 36 b"\x00compress", 37 b"\x00container", 38 b"\x00context", 39 b"\x00crypto", 40 b"\x00debug", 41 b"\x00encoding", 42 b"\x00errors", 43 b"\x00expvar", 44 b"\x00flag", 45 b"\x00fmt", 46 b"\x00go", 47 b"\x00hash", 48 b"\x00image", 49 b"\x00index", 50 b"\x00internal", 51 b"\x00io", 52 b"\x00log", 53 b"\x00math", 54 b"\x00mime", 55 b"\x00net", 56 b"\x00os", 57 b"\x00path", 58 b"\x00reflect", 59 b"\x00regexp", 60 b"\x00runtime", 61 b"\x00sort", 62 b"\x00strconv", 63 b"\x00strings", 64 b"\x00sync", 65 b"\x00syscall", 66 b"\x00text", 67 b"\x00time", 68 b"\x00unicode", 69 b"\x00unsafe", 70 ] 71 72 CRUMB_ID = b' Go build ID: "' 73 CRUMB_VER = b"go1." 74 CRUMB_INF = b"\xFF Go buildinf:" 75 CRUMB_TAIL = b"/src/runtime/runtime.go\x00\x00" 76 CRUMB_DEPS = b"command-line-arguments" 77 CRUMB_FILE = b".go\x00" 78 CRUMB_STACK = [ 79 b"\x00github.com/", 80 b"github.com/", 81 b"\x00type..eq.", 82 b"\x00type..hash.", 83 b"\x00type:.eq.", 84 b"\x00type:.hash.", 85 b"\x00go.buildid", 86 b"vendor/", 87 b"\x00vendor/", 88 b"struct {", 89 b"map[", 90 b"*map.", 91 b"*func(", 92 b"func(", 93 b"\x00main.", 94 b'asn1:"', 95 ] 96 CRUMB_CLEANUP = [ 97 b"crypto/internal/", 98 b"internal/syscall/", 99 b"Mingw-w64 runtime failure:", 100 ] 101 CRUMB_HEADERS = [ 102 b"\x00\x00\x00\x00\x05bufio", 103 b"\x00\x00\x00\x00\x05bytes", 104 b"\x00\x00\x00\x00\x06crypto", 105 b"\x00\x00\x00\x00\x06crypto", 106 b"\x00\x00\x00\x00\x06error", 107 b"\x00\x00\x00\x00\x0Dcrypto/", 108 b"\x00\x00\x00\x00\x0Dcompress/", 109 b"\x00\x00\x00\x00\x0Ecompress/", 110 b"\x00\x00\x00\x00\x0Econtainer/", 111 ] 112 113 TABLE_IMPORTS = compile( 114 b"([\x01-\x05]{0,1})([\x01-\x50]{1})([a-zA-Z0-9/\\-\\*]{2,50})\x00" 115 ) 116 TABLE_STRINGS = compile( 117 b"([\x01-\x50]{1})([a-zA-Z0-9/\\-\\*]{2,50})\\.([a-zA-Z0-9\\./\\[\\]\\]\\*]{4,50})([\x00-\x01]{1})" 118 ) 119 120 121 def _is_valid(c): 122 return ( 123 (c >= 0x41 and c <= 0x5A) 124 or (c >= 0x61 and c <= 0x7A) 125 or (c >= 0x30 and c <= 0x39) 126 or (c >= 0x2C and c <= 0x2F) 127 or c == 0x7B 128 or c == 0x7D 129 or c == 0x5B 130 or c == 0x5D 131 or c == 0x5F 132 or c == 0x3B 133 or c == 0x20 134 or c == 0x28 135 or c == 0x29 136 or c == 0x2A 137 ) 138 139 140 def _is_ext(b, i): 141 # Ignore anything that isn't "fmt.fmt" 142 return ( 143 b[i - 5] == 0x2E and b[i - 4] != 0x66 and b[i - 3] != 0x6D and b[i - 2] != 0x74 144 ) 145 146 147 def _find_header(b): 148 for i in CRUMB_HEADERS: 149 x = b.find(i) 150 if x > 0: 151 return x 152 return -1 153 154 155 def _mask_cleanup(b): 156 x = 0 157 while True: 158 m = TABLE_STRINGS.search(b, x) 159 if m is None: 160 break 161 if _is_ext(b, m.end()): 162 x = m.end() 163 continue 164 print(b[m.start() : m.end() - 1]) 165 for x in range(m.start() + 2, m.end() - 1): 166 b[x] = ord(choice(ascii_letters)) 167 x = m.end() 168 x = 0 169 for i in CRUMB_CLEANUP: 170 p, x = 0, 0 171 if i[0] == 0: 172 p += 1 173 while x < len(b): 174 x = b.find(i) 175 if x <= 0: 176 break 177 _fill_non_zero(b, x + p) 178 del p, x 179 180 181 def _mask_build_id(b): 182 x = b.find(CRUMB_ID) 183 if x <= 0: 184 return 185 for i in range(x + 1, x + 128): 186 if b[i] == 0x22 and b[i + 1] < 0x31: 187 b[i] = 0 188 break 189 b[i] = 0 190 del x 191 192 193 def _mask_build_inf(b): 194 x = b.find(CRUMB_INF) 195 if x > 0: 196 for i in range(x + 2, x + 14): 197 b[i] = 0 198 x = 0 199 while x < len(b): 200 x = b.find(CRUMB_FILE, x + 1) 201 if x <= 0 or b[x - 1] == 0: 202 break 203 s = x 204 while s > x - 128: 205 if b[s] == 0: 206 break 207 s -= 1 208 if x - s < 64 and x - s > 0: 209 for i in range(s, x): 210 b[i] = 0 211 del s 212 del x 213 214 215 def _mask_tail(b, log): 216 for i in CRUMB_STACK: 217 p, x = 0, 0 218 if i[0] == 0: 219 p += 1 220 while x < len(b): 221 x = b.find(i) 222 if x <= 0: 223 break 224 _fill_non_zero(b, x + p, real=True) 225 del p, x 226 if callable(log): 227 log("Removing unused package names..") 228 for i in range(0, len(PACKAGES)): 229 x = 0 230 if callable(log) and i % 10 == 0: 231 log(f"Checking package {i+1} out of {len(PACKAGES)}..") 232 while x < len(b): 233 x = b.find(PACKAGES[i], x) 234 if x <= 0: 235 break 236 if b[x + 3] == 0: 237 x += 2 238 continue 239 _fill_non_zero(b, x + 1) 240 del x 241 if callable(log): 242 log("Looking for path values..") 243 x = b.find(CRUMB_TAIL) 244 if x <= 0: 245 return 246 while x > 0: 247 if b[x] == 0 and b[x - 1] != 0 and b[x - 1] < 0x21 and b[x - 2] != 0: 248 if callable(log): 249 log(f"Found start of paths at 0x{x:X}") 250 break 251 x -= 1 252 if x <= 0: 253 return 254 c = 0 255 while x < len(b): 256 if b[x] == 0: 257 c += 1 258 x += 1 259 continue 260 if c > 3: 261 break 262 x, c = _fill_non_zero(b, x), 0 263 del x, c 264 265 266 def _mask_tables(b, log): 267 for _ in range(0, 2): 268 _mask_tables_inner(b, log) 269 270 271 def _mask_deps(b, start, log): 272 x = start 273 for r in range(0, 2): 274 if callable(log): 275 log(f"Searching for command line args (round {r+1})..") 276 x = b.find(CRUMB_DEPS, start) 277 if x <= 0: 278 continue 279 x -= 5 280 while x < len(b): 281 if b[x] < 0x21 and b[x + 1] > 0x7E: 282 break 283 if b[x] > 0x21: 284 b[x] = 0 285 x += 1 286 del x 287 288 289 def _mask_tables_inner(b, log): 290 x = -1 291 for i in CRUMB_HEADERS: 292 x = b.find(i) 293 if x > 0: 294 break 295 if x <= 0: 296 return 297 c = 0 298 while True: 299 s, e = _find_next_vtr(b, x) 300 if s == -1: 301 break 302 if (e - s) > 3: 303 for i in range(s, e): 304 # NOTE(dij): These MUST be random chars or the program will CRASH!! 305 b[i] = ord(choice(ascii_letters)) 306 x = e 307 c += 1 308 del x 309 if callable(log): 310 log(f"Masked {c} stack trace strings!") 311 del c 312 313 314 def _fill_non_zero(b, start, max=256, real=False): 315 for i in range(start, start + max): 316 if b[i] == 0 or (real and b[i] < 32): 317 return i 318 b[i] = 0 319 return start + max 320 321 322 def _mask_version(b, start, root=None, path=None): 323 if root is not None and len(root) > 0: 324 m = root.encode("UTF-8") 325 x = start + 1 326 while x > start: 327 x = b.find(m, x) 328 if x == -1: 329 break 330 _fill_non_zero(b, x) 331 del x 332 if path is not None and len(path) > 0: 333 m = path.encode("UTF-8") 334 x = start + 1 335 while x > start: 336 x = b.find(m, x) 337 if x == -1: 338 break 339 _fill_non_zero(b, x) 340 del x 341 x = b.find(CRUMB_VER, start) 342 while x > start and x < len(b): 343 b[x], b[x + 1], b[x + 2] = 0, 0, 0 344 x += 3 345 for i in range(0, 16): 346 x += len(CRUMB_VER) + i 347 if b[x] < 0x2E or b[x] < 0x39: 348 break 349 b[x] = 0 350 x = b.find(CRUMB_VER) 351 del x 352 353 354 def _find_next_vtr(b, start, zeros=32, max_len=128): 355 # Golang string identifiers are usually BB<str>. 356 # Two bytes, the first one idk what it means, it's usually 1|0 (but not always) 357 # and then a byte that specifies how long the following string is. 358 # 359 # We use this to our advantage by reading this identifier and discounting anything 360 # that A). doesn't fit Go name conventions and anything that doesn't equal the 361 # supplied length. This allows us to scroll through the virtual string table 362 # (vtr). 363 i, z, c, s = 0, 0, 0, 0 364 for i in range(start, start + max_len): 365 if z > zeros: 366 break 367 if s > start: 368 if c > 0 and (i - s) > c: 369 s, c = 0, 0 370 continue 371 if b[i] == 0 or not _is_valid(b[i]): 372 if (b[i] == 0x3A and b[i + 1] == 0x22) or ( 373 b[i] == 0x22 and b[i - 1] == 0x3A 374 ): # Find usage of :" 375 continue 376 if b[i] == 0x22: # Scroll back to find the missing " 377 q = i - 1 378 while q > start and q > q - 64: 379 if b[q] == 0x22: 380 break 381 q -= 1 382 if b[q] == 0x22: 383 continue 384 del q 385 if ( 386 b[i] == 0x3C 387 and b[i + 1] == 0x2D 388 and (b[i - 1] == 0x6E or b[i + 2] == 0x63) 389 ): # Check for <-chan or chan<- 390 continue 391 if i - s != c: 392 s, c = 0, 0 393 continue 394 return i - (i - s), i 395 continue 396 if b[i] == 0: 397 z += 1 398 continue 399 else: 400 z = 0 401 if s == 0 and b[i] >= 0x30 and b[i] <= 0x39: 402 # No valid identifiers start with a number. 403 continue 404 if s == 0 and _is_valid(b[i]) and b[i - 1] != 0: 405 s, c = i, b[i - 1] 406 del z, c, s 407 return -1, i 408 409 410 def strip_binary(file, out, log=None, root=None, path=None): 411 with open(file, "rb") as f: 412 b = bytearray(f.read()) 413 x = _find_header(b) 414 if x <= 0: 415 if callable(log): 416 log("Could not find header (are you using Garble?)") 417 return 418 _mask_tables(b, log) 419 _mask_tail(b, log) 420 _mask_deps(b, x, log) 421 if callable(log): 422 log("Removing version info..") 423 _mask_version(b, x, root, path) 424 _mask_build_id(b) 425 _mask_build_inf(b) 426 if callable(log): 427 log("Cleaning up..") 428 _mask_cleanup(b) 429 del x 430 with open(out, "wb") as f: 431 f.write(b) 432 del b 433 434 435 if __name__ == "__main__": 436 if len(argv) != 3: 437 print(f"{argv[0]} <file> <out>", file=stderr) 438 exit(1) 439 440 strip_binary(argv[1], argv[2], print)