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)