aboutsummaryrefslogtreecommitdiff
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2015, 2018, 2020, 2022 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2022 Chris Marusich <cmmarusich@gmail.com>
;;; Copyright © 2022 Pierre Langlois <pierre.langlois@gmx.com>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (test-gremlin)
  #:use-module (guix elf)
  #:use-module (guix tests)
  #:use-module ((guix utils) #:select (call-with-temporary-directory
                                       target-aarch64?))
  #:use-module (guix build utils)
  #:use-module (guix build gremlin)
  #:use-module (gnu packages bootstrap)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-26)
  #:use-module (srfi srfi-34)
  #:use-module (srfi srfi-64)
  #:use-module (rnrs io ports)
  #:use-module (ice-9 popen)
  #:use-module (ice-9 rdelim)
  #:use-module (ice-9 regex)
  #:use-module (ice-9 match))

(define %guile-executable
  (match (false-if-exception (readlink "/proc/self/exe"))
    ((? string? program)
     (and (file-exists? program) (elf-file? program)
          program))
    (_
     #f)))

(define read-elf
  (compose parse-elf get-bytevector-all))

(define c-compiler
  (or (which "gcc") (which "cc") (which "g++")))


(test-begin "gremlin")

(unless %guile-executable (test-skip 1))
(test-assert "elf-dynamic-info-needed, executable"
  (let* ((elf     (call-with-input-file %guile-executable read-elf))
         (dyninfo (elf-dynamic-info elf)))
    (or (not dyninfo)                             ;static executable
        (lset<= string=?
                (list (string-append "libguile-" (effective-version))
                      "libc")
                (map (lambda (lib)
                       (string-take lib (string-contains lib ".so")))
                     (elf-dynamic-info-needed dyninfo))))))

(unless (and %guile-executable (not (getenv "LD_LIBRARY_PATH"))
             (file-needed %guile-executable) ;statically linked?
             ;; When Guix has been built on a foreign distro, using a
             ;; toolchain and libraries from that foreign distro, it is not
             ;; unusual for the runpath to be empty.
             (pair? (file-runpath %guile-executable)))
  (test-skip 1))
(test-assert "file-needed/recursive"
  (let* ((needed (file-needed/recursive %guile-executable))
         (pipe   (dynamic-wind
                   (lambda ()
                     ;; Tell ld.so to list loaded objects, like 'ldd' does.
                     (setenv "LD_TRACE_LOADED_OBJECTS" "yup"))
                   (lambda ()
                     (open-pipe* OPEN_READ %guile-executable))
                   (lambda ()
                     (unsetenv "LD_TRACE_LOADED_OBJECTS")))))
    (define ldd-rx
      (make-regexp "^[[:blank:]]+([[:graph:]]+ => )?([[:graph:]]+) .*$"))

    (define (read-ldd-output port)
      ;; Read from PORT output in GNU ldd format.
      (let loop ((result '()))
        (match (read-line port)
          ((? eof-object?)
           (reverse result))
          ((= (cut regexp-exec ldd-rx <>) m)
           (if m
               (loop (cons (match:substring m 2) result))
               (loop result))))))
    (define ground-truth
      (remove (lambda (entry)
                ;; See vdso(7) for the list of vDSO names across
                ;; architectures.
                (or (string-prefix? "linux-vdso.so" entry)
                    (string-prefix? "linux-vdso32.so" entry) ;32-bit powerpc
                    (string-prefix? "linux-vdso64.so" entry) ;64-bit powerpc
                    (string-prefix? "linux-gate.so" entry)   ;i386
                    ;; FIXME: ELF files on aarch64 do not always include a
                    ;; NEEDED entry for the dynamic linker, and it is unclear
                    ;; if that is OK.  See: https://issues.guix.gnu.org/52943
                    (and (target-aarch64?)
                         (string-contains entry (glibc-dynamic-linker)))))
              (read-ldd-output pipe)))

    (and (zero? (close-pipe pipe))
         ;; It's OK if file-needed/recursive returns multiple entries that are
         ;; different strings referring to the same file.  This appears to be a
         ;; benign edge case.  See: https://issues.guix.gnu.org/52940
         (lset= file=? (pk 'truth ground-truth) (pk 'needed needed)))))

(test-equal "expand-origin"
  '("OOO/../lib"
    "OOO"
    "../OOO/bar/OOO/baz"
    "ORIGIN/foo")
  (map (cut expand-origin <> "OOO")
       '("$ORIGIN/../lib"
         "${ORIGIN}"
         "../${ORIGIN}/bar/$ORIGIN/baz"
         "ORIGIN/foo")))

(unless c-compiler
  (test-skip 1))
(test-equal "strip-runpath"
  "hello\n"
  (call-with-temporary-directory
   (lambda (directory)
     (with-directory-excursion directory
       (call-with-output-file "t.c"
         (lambda (port)
           (display "#include <stdio.h>\n" port)
           (display "int main () { puts(\"hello\"); }" port)))
       (invoke c-compiler "t.c"
               "-Wl,--enable-new-dtags" "-Wl,-rpath=/foo" "-Wl,-rpath=/bar")
       (let* ((dyninfo (elf-dynamic-info
                        (parse-elf (call-with-input-file "a.out"
                                     get-bytevector-all))))
              (old     (elf-dynamic-info-runpath dyninfo))
              (new     (strip-runpath "a.out"))
              (new*    (strip-runpath "a.out")))
         (validate-needed-in-runpath "a.out")
         (and (member "/foo" old) (member "/bar" old)
              (not (member "/foo" new))
              (not (member "/bar" new))
              (equal? new* new)
              (let* ((pipe (open-input-pipe "./a.out"))
                     (str  (get-string-all pipe)))
                (close-pipe pipe)
                str)))))))

(unless c-compiler
  (test-skip 1))
(test-equal "set-file-runpath + file-runpath"
  "hello\n"
  (call-with-temporary-directory
   (lambda (directory)
     (with-directory-excursion directory
       (call-with-output-file "t.c"
         (lambda (port)
           (display "#include <stdio.h>\n" port)
           (display "int main () { puts(\"hello\"); }" port)))

       (invoke c-compiler "t.c"
               "-Wl,--enable-new-dtags" "-Wl,-rpath=/xxxxxxxxx")

       (let ((original-runpath (file-runpath "a.out")))
         (and (member "/xxxxxxxxx" original-runpath)
              (guard (c ((runpath-too-long-error? c)
                         (string=? "a.out" (runpath-too-long-error-file c))))
                (set-file-runpath "a.out" (list (make-string 777 #\y))))
              (let ((runpath (delete "/xxxxxxxxx" original-runpath)))
                (set-file-runpath "a.out" runpath)
                (equal? runpath (file-runpath "a.out")))
              (let* ((pipe (open-input-pipe "./a.out"))
                     (str  (get-string-all pipe)))
                (close-pipe pipe)
                str)))))))

(unless c-compiler
  (test-skip 1))
(test-equal "elf-dynamic-info-soname"
  "libfoo.so.2"
  (call-with-temporary-directory
   (lambda (directory)
     (with-directory-excursion directory
       (call-with-output-file "t.c"
         (lambda (port)
           (display "// empty file" port)))
       (invoke c-compiler "t.c"
               "-shared" "-Wl,-soname,libfoo.so.2")
       (let* ((dyninfo (elf-dynamic-info
                       (parse-elf (call-with-input-file "a.out"
                                    get-bytevector-all))))
              (soname  (elf-dynamic-info-soname dyninfo)))
	 soname)))))

(test-end "gremlin")
66 } /* Generic.Subheading */ .highlight .gt { color: #aa0000 } /* Generic.Traceback */ .highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */ .highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */ .highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */ .highlight .kp { color: #008800 } /* Keyword.Pseudo */ .highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */ .highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */ .highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */ .highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */ .highlight .na { color: #336699 } /* Name.Attribute */ .highlight .nb { color: #003388 } /* Name.Builtin */ .highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */ .highlight .no { color: #003366; font-weight: bold } /* Name.Constant */ .highlight .nd { color: #555555 } /* Name.Decorator */ .highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */ .highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */ .highlight .nl { color: #336699; font-style: italic } /* Name.Label */ .highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */ .highlight .py { color: #336699; font-weight: bold } /* Name.Property */ .highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #336699 } /* Name.Variable */ .highlight .ow { color: #008800 } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */ .highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ .highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ .highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */

TracifyJS

This is a provisional tool for tracing the flow of data in JS programs. Made for personal reverse-engineering needs.

This tool consists of 2 parts:

As a provisional tool, TracifyJS can at any time be rearranged, moved entirely to another repo or replaced with something better. Please don't rely on anything being where it is now. Better make your own clone if you need it.

Working with templates

The modified UglifyJS by itslef knows nothing about tracing values during execution of a script. It merely allows one to replace certain expressions (e.g. additions, function calls) with something else. For example, consider this sample script

function fib(n, prev=1, prev_prev=0) {
    if (n === 0)
        return prev_prev;

    if (n === 1)
        return prev;

    return fib(n - 1, prev + prev_prev, prev);
}

console.log(fib(15));

Assuming it's in sample-script.js, we can do

uglifyjs sample-script.js \
    --beautify \
    "template_for_CALL='(console.log(\"call at line \" + /*line*/), /*expression*//*parented_args*/)'"

it should print

function fib(n, prev = 1, prev_prev = 0) {
    if (n === 0) return prev_prev;
    if (n === 1) return prev;
    return (console.log("call at line " + 8), fib(n - 1, prev + prev_prev, prev));
}

console.log((console.log("call at line " + 11), fib(15)));

As you can see, we used a template to dictate the way UglifyJS outputs function calls. All occurances of /*line*/ /*expression*/, and /*parented_args*/ in a call template get substituted for their respective pieces of code. Template text outside /* and */ delimiters gets printed as is (although changes to the amount of whitespace might occur).

Templates should be specified as options to --beautify (or to --output-opts). They should be given in a form of JavaScript sequence of assignments, e.g. template_for_CALL='something',template_for_PROPERTY_CALL="something-else" (this syntax is also used for other options in the upstream UglifyJS).

There are a few more details. Firstly, each kind of template has its own set of permitted substitutions which includes at least /*line*/, /*col*/ and /**/ (empty substitution). With the above CALL template example we omitted (for brevity) the /*optional*/, /*col*/ and /**/ substitutions. Additionally, the */ delimiter can be replaced with **/ to cause the text immediately after substitution to be ignored until either whitespace or slash / is encountered. This can be used as a hack to write templates that are still syntactically correct JavaScript so that your IDE highlights and indents them correctly. See the included templates for examples.

Also, please keep in mind that the template engine isn't very smart when it comes to strings. If your template includes a string literal with braces or whitespace and you use an output option like max_line_len, things might break. This shouldn't be a problem most of the time, though.

Tracifying code

The templates system allows one to dictate different types of code modifications without having to modify (and possibly repackage, depending on one's workflow) our modified UglifyJS. That's cool but if you're still reading, you probably expect to get some ready-to-use tracing tool, not just an (incomplete) JS expression templating system, right?

The trace-*.js snippets in this repository are what you're looking for. They allow function calls, binary expressions and values used/produced by them to be logged in a variable called simply tracing.

Here are some shell functions useful for passing the snippets to UglifyJS. Note that besides the templates we also specify a preamble — static piece of code to be included at the beginning of the output. Preamble is a feature of upstream UglifyJS.

TRACIFY_DIR="$(pwd)"

function file_as_js_string {
    printf "'%s'" \
           "$(tr '\n' '\034' < "$1" |
                  sed 's/\\/\\\\/g;s/\o034/\\n/g;'"s/'/\\\\'/g;")"
}

function preamble_as_js_string {
    file_as_js_string "$TRACIFY_DIR/trace-preamble.js"
}

function tracify_options {
    printf 'preamble='
    if [ "x" = "${NO_PREAMBLE:+x}" ]; then
        printf "''"
    else
        preamble_as_js_string
    fi

    for TYPE in BINARY LAZY_BINARY CALL PROPERTY_CALL; do
        printf ",template_for_%s=%s" \
               "$TYPE" \
               "$(file_as_js_string \
                      "$TRACIFY_DIR/trace-template-for-$TYPE.js")"
    done
}

function tracify {
    uglifyjs --beautify "$(tracify_options)" "$@"
}

After defining these in your shell, you can do e.g.

tracify sample-script.js > sample-script-tracified.js

If you're evaluating multiple tracified scripts in the same scope, you'll want to only include the preamble in the first one. Using functions above, the rest could be tracified like this

NO_PREAMBLE=omit tracify another-script.js > another-script-tracified.js

Evaluating and inspecting traces

When reverse-engineering some website's logic, you'll most likely run the tracified code in the browser. How you do it is up to you. Pasting it manually, "serving" with Mitmproxy, substituting scripts using some quick and dirty browser extension… Either way, don't forget to update the integrity checksum if they are used :)

Once the code has run, open JavaScript console in the context of that page. You can get the entire trace with

tracing.get_log()

This will be a list of log entry objects, each looking like this

{
​​    op_name: "+"
    line: 8
    column: 22
    ​​id: 71
​​    parent_call: Object { op_name: "call", line: 8, column: 11,  }
​​    left: 377
​​    right: 233
​​    result: 610
}

The left and right properties are of course specific to binary operations. Log entries of function calls will not have these but they will instead have e.g. a function_object property. You get the point.

Feel free to use JavaScript as an aid when inspecting traces

tracing.get_log().filter(op => op.function_object?.name === "jA")

You also get a map of objects (operands, function arguments, results, etc.) to lists of log entries they appear in. You can use it like this

tracing.get_objects().get(610) // How did 610 get produced?

Finally, your particular use case might require changes to the templates. Maybe the script you're RE'ing causes the page to get reloaded and you have no access to the tracing object? You might then want to modify the preamble to send the logs to your server, for example with the beacon API. Maybe the overhead of tracing is too big? Find out if you can limit the tracing to only a subset of expressions and still achieve the goal. Finally, avoiding name clashes with traced code and guarding against redefinitions of well-known properties/functions (think Map.prototype.get = "trololo";) are beyond the scope of this prototype. These should be easy to work around, though, if you're able to replay the browser session somehow.

Copying

Code on this git branch is Copyright 2024 Wojtek Kosior <koszko@koszko.org>, released under the terms of Creative Commons Zero v1.0.

This is public domain software made and released as a gift to the public. You can legally use it any way you want. However, I, the author, kindly request (without legal requirement) that you don't integrate it into any proprietary or otherwise harmful product. Please, make your derivative work free/libre software and a gift to the public as well!