├── .cshellrc ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc.json ├── .vscode ├── settings.json └── tasks.json ├── COPYING ├── DEVELOPMENT.md ├── Makefile ├── README.md ├── assets ├── cshell │ ├── test.js │ ├── vm-arm32 │ ├── vm-arm64 │ ├── vm-pty │ ├── vm-x64 │ └── vm-x86 ├── initrd │ ├── .cshellrc │ ├── .profile │ ├── entropy.c │ ├── group │ ├── init-arm32 │ ├── init-arm64 │ ├── init-x64 │ ├── init-x86 │ ├── motd │ └── passwd ├── target │ ├── module.c │ └── target.c └── toolchain │ ├── binutils-2.34-sysroot.patch │ ├── cpp.patch │ ├── frida.patch │ ├── gcc.patch │ └── glibc.patch ├── cmdlet.js ├── eslint.config.mjs ├── package ├── package-lock.json ├── package.json ├── src ├── breakpoints │ ├── bp.ts │ ├── bps.ts │ ├── code.ts │ ├── memory.ts │ ├── regs.ts │ ├── replace.ts │ └── trace.ts ├── cmdlets │ ├── breakpoints │ │ ├── bp.ts │ │ ├── code.ts │ │ ├── mem.ts │ │ ├── reg.ts │ │ ├── replace.ts │ │ └── trace.ts │ ├── data │ │ ├── assembly.ts │ │ ├── copy.ts │ │ ├── dump.ts │ │ ├── dumpfile.ts │ │ ├── read.ts │ │ ├── string.ts │ │ └── write.ts │ ├── development │ │ ├── debug.ts │ │ └── js.ts │ ├── files │ │ ├── cat.ts │ │ ├── fd.ts │ │ ├── src.ts │ │ ├── sz.ts │ │ └── tmp.ts │ ├── math │ │ └── math.ts │ ├── memory │ │ ├── sym.ts │ │ └── vm.ts │ ├── misc │ │ ├── corpse │ │ │ ├── core_filter.ts │ │ │ ├── core_pattern.ts │ │ │ ├── corpse.ts │ │ │ ├── dumpable.ts │ │ │ ├── fork.ts │ │ │ ├── madvise.ts │ │ │ ├── mem.ts │ │ │ ├── proc.ts │ │ │ ├── rlimit.ts │ │ │ └── selinux.ts │ │ ├── echo.ts │ │ ├── errno.ts │ │ ├── exit.ts │ │ ├── grep.ts │ │ ├── help.ts │ │ ├── history.ts │ │ ├── log.ts │ │ ├── macro.ts │ │ ├── print.ts │ │ ├── sh.ts │ │ └── var.ts │ ├── modules │ │ ├── ld.ts │ │ └── mod.ts │ ├── thread │ │ ├── bt.ts │ │ ├── hot.ts │ │ ├── thread.ts │ │ └── tls.ts │ └── trace │ │ └── trace.ts ├── commands │ ├── cmdlet.ts │ ├── cmdlets.ts │ └── command.ts ├── entrypoint.ts ├── io │ ├── char.ts │ ├── input.ts │ ├── output.ts │ ├── parser.ts │ ├── token.ts │ └── zmodem │ │ ├── block.ts │ │ ├── constants.ts │ │ ├── crc.ts │ │ ├── frame.ts │ │ ├── input.ts │ │ ├── output.ts │ │ ├── sz.ts │ │ └── zmodem.ts ├── macros │ └── macros.ts ├── memory │ ├── mem.ts │ └── overlay.ts ├── misc │ ├── base64.ts │ ├── exception.ts │ ├── files.ts │ ├── format.ts │ ├── numeric.ts │ ├── regex.ts │ └── version.ts ├── terminal │ ├── history.ts │ └── line.ts ├── tls │ ├── arm.ts │ ├── arm64.ts │ ├── ia32.ts │ ├── tls.ts │ └── x64.ts ├── traces │ ├── block.ts │ ├── call.ts │ ├── coverage │ │ ├── coverage.ts │ │ └── trace.ts │ └── trace.ts └── vars │ ├── var.ts │ └── vars.ts ├── tsconfig.json └── version.js /.cshellrc: -------------------------------------------------------------------------------- 1 | # allocate some memory, assign a variable and dump it 2 | malloc 32 3 | v p ret 4 | d p 5 | 6 | # load a new commandlet from a script file and execute 7 | js cmdlet.js 8 | test1 malloc 9 | 10 | # show the first instruction of exit 11 | l exit 1 12 | 13 | # set up a macro 14 | m test3 15 | l main 16 | q 17 | 18 | !test3 19 | 20 | # set up a breakpoint 21 | @f malloc 1 ? 22 | != $pc 0 23 | q 24 | bt 25 | @f #1 # 26 | q 27 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Frida C-Shell", 3 | "image": "frida-cshell", 4 | "customizations": { 5 | "vscode": { 6 | "extensions": [ 7 | "dbaeumer.vscode-eslint", 8 | "rvest.vs-code-prettier-eslint", 9 | "ms-vscode.cpptools", 10 | "ms-azuretools.vscode-docker", 11 | "ms-vscode.makefile-tools", 12 | "ms-vscode.cpptools-extension-pack" 13 | ] 14 | } 15 | }, 16 | "runArgs": [ 17 | "--cap-add=SYS_PTRACE", 18 | "--security-opt", 19 | "seccomp=unconfined" 20 | ], 21 | "mounts": [ 22 | "source=${localWorkspaceFolder},target=/home/ws,type=bind", 23 | "source=${localEnv:HOME},target=/home/share,type=bind" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | id: checkout 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - name: Node 17 | id: node 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: '20.x' 21 | - name: Install 22 | id: install 23 | run: npm install 24 | - name: Release 25 | id: release 26 | uses: justincy/github-action-npm-release@2.0.1 27 | - name: Upload JS 28 | id: uploadjs 29 | uses: actions/upload-release-asset@v1 30 | if: ${{ steps.release.outputs.released == 'true' }} 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | with: 34 | upload_url: ${{ steps.release.outputs.upload_url }} 35 | asset_path: ${{ github.workspace }}/frida-cshell.js 36 | asset_name: frida-cshell-${{ github.ref_name }}.js 37 | asset_content_type: text/javascript 38 | - name: Upload SH 39 | id: uploadsh 40 | uses: actions/upload-release-asset@v1 41 | if: ${{ steps.release.outputs.released == 'true' }} 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | with: 45 | upload_url: ${{ steps.release.outputs.upload_url }} 46 | asset_path: ${{ github.workspace }}/frida-cshell 47 | asset_name: frida-cshell-${{ github.ref_name }} 48 | asset_content_type: text/x-shellscript 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | frida-cshell.js 3 | frida-cshell 4 | src/version.ts 5 | core 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "arrowParens": "avoid", 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "quoteProps": "consistent", 8 | "trailingComma": "all", 9 | "bracketSpacing": true, 10 | "bracketSameLine": true 11 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint", 3 | "editor.formatOnPaste": true, 4 | "editor.formatOnSave": true, 5 | "[c]": { 6 | "editor.defaultFormatter": "ms-vscode.cpptools" 7 | }, 8 | "cmake.configureOnOpen": false, 9 | "makefile.configureOnOpen": false, 10 | "typescript.tsc.autoDetect": "off", 11 | } 12 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | wxWindows Library Licence, Version 3.1 2 | ====================================== 3 | 4 | Copyright (c) 1998-2005 Julian Smart, Robert Roebling et al 5 | 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this licence document, but changing it is not allowed. 8 | 9 | WXWINDOWS LIBRARY LICENCE 10 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 11 | 12 | This library is free software; you can redistribute it and/or modify it 13 | under the terms of the GNU Library General Public Licence as published by 14 | the Free Software Foundation; either version 2 of the Licence, or (at your 15 | option) any later version. 16 | 17 | This library is distributed in the hope that it will be useful, but WITHOUT 18 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public 20 | Licence for more details. 21 | 22 | You should have received a copy of the GNU Library General Public Licence 23 | along with this software, usually in a file named COPYING.LIB. If not, 24 | write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth 25 | Floor, Boston, MA 02110-1301 USA. 26 | 27 | EXCEPTION NOTICE 28 | 29 | 1. As a special exception, the copyright holders of this library give 30 | permission for additional uses of the text contained in this release of the 31 | library as licenced under the wxWindows Library Licence, applying either 32 | version 3.1 of the Licence, or (at your option) any later version of the 33 | Licence as published by the copyright holders of version 3.1 of the Licence 34 | document. 35 | 36 | 2. The exception is that you may use, copy, link, modify and distribute 37 | under your own terms, binary object code versions of works based on the 38 | Library. 39 | 40 | 3. If you copy code from files distributed under the terms of the GNU 41 | General Public Licence or the GNU Library General Public Licence into a 42 | copy of this library, as this licence permits, the exception does not apply 43 | to the code that you add in this way. To avoid misleading anyone as to the 44 | status of such modified files, you must delete this exception notice from 45 | such code and/or adjust the licensing conditions notice accordingly. 46 | 47 | 4. If you write modifications of your own for this library, it is your 48 | choice whether to permit this exception to apply to your modifications. If 49 | you do not wish that, you must delete the exception notice from such code 50 | and/or adjust the licensing conditions notice accordingly. 51 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX:=frida-cshell 2 | STEPS:= $(shell grep "^FROM" Dockerfile | cut -d " " -f 4) 3 | 4 | .PHONY: all 5 | 6 | all: $(STEPS) 7 | 8 | define BUILD_STEP 9 | $(1): 10 | DOCKER_BUILDKIT=1 \ 11 | docker build \ 12 | --build-arg http_proxy=$$$$http_proxy \ 13 | --build-arg https_proxy=$$$$https_proxy \ 14 | -t $(PREFIX)-$(1) \ 15 | --target $(1) \ 16 | . 17 | 18 | run-$(1): $(1) 19 | docker run \ 20 | -ti \ 21 | --rm \ 22 | --name $(PREFIX)-$(1) \ 23 | -u root \ 24 | -v $(HOME):/home/share \ 25 | $(PREFIX)-$(1) 26 | endef 27 | 28 | $(foreach step,$(STEPS),$(eval $(call BUILD_STEP,$(step)))) 29 | 30 | 31 | save: $(STEPS) 32 | docker save $(foreach step,$(STEPS),$(PREFIX)-$(step)) | xz -T0 -c > $(PREFIX)-images.tar.xz 33 | 34 | prune: 35 | docker builder prune -f 36 | docker image prune -f 37 | -------------------------------------------------------------------------------- /assets/cshell/test.js: -------------------------------------------------------------------------------- 1 | console.log(`Process: ${Process.id}`); 2 | -------------------------------------------------------------------------------- /assets/cshell/vm-arm32: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cmd="$@" 3 | # see https://bugs.launchpad.net/qemu/+bug/1790975 4 | qemu-system-arm \ 5 | -machine virtualization=true \ 6 | -machine virt,highmem=off \ 7 | -smp 4 \ 8 | -kernel /root/zImage-arm32 \ 9 | -initrd /root/initramfs-arm32.img \ 10 | -append "earlyprintk=serial,ttyAMA0 console=ttyAMA0 coredump_filter=0x3f FRIDA_INJECT=$FRIDA_INJECT cmd=\"$cmd\"" \ 11 | -m 4096M \ 12 | -net nic,id=eth \ 13 | -net user,id=mynet,net=192.168.76.0/24 \ 14 | -virtfs local,path=/home/share/,mount_tag=host0,security_model=passthrough,id=host0 \ 15 | -virtfs local,path=/home/ws/,mount_tag=host1,security_model=passthrough,id=host1 \ 16 | -nographic \ 17 | -no-reboot -------------------------------------------------------------------------------- /assets/cshell/vm-arm64: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cmd="$@" 3 | qemu-system-aarch64 \ 4 | -M virt \ 5 | -machine virtualization=true \ 6 | -machine type=virt \ 7 | -cpu cortex-a72 \ 8 | -smp 4 \ 9 | -kernel /root/zImage-arm64 \ 10 | -initrd /root/initramfs-arm64.img \ 11 | -append "earlyprintk=serial,ttyAMA0 console=ttyAMA0 coredump_filter=0x3f FRIDA_INJECT=$FRIDA_INJECT cmd=\"$cmd\"" \ 12 | -m 4096M \ 13 | -net nic,id=eth \ 14 | -net user,id=mynet,net=192.168.76.0/24 \ 15 | -virtfs local,path=/home/share/,mount_tag=host0,security_model=passthrough,id=host0 \ 16 | -virtfs local,path=/home/ws/,mount_tag=host1,security_model=passthrough,id=host1 \ 17 | -nographic \ 18 | -no-reboot -------------------------------------------------------------------------------- /assets/cshell/vm-pty: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cmd="$@" 3 | # see https://bugs.launchpad.net/qemu/+bug/1790975 4 | qemu-system-arm \ 5 | -machine virtualization=true \ 6 | -machine virt,highmem=off \ 7 | -smp 4 \ 8 | -kernel /root/zImage-arm32 \ 9 | -initrd /root/initramfs-arm32.img \ 10 | -append "earlyprintk=serial,ttyAMA0 console=ttyAMA0 coredump_filter=0x3f FRIDA_INJECT=$FRIDA_INJECT cmd=\"$cmd\"" \ 11 | -m 4096M \ 12 | -net nic,id=eth \ 13 | -net user,id=mynet,net=192.168.76.0/24 \ 14 | -virtfs local,path=/home/share/,mount_tag=host0,security_model=passthrough,id=host0 \ 15 | -virtfs local,path=/home/ws/,mount_tag=host1,security_model=passthrough,id=host1 \ 16 | -nographic \ 17 | -no-reboot \ 18 | -serial pty 19 | -------------------------------------------------------------------------------- /assets/cshell/vm-x64: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cmd="$@" 3 | qemu-system-x86_64 \ 4 | -smp 4 \ 5 | -kernel /root/bzImage-x64 \ 6 | -initrd /root/initramfs-x64.img \ 7 | -append "earlyprintk=serial,ttyS0 console=ttyS0 coredump_filter=0x3f FRIDA_INJECT=$FRIDA_INJECT cmd=\"$cmd\"" \ 8 | -m 4096M \ 9 | -net nic,id=eth \ 10 | -net user,id=mynet,net=192.168.76.0/24 \ 11 | -virtfs local,path=/home/share/,mount_tag=host0,security_model=passthrough,id=host0 \ 12 | -virtfs local,path=/home/ws/,mount_tag=host1,security_model=passthrough,id=host1 \ 13 | -nographic \ 14 | -no-reboot -------------------------------------------------------------------------------- /assets/cshell/vm-x86: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cmd="$@" 3 | qemu-system-i386 \ 4 | -smp 4 \ 5 | -kernel /root/bzImage-x86 \ 6 | -initrd /root/initramfs-x86.img \ 7 | -append "earlyprintk=serial,ttyS0 console=ttyS0 coredump_filter=0x3f FRIDA_INJECT=$FRIDA_INJECT cmd=\"$cmd\"" \ 8 | -m 4096M \ 9 | -net nic,id=eth \ 10 | -net user,id=mynet,net=192.168.76.0/24 \ 11 | -virtfs local,path=/home/share/,mount_tag=host0,security_model=passthrough,id=host0 \ 12 | -virtfs local,path=/home/ws/,mount_tag=host1,security_model=passthrough,id=host1 \ 13 | -nographic \ 14 | -no-reboot -------------------------------------------------------------------------------- /assets/initrd/.cshellrc: -------------------------------------------------------------------------------- 1 | # allocate some memory, assign a variable and dump it 2 | malloc 32 3 | v p ret 4 | d p 5 | 6 | # load a new commandlet from a script file and execute 7 | src /home/ws/src.js 8 | test malloc 9 | 10 | # show the first instruction of exit 11 | l exit 1 12 | 13 | -------------------------------------------------------------------------------- /assets/initrd/.profile: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ ! -z "$cmd" ]; then 3 | $cmd 4 | fi 5 | -------------------------------------------------------------------------------- /assets/initrd/entropy.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define ENTROPY_SIZE 4096 11 | #define ENTROPY_COUNT 256 12 | 13 | typedef struct 14 | { 15 | struct rand_pool_info pool_info; 16 | uint8_t data[ENTROPY_SIZE]; 17 | } entropy_t; 18 | 19 | int main(int argc, char **argv) 20 | { 21 | entropy_t entropy = { 22 | .pool_info = { 23 | .buf_size = ENTROPY_SIZE, 24 | .entropy_count = ENTROPY_SIZE * 8, 25 | }, 26 | .data = {0}}; 27 | int fd = open("/dev/random", O_RDWR); 28 | if (fd < 0) 29 | { 30 | perror("failed to open /dev/random"); 31 | return EXIT_FAILURE; 32 | } 33 | 34 | for (uint32_t i = 0; i < ENTROPY_COUNT; i++) 35 | { 36 | if (ioctl(fd, RNDADDENTROPY, &entropy) != 0) 37 | { 38 | perror("failed to write entropy"); 39 | return EXIT_FAILURE; 40 | } 41 | } 42 | 43 | close(fd); 44 | 45 | return EXIT_SUCCESS; 46 | } 47 | -------------------------------------------------------------------------------- /assets/initrd/group: -------------------------------------------------------------------------------- 1 | root:x:0: -------------------------------------------------------------------------------- /assets/initrd/init-arm32: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mount -t proc none /proc 3 | mount -t sysfs none /sys 4 | mount -t tmpfs none /tmp 5 | mount -t devtmpfs none /dev 6 | mount -t 9p -o rw,sync,dirsync,relatime,trans=virtio,msize=131072,version=9p2000.L host0 /home/share 7 | mount -t 9p -o rw,sync,dirsync,relatime,trans=virtio,msize=131072,version=9p2000.L host1 /home/ws 8 | ip addr add 192.168.76.20/24 dev eth0 9 | ip link set eth0 up 10 | ip route add default via 192.168.76.2 dev eth0 11 | export LD_LIBRARY_PATH=/lib:/lib64 12 | /sbin/entropy 13 | cat /etc/motd 14 | export PS1='kshell:\w\$ ' 15 | exec setsid sh -l -c 'exec sh /dev/ttyAMA0 2>&1' 16 | -------------------------------------------------------------------------------- /assets/initrd/init-arm64: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mount -t proc none /proc 3 | mount -t sysfs none /sys 4 | mount -t tmpfs none /tmp 5 | mount -t devtmpfs none /dev 6 | mount -t 9p -o rw,sync,dirsync,relatime,trans=virtio,msize=131072,version=9p2000.L host0 /home/share 7 | mount -t 9p -o rw,sync,dirsync,relatime,trans=virtio,msize=131072,version=9p2000.L host1 /home/ws 8 | ip addr add 192.168.76.20/24 dev eth0 9 | ip link set eth0 up 10 | ip route add default via 192.168.76.2 dev eth0 11 | export LD_LIBRARY_PATH=/lib:/lib64 12 | /sbin/entropy 13 | cat /etc/motd 14 | export PS1='kshell:\w\$ ' 15 | exec setsid sh -l -c 'exec sh /dev/ttyAMA0 2>&1' 16 | -------------------------------------------------------------------------------- /assets/initrd/init-x64: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mount -t proc none /proc 3 | mount -t sysfs none /sys 4 | mount -t tmpfs none /tmp 5 | mount -t devtmpfs none /dev 6 | mount -t 9p -o rw,sync,dirsync,relatime,trans=virtio,msize=131072,version=9p2000.L host0 /home/share 7 | mount -t 9p -o rw,sync,dirsync,relatime,trans=virtio,msize=131072,version=9p2000.L host1 /home/ws 8 | ip addr add 192.168.76.20/24 dev eth0 9 | ip link set eth0 up 10 | ip route add default via 192.168.76.2 dev eth0 11 | export LD_LIBRARY_PATH=/lib:/lib64 12 | /sbin/entropy 13 | cat /etc/motd 14 | export PS1='kshell:\w\$ ' 15 | exec setsid sh -l -c 'exec sh /dev/ttyS0 2>&1' 16 | -------------------------------------------------------------------------------- /assets/initrd/init-x86: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mount -t proc none /proc 3 | mount -t sysfs none /sys 4 | mount -t tmpfs none /tmp 5 | mount -t devtmpfs none /dev 6 | mount -t 9p -o rw,sync,dirsync,relatime,trans=virtio,msize=131072,version=9p2000.L host0 /home/share 7 | mount -t 9p -o rw,sync,dirsync,relatime,trans=virtio,msize=131072,version=9p2000.L host1 /home/ws 8 | ip addr add 192.168.76.20/24 dev eth0 9 | ip link set eth0 up 10 | ip route add default via 192.168.76.2 dev eth0 11 | export LD_LIBRARY_PATH=/lib:/lib64 12 | /sbin/entropy 13 | cat /etc/motd 14 | export PS1='kshell:\w\$ ' 15 | exec setsid sh -l -c 'exec sh /dev/ttyS0 2>&1' 16 | -------------------------------------------------------------------------------- /assets/initrd/motd: -------------------------------------------------------------------------------- 1 | DOCKER 22 - TEST ENVIRONMENT 2 | 3 | **************************************** 4 | * To exit the emulator press CTRL-A, X * 5 | **************************************** 6 | 7 | -------------------------------------------------------------------------------- /assets/initrd/passwd: -------------------------------------------------------------------------------- 1 | root:7UgX8oG8/gbw2:0:0:Linux User,,,:/root:/bin/sh -------------------------------------------------------------------------------- /assets/target/module.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void __attribute__((constructor)) my_constructor() 4 | { 5 | puts("Shared object loaded\n"); 6 | } 7 | 8 | void __attribute__((destructor)) my_destructor() 9 | { 10 | puts("Shared object unloaded\n"); 11 | } 12 | -------------------------------------------------------------------------------- /assets/target/target.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define NUM_THREADS 5 11 | #define PORT 1337 12 | #define BUFFER_SIZE 1024 13 | 14 | typedef unsigned int uint; 15 | 16 | __attribute__((noinline)) void my_memcpy(void *dest, const void *src, size_t n) 17 | { 18 | memcpy(dest, src, n); 19 | } 20 | 21 | const char test[] = "TEST_STRING"; 22 | 23 | void my_h(uint i) 24 | { 25 | printf("chain: %u\n", i); 26 | } 27 | 28 | void my_g(uint i) 29 | { 30 | if ((i % 2) == 0) 31 | { 32 | my_h(1); 33 | } 34 | else 35 | { 36 | my_h(2); 37 | } 38 | } 39 | 40 | void my_f(uint i) 41 | { 42 | if ((i % 2) == 0) 43 | { 44 | my_g(1); 45 | } 46 | else 47 | { 48 | my_g(2); 49 | } 50 | } 51 | 52 | void my_e(uint i) 53 | { 54 | if ((i % 2) == 0) 55 | { 56 | my_f(1); 57 | } 58 | else 59 | { 60 | my_f(2); 61 | } 62 | } 63 | 64 | void my_d(uint i) 65 | { 66 | if ((i % 2) == 0) 67 | { 68 | my_e(1); 69 | } 70 | else 71 | { 72 | my_e(2); 73 | } 74 | } 75 | 76 | void my_c(uint i) 77 | { 78 | if ((i % 2) == 0) 79 | { 80 | my_d(1); 81 | } 82 | else 83 | { 84 | my_d(2); 85 | } 86 | } 87 | 88 | void my_b(uint i) 89 | { 90 | if ((i % 2) == 0) 91 | { 92 | my_c(1); 93 | } 94 | else 95 | { 96 | my_c(2); 97 | } 98 | } 99 | 100 | void my_a(uint i) 101 | { 102 | for (uint n = 0; n < 3; n++) 103 | { 104 | if ((i % 2) == 0) 105 | { 106 | my_b(1); 107 | } 108 | else 109 | { 110 | my_b(2); 111 | } 112 | } 113 | } 114 | 115 | static void *busy_loop(void *arg) 116 | { 117 | char thread_name[16] = {0}; 118 | int index = *(int *)arg; 119 | printf("Thread %d started\n", index); 120 | 121 | snprintf(thread_name, sizeof(thread_name), "Child-%d", index); 122 | pthread_setname_np(pthread_self(), thread_name); 123 | 124 | long limit = (index + 1) * 10000000L; 125 | 126 | while (true) 127 | { 128 | for (volatile long i = 0; i < limit; i++) 129 | ; 130 | 131 | usleep(500000); 132 | } 133 | 134 | return NULL; 135 | } 136 | 137 | static void *network_loop(void *arg) 138 | { 139 | char thread_name[] = "Network"; 140 | pthread_setname_np(pthread_self(), thread_name); 141 | int server_fd, client_fd; 142 | struct sockaddr_in server_addr, client_addr; 143 | socklen_t addr_len = sizeof(client_addr); 144 | char buffer[BUFFER_SIZE]; 145 | ssize_t bytes_read; 146 | 147 | server_fd = socket(AF_INET, SOCK_STREAM, 0); 148 | memset(&server_addr, 0, sizeof(server_addr)); 149 | server_addr.sin_family = AF_INET; 150 | server_addr.sin_addr.s_addr = INADDR_ANY; 151 | server_addr.sin_port = htons(PORT); 152 | bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); 153 | listen(server_fd, 5); 154 | 155 | while (1) 156 | { 157 | client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len); 158 | while ((bytes_read = read(client_fd, buffer, sizeof(buffer))) > 0) 159 | { 160 | write(client_fd, buffer, bytes_read); 161 | } 162 | close(client_fd); 163 | } 164 | 165 | close(server_fd); 166 | return NULL; 167 | } 168 | 169 | int main(int argc, char **argv, char **envp) 170 | { 171 | pthread_t threads[NUM_THREADS] = {0}; 172 | pthread_t network_thread = 0; 173 | int thread_indices[NUM_THREADS] = {0}; 174 | 175 | int fd = open("/dev/null", O_RDWR); 176 | dup2(fd, STDIN_FILENO); 177 | dup2(fd, STDOUT_FILENO); 178 | dup2(fd, STDERR_FILENO); 179 | close(fd); 180 | 181 | pthread_setname_np(pthread_self(), "Parent"); 182 | 183 | for (int i = 0; i < NUM_THREADS; i++) 184 | { 185 | thread_indices[i] = i; 186 | if (pthread_create(&threads[i], NULL, busy_loop, &thread_indices[i]) != 0) 187 | { 188 | perror("Failed to create thread"); 189 | return 1; 190 | } 191 | } 192 | 193 | if (pthread_create(&network_thread, NULL, network_loop, NULL) != 0) 194 | { 195 | perror("Failed to create network thread"); 196 | return 1; 197 | } 198 | 199 | while (true) 200 | { 201 | 202 | my_a(rand()); 203 | 204 | char *buf = malloc(sizeof(test)); 205 | 206 | if (buf == NULL) 207 | break; 208 | 209 | my_memcpy(buf, test, sizeof(test)); 210 | 211 | puts(buf); 212 | 213 | free(buf); 214 | 215 | for (volatile long i = 0; i < 100000000L; i++) 216 | ; 217 | 218 | usleep(500000); 219 | } 220 | 221 | for (int i = 0; i < NUM_THREADS; i++) 222 | { 223 | pthread_join(threads[i], NULL); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /assets/toolchain/binutils-2.34-sysroot.patch: -------------------------------------------------------------------------------- 1 | Signed-off-by: Sven Rebhan 2 | 3 | Always try to prepend the sysroot prefix to absolute filenames first. 4 | 5 | http://bugs.gentoo.org/275666 6 | http://sourceware.org/bugzilla/show_bug.cgi?id=10340 7 | 8 | --- 9 | ld/ldfile.c | 11 +++++++++-- 10 | 1 file changed, 9 insertions(+), 2 deletions(-) 11 | 12 | --- a/ld/ldfile.c 13 | +++ b/ld/ldfile.c 14 | @@ -339,18 +339,25 @@ 15 | directory first. */ 16 | if (!entry->flags.maybe_archive) 17 | { 18 | - if (entry->flags.sysrooted && IS_ABSOLUTE_PATH (entry->filename)) 19 | + /* For absolute pathnames, try to always open the file in the 20 | + sysroot first. If this fails, try to open the file at the 21 | + given location. */ 22 | + entry->flags.sysrooted = is_sysrooted_pathname (entry->filename); 23 | + if (!entry->flags.sysrooted && IS_ABSOLUTE_PATH (entry->filename) 24 | + && ld_sysroot) 25 | { 26 | char *name = concat (ld_sysroot, entry->filename, 27 | (const char *) NULL); 28 | if (ldfile_try_open_bfd (name, entry)) 29 | { 30 | entry->filename = name; 31 | + entry->flags.sysrooted = TRUE; 32 | return TRUE; 33 | } 34 | free (name); 35 | } 36 | - else if (ldfile_try_open_bfd (entry->filename, entry)) 37 | + 38 | + if (ldfile_try_open_bfd (entry->filename, entry)) 39 | return TRUE; 40 | 41 | if (IS_ABSOLUTE_PATH (entry->filename)) 42 | -------------------------------------------------------------------------------- /assets/toolchain/cpp.patch: -------------------------------------------------------------------------------- 1 | --- gcc/include-fixed/limits.h 2023-07-26 13:52:38.544657025 +0000 2 | +++ gcc/include-fixed/limits.h 2023-07-26 13:53:00.912657343 +0000 3 | @@ -149,4 +149,8 @@ 4 | # define ULLONG_WIDTH __LONG_LONG_WIDTH__ 5 | #endif 6 | 7 | +#ifndef PATH_MAX 8 | +# define PATH_MAX (4096) 9 | +#endif 10 | + 11 | #endif /* _LIMITS_H___ */ 12 | -------------------------------------------------------------------------------- /assets/toolchain/frida.patch: -------------------------------------------------------------------------------- 1 | --- meson.build 2024-07-01 10:38:56.579118441 +0000 2 | +++ meson.build.patched 2024-07-01 10:38:52.443042274 +0000 3 | @@ -296,7 +296,7 @@ 4 | endif 5 | 6 | foreach library : openssl_libraries 7 | - dependencies += compiler.find_library(library) 8 | + dependencies += compiler.find_library(library, static: true) 9 | endforeach 10 | 11 | # We may need to add some defines for static builds 12 | -------------------------------------------------------------------------------- /assets/toolchain/gcc.patch: -------------------------------------------------------------------------------- 1 | --- libsanitizer/sanitizer_common/sanitizer_platform_limits_posix.cc 2023-07-26 12:51:49.904605157 +0000 2 | +++ libsanitizer/sanitizer_common/sanitizer_platform_limits_posix.cc 2023-07-26 12:51:54.012605215 +0000 3 | @@ -1158,7 +1158,7 @@ 4 | CHECK_SIZE_AND_OFFSET(ipc_perm, cgid); 5 | #if !defined(__aarch64__) || !SANITIZER_LINUX || __GLIBC_PREREQ (2, 21) 6 | /* On aarch64 glibc 2.20 and earlier provided incorrect mode field. */ 7 | -CHECK_SIZE_AND_OFFSET(ipc_perm, mode); 8 | +//CHECK_SIZE_AND_OFFSET(ipc_perm, mode); 9 | #endif 10 | 11 | CHECK_TYPE_SIZE(shmid_ds); 12 | -------------------------------------------------------------------------------- /assets/toolchain/glibc.patch: -------------------------------------------------------------------------------- 1 | --- configure 2023-07-26 10:41:28.644493972 +0000 2 | +++ configure 2023-07-26 10:40:35.780493220 +0000 3 | @@ -4601,7 +4601,7 @@ 4 | ac_prog_version=`$AS --version 2>&1 | sed -n 's/^.*GNU assembler.* \([0-9]*\.[0-9.]*\).*$/\1/p'` 5 | case $ac_prog_version in 6 | '') ac_prog_version="v. ?.??, bad"; ac_verc_fail=yes;; 7 | - 2.1[0-9][0-9]*|2.2[5-9]*|2.[3-9][0-9]*|[3-9].*|[1-9][0-9]*) 8 | + 2.1[0-9][0-9]*|2.2[5-9]*|2.[3-9][0-9]*|[3-9].*|[1-9][0-9]*|4.*) 9 | ac_prog_version="$ac_prog_version, ok"; ac_verc_fail=no;; 10 | *) ac_prog_version="$ac_prog_version, bad"; ac_verc_fail=yes;; 11 | -------------------------------------------------------------------------------- /cmdlet.js: -------------------------------------------------------------------------------- 1 | function dumpStuff(tokens, name, length) { 2 | if (tokens.length !== 1) throw new Error("expected 1 argument"); 3 | const address = tokens[0].toVar().toPointer(); 4 | Output.writeln(`${name}: ${address}`); 5 | 6 | const bytes = Mem.readBytes(address, length); 7 | const dump = hexdump(bytes.buffer, { 8 | length: length, 9 | header: true, 10 | ansi: true, 11 | address: address, 12 | }); 13 | const prefixed = dump.split('\n').join(`\n${Output.green("0x")}`); 14 | Output.writeln(` ${prefixed}`); 15 | } 16 | 17 | return [ 18 | { 19 | name: "test1", 20 | 21 | runSync: function(tokens) { 22 | try { 23 | dumpStuff(tokens, "test1", 16); 24 | return Var.ZERO; 25 | } catch { 26 | return this.usage(); 27 | } 28 | }, 29 | }, 30 | { 31 | name: "test2", 32 | 33 | runSync: function(tokens) { 34 | try { 35 | dumpStuff(tokens, "test2", 32); 36 | return Var.ZERO; 37 | } catch { 38 | return this.usage(); 39 | } 40 | }, 41 | }, 42 | ] 43 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import tseslint from "typescript-eslint"; 4 | 5 | 6 | export default [ 7 | {files: ["**/*.{js,mjs,cjs,ts}"]}, 8 | {languageOptions: { globals: globals.browser }}, 9 | pluginJs.configs.recommended, 10 | ...tseslint.configs.recommended, 11 | { 12 | rules: { 13 | "no-unused-vars": "off", 14 | "@typescript-eslint/no-unused-vars": [ 15 | "error", // or "error" 16 | { 17 | "argsIgnorePattern": "^_", 18 | "varsIgnorePattern": "^_", 19 | "caughtErrorsIgnorePattern": "^_" 20 | } 21 | ] 22 | } 23 | } 24 | ]; 25 | -------------------------------------------------------------------------------- /package: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | TEMPDIR=$(mktemp -d package-cshell.XXXXXXXXXX) 3 | trap "rm -rf $TEMPDIR" EXIT 4 | 5 | echo '#!/bin/bash' > $TEMPDIR/frida-cshell 6 | echo 'SCRIPT=$(cat <> $TEMPDIR/frida-cshell 7 | gzip -c frida-cshell.js | base64 >> $TEMPDIR/frida-cshell 8 | echo 'EOF' >> $TEMPDIR/frida-cshell 9 | echo ')' >> $TEMPDIR/frida-cshell 10 | 11 | cat <> $TEMPDIR/frida-cshell 12 | debug= 13 | file= 14 | name= 15 | pid= 16 | 17 | show_help () { 18 | echo Usage 19 | echo " \$0 [OPTION?]" 20 | echo 21 | echo Help Options: 22 | echo " -h, show help options" 23 | echo 24 | echo Application Options: 25 | echo " -f spawn FILE" 26 | echo " -n attach to NAME" 27 | echo " -p attach to PID" 28 | echo " -d enable debug mode" 29 | echo 30 | } 31 | 32 | if [[ "\$1" == "--help" ]]; then 33 | show_help 34 | exit 0 35 | fi 36 | 37 | while getopts ":f:hn:p:d" opt; do 38 | case \$opt in 39 | f) 40 | file=\$OPTARG 41 | ;; 42 | h) 43 | help=true 44 | ;; 45 | n) 46 | name=\$OPTARG 47 | ;; 48 | p) 49 | pid=\$OPTARG 50 | ;; 51 | d) 52 | debug=true 53 | ;; 54 | :) 55 | echo "Option - \$OPTARG requires an argument." 56 | exit 1 57 | ;; 58 | *) 59 | echo "Invalid option: - \$OPTARG" 60 | show_help 61 | exit 1 62 | ;; 63 | esac 64 | done 65 | 66 | if [ \${help} ]; then 67 | show_help 68 | exit 0 69 | fi 70 | 71 | FRIDA_INJECT="\${FRIDA_INJECT:-frida-inject}" 72 | 73 | if ! command -v \$FRIDA_INJECT &> /dev/null 74 | then 75 | echo "\$FRIDA_INJECT could not be found. Try setting set FRIDA_INJECT environment variable" 76 | exit 1 77 | fi 78 | 79 | if [ \${debug} ]; then 80 | opt="{\"debug\":true}" 81 | else 82 | opt="{\"debug\":false}" 83 | fi 84 | 85 | if [ -z \${file} ] && [ -z \${name} ] && [ -z \${pid} ]; then 86 | echo "file, name or pid must be specified"; 87 | show_help 88 | exit 1 89 | fi 90 | 91 | FILE=\$(mktemp /tmp/frida-cshell.XXXXXXXXXX) 92 | trap "rm -f \$FILE" EXIT 93 | 94 | if command -v base64 &> /dev/null; then 95 | echo "\$SCRIPT" | base64 -d | gzip -d > \$FILE 96 | else 97 | BASE64=\$(mktemp /tmp/frida-cshell-base64.XXXXXXXXXX) 98 | cat < \$BASE64 99 | #!/usr/bin/awk -f 100 | 101 | function decode64() 102 | { 103 | b64="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 104 | while( getline < "/dev/stdin" ) 105 | { 106 | l = length( \\\$0 ); 107 | for( i = 1; i <= l; ++i ) 108 | { 109 | c = index( b64, substr( \\\$0, i, 1 ) ); 110 | if( c-- ) 111 | { 112 | for( b = 0; b < 6; ++b ) 113 | { 114 | o = o*2+int( c/32 ); 115 | c = (c*2)%64; 116 | if( ++obc == 8 ) 117 | { 118 | printf "%c", o; 119 | obc = 0; 120 | o = 0; 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | BEGIN { decode64() } 129 | EOI 130 | 131 | trap "rm -f \$BASE64" EXIT 132 | echo "\$SCRIPT" | awk -f "\$BASE64" | gzip -d > \$FILE 133 | fi 134 | 135 | if [ \${file} ]; then 136 | if [ \${name} ] || [ \${pid}]; then 137 | echo "file, name and pid are mutually exclusive" 138 | exit 1 139 | fi 140 | exec \$FRIDA_INJECT --interactive -s \$FILE -P \$opt -f \$file 141 | elif [ \${name} ]; then 142 | if [ \${file} ] || [ \${pid}]; then 143 | echo "file, name and pid are mutually exclusive" 144 | exit 1 145 | fi 146 | exec \$FRIDA_INJECT --interactive -s \$FILE -P \$opt -n \$name 147 | elif [ \${pid} ]; then 148 | if [ \${file} ] || [ \${name}]; then 149 | echo "file, name and pid are mutually exclusive" 150 | exit 1 151 | fi 152 | exec \$FRIDA_INJECT --interactive -s \$FILE -P \$opt -p \$pid 153 | fi 154 | 155 | EOF 156 | 157 | cp $TEMPDIR/frida-cshell ./frida-cshell 158 | chmod +x ./frida-cshell 159 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frida-cshell", 3 | "version": "1.8.12", 4 | "description": "Frida's CShell", 5 | "scripts": { 6 | "prepare": "npm run version && npm run build && npm run package && npm run copy", 7 | "build": "frida-compile src/entrypoint.ts -o frida-cshell.js -c", 8 | "lint": "eslint src", 9 | "pretty": "npx prettier --write src", 10 | "version": "node version.js", 11 | "package": "./package", 12 | "copy": "cp .cshellrc ~/.cshellrc" 13 | }, 14 | "devDependencies": { 15 | "@eslint/js": "^9.10.0", 16 | "@types/frida-gum": "^18.7", 17 | "eslint": "^9.10.0", 18 | "frida-compile": "^16.4.1", 19 | "globals": "^15.9.0", 20 | "prettier": "^3.3.3", 21 | "replace": "^1.2.2", 22 | "typescript-eslint": "^8.6.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/breakpoints/bps.ts: -------------------------------------------------------------------------------- 1 | import { Bp, BpKind, BpType } from './bp.js'; 2 | 3 | export class Bps { 4 | private static byIndex: Map = new Map(); 5 | 6 | private constructor() {} 7 | 8 | public static add(bp: Bp) { 9 | const idx = this.getNextFreeIndex(bp.type); 10 | const key = this.buildKey(bp.type, idx); 11 | 12 | this.checkOverlaps(bp); 13 | this.byIndex.set(key, bp); 14 | } 15 | 16 | public static checkOverlaps(bp: Bp): void { 17 | const overlapping = Array.from(this.byIndex.values()).some( 18 | b => 19 | b.overlaps(bp) && (b.kind !== BpKind.Code || bp.kind !== BpKind.Code), 20 | ); 21 | 22 | if (overlapping) { 23 | throw new Error(`breakpoint overlaps existing breakpoint:\n\t`); 24 | } 25 | } 26 | 27 | public static getNextFreeIndex(type: BpType): number { 28 | let idx = 1; 29 | while (true) { 30 | const key = this.buildKey(type, idx); 31 | if (!this.byIndex.has(key)) return idx; 32 | idx++; 33 | } 34 | } 35 | 36 | private static buildKey(type: BpType, index: number): string { 37 | return `${type}:${index.toString()}`; 38 | } 39 | 40 | public static get(type: BpType, idx: number): Bp | null { 41 | const key = this.buildKey(type, idx); 42 | return this.byIndex.get(key) ?? null; 43 | } 44 | 45 | public static delete(type: BpType, idx: number): Bp { 46 | const key = this.buildKey(type, idx); 47 | 48 | if (!this.byIndex.has(key)) 49 | throw new Error(`breakpoint #${idx} doesn't exist`); 50 | 51 | const bp = this.byIndex.get(key) as Bp; 52 | this.byIndex.delete(key); 53 | bp.disable(); 54 | return bp; 55 | } 56 | 57 | public static all(): Bp[] { 58 | const items: Bp[] = Array.from(this.byIndex.values()).sort((b1, b2) => 59 | b1.compare(b2), 60 | ); 61 | return items; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/breakpoints/code.ts: -------------------------------------------------------------------------------- 1 | import { Output } from '../io/output.js'; 2 | import { Overlay } from '../memory/overlay.js'; 3 | import { Regs } from './regs.js'; 4 | import { Format } from '../misc/format.js'; 5 | import { Var } from '../vars/var.js'; 6 | import { Bp, BpKind, BpType } from './bp.js'; 7 | import { Tls } from '../tls/tls.js'; 8 | 9 | export abstract class BpCode extends Bp { 10 | public kind: BpKind = BpKind.Code; 11 | private static readonly BP_CODE_LENGTH: number = 16; 12 | 13 | protected listener: InvocationListener | null; 14 | private overlay: string | null = null; 15 | 16 | protected constructor( 17 | index: number, 18 | address: Var | null, 19 | hits: number | null, 20 | ) { 21 | super(index, address, BpCode.BP_CODE_LENGTH, hits); 22 | this.listener = null; 23 | this.overlay = null; 24 | } 25 | 26 | public enable() { 27 | if (this.address === null) return; 28 | if (this.listener !== null) return; 29 | this.overlay = Overlay.add(this.address.toPointer(), this.length); 30 | const addr = this.address; 31 | this.enableCode(addr); 32 | } 33 | 34 | protected abstract enableCode(addr: Var): void; 35 | 36 | public override disable(): void { 37 | if (this.listener === null) return; 38 | this.listener.detach(); 39 | this.listener = null; 40 | Interceptor.flush(); 41 | if (this.overlay === null) return; 42 | Overlay.remove(this.overlay); 43 | } 44 | 45 | protected break( 46 | threadId: ThreadId, 47 | ctx: CpuContext, 48 | returnAddress: NativePointer, 49 | retVal: InvocationReturnValue | null = null, 50 | ) { 51 | if (this.hits === 0) return; 52 | 53 | Regs.setThreadId(threadId); 54 | Regs.setContext(ctx); 55 | Regs.setReturnAddress(returnAddress); 56 | Regs.setBreakpointId(this.index); 57 | Regs.setTls(Tls.getTls()); 58 | if (retVal !== null) Regs.setRetVal(retVal); 59 | 60 | try { 61 | if (this.runConditions()) { 62 | if (this.hits > 0) this.hits--; 63 | this.stopped(threadId, ctx); 64 | this.runCommands(); 65 | } 66 | } finally { 67 | Regs.clear(); 68 | } 69 | } 70 | 71 | protected stopped(threadId: ThreadId, ctx: CpuContext) { 72 | Output.clearLine(); 73 | Output.writeln(Output.yellow('-'.repeat(80))); 74 | Output.writeln( 75 | [ 76 | `${Output.yellow('|')} Break`, 77 | Output.green(`#${this.index}`), 78 | `[${this.type}]`, 79 | Output.yellow(this.literal), 80 | `@ $pc=${Output.blue(Format.toHexString(ctx.pc))}`, 81 | `$tid=${threadId}`, 82 | ].join(' '), 83 | ); 84 | Output.writeln(Output.yellow('-'.repeat(80))); 85 | } 86 | 87 | protected formatLength(): string { 88 | return ''; 89 | } 90 | } 91 | 92 | export class BpCodeInstruction extends BpCode { 93 | public type: BpType = BpType.Instruction; 94 | public readonly supports_commands: boolean = true; 95 | 96 | public constructor(index: number, address: Var | null, hits: number | null) { 97 | super(index, address, hits); 98 | } 99 | 100 | protected override enableCode(addr: Var): void { 101 | // eslint-disable-next-line @typescript-eslint/no-this-alias 102 | const bp = this; 103 | this.listener = Interceptor.attach( 104 | addr.toPointer(), 105 | function (this: InvocationContext, _args: InvocationArguments) { 106 | bp.break(this.threadId, this.context, this.returnAddress); 107 | }, 108 | ); 109 | } 110 | } 111 | 112 | export class BpFunctionEntry extends BpCode { 113 | public type: BpType = BpType.FunctionEntry; 114 | public readonly supports_commands: boolean = true; 115 | 116 | public constructor(index: number, address: Var | null, hits: number | null) { 117 | super(index, address, hits); 118 | } 119 | 120 | protected override enableCode(addr: Var): void { 121 | // eslint-disable-next-line @typescript-eslint/no-this-alias 122 | const bp = this; 123 | this.listener = Interceptor.attach(addr.toPointer(), { 124 | onEnter() { 125 | bp.break(this.threadId, this.context, this.returnAddress); 126 | }, 127 | }); 128 | } 129 | } 130 | 131 | export class BpFunctionExit extends BpCode { 132 | public type: BpType = BpType.FunctionExit; 133 | public readonly supports_commands: boolean = true; 134 | 135 | public constructor(index: number, address: Var | null, hits: number | null) { 136 | super(index, address, hits); 137 | } 138 | 139 | protected override enableCode(addr: Var): void { 140 | // eslint-disable-next-line @typescript-eslint/no-this-alias 141 | const bp = this; 142 | this.listener = Interceptor.attach(addr.toPointer(), { 143 | onLeave(retVal) { 144 | bp.break(this.threadId, this.context, this.returnAddress, retVal); 145 | }, 146 | }); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/breakpoints/memory.ts: -------------------------------------------------------------------------------- 1 | import { Output } from '../io/output.js'; 2 | import { Mem } from '../memory/mem.js'; 3 | import { Format } from '../misc/format.js'; 4 | import { Tls } from '../tls/tls.js'; 5 | import { Var } from '../vars/var.js'; 6 | import { Bp, BpKind, BpType } from './bp.js'; 7 | import { Regs } from './regs.js'; 8 | 9 | class MemoryCallbacks implements MemoryAccessCallbacks { 10 | onAccess = function (details: MemoryAccessDetails) { 11 | const idx = details.rangeIndex; 12 | const bps = BpMemory.getAllActive(); 13 | 14 | if (idx >= bps.length) 15 | throw new Error(`failed to find memory breakpoint idx: ${idx}`); 16 | 17 | const bp = bps[idx] as BpMemory; 18 | bp.break(details); 19 | BpMemory.refresh(); 20 | }; 21 | } 22 | 23 | export abstract class BpMemory extends Bp { 24 | private static callbacks = new MemoryCallbacks(); 25 | private static memoryBps: Map = new Map(); 26 | 27 | public static register(bp: BpMemory) { 28 | const key = this.buildKey(bp.type, bp.index); 29 | this.memoryBps.set(key, bp); 30 | this.refresh(); 31 | } 32 | 33 | private static buildKey(type: BpType, index: number): string { 34 | return `${type}:${index.toString()}`; 35 | } 36 | 37 | public static unregister(bp: BpMemory) { 38 | const key = this.buildKey(bp.type, bp.index); 39 | this.memoryBps.delete(key); 40 | this.refresh(); 41 | } 42 | 43 | public static refresh() { 44 | this.disableAll(); 45 | this.enableAll(); 46 | } 47 | 48 | public static disableAll() { 49 | MemoryAccessMonitor.disable(); 50 | } 51 | 52 | public static enableAll() { 53 | const ranges = Array.from(this.memoryBps.values()) 54 | .map(bp => bp.getRange()) 55 | .filter(r => r !== null) as MemoryAccessRange[]; 56 | 57 | if (ranges.length === 0) return; 58 | MemoryAccessMonitor.enable(ranges, this.callbacks); 59 | } 60 | 61 | public static getAllActive(): BpMemory[] { 62 | const bps = Array.from(this.memoryBps.values()); 63 | return bps.filter(bp => bp.getRange() !== null); 64 | } 65 | 66 | public static addressHasBreakpoint(address: NativePointer): boolean { 67 | const ranges = Array.from(this.memoryBps.values()) 68 | .map(bp => bp.getRange()) 69 | .filter(r => r !== null) as MemoryAccessRange[]; 70 | const aligned = Mem.pageAlignDown(address); 71 | 72 | return ranges.some(range => { 73 | if (range.base.add(range.size).compare(aligned) <= 0) return false; 74 | if (range.base.compare(aligned.add(Process.pageSize)) >= 0) return false; 75 | return true; 76 | }); 77 | } 78 | 79 | public kind: BpKind = BpKind.Memory; 80 | public readonly supports_commands: boolean = true; 81 | 82 | enable(): void { 83 | BpMemory.register(this); 84 | } 85 | 86 | disable(): void { 87 | BpMemory.unregister(this); 88 | } 89 | 90 | public break(details: MemoryAccessDetails) { 91 | if (!this.checkOperation(details.operation)) return; 92 | 93 | if (this.hits === 0) return; 94 | 95 | Regs.setAddress(details.address); 96 | Regs.setPc(details.from); 97 | Regs.setBreakpointId(this.index); 98 | Regs.setTls(Tls.getTls()); 99 | 100 | try { 101 | if (this.runConditions()) { 102 | if (this.hits > 0) this.hits--; 103 | 104 | Output.clearLine(); 105 | Output.writeln(Output.yellow('-'.repeat(80))); 106 | Output.writeln( 107 | [ 108 | `${Output.yellow('|')} Break`, 109 | Output.green(`#${this.index}`), 110 | `[${this.type}]`, 111 | Output.yellow(this.literal), 112 | `@ $pc=${Output.blue(Format.toHexString(details.from))}`, 113 | `$addr=${Output.blue(Format.toHexString(details.address))}`, 114 | ].join(' '), 115 | ); 116 | Output.writeln(Output.yellow('-'.repeat(80))); 117 | this.runCommands(); 118 | } 119 | } finally { 120 | Regs.clear(); 121 | } 122 | } 123 | 124 | protected abstract checkOperation(operation: MemoryOperation): boolean; 125 | 126 | protected formatLength(): string { 127 | return `[length:${this.length}]`; 128 | } 129 | 130 | public getRange(): MemoryAccessRange | null { 131 | if (this.address === null) return null; 132 | if (this.length === 0) return null; 133 | if (this.hits === 0) return null; 134 | return { 135 | base: this.address.toPointer() as NativePointer, 136 | size: this.length, 137 | }; 138 | } 139 | } 140 | 141 | export class BpReadMemory extends BpMemory { 142 | public type: BpType = BpType.MemoryRead; 143 | 144 | public constructor( 145 | index: number, 146 | address: Var | null, 147 | length: number | null, 148 | hits: number | null, 149 | ) { 150 | super(index, address, length, hits); 151 | } 152 | 153 | protected checkOperation(operation: MemoryOperation): boolean { 154 | return operation === 'read'; 155 | } 156 | } 157 | 158 | export class BpWriteMemory extends BpMemory { 159 | public type: BpType = BpType.MemoryWrite; 160 | 161 | public constructor( 162 | index: number, 163 | address: Var | null, 164 | length: number | null, 165 | hits: number | null, 166 | ) { 167 | super(index, address, length, hits); 168 | } 169 | 170 | protected checkOperation(operation: MemoryOperation): boolean { 171 | return operation === 'write'; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/breakpoints/replace.ts: -------------------------------------------------------------------------------- 1 | import { Output } from '../io/output.js'; 2 | import { Format } from '../misc/format.js'; 3 | import { Var } from '../vars/var.js'; 4 | import { Bp, BpKind, BpType } from './bp.js'; 5 | 6 | export class BpReplacement extends Bp { 7 | private static readonly BP_REPLACEMENT_LENGTH: number = 16; 8 | 9 | readonly type: BpType = BpType.Replacement; 10 | readonly kind: BpKind = BpKind.Replacement; 11 | readonly supports_commands: boolean = false; 12 | 13 | protected target: Var; 14 | public trampoline: Var | null = null; 15 | 16 | public constructor(index: number, address: Var, target: Var) { 17 | super(index, address, BpReplacement.BP_REPLACEMENT_LENGTH, null); 18 | this.target = target; 19 | } 20 | 21 | protected formatLength(): string { 22 | return ''; 23 | } 24 | 25 | enable(): void { 26 | if (this.address === null) return; 27 | try { 28 | const ptr = Interceptor.replaceFast( 29 | this.address.toPointer(), 30 | this.target.toPointer(), 31 | ); 32 | this.trampoline = new Var(uint64(ptr.toString())); 33 | } catch (error) { 34 | throw new Error( 35 | `failed to replace ${Format.toHexString(this.address.toPointer())} with ${Format.toHexString(this.target.toPointer())}, ${error}`, 36 | ); 37 | } 38 | } 39 | 40 | disable(): void { 41 | if (this.address === null) return; 42 | Interceptor.revert(this.address.toPointer()); 43 | } 44 | 45 | public override toString(): string { 46 | const idxString = Output.green(`#${this.index.toString()}.`.padEnd(4, ' ')); 47 | const targetString = `target: ${Output.blue(this.target.getLiteral())}`; 48 | const addressString = `address: ${Output.blue(this.literal)}`; 49 | let tranmpolineString = ''; 50 | if (this.trampoline !== null) { 51 | tranmpolineString = `trampoline: ${Output.blue(Format.toHexString(this.trampoline.toPointer()))}`; 52 | } 53 | return `${idxString} ${addressString} -> ${targetString} [${tranmpolineString}]`; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/cmdlets/breakpoints/code.ts: -------------------------------------------------------------------------------- 1 | import { BpType } from '../../breakpoints/bp.js'; 2 | import { Bps } from '../../breakpoints/bps.js'; 3 | import { 4 | BpCodeInstruction, 5 | BpFunctionEntry, 6 | BpFunctionExit, 7 | } from '../../breakpoints/code.js'; 8 | import { BpCoverage } from '../../breakpoints/trace.js'; 9 | import { CmdLetBase } from '../../commands/cmdlet.js'; 10 | import { Output } from '../../io/output.js'; 11 | import { Token } from '../../io/token.js'; 12 | import { Var } from '../../vars/var.js'; 13 | import { TypedBpCmdLet } from './bp.js'; 14 | 15 | abstract class CodeBpCmdLet extends TypedBpCmdLet { 16 | protected runCreate(tokens: Token[]): Var | null { 17 | const vars = this.transformOptional( 18 | tokens, 19 | [this.parseVar], 20 | [this.parseNumberOrAll, this.parseConditional], 21 | ); 22 | if (vars === null) return null; 23 | const [[addr], [hits, cond]] = vars as [ 24 | [Var], 25 | [number | null, number | null], 26 | ]; 27 | const conditional = cond === null ? false : true; 28 | 29 | const idx = Bps.getNextFreeIndex(this.bpType); 30 | switch (this.bpType) { 31 | case BpType.Instruction: { 32 | const bp = new BpCodeInstruction(idx, addr, hits); 33 | Bps.add(bp); 34 | Output.writeln(`Created ${bp.toString()}`); 35 | this.editBreakpoint(bp, conditional); 36 | break; 37 | } 38 | case BpType.FunctionEntry: { 39 | const bp = new BpFunctionEntry(idx, addr, hits); 40 | Bps.add(bp); 41 | Output.writeln(`Created ${bp.toString()}`); 42 | this.editBreakpoint(bp, conditional); 43 | break; 44 | } 45 | case BpType.FunctionExit: { 46 | const bp = new BpFunctionExit(idx, addr, hits); 47 | Bps.add(bp); 48 | Output.writeln(`Created ${bp.toString()}`); 49 | this.editBreakpoint(bp, conditional); 50 | break; 51 | } 52 | case BpType.Coverage: { 53 | const bp = new BpCoverage(idx, addr, hits); 54 | Bps.add(bp); 55 | Output.writeln(`Created ${bp.toString()}`); 56 | this.editBreakpoint(bp, conditional); 57 | break; 58 | } 59 | default: 60 | throw new Error(`unexpected breakpoint type: ${this.bpType}`); 61 | } 62 | 63 | return Var.fromId(idx); 64 | } 65 | 66 | protected runModify(tokens: Token[]): Var | null { 67 | const vars = this.transformOptional( 68 | tokens, 69 | [this.parseIndex, this.parseVar], 70 | [this.parseNumberOrAll, this.parseConditional], 71 | ); 72 | if (vars === null) return null; 73 | const [[index, addr], [hits, cond]] = vars as [ 74 | [number, Var], 75 | [number | null, string | null], 76 | ]; 77 | 78 | const conditional = cond === null ? false : true; 79 | 80 | const bp = Bps.get(this.bpType, index); 81 | if (bp === null) throw new Error(`breakpoint #${index} doesn't exist`); 82 | 83 | Bps.checkOverlaps(bp); 84 | 85 | bp.disable(); 86 | bp.address = addr; 87 | bp.hits = hits ?? -1; 88 | 89 | Output.writeln(`Modified ${bp.toString()}`); 90 | this.editBreakpoint(bp, conditional); 91 | 92 | return Var.fromId(index); 93 | } 94 | 95 | protected override usageCreate(): string { 96 | const usage: string = ` 97 | ${this.name} 0 - create ${this.bpType} breakpoint without assigning an address 98 | 99 | ${this.name} addr - create ${this.bpType} breakpoint without a hit limit 100 | addr the address to create the breakpoint 101 | 102 | ${this.name} addr hits - create ${this.bpType} breakpoint 103 | addr the address to create the breakpoint 104 | hits the number of times the breakpoint should fire 105 | 106 | ${this.name} addr hits ${TypedBpCmdLet.CONDITIONAL_CHAR} - create ${this.bpType} breakpoint with conditions 107 | addr the address to create the breakpoint 108 | hits the number of times the breakpoint should fire 109 | `; 110 | 111 | return usage; 112 | } 113 | 114 | protected override usageModify(): string { 115 | const usage: string = ` 116 | ${this.name} ${CmdLetBase.NUM_CHAR}n addr - modify a ${this.bpType} breakpoint without a hit limit 117 | ${CmdLetBase.NUM_CHAR}n the number of the breakpoint to modify 118 | addr the address to move the breakpoint 119 | 120 | ${this.name} ${CmdLetBase.NUM_CHAR}n addr hits - modify a ${this.bpType} breakpoint 121 | ${CmdLetBase.NUM_CHAR}n the number of the breakpoint to modify 122 | addr the address to move the breakpoint 123 | hits the number of times the breakpoint should fire 124 | 125 | ${this.name} ${CmdLetBase.NUM_CHAR}n addr hits ${TypedBpCmdLet.CONDITIONAL_CHAR} - modify a ${this.bpType} breakpoint with conditions 126 | ${CmdLetBase.NUM_CHAR}n the number of the breakpoint to modify 127 | addr the address to move the breakpoint 128 | hits the number of times the breakpoint should fire`; 129 | return usage; 130 | } 131 | } 132 | 133 | export class InsnBpCmdLet extends CodeBpCmdLet { 134 | name = '@i'; 135 | bpType = BpType.Instruction; 136 | help = `${this.bpType} breakpoint`; 137 | } 138 | 139 | export class FunctionEntryBpCmdLet extends CodeBpCmdLet { 140 | name = '@f'; 141 | bpType = BpType.FunctionEntry; 142 | help = `${this.bpType} breakpoint`; 143 | } 144 | 145 | export class FunctionExitBpCmdLet extends CodeBpCmdLet { 146 | name = '@F'; 147 | bpType = BpType.FunctionExit; 148 | help = `${this.bpType} breakpoint`; 149 | } 150 | 151 | export class CoverageBpCmdLet extends CodeBpCmdLet { 152 | name = '@c'; 153 | bpType = BpType.Coverage; 154 | help = `${this.bpType} breakpoint`; 155 | } 156 | -------------------------------------------------------------------------------- /src/cmdlets/breakpoints/mem.ts: -------------------------------------------------------------------------------- 1 | import { BpType } from '../../breakpoints/bp.js'; 2 | import { Bps } from '../../breakpoints/bps.js'; 3 | import { BpReadMemory, BpWriteMemory } from '../../breakpoints/memory.js'; 4 | import { CmdLetBase } from '../../commands/cmdlet.js'; 5 | import { Output } from '../../io/output.js'; 6 | import { Token } from '../../io/token.js'; 7 | import { Var } from '../../vars/var.js'; 8 | import { TypedBpCmdLet } from './bp.js'; 9 | 10 | abstract class MemoryBpCmdLet extends TypedBpCmdLet { 11 | protected runCreate(tokens: Token[]): Var | null { 12 | const vars = this.transformOptional( 13 | tokens, 14 | [this.parseVar, this.parseVar], 15 | [this.parseNumberOrAll, this.parseConditional], 16 | ); 17 | if (vars === null) return null; 18 | const [[addr, length], [hits, cond]] = vars as [ 19 | [Var, Var], 20 | [number | null, string | null], 21 | ]; 22 | const conditional = cond === null ? false : true; 23 | 24 | const idx = Bps.getNextFreeIndex(this.bpType); 25 | switch (this.bpType) { 26 | case BpType.MemoryRead: { 27 | const bp = new BpReadMemory(idx, addr, length.toU64().toNumber(), hits); 28 | Bps.add(bp); 29 | Output.writeln(`Created ${bp.toString()}`); 30 | this.editBreakpoint(bp, conditional); 31 | break; 32 | } 33 | case BpType.MemoryWrite: { 34 | const bp = new BpWriteMemory( 35 | idx, 36 | addr, 37 | length.toU64().toNumber(), 38 | hits, 39 | ); 40 | Bps.add(bp); 41 | Output.writeln(`Created ${bp.toString()}`); 42 | this.editBreakpoint(bp, conditional); 43 | break; 44 | } 45 | default: 46 | throw new Error(`unexpected breakpoint type: ${this.bpType}`); 47 | } 48 | 49 | return Var.fromId(idx); 50 | } 51 | 52 | protected runModify(tokens: Token[]): Var | null { 53 | const vars = this.transformOptional( 54 | tokens, 55 | [this.parseIndex, this.parseVar, this.parseVar], 56 | [this.parseNumberOrAll, this.parseConditional], 57 | ); 58 | if (vars === null) return null; 59 | const [[index, addr, length], [hits, cond]] = vars as [ 60 | [number, Var, Var], 61 | [number | null, string | null], 62 | ]; 63 | const conditional = cond === null ? false : true; 64 | 65 | const bp = Bps.get(this.bpType, index); 66 | if (bp === null) throw new Error(`breakpoint #${index} doesn't exist`); 67 | 68 | Bps.checkOverlaps(bp); 69 | 70 | bp.disable(); 71 | bp.address = addr; 72 | bp.hits = hits ?? -1; 73 | bp.length = length === null ? 0 : length.toU64().toNumber(); 74 | 75 | Output.writeln(`Modified ${bp.toString()}`); 76 | this.editBreakpoint(bp, conditional); 77 | 78 | return Var.fromId(index); 79 | } 80 | 81 | protected override usageCreate(): string { 82 | const usage: string = ` 83 | ${this.name} 0 0 - create ${this.bpType} breakpoint without assigning an address 84 | 85 | ${this.name} addr len - create ${this.bpType} breakpoint without a hit limit 86 | addr the address to create the breakpoint 87 | len the length of the memory region to watch 88 | 89 | ${this.name} addr len hits - create ${this.bpType} breakpoint 90 | addr the address to create the breakpoint 91 | len the length of the memory region to watch 92 | hits the number of times the breakpoint should fire 93 | 94 | ${this.name} addr len hits ${TypedBpCmdLet.CONDITIONAL_CHAR} - create ${this.bpType} breakpoint with conditions 95 | addr the address to create the breakpoint 96 | len the length of the memory region to watch 97 | hits the number of times the breakpoint should fire`; 98 | return usage; 99 | } 100 | 101 | protected override usageModify(): string { 102 | const usage: string = ` 103 | ${this.name} ${CmdLetBase.NUM_CHAR}n addr len - modify a ${this.bpType} breakpoint without a hit limit 104 | ${CmdLetBase.NUM_CHAR}n the number of the breakpoint to modify 105 | addr the address to move the breakpoint 106 | len the length of the memory region to watch 107 | 108 | ${this.name} ${CmdLetBase.NUM_CHAR}n addr len hits - modify a ${this.bpType} breakpoint 109 | ${CmdLetBase.NUM_CHAR}n the number of the breakpoint to modify 110 | addr the address to move the breakpoint 111 | len the length of the memory region to watch 112 | hits the number of times the breakpoint should fire 113 | 114 | ${this.name} ${CmdLetBase.NUM_CHAR}n addr len hits ${TypedBpCmdLet.CONDITIONAL_CHAR} - modify a ${this.bpType} breakpoint with conditions 115 | ${CmdLetBase.NUM_CHAR}n the number of the breakpoint to modify 116 | addr the address to move the breakpoint 117 | len the length of the memory region to watch 118 | hits the number of times the breakpoint should fire`; 119 | return usage; 120 | } 121 | } 122 | 123 | export class ReadBpCmdLet extends MemoryBpCmdLet { 124 | name = '@r'; 125 | bpType = BpType.MemoryRead; 126 | help = `${this.bpType} breakpoint`; 127 | } 128 | 129 | export class WriteBpCmdLet extends MemoryBpCmdLet { 130 | name = '@w'; 131 | bpType = BpType.MemoryWrite; 132 | help = `${this.bpType} breakpoint`; 133 | } 134 | -------------------------------------------------------------------------------- /src/cmdlets/breakpoints/reg.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Vars } from '../../vars/vars.js'; 4 | import { Token } from '../../io/token.js'; 5 | import { Var } from '../../vars/var.js'; 6 | import { Regs } from '../../breakpoints/regs.js'; 7 | 8 | export class RegCmdLet extends CmdLetBase { 9 | name = 'R'; 10 | category = 'breakpoints'; 11 | help = 'register management'; 12 | 13 | private static readonly USAGE: string = `Usage: R 14 | R - show the values of all registers 15 | 16 | R name - display the value of a named register 17 | name the name of the register to display 18 | 19 | R name value - assign a value to a register 20 | name the name of the register to assign 21 | value the value to assign`; 22 | 23 | public runSync(tokens: Token[]): Var { 24 | const vars = this.transformOptional( 25 | tokens, 26 | [], 27 | [this.parseRegister, this.parseVar], 28 | ); 29 | if (vars === null) return this.usage(); 30 | const [_, [name, address]] = vars as [[], [string | null, Var | null]]; 31 | 32 | if (address === null) { 33 | if (name === null) { 34 | Output.writeln('Registers:'); 35 | for (const [key, value] of Regs.all()) { 36 | Output.writeln( 37 | `${Output.bold(key.padEnd(4, ' '))}: ${value.toString()}`, 38 | true, 39 | ); 40 | } 41 | return Vars.getRet(); 42 | } else { 43 | const val = Regs.get(name); 44 | Output.writeln(`Register ${name}, value: ${val.toString()}`); 45 | return val; 46 | } 47 | } else { 48 | if (name === null) throw new Error('argument parsing error'); 49 | Regs.set(name, address); 50 | Output.writeln(`Register ${name}, set to value: ${address.toString()}`); 51 | return address; 52 | } 53 | } 54 | 55 | protected parseRegister(token: Token): string | null { 56 | if (token === null) return null; 57 | const literal = token.getLiteral(); 58 | if (literal.startsWith('$')) { 59 | return literal.slice(1); 60 | } else { 61 | return literal; 62 | } 63 | } 64 | 65 | public usage(): Var { 66 | Output.writeln(RegCmdLet.USAGE); 67 | return Var.ZERO; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/cmdlets/breakpoints/replace.ts: -------------------------------------------------------------------------------- 1 | import { Token } from '../../io/token.js'; 2 | import { Var } from '../../vars/var.js'; 3 | import { TypedBpCmdLet } from './bp.js'; 4 | import { BpType } from '../../breakpoints/bp.js'; 5 | import { Bps } from '../../breakpoints/bps.js'; 6 | import { BpReplacement } from '../../breakpoints/replace.js'; 7 | import { Output } from '../../io/output.js'; 8 | import { CmdLetBase } from '../../commands/cmdlet.js'; 9 | 10 | export class ReplaceCmdLet extends TypedBpCmdLet { 11 | name = 'replace'; 12 | bpType = BpType.Replacement; 13 | help = `replace a function with another implementation`; 14 | 15 | public runCreate(tokens: Token[]): Var | null { 16 | const vars = this.transform(tokens, [this.parseVar, this.parseVar]); 17 | if (vars === null) return null; 18 | const [address, target] = vars as [Var, Var]; 19 | 20 | try { 21 | const index = Bps.getNextFreeIndex(this.bpType); 22 | const bp = new BpReplacement(index, address, target); 23 | Bps.add(bp); 24 | bp.enable(); 25 | Output.writeln(`Created ${bp.toString()}`); 26 | return Var.fromId(index); 27 | } catch (error) { 28 | throw new Error(`failed to replace ${address} with ${target}, ${error}`); 29 | } 30 | } 31 | 32 | /* 33 | * This function doesn't actually modify the breakpoint, but rather since it 34 | * is called berfore runShow in the parent it allows us to match those same 35 | * arguments and overload it to return the trampoline address in the event 36 | * that a breakpoint id was specified. 37 | */ 38 | protected override runModify(tokens: Token[]): Var | null { 39 | const vars = this.transform(tokens, [this.parseIndex]); 40 | if (vars === null) return null; 41 | const [index] = vars as [number]; 42 | const bp = Bps.get(this.bpType, index) as BpReplacement; 43 | if (bp === null) throw new Error(`breakpoint #${index} doesn't exist`); 44 | Output.writeln(bp.toString()); 45 | return bp.trampoline; 46 | } 47 | 48 | protected override usageShow(): string { 49 | const usage: string = ` 50 | ${this.name} - show all ${this.bpType} breakpoints 51 | 52 | ${this.name} ${CmdLetBase.NUM_CHAR}n - show a ${this.bpType} breakpoint (returns the address of the trampoline) 53 | ${CmdLetBase.NUM_CHAR}n the number of the breakpoint to show`; 54 | return usage; 55 | } 56 | 57 | protected override usageCreate(): string { 58 | const usage: string = ` 59 | replace dest src - replace function 60 | dest the address/symbol of the function to replace 61 | src the address/symbol of the function to replace with`; 62 | return usage; 63 | } 64 | 65 | protected override usageModify(): string { 66 | return Output.bold('unsupported'); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/cmdlets/data/assembly.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Format } from '../../misc/format.js'; 4 | import { Token } from '../../io/token.js'; 5 | import { Var } from '../../vars/var.js'; 6 | import { Mem } from '../../memory/mem.js'; 7 | import { Overlay } from '../../memory/overlay.js'; 8 | 9 | export class AssemblyCmdLet extends CmdLetBase { 10 | name = 'l'; 11 | category = 'data'; 12 | help = 'disassembly listing'; 13 | 14 | private static readonly DEFAULT_LENGTH: number = 10; 15 | private static readonly USAGE: string = `Usage: l 16 | 17 | l address - show disassembly listing 18 | address the address/symbol to disassemble 19 | bytes the number of instructions to disassemble (default ${AssemblyCmdLet.DEFAULT_LENGTH})`; 20 | 21 | public runSync(tokens: Token[]): Var { 22 | const vars = this.transformOptional( 23 | tokens, 24 | [this.parseVar], 25 | [this.parseVar], 26 | ); 27 | if (vars === null) return this.usage(); 28 | const [[v0], [v1]] = vars as [[Var], [Var | null]]; 29 | 30 | const address = v0.toPointer(); 31 | const length = 32 | v1 === null ? AssemblyCmdLet.DEFAULT_LENGTH : v1.toU64().toNumber(); 33 | 34 | if (length > 100) throw new Error(`too many instructions: ${length}`); 35 | 36 | return this.list(address, length); 37 | } 38 | 39 | private list(address: NativePointer, length: number): Var { 40 | let cursor = address; 41 | const isThumb = this.isThumb(address); 42 | if (isThumb) { 43 | const mask = ptr(1).not(); 44 | cursor = cursor.and(mask); 45 | } 46 | let buffer = new Uint8Array(0); 47 | 48 | try { 49 | const minLength = this.maxInstructionLen(); 50 | const copy = Memory.alloc(Process.pageSize); 51 | let hasOverlaps = false; 52 | 53 | for (let i = 1; i <= length; i++) { 54 | if (buffer.byteLength < minLength) { 55 | const newBuff = this.readMaxBytes( 56 | cursor.add(buffer.byteLength), 57 | minLength - buffer.byteLength, 58 | ); 59 | buffer = this.concatBuffers(buffer, newBuff); 60 | } 61 | 62 | Mem.writeBytes(copy, buffer); 63 | 64 | let insn = Instruction.parse(cursor.add(isThumb ? 1 : 0)); 65 | const overlaps = Overlay.overlaps(cursor, insn.size); 66 | 67 | if (overlaps) { 68 | hasOverlaps = true; 69 | insn = Instruction.parse(copy.add(isThumb ? 1 : 0)); 70 | } 71 | 72 | if (insn.size > buffer.length) 73 | throw new Error( 74 | `failed to parse instruction at ${cursor}, not enough bytes: ${buffer.length}`, 75 | ); 76 | 77 | const idx = `#${i.toString()}`.padStart(4); 78 | const insnBytes = buffer.slice(0, insn.size); 79 | const bytesStr = Array.from(insnBytes) 80 | .map(n => n.toString(16).padStart(2, '0')) 81 | .join(' '); 82 | 83 | Output.writeln( 84 | [ 85 | `${Output.bold(idx)}:`, 86 | `${Output.green(Format.toHexString(cursor))}:`, 87 | `${Output.yellow(insn.toString().padEnd(40))}`, 88 | `${Output.blue(bytesStr)}`, 89 | overlaps ? `${Output.red('*')}` : '', 90 | ].join(' '), 91 | true, 92 | ); 93 | 94 | cursor = cursor.add(insn.size); 95 | buffer = buffer.slice(insn.size); 96 | } 97 | 98 | if (hasOverlaps) { 99 | Output.writeln( 100 | `${Output.red('*')} offset in RIP relative instruction may be incorrect due to conflicting breakpoint`, 101 | ); 102 | } 103 | 104 | return new Var(uint64(cursor.toString())); 105 | } catch (error) { 106 | throw new Error( 107 | `failed to parse instruction at ${Format.toHexString(cursor)} (${Format.toHexString(buffer.byteLength)} bytes available), ${error}`, 108 | ); 109 | } 110 | } 111 | 112 | private isThumb(address: NativePointer): boolean { 113 | if (Process.arch !== 'arm') return false; 114 | if (address.and(1).equals(ptr(0))) return false; 115 | return true; 116 | } 117 | 118 | private maxInstructionLen(): number { 119 | switch (Process.arch) { 120 | case 'arm': 121 | case 'arm64': 122 | return 4; 123 | case 'ia32': 124 | case 'x64': 125 | return 15; 126 | default: 127 | throw new Error(`unsupported architecutre: ${Process.arch}`); 128 | } 129 | } 130 | 131 | private readMaxBytes(ptr: NativePointer, length: number): Uint8Array { 132 | for (let i = length; i > 0; i--) { 133 | try { 134 | const bytes = Mem.readBytes(ptr, i); 135 | return bytes; 136 | } catch { 137 | continue; 138 | } 139 | } 140 | return new Uint8Array(0); 141 | } 142 | 143 | private concatBuffers(buffer1: Uint8Array, buffer2: Uint8Array): Uint8Array { 144 | const concatenatedBuffer = new Uint8Array(buffer1.length + buffer2.length); 145 | concatenatedBuffer.set(buffer1, 0); 146 | concatenatedBuffer.set(buffer2, buffer1.byteLength); 147 | return concatenatedBuffer; 148 | } 149 | 150 | public usage(): Var { 151 | Output.writeln(AssemblyCmdLet.USAGE); 152 | return Var.ZERO; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/cmdlets/data/copy.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Format } from '../../misc/format.js'; 4 | import { Token } from '../../io/token.js'; 5 | import { Var } from '../../vars/var.js'; 6 | import { Mem } from '../../memory/mem.js'; 7 | 8 | export class CopyCmdLet extends CmdLetBase { 9 | name = 'cp'; 10 | category = 'data'; 11 | help = 'copy data in memory'; 12 | 13 | private static readonly USAGE: string = `Usage: cp 14 | 15 | cp dest src bytes - copy data 16 | dest the address/symbol to write to 17 | src the address/symbol to read from 18 | bytes the numer of bytes to read`; 19 | 20 | public runSync(tokens: Token[]): Var { 21 | const vars = this.transform(tokens, [ 22 | this.parseVar, 23 | this.parseVar, 24 | this.parseVar, 25 | ]); 26 | if (vars === null) return this.usage(); 27 | const [v0, v1, v2] = vars as [Var, Var, Var]; 28 | 29 | const dst = v0.toPointer(); 30 | const src = v1.toPointer(); 31 | const len = v2.toU64().toNumber(); 32 | 33 | try { 34 | const buff = Mem.readBytes(src, len); 35 | Mem.writeBytes(dst, buff); 36 | } catch (error) { 37 | throw new Error( 38 | `failed to copy ${len} bytes from ${Format.toHexString(src)} to ${Format.toHexString(dst)}, ${error}`, 39 | ); 40 | } 41 | return v0; 42 | } 43 | 44 | public usage(): Var { 45 | Output.writeln(CopyCmdLet.USAGE); 46 | return Var.ZERO; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/cmdlets/data/dumpfile.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Format } from '../../misc/format.js'; 4 | import { Token } from '../../io/token.js'; 5 | import { Var } from '../../vars/var.js'; 6 | import { Mem } from '../../memory/mem.js'; 7 | 8 | export class DumpFileCmdLet extends CmdLetBase { 9 | name = 'df'; 10 | category = 'data'; 11 | help = 'dump data to file'; 12 | 13 | private static readonly USAGE: string = `Usage: df 14 | 15 | df filename address bytes - show data 16 | filename the name of the file to dump to 17 | adress the address/symbol to dump from 18 | count the count of fields to dump`; 19 | 20 | public runSync(tokens: Token[]): Var { 21 | const vars = this.transform(tokens, [ 22 | this.parseString, 23 | this.parseVar, 24 | this.parseVar, 25 | ]); 26 | if (vars === null) return this.usage(); 27 | const [filename, address, length] = vars as [string, Var, Var]; 28 | this.dump(filename, address.toPointer(), length.toU64().toNumber()); 29 | return new Var(filename); 30 | } 31 | 32 | private dump(filename: string, address: NativePointer, length: number) { 33 | try { 34 | const bytes = Mem.readBytes(address, length); 35 | Output.debug(`writing ${length} bytes from ${address} to ${filename}`); 36 | File.writeAllBytes(filename, bytes.buffer as ArrayBuffer); 37 | } catch (error) { 38 | throw new Error( 39 | `failed to dump ${Format.toHexString(length)} bytes from ${Format.toHexString(address)} to ${filename}, ${error}`, 40 | ); 41 | } 42 | } 43 | 44 | public usage(): Var { 45 | Output.writeln(DumpFileCmdLet.USAGE); 46 | return Var.ZERO; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/cmdlets/data/read.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Format } from '../../misc/format.js'; 4 | import { Token } from '../../io/token.js'; 5 | import { Var } from '../../vars/var.js'; 6 | import { Mem } from '../../memory/mem.js'; 7 | 8 | export class ReadCmdLet extends CmdLetBase { 9 | name = 'r'; 10 | category = 'data'; 11 | help = 'read data from memory'; 12 | 13 | public runSync(tokens: Token[]): Var { 14 | const vars = this.transform(tokens, [this.parseWidth, this.parseVar]); 15 | if (vars === null) return this.usage(); 16 | const [length, v1] = vars as [number, Var]; 17 | 18 | const address = v1.toPointer(); 19 | 20 | const buff = Mem.readBytes(address, length); 21 | const copy = Memory.alloc(Process.pageSize); 22 | Mem.writeBytes(copy, buff); 23 | 24 | return new Var(this.read(copy, length)); 25 | } 26 | 27 | private read(address: NativePointer, length: number): UInt64 { 28 | switch (length) { 29 | case 1: { 30 | const val = address.readU8(); 31 | Output.writeln( 32 | `Read value: 0x${val.toString(16).padStart(2, '0')} = ${val.toString()} `, 33 | ); 34 | return uint64(val); 35 | } 36 | case 2: { 37 | const val = address.readU16(); 38 | Output.writeln( 39 | `Read value: 0x${val.toString(16).padStart(4, '0')} = ${val.toString()} `, 40 | ); 41 | return uint64(val); 42 | } 43 | case 4: { 44 | const val = address.readU32(); 45 | Output.writeln( 46 | `Read value: 0x${val.toString(16).padStart(8, '0')} = ${val.toString()} `, 47 | ); 48 | return uint64(val); 49 | } 50 | case 8: { 51 | const val = address.readU64(); 52 | Output.writeln( 53 | `Read value: ${Format.toHexString(val)} = ${val.toString()} `, 54 | ); 55 | return val; 56 | } 57 | default: 58 | throw new Error(`unsupported length: ${length}`); 59 | } 60 | } 61 | 62 | public usage(): Var { 63 | const usage: string = `Usage: r 64 | 65 | r n address - read 'n' bytes from memory 66 | n the number of bytes to read (1, 2, 4 or 8). 67 | address the address/symbol to read from`; 68 | 69 | Output.writeln(usage); 70 | return Var.ZERO; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/cmdlets/data/string.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Format } from '../../misc/format.js'; 4 | import { Token } from '../../io/token.js'; 5 | import { Var } from '../../vars/var.js'; 6 | import { Mem } from '../../memory/mem.js'; 7 | 8 | export class DumpStringCmdLet extends CmdLetBase { 9 | name = 'ds'; 10 | category = 'data'; 11 | help = 'dump string'; 12 | 13 | private static readonly MAX_STRING_LENGTH: number = 1024; 14 | private static readonly USAGE: string = `Usage: ds 15 | 16 | ds address - show string 17 | adress the address/symbol to show`; 18 | 19 | public runSync(tokens: Token[]): Var { 20 | const vars = this.transform(tokens, [this.parseVar]); 21 | if (vars === null) return this.usage(); 22 | const [arg] = vars as [Var]; 23 | this.dump(arg); 24 | return arg; 25 | } 26 | 27 | private dump(arg: Var) { 28 | const name = arg.getLiteral(); 29 | const address = arg.toPointer(); 30 | const length = DumpStringCmdLet.MAX_STRING_LENGTH; 31 | let bytes: Uint8Array = new Uint8Array(0); 32 | let lastError: Error | null = null; 33 | while (length > 0) { 34 | try { 35 | bytes = Mem.readBytes(address, length); 36 | break; 37 | } catch (error) { 38 | if (error instanceof Error) { 39 | lastError = error; 40 | } 41 | continue; 42 | } 43 | } 44 | 45 | if (length === 0) { 46 | throw new Error( 47 | `failed to read string from ${Format.toHexString(address)}, ${lastError}`, 48 | ); 49 | } 50 | 51 | const cp = Memory.alloc(length + 1); 52 | cp.writeByteArray(bytes.buffer as ArrayBuffer); 53 | 54 | const value = cp.readUtf8String(); 55 | if (value === null || value.length === 0) { 56 | Output.writeln( 57 | `No string found at ${Output.green(name)}: ${Output.yellow(Format.toHexString(address))}`, 58 | ); 59 | } else { 60 | Output.writeln( 61 | [ 62 | Output.green(name), 63 | '=', 64 | `${Output.blue("'")}${Output.yellow(value)}${Output.blue("'")},`, 65 | `length: ${Output.blue(value.length.toString())},`, 66 | `(${Output.blue(`0x${value.length.toString(16)}`)})`, 67 | ].join(' '), 68 | ); 69 | } 70 | } 71 | 72 | public usage(): Var { 73 | Output.writeln(DumpStringCmdLet.USAGE); 74 | return Var.ZERO; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/cmdlets/data/write.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Format } from '../../misc/format.js'; 4 | import { Token } from '../../io/token.js'; 5 | import { Var } from '../../vars/var.js'; 6 | import { Mem } from '../../memory/mem.js'; 7 | 8 | export class WriteCmdLet extends CmdLetBase { 9 | name = 'w'; 10 | category = 'data'; 11 | help = 'write data to memory'; 12 | 13 | public runSync(tokens: Token[]): Var { 14 | const vars = this.transform(tokens, [ 15 | this.getLength, 16 | this.parseVar, 17 | this.parseVar, 18 | ]); 19 | if (vars === null) return this.usage(); 20 | const [length, v1, v2] = vars as [number, Var, Var]; 21 | 22 | const address = v1.toPointer(); 23 | const val = v2.toU64(); 24 | 25 | const max = this.getMax(length); 26 | if (val.compare(max) > 0) { 27 | throw new Error( 28 | `value: ${Format.toHexString(val)} larger than maximum ${Format.toHexString(max)}`, 29 | ); 30 | } 31 | 32 | const copy = Memory.alloc(Process.pageSize); 33 | this.write(copy, val, length); 34 | const buff = Mem.readBytes(copy, length); 35 | Mem.writeBytes(address, buff); 36 | 37 | return v1; 38 | } 39 | 40 | private getLength(token: Token): number | null { 41 | const literal = token.getLiteral(); 42 | switch (literal) { 43 | case '1': 44 | return 1; 45 | case '2': 46 | return 2; 47 | case '4': 48 | return 4; 49 | case '8': 50 | return 8; 51 | default: 52 | return null; 53 | } 54 | } 55 | 56 | private getMax(length: number): UInt64 { 57 | switch (length) { 58 | case 1: 59 | return uint64(0xff); 60 | case 2: 61 | return uint64(0xffff); 62 | case 4: 63 | return uint64(0xffffffff); 64 | case 8: 65 | return uint64('0xffffffffffffffff'); 66 | default: 67 | throw new Error(`unsupported length: ${length}`); 68 | } 69 | } 70 | 71 | private write(address: NativePointer, val: UInt64, length: number) { 72 | switch (length) { 73 | case 1: 74 | address.writeU8(val.toNumber()); 75 | Output.writeln( 76 | `Wrote value: 0x${val.toString(16).padStart(2, '0')} = ${val.toString()} to ${Format.toHexString(address)}`, 77 | ); 78 | break; 79 | 80 | case 2: 81 | address.writeU16(val.toNumber()); 82 | Output.writeln( 83 | `Wrote value: 0x${val.toString(16).padStart(4, '0')} = ${val.toString()} to ${Format.toHexString(address)}`, 84 | ); 85 | break; 86 | 87 | case 4: 88 | address.writeU32(val.toNumber()); 89 | Output.writeln( 90 | `Wrote value: 0x${val.toString(16).padStart(8, '0')} = ${val.toString()} to ${Format.toHexString(address)}`, 91 | ); 92 | break; 93 | case 8: 94 | address.writeU64(val); 95 | Output.writeln( 96 | `Wrote value: ${Format.toHexString(val)} = ${val.toString()} to ${Format.toHexString(address)}`, 97 | ); 98 | break; 99 | default: 100 | throw new Error(`unsupported length: ${length}`); 101 | } 102 | } 103 | 104 | public usage(): Var { 105 | const usage: string = `Usage: w 106 | 107 | w n address value - write 'n' bytes to memory 108 | n the number of bytes to read (1, 2, 4 or 8). 109 | address the address/symbol to write to 110 | value the value to write`; 111 | Output.writeln(usage); 112 | return Var.ZERO; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/cmdlets/development/debug.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Token } from '../../io/token.js'; 4 | import { Var } from '../../vars/var.js'; 5 | 6 | export class DebugCmdLet extends CmdLetBase { 7 | name = 'debug'; 8 | category = 'development'; 9 | help = 'toggle debug mode'; 10 | 11 | private static readonly USAGE: string = `Usage: debug 12 | debug - toggle debug mode`; 13 | 14 | public runSync(_tokens: Token[]): Var { 15 | const debug = !Output.getDebugging(); 16 | if (debug) { 17 | Output.writeln(`debug mode ${Output.green('enabled')}`); 18 | } else { 19 | Output.writeln(`debug mode ${Output.red('disabled')}`); 20 | } 21 | Output.setDebugging(debug); 22 | 23 | return Var.ZERO; 24 | } 25 | 26 | public usage(): Var { 27 | Output.writeln(DebugCmdLet.USAGE); 28 | return Var.ZERO; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/cmdlets/files/cat.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Token } from '../../io/token.js'; 4 | import { Var } from '../../vars/var.js'; 5 | 6 | export class CatCmdLet extends CmdLetBase { 7 | name = 'cat'; 8 | category = 'files'; 9 | help = 'dump a file'; 10 | 11 | private static readonly USAGE: string = `Usage: cat 12 | 13 | cat file - dump file 14 | file the file to dump`; 15 | 16 | public runSync(tokens: Token[]): Var { 17 | const vars = this.transform(tokens, [this.parseString]); 18 | if (vars === null) return this.usage(); 19 | 20 | const [file] = vars as [string]; 21 | Output.writeln(`Dumping file: ${Output.green(file)}`); 22 | 23 | try { 24 | const text = File.readAllText(file); 25 | const lines = text.split('\n'); 26 | lines.forEach(l => Output.writeln(Output.yellow(l), true)); 27 | } catch { 28 | Output.writeln(`failed to read file: ${Output.green(file)}`); 29 | } 30 | 31 | return new Var(file); 32 | } 33 | 34 | public usage(): Var { 35 | Output.writeln(CatCmdLet.USAGE); 36 | return Var.ZERO; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/cmdlets/files/src.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { CharCode } from '../../io/char.js'; 3 | import { Input } from '../../io/input.js'; 4 | import { Output } from '../../io/output.js'; 5 | import { Token } from '../../io/token.js'; 6 | import { Format } from '../../misc/format.js'; 7 | import { History } from '../../terminal/history.js'; 8 | import { Var } from '../../vars/var.js'; 9 | 10 | export class SrcCmdLet extends CmdLetBase { 11 | name = 'src'; 12 | category = 'files'; 13 | help = 'run commands from file'; 14 | 15 | private static readonly USAGE: string = `Usage: src 16 | 17 | src path - run commands from file 18 | path the absolute path of the file to load (note that paths with spaces must be quoted)`; 19 | 20 | private static lastPath: string | null = null; 21 | 22 | public static async loadInitScript(path: string) { 23 | this.lastPath = path; 24 | const src = new SrcCmdLet(); 25 | await src.runScript(path); 26 | } 27 | 28 | public override runSync(_tokens: Token[]): Var { 29 | throw new Error("can't run in synchronous mode"); 30 | } 31 | 32 | public override async run(tokens: Token[]): Promise { 33 | const vars = this.transformOptional(tokens, [], [this.parseString]); 34 | if (vars === null) return this.usage(); 35 | // eslint-disable-next-line prefer-const 36 | let [_, [name]] = vars as [[], [string | null]]; 37 | if (name === null) { 38 | if (SrcCmdLet.lastPath === null) throw new Error('path not initialized'); 39 | 40 | await this.runScript(SrcCmdLet.lastPath); 41 | return new Var(SrcCmdLet.lastPath); 42 | } else { 43 | SrcCmdLet.lastPath = name; 44 | await this.runScript(name); 45 | return new Var(name); 46 | } 47 | } 48 | 49 | private async runScript(path: string) { 50 | try { 51 | Output.writeln(`Loading: ${Output.green(path)}`); 52 | 53 | const initScript = File.readAllText(path); 54 | const lines = initScript.split('\n'); 55 | 56 | History.clearLine(); 57 | Input.prompt(); 58 | 59 | for (const line of lines) { 60 | if (line.length === 0) continue; 61 | if (line.charAt(0) === '#') continue; 62 | Output.write(line); 63 | const bytes = Format.toByteArray( 64 | `${line}${String.fromCharCode(CharCode.CR)}`, 65 | ); 66 | await Input.read(bytes); 67 | } 68 | 69 | Output.clearLine(); 70 | Output.writeln(`Loaded: ${Output.green(path)}`); 71 | } catch (_) { 72 | /* Ignore the error */ 73 | } 74 | } 75 | 76 | public usage(): Var { 77 | Output.writeln(SrcCmdLet.USAGE); 78 | return Var.ZERO; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/cmdlets/files/sz.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Input } from '../../io/input.js'; 3 | import { Output } from '../../io/output.js'; 4 | import { Token } from '../../io/token.js'; 5 | import { InputBuffer } from '../../io/zmodem/input.js'; 6 | import { OutputBuffer } from '../../io/zmodem/output.js'; 7 | import { Sz } from '../../io/zmodem/sz.js'; 8 | import { Zmodem } from '../../io/zmodem/zmodem.js'; 9 | import { Files } from '../../misc/files.js'; 10 | import { Version } from '../../misc/version.js'; 11 | import { Var } from '../../vars/var.js'; 12 | 13 | export class SzCmdLet extends CmdLetBase { 14 | name = 'sz'; 15 | category = 'files'; 16 | help = 'send a file using Z-Modem'; 17 | 18 | private static readonly USAGE: string = `Usage: sz 19 | 20 | cat file - send file 21 | file the file to send`; 22 | 23 | public override runSync(_tokens: Token[]): Var { 24 | throw new Error("can't run in synchronous mode"); 25 | } 26 | 27 | public override async run(tokens: Token[]): Promise { 28 | const vars = this.transform(tokens, [this.parseString]); 29 | if (vars === null) return this.usage(); 30 | 31 | const [filePath] = vars as [string]; 32 | Output.writeln(`Sending file: ${Output.green(filePath)}`); 33 | 34 | const debugFileName = Files.getRandomFileName('debug'); 35 | let debug = (_msg: string) => {}; 36 | 37 | if (Output.getDebugging()) { 38 | Output.debug(`writing debug to: ${debugFileName}`); 39 | const debugFile = new File(debugFileName, 'w'); 40 | debug = (msg: string) => { 41 | debugFile.write(`${msg}\n`); 42 | debugFile.flush(); 43 | }; 44 | } 45 | 46 | debug('Starting transmission'); 47 | 48 | Output.writeln(`Transmission will start in 2 seconds....`); 49 | Output.writeln(); 50 | 51 | const input = new InputBuffer(debug); 52 | const output = new OutputBuffer(debug); 53 | 54 | Input.setInterceptRaw(input); 55 | try { 56 | await Sz.sleep(2000); 57 | const zmodem = new Zmodem(input, output, debug); 58 | await zmodem.send(filePath); 59 | } catch (error) { 60 | if (error instanceof Error) { 61 | debug(`Error: ${error.message}`); 62 | debug(`Stack: ${error.stack}`); 63 | } else { 64 | debug(`Error: Unknown error`); 65 | } 66 | } finally { 67 | Input.setInterceptRaw(null); 68 | this.checkDebugFile(debugFileName); 69 | } 70 | return new Var(filePath); 71 | } 72 | 73 | private checkDebugFile(debugFileName: string) { 74 | Output.debug('ZModem output...'); 75 | try { 76 | const debugFile = new File(debugFileName, 'r'); 77 | for ( 78 | let line = debugFile.readLine(); 79 | line.length != 0; 80 | line = debugFile.readLine() 81 | ) { 82 | Output.debug(`\t${Output.yellow(line.trimEnd())}`); 83 | } 84 | } finally { 85 | Output.debug('ZModem output complete'); 86 | } 87 | } 88 | 89 | public usage(): Var { 90 | Output.writeln(SzCmdLet.USAGE); 91 | return Var.ZERO; 92 | } 93 | 94 | public override isSupported(): boolean { 95 | switch (Process.platform) { 96 | case 'linux': 97 | if (Version.VERSION >= Version.BINARY_MODE_MIN_VERSION) { 98 | return true; 99 | } else { 100 | return false; 101 | } 102 | case 'windows': 103 | case 'barebone': 104 | case 'darwin': 105 | case 'freebsd': 106 | case 'qnx': 107 | default: 108 | return false; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/cmdlets/files/tmp.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Token } from '../../io/token.js'; 4 | import { Files } from '../../misc/files.js'; 5 | import { Var } from '../../vars/var.js'; 6 | 7 | export class TmpCmdLet extends CmdLetBase { 8 | name = 'tmp'; 9 | category = 'files'; 10 | help = 'generate a temporary filename'; 11 | 12 | private static readonly USAGE: string = `Usage: tmp 13 | 14 | temp ext - generate temporary filename 15 | ext the extension for the filename`; 16 | 17 | public runSync(tokens: Token[]): Var { 18 | const vars = this.transform(tokens, [this.parseString]); 19 | if (vars === null) return this.usage(); 20 | 21 | const [ext] = vars as [string]; 22 | 23 | const filename = Files.getRandomFileName(ext); 24 | return new Var(filename); 25 | } 26 | 27 | public usage(): Var { 28 | Output.writeln(TmpCmdLet.USAGE); 29 | return Var.ZERO; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/cmdlets/memory/vm.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Format } from '../../misc/format.js'; 4 | import { Token } from '../../io/token.js'; 5 | import { Var } from '../../vars/var.js'; 6 | import { Regex } from '../../misc/regex.js'; 7 | 8 | export class VmCmdLet extends CmdLetBase { 9 | name = 'vm'; 10 | category = 'memory'; 11 | help = 'display virtual memory ranges'; 12 | 13 | private static readonly USAGE: string = `Usage: vm 14 | 15 | vm - show all mappings 16 | 17 | vm address - show mapping for address 18 | address the address/symbol to show mapping information for 19 | 20 | vm module - show mappings for a module 21 | module the name of the module to show mapping information for`; 22 | 23 | public runSync(tokens: Token[]): Var { 24 | const retWithAddress = this.runShowAddress(tokens); 25 | if (retWithAddress !== null) return retWithAddress; 26 | 27 | const retWithWildCard = this.runShowNamed(tokens); 28 | if (retWithWildCard !== null) return retWithWildCard; 29 | 30 | return this.usage(); 31 | } 32 | 33 | private runShowAddress(tokens: Token[]): Var | null { 34 | const vars = this.transform(tokens, [this.parseVar]); 35 | if (vars === null) return null; 36 | const [v0] = vars as [Var]; 37 | 38 | const address = v0.toPointer(); 39 | 40 | const matches = Process.enumerateRanges('---').filter( 41 | r => r.base <= address && r.base.add(r.size) > address, 42 | ); 43 | if (matches.length === 1) { 44 | const r = matches[0] as RangeDetails; 45 | Output.writeln( 46 | `Address: ${Format.toHexString(address)} is within allocation:`, 47 | ); 48 | this.printMapping(r); 49 | return v0; 50 | } else { 51 | Output.writeln( 52 | `Address: ${Format.toHexString(address)} is not found within an allocation:`, 53 | ); 54 | const before = Process.enumerateRanges('---').filter( 55 | r => r.base <= address, 56 | ); 57 | if (before.length === 0) { 58 | Output.writeln('No previous mapping'); 59 | } else { 60 | const r = before[before.length - 1] as RangeDetails; 61 | Output.writeln('Previous mapping'); 62 | this.printMapping(r); 63 | } 64 | const after = Process.enumerateRanges('---').filter( 65 | r => r.base.add(r.size) > address, 66 | ); 67 | if (after.length === 0) { 68 | Output.writeln('No next mapping'); 69 | } else { 70 | const r = after[0] as RangeDetails; 71 | Output.writeln('Next mapping'); 72 | this.printMapping(r); 73 | } 74 | } 75 | return v0; 76 | } 77 | 78 | private printMapping(r: RangeDetails, filter: boolean = true) { 79 | const limit = r.base.add(r.size); 80 | let fileInfo = ''; 81 | if (r.file !== undefined) { 82 | fileInfo = [ 83 | `offset: ${Output.yellow(Format.toHexString(r.file.offset))},`, 84 | `name: ${Output.blue(r.file.path)}`, 85 | ].join(' '); 86 | } 87 | Output.writeln( 88 | [ 89 | `\t${Output.green(Format.toHexString(r.base))}-${Output.green(Format.toHexString(limit))}`, 90 | `${Output.bold(Output.yellow(r.protection))}`, 91 | `${Output.bold(Format.toSize(r.size))}`, 92 | fileInfo, 93 | ].join(' '), 94 | filter, 95 | ); 96 | } 97 | 98 | private runShowNamed(tokens: Token[]): Var | null { 99 | const vars = this.transformOptional(tokens, [], [this.parseLiteral]); 100 | if (vars === null) return null; 101 | const [_, [name]] = vars as [[], [string | null]]; 102 | 103 | if (name === null) { 104 | Process.enumerateRanges('---').forEach(r => { 105 | this.printMapping(r, true); 106 | }); 107 | return Var.ZERO; 108 | } else if (Regex.isGlob(name)) { 109 | const regex = Regex.globToRegex(name); 110 | if (regex === null) return this.usage(); 111 | 112 | const modules = Process.enumerateModules().filter(m => 113 | m.name.match(regex), 114 | ); 115 | modules.sort(); 116 | modules.forEach(m => { 117 | m.enumerateRanges('---').forEach(r => { 118 | this.printMapping(r, true); 119 | }); 120 | }); 121 | if (modules.length === 1) { 122 | const module = modules[0] as Module; 123 | return new Var( 124 | uint64(module.base.toString()), 125 | `Module: ${module.name}`, 126 | ); 127 | } else { 128 | return Var.ZERO; 129 | } 130 | } else { 131 | const mod = Process.findModuleByName(name); 132 | if (mod === null) { 133 | Output.writeln(`Module: ${name} not found`); 134 | return Var.ZERO; 135 | } 136 | 137 | mod.enumerateRanges('---').forEach(r => { 138 | this.printMapping(r, true); 139 | }); 140 | return new Var(uint64(mod.base.toString()), `Module: ${mod.name}`); 141 | } 142 | } 143 | 144 | public usage(): Var { 145 | Output.writeln(VmCmdLet.USAGE); 146 | return Var.ZERO; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/cmdlets/misc/corpse/core_filter.ts: -------------------------------------------------------------------------------- 1 | export class CoreFilter { 2 | private static readonly CORE_FILTER: string = '/proc/self/coredump_filter'; 3 | public static readonly NEEDED: UInt64 = uint64(0x1ff); 4 | private constructor() {} 5 | 6 | public static set(value: UInt64) { 7 | const text = `0x${value.toString(16)}`; 8 | try { 9 | File.writeAllText(CoreFilter.CORE_FILTER, text); 10 | } catch (error) { 11 | throw new Error(`failed to set core filter, ${error}`); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/cmdlets/misc/corpse/core_pattern.ts: -------------------------------------------------------------------------------- 1 | export class CorePattern { 2 | private static readonly CORE_PATTERN: string = 3 | '/proc/sys/kernel/core_pattern'; 4 | private static readonly CORE_USES_PID: string = 5 | '/proc/sys/kernel/core_uses_pid'; 6 | 7 | private constructor() {} 8 | 9 | public static get(): string { 10 | const value = File.readAllText(CorePattern.CORE_PATTERN).trimEnd(); 11 | if (value.startsWith('|')) 12 | throw new Error( 13 | `core pattern must not start with '|' - value: '${value}'`, 14 | ); 15 | 16 | if (value.indexOf('%') !== -1) 17 | throw new Error(`core pattern must not contain '%' - value: '${value}'`); 18 | 19 | return value; 20 | } 21 | 22 | public static appendPid(): boolean { 23 | try { 24 | const text = File.readAllText(CorePattern.CORE_USES_PID).trimEnd(); 25 | const value = uint64(text); 26 | if (value.equals(0)) { 27 | return false; 28 | } else { 29 | return true; 30 | } 31 | } catch { 32 | return false; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/cmdlets/misc/corpse/dumpable.ts: -------------------------------------------------------------------------------- 1 | export class Dumpable { 2 | private static readonly PR_SET_DUMPABLE: number = 4; 3 | private fnSetPrctl: SystemFunction; 4 | 5 | public constructor() { 6 | const pPrctl = Module.findExportByName(null, 'prctl'); 7 | if (pPrctl === null) throw new Error('failed to find prctl'); 8 | 9 | this.fnSetPrctl = new SystemFunction(pPrctl, 'int', ['int', 'size_t']); 10 | } 11 | 12 | public set(value: number) { 13 | const ret = this.fnSetPrctl( 14 | Dumpable.PR_SET_DUMPABLE, 15 | value, 16 | ) as UnixSystemFunctionResult; 17 | if (ret.value !== 0) 18 | throw new Error(`failed to prctl, errno: ${ret.errno}`); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/cmdlets/misc/corpse/fork.ts: -------------------------------------------------------------------------------- 1 | export class Fork { 2 | private fnFork: SystemFunction; 3 | public constructor() { 4 | const pFork = Module.findExportByName(null, 'fork'); 5 | if (pFork === null) throw new Error('failed to find fork'); 6 | 7 | this.fnFork = new SystemFunction(pFork, 'int', []); 8 | } 9 | 10 | public fork( 11 | parentCallback: (childPid: number) => void, 12 | childCallback: () => void, 13 | ): number { 14 | const ret = this.fnFork() as UnixSystemFunctionResult; 15 | if (ret.value === -1) { 16 | throw new Error(`failed to clone, errno: ${ret.errno}`); 17 | } else if (ret.value === 0) { 18 | childCallback(); 19 | } else { 20 | parentCallback(ret.value); 21 | } 22 | 23 | return ret.value; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/cmdlets/misc/corpse/madvise.ts: -------------------------------------------------------------------------------- 1 | import { Mem } from '../../../memory/mem.js'; 2 | 3 | export class Madvise { 4 | private static readonly MADV_DOFORK: number = 11; 5 | private static readonly MADV_DODUMP: number = 17; 6 | // int madvise(void addr[.length], size_t length, int advice); 7 | private fnMadvise: SystemFunction< 8 | number, 9 | [NativePointer, UInt64 | number, number] 10 | >; 11 | 12 | constructor() { 13 | const pMadvise = Module.findExportByName(null, 'madvise'); 14 | if (pMadvise === null) throw new Error('failed to find madvise'); 15 | 16 | this.fnMadvise = new SystemFunction(pMadvise, 'int', [ 17 | 'pointer', 18 | 'size_t', 19 | 'int', 20 | ]); 21 | } 22 | 23 | private doFork(base: NativePointer, size: number) { 24 | const ret = this.fnMadvise( 25 | base, 26 | size, 27 | Madvise.MADV_DOFORK, 28 | ) as UnixSystemFunctionResult; 29 | if (ret.value !== 0) 30 | throw new Error( 31 | `failed to madvise, errno: ${ret.errno}, base: ${base}, size: ${size}`, 32 | ); 33 | } 34 | 35 | private doDump(base: NativePointer, size: number) { 36 | const ret = this.fnMadvise( 37 | base, 38 | size, 39 | Madvise.MADV_DODUMP, 40 | ) as UnixSystemFunctionResult; 41 | if (ret.value !== 0) 42 | throw new Error( 43 | `failed to madvise, errno: ${ret.errno}, base: ${base}, size: ${size}`, 44 | ); 45 | } 46 | 47 | private alignRange( 48 | base: NativePointer, 49 | size: number, 50 | ): { base: NativePointer; size: number } { 51 | const limit = base.add(size); 52 | const alignedLimit = Mem.pageAlignUp(limit); 53 | const alignedBase = Mem.pageAlignDown(base); 54 | const alignedSize = alignedLimit.sub(alignedBase).toUInt32(); 55 | return { base: alignedBase, size: alignedSize }; 56 | } 57 | 58 | private forAlignedRanges(fn: (base: NativePointer, size: number) => void) { 59 | Process.enumerateRanges('---').forEach(r => { 60 | const { base, size } = this.alignRange(r.base, r.size); 61 | fn(base, size); 62 | }); 63 | } 64 | 65 | public tryForkAll(debug: (msg: string) => void) { 66 | this.forAlignedRanges((base, size) => { 67 | try { 68 | this.doFork(base, size); 69 | } catch (e) { 70 | debug( 71 | `failed to madvise MADV_DOFORK range: ${e}, base: ${base}, size: ${size}`, 72 | ); 73 | } 74 | }); 75 | } 76 | 77 | public tryDumpAll(debug: (msg: string) => void) { 78 | this.forAlignedRanges((base, size) => { 79 | try { 80 | this.doDump(base, size); 81 | } catch (e) { 82 | debug( 83 | `failed to madvise MADV_DODUMP range: ${e}, base: ${base}, size: ${size}`, 84 | ); 85 | } 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/cmdlets/misc/corpse/mem.ts: -------------------------------------------------------------------------------- 1 | export enum MemProtection { 2 | PROT_NONE = 0, 3 | PROT_READ = 0x1, 4 | PROT_WRITE = 0x2, 5 | PROT_READ_WRITE = PROT_READ | PROT_WRITE, 6 | } 7 | 8 | export class Mem { 9 | private static readonly MAP_PRIVATE: number = 0x2; 10 | private static readonly MAP_ANONYMOUS: number = 0x20; 11 | 12 | private static readonly MAP_FAILED: NativePointer = ptr(-1); 13 | 14 | /* void *mmap(void addr[.length], size_t length, int prot, int flags, 15 | int fd, off_t offset); */ 16 | private fnMmap: SystemFunction< 17 | NativePointer, 18 | [NativePointer, number | UInt64, number, number, number, number | UInt64] 19 | >; 20 | 21 | /* int mprotect(void addr[.len], size_t len, int prot); */ 22 | private fnMprotect: SystemFunction< 23 | number, 24 | [NativePointer, number | UInt64, number] 25 | >; 26 | 27 | public constructor() { 28 | const pMmap = Module.findExportByName(null, 'mmap'); 29 | if (pMmap === null) throw new Error('failed to find mmap'); 30 | 31 | this.fnMmap = new SystemFunction(pMmap, 'pointer', [ 32 | 'pointer', 33 | 'size_t', 34 | 'int', 35 | 'int', 36 | 'int', 37 | 'size_t', 38 | ]); 39 | 40 | const pMprotect = Module.findExportByName(null, 'mprotect'); 41 | if (pMprotect === null) throw new Error('failed to find mprotect'); 42 | 43 | this.fnMprotect = new SystemFunction(pMprotect, 'int', [ 44 | 'pointer', 45 | 'size_t', 46 | 'int', 47 | ]); 48 | } 49 | 50 | public map_anonymous(size: number): NativePointer { 51 | const ret = this.fnMmap( 52 | ptr(0), 53 | size, 54 | MemProtection.PROT_READ | MemProtection.PROT_WRITE, 55 | Mem.MAP_ANONYMOUS | Mem.MAP_PRIVATE, 56 | -1, 57 | 0, 58 | ) as UnixSystemFunctionResult; 59 | if (ret.value.equals(Mem.MAP_FAILED)) 60 | throw new Error(`failed to mmap, errno: ${ret.errno}`); 61 | return ret.value; 62 | } 63 | 64 | public protect(addr: NativePointer, size: number, prot: MemProtection) { 65 | const ret = this.fnMprotect( 66 | addr, 67 | size, 68 | prot, 69 | ) as UnixSystemFunctionResult; 70 | if (ret.value === -1) 71 | throw new Error(`failed to mprotect, errno: ${ret.errno}`); 72 | } 73 | 74 | public static pageAlign(size: number): number { 75 | const mask = Process.pageSize - 1; 76 | return (size + mask) & ~mask; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/cmdlets/misc/corpse/proc.ts: -------------------------------------------------------------------------------- 1 | export type WaitStatus = { 2 | exitStatus: number | null; 3 | termSignal: number | null; 4 | stopped: boolean; 5 | }; 6 | 7 | export class Proc { 8 | private static readonly INT_SIZE: number = 4; 9 | private static readonly WNOHANG: number = 1; 10 | public static readonly SIGABRT: number = 6; 11 | public static readonly SIGKILL: number = 9; 12 | private static readonly SIGSET_SIZE: number = 8; 13 | public static readonly SIG_DFL: NativePointer = ptr(0); 14 | 15 | private fnWaitPid: SystemFunction; 16 | private fnGetPid: SystemFunction; 17 | private fnKill: SystemFunction; 18 | private fnSyscall: SystemFunction< 19 | number, 20 | [number | UInt64, number, NativePointer, NativePointer, number | UInt64] 21 | >; 22 | 23 | public constructor() { 24 | const pWaitPid = Module.findExportByName(null, 'waitpid'); 25 | if (pWaitPid === null) throw new Error('failed to find waitpid'); 26 | 27 | this.fnWaitPid = new SystemFunction(pWaitPid, 'int', [ 28 | 'int', 29 | 'pointer', 30 | 'int', 31 | ]); 32 | 33 | const pGetPid = Module.findExportByName(null, 'getpid'); 34 | if (pGetPid === null) throw new Error('failed to find getpid'); 35 | 36 | this.fnGetPid = new SystemFunction(pGetPid, 'int', []); 37 | 38 | const pKill = Module.findExportByName(null, 'kill'); 39 | if (pKill === null) throw new Error('failed to find kill'); 40 | 41 | this.fnKill = new SystemFunction(pKill, 'int', ['int', 'int']); 42 | 43 | const pSyscall = Module.findExportByName(null, 'syscall'); 44 | if (pSyscall === null) throw new Error('failed to find syscall'); 45 | 46 | this.fnSyscall = new SystemFunction(pSyscall, 'int', [ 47 | 'size_t', 48 | 'int', 49 | 'pointer', 50 | 'pointer', 51 | 'size_t', 52 | ]); 53 | } 54 | 55 | public kill(pid: number, signal: number) { 56 | const ret = this.fnKill(pid, signal) as UnixSystemFunctionResult; 57 | if (ret.value === -1) 58 | throw new Error(`failed to kill, errno: ${ret.errno}`); 59 | } 60 | 61 | public getpid(): number { 62 | const ret = this.fnGetPid() as UnixSystemFunctionResult; 63 | return ret.value; 64 | } 65 | 66 | public waitpid(pid: number): WaitStatus { 67 | const pStatus = Memory.alloc(Proc.INT_SIZE); 68 | const ret = this.fnWaitPid( 69 | pid, 70 | pStatus, 71 | Proc.WNOHANG, 72 | ) as UnixSystemFunctionResult; 73 | if (ret.value === -1) 74 | throw new Error(`failed to waitpid, errno: ${ret.errno}`); 75 | 76 | if (ret.value === 0) { 77 | return { 78 | exitStatus: null, 79 | termSignal: null, 80 | stopped: false, 81 | }; 82 | } 83 | 84 | if (ret.value !== pid) 85 | throw new Error('failed to waitpid ${pid} got ${ret.value}'); 86 | const status = pStatus.readInt(); 87 | if (Proc.wifExited(status)) { 88 | const exitStatus = Proc.wExitStatus(status); 89 | return { 90 | exitStatus, 91 | termSignal: null, 92 | stopped: true, 93 | }; 94 | } else if (Proc.wifSignalled(status)) { 95 | const termSignal = Proc.wTermSig(status); 96 | return { 97 | exitStatus: null, 98 | termSignal, 99 | stopped: true, 100 | }; 101 | } else { 102 | throw new Error(`failed to waitpid, pid: ${pid}, status: ${status}`); 103 | } 104 | } 105 | 106 | private static wExitStatus(status: number): number { 107 | return (status & 0xff00) >> 8; 108 | } 109 | 110 | private static wTermSig(status: number): number { 111 | return status & 0x7f; 112 | } 113 | 114 | private static wifExited(status: number): boolean { 115 | return Proc.wTermSig(status) === 0; 116 | } 117 | 118 | private static wifSignalled(status: number): boolean { 119 | const signal = Proc.wTermSig(status); 120 | return signal !== 0 && signal !== 0x7f; 121 | } 122 | 123 | public rt_sigaction(signal: number, sa_handler: NativePointer) { 124 | const sigactionSize = 125 | 4 * Process.pointerSize + Proc.INT_SIZE + Proc.SIGSET_SIZE; 126 | const sigaction = Memory.alloc(sigactionSize); 127 | sigaction.writePointer(sa_handler); 128 | const rtSigactionNumber = Proc.getRtSigactionSyscallNumber(); 129 | 130 | const ret = this.fnSyscall( 131 | rtSigactionNumber, 132 | signal, 133 | sigaction, 134 | ptr(0), 135 | Proc.SIGSET_SIZE, 136 | ) as UnixSystemFunctionResult; 137 | if (ret.value === -1) 138 | throw new Error(`failed to rt_sigaction, errno: ${ret.errno}`); 139 | } 140 | 141 | private static getRtSigactionSyscallNumber(): number { 142 | switch (Process.arch) { 143 | case 'arm': 144 | return 174; 145 | case 'arm64': 146 | return 134; 147 | case 'ia32': 148 | return 174; 149 | case 'x64': 150 | return 13; 151 | 152 | default: 153 | throw new Error(`unsupported architecture: ${Process.arch}`); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/cmdlets/misc/corpse/rlimit.ts: -------------------------------------------------------------------------------- 1 | export type Rlimits = { 2 | sortLimit: Int64; 3 | hardLimit: Int64; 4 | }; 5 | 6 | export class Rlimit { 7 | private static readonly RLIMIT_CORE: number = 4; 8 | private fnSetrlimit: SystemFunction; 9 | 10 | public static readonly UNLIMITED: Rlimits = { 11 | sortLimit: int64(-1), 12 | hardLimit: int64(-1), 13 | }; 14 | 15 | public constructor() { 16 | const pSetrlimit = Module.findExportByName(null, 'setrlimit'); 17 | if (pSetrlimit === null) throw new Error('failed to find setrlimit'); 18 | 19 | this.fnSetrlimit = new SystemFunction(pSetrlimit, 'int', [ 20 | 'int', 21 | 'pointer', 22 | ]); 23 | } 24 | 25 | public set(rlimits: Rlimits) { 26 | const buffer = Memory.alloc(Process.pointerSize * 2); 27 | let cursor = buffer; 28 | cursor.writeLong(rlimits.sortLimit); 29 | cursor = cursor.add(Process.pointerSize); 30 | cursor.writeLong(rlimits.hardLimit); 31 | cursor = cursor.add(Process.pointerSize); 32 | 33 | const ret = this.fnSetrlimit( 34 | Rlimit.RLIMIT_CORE, 35 | buffer, 36 | ) as UnixSystemFunctionResult; 37 | if (ret.value !== 0) 38 | throw new Error(`failed to setrlimit, errno: ${ret.errno}`); 39 | } 40 | 41 | public isUnlimited(rlimits: Rlimits): boolean { 42 | if (!rlimits.sortLimit.equals(Rlimit.UNLIMITED.sortLimit)) return false; 43 | if (!rlimits.hardLimit.equals(Rlimit.UNLIMITED.hardLimit)) return false; 44 | return true; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/cmdlets/misc/corpse/selinux.ts: -------------------------------------------------------------------------------- 1 | export class SeLinux { 2 | private static readonly SELINUX_PATH: string = '/sys/fs/selinux/enforce'; 3 | 4 | private constructor() {} 5 | 6 | public static isPermissive(): boolean { 7 | try { 8 | const value = File.readAllText(SeLinux.SELINUX_PATH).trimEnd(); 9 | if (value === '0') return true; 10 | return false; 11 | } catch { 12 | return false; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/cmdlets/misc/echo.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Token } from '../../io/token.js'; 4 | import { Var } from '../../vars/var.js'; 5 | 6 | export class EchoCmdLet extends CmdLetBase { 7 | name = 'echo'; 8 | category = 'misc'; 9 | help = 'toggle echo mode'; 10 | 11 | public static echo: boolean = true; 12 | 13 | private static readonly USAGE: string = `Usage: echo 14 | echo on - enable echo (default) 15 | 16 | echo off - disable echo`; 17 | 18 | public runSync(tokens: Token[]): Var { 19 | const vars = this.transformOptional(tokens, [], [this.parseSwitch]); 20 | if (vars === null) return this.usage(); 21 | const [, [state]] = vars as [[], [boolean | null]]; 22 | if (state === null) { 23 | Output.writeln( 24 | `echo is [${EchoCmdLet.echo ? Output.green('on') : Output.red('off')}]`, 25 | ); 26 | return Var.ZERO; 27 | } 28 | EchoCmdLet.echo = state; 29 | return Var.ZERO; 30 | } 31 | 32 | protected parseSwitch(token: Token): boolean | null { 33 | const literal = token.getLiteral(); 34 | if (literal === 'on') return true; 35 | if (literal === 'off') return false; 36 | return null; 37 | } 38 | 39 | public usage(): Var { 40 | Output.writeln(EchoCmdLet.USAGE); 41 | return Var.ZERO; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/cmdlets/misc/errno.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Token } from '../../io/token.js'; 4 | import { Var } from '../../vars/var.js'; 5 | 6 | export class ErrnoCmdLet extends CmdLetBase { 7 | name = 'errno'; 8 | category = 'misc'; 9 | help = 'displays the errno value'; 10 | private static readonly USAGE: string = `Usage: errno 11 | 12 | errno - display the errno value`; 13 | 14 | private fnErrnoLocation: SystemFunction | null = null; 15 | 16 | public runSync(tokens: Token[]): Var { 17 | const retGetErrno = this.getErrno(tokens); 18 | if (retGetErrno !== null) return retGetErrno; 19 | 20 | const retSetErrno = this.setErrno(tokens); 21 | if (retSetErrno !== null) return retSetErrno; 22 | 23 | return this.usage(); 24 | } 25 | 26 | private setErrno(tokens: Token[]): Var | null { 27 | const vars = this.transform(tokens, [this.parseVar]); 28 | if (vars === null) return null; 29 | const [value] = vars as [Var]; 30 | 31 | const errno = value.toU64().toNumber(); 32 | const location = this.getErrnoLocation(); 33 | location.writeInt(errno); 34 | return value; 35 | } 36 | 37 | private getErrno(tokens: Token[]): Var | null { 38 | if (tokens.length !== 0) return null; 39 | const location = this.getErrnoLocation(); 40 | 41 | const errno = location.readInt(); 42 | Output.writeln(`errno: ${errno}`); 43 | 44 | return new Var(uint64(errno), 'errno'); 45 | } 46 | 47 | private getErrnoLocation(): NativePointer { 48 | const fnErrnoLocation = this.fnErrnoLocation as SystemFunction< 49 | NativePointer, 50 | [] 51 | >; 52 | const location = 53 | fnErrnoLocation() as UnixSystemFunctionResult; 54 | if (location.value.equals(ptr(0))) 55 | throw new Error('failed to get __errno_location()'); 56 | return location.value; 57 | } 58 | 59 | public usage(): Var { 60 | Output.writeln(ErrnoCmdLet.USAGE); 61 | return Var.ZERO; 62 | } 63 | 64 | public override isSupported(): boolean { 65 | switch (Process.platform) { 66 | case 'linux': { 67 | const pErrnoLocation = Module.findExportByName( 68 | null, 69 | '__errno_location', 70 | ); 71 | if (pErrnoLocation === null) return false; 72 | this.fnErrnoLocation = new SystemFunction( 73 | pErrnoLocation, 74 | 'pointer', 75 | [], 76 | ); 77 | return true; 78 | } 79 | case 'darwin': 80 | case 'freebsd': 81 | case 'qnx': 82 | case 'windows': 83 | case 'barebone': 84 | default: 85 | return false; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/cmdlets/misc/exit.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Token } from '../../io/token.js'; 4 | import { Var } from '../../vars/var.js'; 5 | 6 | export class ExitCmdLet extends CmdLetBase { 7 | name = 'exit'; 8 | category = 'misc'; 9 | help = 'exits the shell'; 10 | override visible = false; 11 | 12 | public runSync(_: Token[]): Var { 13 | return this.usage(); 14 | } 15 | 16 | public usage(): Var { 17 | Output.writeln('Press CTRL+C to exit.'); 18 | return Var.ZERO; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/cmdlets/misc/grep.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Token } from '../../io/token.js'; 4 | import { Var } from '../../vars/var.js'; 5 | 6 | export class GrepCmdLet extends CmdLetBase { 7 | name = 'grep'; 8 | category = 'misc'; 9 | help = 'filter output'; 10 | 11 | private static readonly USAGE: string = `Usage: grep 12 | 13 | grep - clear output filter 14 | 15 | grep regex - filter output 16 | regex the regex to use to filter the output`; 17 | 18 | public runSync(tokens: Token[]): Var { 19 | const vars = this.transformOptional(tokens, [], [this.parseLiteral]); 20 | if (vars === null) return this.usage(); 21 | // eslint-disable-next-line prefer-const 22 | let [_, [filter]] = vars as [[], [string | null]]; 23 | if (filter === null) { 24 | Output.clearFilter(); 25 | Output.writeln('output filter cleared'); 26 | } else { 27 | try { 28 | if ( 29 | filter.length > 1 && 30 | filter.startsWith('"') && 31 | filter.endsWith('"') 32 | ) { 33 | filter = filter.slice(1, filter.length - 1); 34 | } 35 | Output.setFilter(filter); 36 | Output.writeln( 37 | [ 38 | 'output filter set to ', 39 | Output.blue("'"), 40 | Output.green(filter), 41 | Output.blue("'"), 42 | ].join(''), 43 | ); 44 | } catch { 45 | Output.writeln(`invalid regex: ${filter}`); 46 | } 47 | } 48 | return Var.ZERO; 49 | } 50 | 51 | public usage(): Var { 52 | Output.writeln(GrepCmdLet.USAGE); 53 | return Var.ZERO; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/cmdlets/misc/help.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase, CmdLet } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { CmdLets } from '../../commands/cmdlets.js'; 4 | import { Token } from '../../io/token.js'; 5 | import { Var } from '../../vars/var.js'; 6 | 7 | export class HelpCmdLet extends CmdLetBase { 8 | name = 'help'; 9 | category = 'misc'; 10 | help = 'print this message'; 11 | 12 | public runSync(tokens: Token[]): Var { 13 | const vars = this.transform(tokens, [this.parseLiteral]); 14 | if (vars === null) return this.usage(); 15 | const [name] = vars as [string]; 16 | const cmdlet = CmdLets.getByName(name); 17 | if (cmdlet === null) return this.usage(); 18 | 19 | return cmdlet.usage(); 20 | } 21 | 22 | public usage(): Var { 23 | const cmdlets = CmdLets.all().filter(c => c.visible); 24 | const groups: Map = cmdlets.reduce((result, item) => { 25 | const category = item.category; 26 | if (!result.has(category)) { 27 | result.set(category, []); 28 | } 29 | 30 | result.get(category)?.push(item); 31 | return result; 32 | }, new Map()); 33 | 34 | Array.from(groups.entries()) 35 | .sort(([k1, _v1], [k2, _v2]) => k1.localeCompare(k2)) 36 | .forEach(([k, v]) => { 37 | Output.writeln(`${Output.bold(Output.blue(k))}:`); 38 | Array.from(v) 39 | .sort((c1, c2) => c1.name.localeCompare(c2.name)) 40 | .forEach(c => { 41 | Output.writeln( 42 | `\t${Output.green(c.name.padEnd(10, ' '))}: ${Output.yellow(c.help)}`, 43 | ); 44 | }); 45 | }); 46 | 47 | Output.writeln(); 48 | Output.writeln('For more information about a command use:'); 49 | Output.writeln(`\t${Output.green('help')} ${Output.yellow('')}`); 50 | return Var.ZERO; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/cmdlets/misc/history.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { History } from '../../terminal/history.js'; 4 | import { Token } from '../../io/token.js'; 5 | import { Var } from '../../vars/var.js'; 6 | 7 | export class HistoryCmdLet extends CmdLetBase { 8 | name = 'h'; 9 | category = 'misc'; 10 | help = 'command history'; 11 | 12 | private static readonly USAGE: string = `Usage: h 13 | 14 | h - show history 15 | 16 | h index - rerun history item 17 | index the index of the item to rerun`; 18 | 19 | public runSync(_tokens: Token[]): Var { 20 | throw new Error('not supported'); 21 | } 22 | 23 | public override async run(tokens: Token[]): Promise { 24 | const vars = this.transformOptional(tokens, [], [this.parseVar]); 25 | if (vars === null) return this.usage(); 26 | const [_, [v0]] = vars as [[], [Var | null]]; 27 | 28 | if (v0 === null) { 29 | const history = Array.from(History.all()); 30 | for (const [i, value] of history.entries()) { 31 | Output.writeln( 32 | [`${i.toString().padStart(3, ' ')}:`, value].join(' '), 33 | true, 34 | ); 35 | } 36 | return Var.ZERO; 37 | } else { 38 | const id = v0.toU64().toNumber(); 39 | 40 | return History.rerun(id); 41 | } 42 | } 43 | 44 | public usage(): Var { 45 | Output.writeln(HistoryCmdLet.USAGE); 46 | return Var.ZERO; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/cmdlets/misc/log.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Token } from '../../io/token.js'; 4 | import { Var } from '../../vars/var.js'; 5 | 6 | export class LogCmdLet extends CmdLetBase { 7 | name = 'log'; 8 | category = 'misc'; 9 | help = 'set log file'; 10 | 11 | private static readonly USAGE: string = `Usage: log 12 | 13 | log - clear log file 14 | 15 | log file - set log file 16 | file the file to log to`; 17 | 18 | public runSync(tokens: Token[]): Var { 19 | const vars = this.transformOptional(tokens, [], [this.parseString]); 20 | if (vars === null) return this.usage(); 21 | const [_, [file]] = vars as [[], [string | null]]; 22 | if (file === null) { 23 | const logName = Output.clearLog(); 24 | Output.writeln('log file cleared'); 25 | if (logName === null) { 26 | return Var.ZERO; 27 | } else { 28 | return new Var(logName); 29 | } 30 | } else { 31 | try { 32 | Output.writeln( 33 | [ 34 | 'log file set to ', 35 | Output.blue("'"), 36 | Output.green(file), 37 | Output.blue("'"), 38 | ].join(''), 39 | ); 40 | Output.setLog(file); 41 | } catch { 42 | Output.writeln(`invalid log file: ${file}`); 43 | } 44 | return new Var(file); 45 | } 46 | } 47 | 48 | public usage(): Var { 49 | Output.writeln(LogCmdLet.USAGE); 50 | return Var.ZERO; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/cmdlets/misc/print.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Token } from '../../io/token.js'; 4 | import { Var } from '../../vars/var.js'; 5 | 6 | export class PrintCmdLet extends CmdLetBase { 7 | name = 'p'; 8 | category = 'misc'; 9 | help = 'print an expression'; 10 | 11 | private static readonly USAGE: string = `Usage: p 12 | p - print an expression 13 | 14 | p exp - print an expression 15 | exp the expression to print`; 16 | 17 | public runSync(tokens: Token[]): Var { 18 | if (tokens.length !== 1) return this.usage(); 19 | const t = tokens[0] as Token; 20 | const val = t.toVar(); 21 | 22 | if (val === null) { 23 | Output.writeln(t.getLiteral()); 24 | return Var.ZERO; 25 | } else { 26 | Output.writeln(`${t.getLiteral()} = ${val}`); 27 | return val; 28 | } 29 | } 30 | 31 | public usage(): Var { 32 | Output.writeln(PrintCmdLet.USAGE); 33 | return Var.ZERO; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/cmdlets/misc/var.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Vars } from '../../vars/vars.js'; 4 | import { Token } from '../../io/token.js'; 5 | import { Var } from '../../vars/var.js'; 6 | 7 | export class VarCmdLet extends CmdLetBase { 8 | name = 'v'; 9 | category = 'misc'; 10 | help = 'variable management'; 11 | 12 | private static readonly USAGE: string = `Usage: v 13 | v - show the values of all variables 14 | 15 | v name - display the value of a named variable 16 | name the name of the variable to display 17 | 18 | v name value - assign a value to a variable 19 | name the name of the variable to assign 20 | value the value to assign 21 | 22 | v name ${CmdLetBase.DELETE_CHAR} - delete a variable 23 | name the name of the variable to delete`; 24 | 25 | public runSync(tokens: Token[]): Var { 26 | const retWithNameAndHash = this.runDelete(tokens); 27 | if (retWithNameAndHash !== null) return retWithNameAndHash; 28 | 29 | const retWithNameAndPointer = this.runSet(tokens); 30 | if (retWithNameAndPointer !== null) return retWithNameAndPointer; 31 | 32 | const retWithName = this.runShow(tokens); 33 | if (retWithName !== null) return retWithName; 34 | 35 | return this.usage(); 36 | } 37 | 38 | private runDelete(tokens: Token[]): Var | null { 39 | const vars = this.transform(tokens, [this.parseLiteral, this.parseDelete]); 40 | if (vars === null) return null; 41 | const [name, _] = vars as [string, string]; 42 | 43 | const val = Vars.delete(name); 44 | if (val === null) { 45 | Output.writeln(`Variable ${name} not assigned`); 46 | return Var.ZERO; 47 | } else { 48 | return val; 49 | } 50 | } 51 | 52 | private runSet(tokens: Token[]): Var | null { 53 | const vars = this.transform(tokens, [this.parseLiteral, this.parseVar]); 54 | if (vars === null) return null; 55 | const [name, value] = vars as [string, Var]; 56 | Vars.set(name, value); 57 | return value; 58 | } 59 | 60 | private runShow(tokens: Token[]): Var | null { 61 | const vars = this.transformOptional(tokens, [], [this.parseLiteral]); 62 | if (vars === null) return null; 63 | const [_, [name]] = vars as [[], [string | null]]; 64 | 65 | if (name === null) { 66 | Output.writeln('Vars:'); 67 | for (const [key, value] of Vars.all()) { 68 | Output.writeln( 69 | [ 70 | `${Output.green(key.padEnd(25, ' '))}:`, 71 | `${Output.yellow(value.toString())}`, 72 | ].join(' '), 73 | true, 74 | ); 75 | } 76 | return Vars.getRet(); 77 | } else { 78 | const val = Vars.get(name); 79 | if (val === null) { 80 | Output.writeln(`Variable ${Output.green(name)} not assigned`); 81 | return Var.ZERO; 82 | } else { 83 | Output.writeln( 84 | [ 85 | `Variable ${Output.green(name)}`, 86 | `value: ${Output.yellow(val.toString())}`, 87 | ].join(' '), 88 | ); 89 | return val; 90 | } 91 | } 92 | } 93 | 94 | public usage(): Var { 95 | Output.writeln(VarCmdLet.USAGE); 96 | return Var.ZERO; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/cmdlets/modules/ld.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Token } from '../../io/token.js'; 4 | import { Var } from '../../vars/var.js'; 5 | 6 | export class LdCmdLet extends CmdLetBase { 7 | name = 'ld'; 8 | category = 'modules'; 9 | help = 'load modules'; 10 | 11 | private static readonly USAGE: string = `Usage: ld 12 | 13 | ld - load a module 14 | 15 | ld path - load a module 16 | path the absolute path of the module to load (note that paths with spaces must be quoted)`; 17 | 18 | public runSync(tokens: Token[]): Var { 19 | const vars = this.transform(tokens, [this.parseString]); 20 | if (vars === null) return this.usage(); 21 | let [name] = vars as [string]; 22 | 23 | if (name.length > 1 && name.startsWith('"') && name.endsWith('"')) { 24 | name = name.slice(1, name.length - 1); 25 | } 26 | 27 | /* "/workspaces/frida-cshell/module.so" */ 28 | Output.writeln(`Loading: ${name}`); 29 | 30 | const mod = Module.load(name); 31 | return new Var(mod.base.toString(), `Module: ${name}`); 32 | } 33 | 34 | public usage(): Var { 35 | Output.writeln(LdCmdLet.USAGE); 36 | return Var.ZERO; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/cmdlets/modules/mod.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Format } from '../../misc/format.js'; 4 | import { Token } from '../../io/token.js'; 5 | import { Var } from '../../vars/var.js'; 6 | import { Regex } from '../../misc/regex.js'; 7 | 8 | export class ModCmdLet extends CmdLetBase { 9 | name = 'mod'; 10 | category = 'modules'; 11 | help = 'display module information'; 12 | 13 | private static readonly USAGE: string = `Usage: mod 14 | 15 | mod - show all modules 16 | 17 | mod address - show module for address 18 | address the address/symbol to show module information for 19 | 20 | mod name - show named module 21 | name the name of the module to show information for`; 22 | 23 | public runSync(tokens: Token[]): Var { 24 | const retWithAddress = this.runShowAddress(tokens); 25 | if (retWithAddress !== null) return retWithAddress; 26 | 27 | const retWithName = this.runShowNamed(tokens); 28 | if (retWithName !== null) return retWithName; 29 | 30 | return this.usage(); 31 | } 32 | 33 | private runShowAddress(tokens: Token[]): Var | null { 34 | const vars = this.transform(tokens, [this.parseVar]); 35 | if (vars === null) return null; 36 | const [v0] = vars as [Var]; 37 | 38 | const address = v0.toPointer(); 39 | const matches = Process.enumerateModules().filter( 40 | m => m.base <= address && m.base.add(m.size) > address, 41 | ); 42 | if (matches.length === 1) { 43 | const m = matches[0] as Module; 44 | Output.writeln( 45 | `Address: ${Format.toHexString(address)} is within module:`, 46 | ); 47 | this.printModule(m); 48 | } else { 49 | Output.writeln( 50 | `Address: ${Format.toHexString(address)} is not found within a module:`, 51 | ); 52 | } 53 | return v0; 54 | } 55 | 56 | private printModule(m: Module, filtered: boolean = true) { 57 | const limit = m.base.add(m.size); 58 | Output.writeln( 59 | [ 60 | `${Output.green(Format.toHexString(m.base))}-${Output.green(Format.toHexString(limit))}`, 61 | Output.bold(Format.toSize(m.size)), 62 | Output.yellow(m.name.padEnd(30, ' ')), 63 | Output.blue(m.path), 64 | ].join(' '), 65 | filtered, 66 | ); 67 | } 68 | 69 | private runShowNamed(tokens: Token[]): Var | null { 70 | const vars = this.transformOptional(tokens, [], [this.parseLiteral]); 71 | if (vars === null) return null; 72 | const [_, [name]] = vars as [[], [string | null]]; 73 | 74 | if (name === null) { 75 | const modules = Process.enumerateModules(); 76 | modules.sort((a, b) => a.base.compare(b.base)); 77 | modules.forEach(m => { 78 | this.printModule(m); 79 | }); 80 | return Var.ZERO; 81 | } else if (Regex.isGlob(name)) { 82 | const regex = Regex.globToRegex(name); 83 | if (regex === null) return this.usage(); 84 | 85 | const modules = Process.enumerateModules().filter(m => 86 | m.name.match(regex), 87 | ); 88 | modules.sort(); 89 | modules.forEach(m => { 90 | this.printModule(m, true); 91 | }); 92 | if (modules.length === 1) { 93 | const module = modules[0] as Module; 94 | return new Var( 95 | uint64(module.base.toString()), 96 | `Module: ${module.name}`, 97 | ); 98 | } else { 99 | return Var.ZERO; 100 | } 101 | } else { 102 | const mod = Process.findModuleByName(name); 103 | if (mod === null) { 104 | Output.writeln(`Module: ${name} not found`); 105 | return Var.ZERO; 106 | } else { 107 | this.printModule(mod); 108 | return new Var(uint64(mod.base.toString()), `Module: ${mod.name}`); 109 | } 110 | } 111 | } 112 | 113 | public usage(): Var { 114 | Output.writeln(ModCmdLet.USAGE); 115 | return Var.ZERO; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/cmdlets/thread/bt.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Regs } from '../../breakpoints/regs.js'; 4 | import { Token } from '../../io/token.js'; 5 | import { Var } from '../../vars/var.js'; 6 | import { BacktraceType, Exception } from '../../misc/exception.js'; 7 | 8 | export class BtCmdLet extends CmdLetBase { 9 | name = 'bt'; 10 | category = 'thread'; 11 | help = 'display backtrace information'; 12 | 13 | private static readonly USAGE: string = `Usage: bt 14 | bt [type] - show the backtrace for the current thread in a breakpoint 15 | type the type of backtrace to show [fuzzy | accurate (default)] 16 | 17 | bt id [type] - show backtrace for thread 18 | id the id of the thread to show backtrace for 19 | type the type of backtrace to show [fuzzy | accurate (default)] 20 | 21 | bt name - show backtrace for thread 22 | name the name of the thread to show backtrace for 23 | type the type of backtrace to show [fuzzy | accurate (default)]`; 24 | 25 | public runSync(tokens: Token[]): Var { 26 | const retCurrent = this.runShowCurrent(tokens); 27 | if (retCurrent !== null) return retCurrent; 28 | 29 | const retWithId = this.runShowId(tokens); 30 | if (retWithId !== null) return retWithId; 31 | 32 | const retWithName = this.runShowNamed(tokens); 33 | if (retWithName !== null) return retWithName; 34 | 35 | return this.usage(); 36 | } 37 | 38 | private runShowCurrent(tokens: Token[]): Var | null { 39 | const vars = this.transformOptional(tokens, [], [this.parseType]); 40 | if (vars === null) return null; 41 | const [_, [type]] = vars as [[], [BacktraceType | undefined | null]]; 42 | 43 | /* 44 | * What we think is a backtrace type might actually be a thread name 45 | * or id. 46 | */ 47 | if (type === undefined) return null; 48 | 49 | const ctx = Regs.getContext(); 50 | if (ctx === null) 51 | throw new Error( 52 | `backtrace requires context, only available in breakpoints`, 53 | ); 54 | 55 | Exception.printBacktrace(ctx, type ?? BacktraceType.Accurate); 56 | return Var.ZERO; 57 | } 58 | 59 | private runShowId(tokens: Token[]): Var | null { 60 | const vars = this.transformOptional( 61 | tokens, 62 | [this.parseVar], 63 | [this.parseType], 64 | ); 65 | if (vars === null) return null; 66 | const [[v0], [type]] = vars as [[Var], [BacktraceType | undefined | null]]; 67 | const id = v0.toU64().toNumber(); 68 | if (type === undefined) throw new Error('invalid backtrace type'); 69 | 70 | const matches = Process.enumerateThreads().filter(t => t.id === id); 71 | if (matches.length === 0) { 72 | Output.writeln(`Thread #${id} not found`); 73 | return Var.ZERO; 74 | } else { 75 | matches.forEach(t => { 76 | Exception.printBacktrace(t.context, type ?? BacktraceType.Accurate); 77 | }); 78 | return new Var(uint64(id), `Thread: ${id}`); 79 | } 80 | } 81 | 82 | private runShowNamed(tokens: Token[]): Var | null { 83 | const vars = this.transformOptional( 84 | tokens, 85 | [this.parseLiteral], 86 | [this.parseType], 87 | ); 88 | if (vars === null) return null; 89 | const [[name], [type]] = vars as [ 90 | [string], 91 | [BacktraceType | undefined | null], 92 | ]; 93 | if (type === undefined) throw new Error('invalid backtrace type'); 94 | 95 | const matches = Process.enumerateThreads().filter(t => t.name === name); 96 | switch (matches.length) { 97 | case 0: 98 | Output.writeln(`Thread: ${name} not found`); 99 | return Var.ZERO; 100 | case 1: { 101 | const t = matches[0] as ThreadDetails; 102 | Exception.printBacktrace(t.context, type ?? BacktraceType.Accurate); 103 | return new Var(uint64(t.id), `Thread: ${t.id}`); 104 | } 105 | default: 106 | matches.forEach(t => { 107 | Exception.printBacktrace(t.context, type ?? BacktraceType.Accurate); 108 | }); 109 | return Var.ZERO; 110 | } 111 | } 112 | 113 | protected parseType(token: Token): BacktraceType | undefined { 114 | if (token === null) return BacktraceType.Accurate; 115 | const literal = token.getLiteral(); 116 | switch (literal) { 117 | case 'fuzzy': 118 | return BacktraceType.Fuzzy; 119 | case 'accurate': 120 | return BacktraceType.Accurate; 121 | default: 122 | return undefined; 123 | } 124 | } 125 | 126 | public usage(): Var { 127 | Output.writeln(BtCmdLet.USAGE); 128 | return Var.ZERO; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/cmdlets/thread/thread.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Token } from '../../io/token.js'; 4 | import { Format } from '../../misc/format.js'; 5 | import { Var } from '../../vars/var.js'; 6 | 7 | export class ThreadCmdLet extends CmdLetBase { 8 | name = 't'; 9 | category = 'thread'; 10 | help = 'display thread information'; 11 | 12 | private static readonly USAGE: string = `Usage: t 13 | 14 | t - show all threads 15 | 16 | t name - show named thread 17 | name the name of the thread to show information for`; 18 | 19 | public runSync(tokens: Token[]): Var { 20 | const retWithId = this.runShowId(tokens); 21 | if (retWithId !== null) return retWithId; 22 | 23 | const retWithName = this.runShowName(tokens); 24 | if (retWithName !== null) return retWithName; 25 | 26 | const retWithoutParams = this.runShowAll(tokens); 27 | if (retWithoutParams !== null) return retWithoutParams; 28 | 29 | return this.usage(); 30 | } 31 | 32 | private runShowId(tokens: Token[]): Var | null { 33 | const vars = this.transform(tokens, [this.parseVar]); 34 | if (vars === null) return null; 35 | const [v0] = vars as [Var]; 36 | 37 | const id = v0.toU64().toNumber(); 38 | 39 | const matches = Process.enumerateThreads().filter(t => t.id === id); 40 | if (matches.length === 0) { 41 | Output.writeln(`Thread #${id} not found`); 42 | return Var.ZERO; 43 | } else { 44 | matches.forEach(t => { 45 | this.printThread(t); 46 | }); 47 | return new Var(uint64(id), `Thread: ${id}`); 48 | } 49 | } 50 | 51 | private printThread(t: ThreadDetails, filtered: boolean = true) { 52 | Output.writeln( 53 | [ 54 | `${Output.yellow(t.id.toString().padStart(5, ' '))}:`, 55 | `${Output.green((t.name ?? '[UNNAMED]').padEnd(15, ' '))}`, 56 | `${Output.blue(t.state)}`, 57 | `pc: ${Output.yellow(Format.toHexString(t.context.pc))}`, 58 | `sp: ${Output.yellow(Format.toHexString(t.context.sp))}`, 59 | ].join(' '), 60 | filtered, 61 | ); 62 | } 63 | 64 | private runShowName(tokens: Token[]): Var | null { 65 | const vars = this.transform(tokens, [this.parseLiteral]); 66 | if (vars === null) return null; 67 | const [name] = vars as [string]; 68 | 69 | const matches = Process.enumerateThreads().filter(t => t.name === name); 70 | switch (matches.length) { 71 | case 0: 72 | Output.writeln(`Thread: ${name} not found`); 73 | return Var.ZERO; 74 | case 1: { 75 | const t = matches[0] as ThreadDetails; 76 | this.printThread(t); 77 | return new Var(uint64(t.id)); 78 | } 79 | default: 80 | matches.forEach(t => { 81 | this.printThread(t, true); 82 | }); 83 | return Var.ZERO; 84 | } 85 | } 86 | 87 | private runShowAll(tokens: Token[]): Var | null { 88 | if (tokens.length !== 0) return null; 89 | 90 | const threads = Process.enumerateThreads(); 91 | switch (threads.length) { 92 | case 0: 93 | Output.writeln('No threads found'); 94 | return Var.ZERO; 95 | case 1: { 96 | const t = threads[0] as ThreadDetails; 97 | this.printThread(t); 98 | return new Var(uint64(t.id), `Thread: ${t.id}`); 99 | } 100 | default: 101 | threads.forEach(t => { 102 | this.printThread(t, true); 103 | }); 104 | return Var.ZERO; 105 | } 106 | } 107 | 108 | public usage(): Var { 109 | Output.writeln(ThreadCmdLet.USAGE); 110 | return Var.ZERO; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/cmdlets/thread/tls.ts: -------------------------------------------------------------------------------- 1 | import { CmdLetBase } from '../../commands/cmdlet.js'; 2 | import { Output } from '../../io/output.js'; 3 | import { Token } from '../../io/token.js'; 4 | import { Var } from '../../vars/var.js'; 5 | import { Tls } from '../../tls/tls.js'; 6 | 7 | export class TlsCmdLet extends CmdLetBase { 8 | name = 'tls'; 9 | category = 'thread'; 10 | help = 'read the TLS pointer'; 11 | 12 | private static readonly USAGE: string = `Usage: tls 13 | tls - get the tls pointer`; 14 | 15 | public runSync(tokens: Token[]): Var { 16 | if (tokens.length > 0) { 17 | Output.writeln(TlsCmdLet.USAGE); 18 | return Var.ZERO; 19 | } 20 | 21 | const tls = Tls.getTls(); 22 | return new Var(uint64(tls.toString())); 23 | } 24 | 25 | public override isSupported(): boolean { 26 | return Tls.isSupported(); 27 | } 28 | 29 | public usage(): Var { 30 | Output.writeln(TlsCmdLet.USAGE); 31 | return Var.ZERO; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/cmdlet.ts: -------------------------------------------------------------------------------- 1 | import { Token } from '../io/token.js'; 2 | import { Var } from '../vars/var.js'; 3 | 4 | export interface CmdLet { 5 | readonly name: string; 6 | category: string; 7 | help: string; 8 | visible: boolean; 9 | usage(): Var; 10 | runSync(tokens: Token[]): Var; 11 | run(tokens: Token[]): Promise; 12 | isSupported(): boolean; 13 | } 14 | 15 | export abstract class CmdLetBase implements CmdLet { 16 | private static readonly UNLIMITED_CHAR: string = '*'; 17 | public static readonly NUM_CHAR: string = '#'; 18 | public static readonly DELETE_CHAR: string = '#'; 19 | public abstract readonly category: string; 20 | public abstract readonly name: string; 21 | public abstract readonly help: string; 22 | public readonly visible: boolean = true; 23 | public abstract usage(): Var; 24 | public abstract runSync(tokens: Token[]): Var; 25 | public async run(tokens: Token[]): Promise { 26 | return this.runSync(tokens); 27 | } 28 | public isSupported(): boolean { 29 | return true; 30 | } 31 | 32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 33 | public transform( 34 | tokens: Token[], 35 | operations: { [K in keyof T]: (token: Token) => T[K] | null }, 36 | ): T | null { 37 | if (tokens.length !== operations.length) return null; 38 | const result = operations.map((operation, index) => 39 | operation(tokens[index] as Token), 40 | ); 41 | if (result.some(v => v === null)) { 42 | return null; 43 | } else { 44 | return result as T; 45 | } 46 | } 47 | 48 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 49 | public transformOptional( 50 | tokens: Token[], 51 | mandatory: { [K in keyof M]: (token: Token) => M[K] | null }, // Fixed typo 52 | optional: { [K in keyof O]: (token: Token) => O[K] | null }, 53 | ): [M, { [K in keyof O]: O[K] | null }] | null { 54 | if (tokens.length < mandatory.length) return null; 55 | if (tokens.length > mandatory.length + optional.length) return null; 56 | 57 | // Mandatory variables 58 | const mVars = mandatory.map((operation, index) => 59 | operation(tokens[index] as Token), 60 | ); 61 | 62 | let failed = false; 63 | 64 | // Optional variables 65 | const oVars = optional.map((operation, index) => { 66 | const token = tokens[index + mandatory.length]; 67 | if (token === undefined) return null; 68 | const result = operation(token); 69 | if (result === null) failed = true; 70 | return result; 71 | }); 72 | 73 | if (failed) return null; 74 | 75 | // If any mandatory variable is null, return null 76 | if (mVars.some(v => v === null)) { 77 | return null; 78 | } else { 79 | // Return both mandatory and optional variables 80 | return [mVars as M, oVars as O]; 81 | } 82 | } 83 | 84 | protected parseVar(token: Token): Var | null { 85 | if (token === null) return null; 86 | return token.toVar(); 87 | } 88 | 89 | protected parseLiteral(token: Token): string | null { 90 | if (token === null) return null; 91 | return token.getLiteral(); 92 | } 93 | 94 | protected parseString(token: Token): string | null { 95 | if (token === null) return null; 96 | return token.getString(); 97 | } 98 | 99 | protected parseWidth(token: Token): number | null { 100 | const literal = token.getLiteral(); 101 | switch (literal) { 102 | case '1': 103 | return 1; 104 | case '2': 105 | return 2; 106 | case '4': 107 | return 4; 108 | case '8': 109 | return 8; 110 | default: 111 | return null; 112 | } 113 | } 114 | 115 | protected parseDelete(token: Token): string | null { 116 | const literal = token.getLiteral(); 117 | if (literal !== CmdLetBase.DELETE_CHAR) return null; 118 | return literal; 119 | } 120 | 121 | protected parseNumberOrAll(token: Token): number | null { 122 | if (token.getLiteral() === CmdLetBase.UNLIMITED_CHAR) return -1; 123 | 124 | const v = token.toVar(); 125 | if (v === null) return null; 126 | 127 | const hits = v.toU64().toNumber(); 128 | return hits; 129 | } 130 | 131 | protected parseIndex(token: Token): number | null { 132 | const literal = token.getLiteral(); 133 | if (literal.startsWith(CmdLetBase.NUM_CHAR)) 134 | return CmdLetBase.parseIndexString(literal); 135 | 136 | const v = token.toVar(); 137 | if (v === null) return null; 138 | return CmdLetBase.parseIndexString(v.getLiteral()); 139 | } 140 | 141 | private static parseIndexString(literal: string): number | null { 142 | if (!literal.startsWith(CmdLetBase.NUM_CHAR)) return null; 143 | 144 | const numStr = literal.slice(1); 145 | const val = parseInt(numStr); 146 | 147 | if (isNaN(val)) return null; 148 | return val; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/commands/command.ts: -------------------------------------------------------------------------------- 1 | import { CmdLets } from './cmdlets.js'; 2 | import { Output } from '../io/output.js'; 3 | import { Format } from '../misc/format.js'; 4 | import { Var } from '../vars/var.js'; 5 | import { Token } from '../io/token.js'; 6 | import { CmdLet } from './cmdlet.js'; 7 | import { Macro, Macros } from '../macros/macros.js'; 8 | import { MacroCmdLet } from '../cmdlets/misc/macro.js'; 9 | import { EchoCmdLet } from '../cmdlets/misc/echo.js'; 10 | 11 | export class Command { 12 | private static readonly MACRO_PREFIX: string = '!'; 13 | public static async run(tokens: Token[]): Promise { 14 | let suppressed = false; 15 | if (!EchoCmdLet.echo && !Output.isSuppressed()) { 16 | Output.suppress(true); 17 | suppressed = true; 18 | } 19 | try { 20 | const cmdlet = this.getCmdlet(tokens); 21 | if (cmdlet !== null) { 22 | return cmdlet.run(tokens.slice(1)); 23 | } 24 | 25 | const macro = this.getMacro(tokens); 26 | if (macro !== null) { 27 | return MacroCmdLet.runSync(macro, tokens.slice(1)); 28 | } 29 | 30 | return this.runFunction(tokens); 31 | } finally { 32 | if (suppressed) Output.suppress(false); 33 | } 34 | } 35 | 36 | public static runSync(tokens: Token[]): Var { 37 | let suppressed = false; 38 | if (!EchoCmdLet.echo && !Output.isSuppressed()) { 39 | Output.suppress(true); 40 | suppressed = true; 41 | } 42 | try { 43 | const cmdlet = this.getCmdlet(tokens); 44 | if (cmdlet !== null) { 45 | return cmdlet.runSync(tokens.slice(1)); 46 | } 47 | 48 | const macro = this.getMacro(tokens); 49 | if (macro !== null) { 50 | return MacroCmdLet.runSync(macro, tokens.slice(1)); 51 | } 52 | 53 | return this.runFunction(tokens); 54 | } finally { 55 | if (suppressed) Output.suppress(false); 56 | } 57 | } 58 | 59 | private static getCmdlet(tokens: Token[]): CmdLet | null { 60 | if (tokens.length === 0) throw new Error('failed to tokenize command'); 61 | const t0 = tokens[0] as Token; 62 | return CmdLets.getByName(t0.getLiteral()); 63 | } 64 | 65 | private static getMacro(tokens: Token[]): Macro | null { 66 | if (tokens.length === 0) throw new Error('failed to tokenize macro'); 67 | const t0 = tokens[0] as Token; 68 | const name = t0.getLiteral(); 69 | if (!name.startsWith(Command.MACRO_PREFIX)) return null; 70 | if (name.length === 1) throw new Error('macro name not supplied'); 71 | const macro = Macros.get(name.slice(1)); 72 | if (macro === null) 73 | throw new Error(`failed to recognozie macro ${Output.green(name)}`); 74 | return macro; 75 | } 76 | 77 | private static runFunction(tokens: Token[]): Var { 78 | if (tokens.length === 0) throw new Error('failed to tokenize command'); 79 | const t0 = tokens[0] as Token; 80 | const v0 = t0.toVar(); 81 | if (v0 === null) { 82 | const command = tokens.map(t => t.getLiteral()).join(' '); 83 | throw new Error( 84 | `request was not understood as an internal command or a detected symbol: '${command}'`, 85 | ); 86 | } 87 | const addr = v0.toPointer(); 88 | return this.executeAddress(addr, tokens.slice(1)); 89 | } 90 | 91 | private static executeAddress(address: NativePointer, tokens: Token[]): Var { 92 | const ptrs: Var[] = []; 93 | const args: NativePointer[] = []; 94 | for (const token of tokens) { 95 | const p = token.toVar(); 96 | if (p === null) { 97 | throw new Error(`failed to parse token: ${token.getLiteral()}`); 98 | } 99 | 100 | ptrs.push(p); 101 | args.push(p.toPointer()); 102 | } 103 | 104 | args.forEach((param, index) => { 105 | Output.debug( 106 | [ 107 | `\t${index}:`, 108 | Format.toHexString(param), 109 | Format.toDecString(param), 110 | ].join(' '), 111 | ); 112 | }); 113 | 114 | const func = new NativeFunction(address, 'pointer', [ 115 | 'pointer', 116 | 'pointer', 117 | 'pointer', 118 | 'pointer', 119 | 'pointer', 120 | 'pointer', 121 | 'pointer', 122 | 'pointer', 123 | ]); 124 | 125 | const ret = func( 126 | args[0] ?? ptr(0), 127 | args[1] ?? ptr(0), 128 | args[2] ?? ptr(0), 129 | args[3] ?? ptr(0), 130 | args[4] ?? ptr(0), 131 | args[5] ?? ptr(0), 132 | args[6] ?? ptr(0), 133 | args[7] ?? ptr(0), 134 | ); 135 | 136 | return new Var(uint64(ret.toString())); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/entrypoint.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The script can send strings to frida-inject to write to its stdout or 3 | * stderr. This can be done either inside the RPC handler for receiving 4 | * input from frida-inject, or elsewhere at any arbitrary point in the 5 | * script. We use the following syntax: 6 | * 7 | * send(['frida:stdout', 'DATA']); 8 | * send(['frida:stderr', 'DATA']); 9 | */ 10 | import { Input } from './io/input.js'; 11 | import { Output } from './io/output.js'; 12 | import { SrcCmdLet } from './cmdlets/files/src.js'; 13 | import { Exception } from './misc/exception.js'; 14 | import { Version } from './misc/version.js'; 15 | import { Format } from './misc/format.js'; 16 | 17 | export const DEFAULT_SRC_PATH: string = `${Process.getHomeDir()}/.cshellrc`; 18 | 19 | type InitParams = { 20 | debug: boolean; 21 | }; 22 | 23 | rpc.exports = { 24 | async init(stage: string, params: InitParams | null = null) { 25 | if (params != null) { 26 | Output.writeln(`params: ${JSON.stringify(params)}`); 27 | Output.setDebugging(params.debug); 28 | } 29 | Output.debug(`init - stage: ${stage}`); 30 | Output.banner(); 31 | Process.setExceptionHandler(Exception.exceptionHandler); 32 | await SrcCmdLet.loadInitScript(DEFAULT_SRC_PATH); 33 | Input.prompt(); 34 | }, 35 | /** 36 | * Support reading from stdin for communications with the injected script. 37 | * With the console in its default canonical mode, we will read a line at a 38 | * time when the user presses enter and send it to a registered RPC method 39 | * in the script as follows. Here, the data parameter is the string typed 40 | * by the user including the newline. 41 | */ 42 | async onFridaStdin(data: string, bytes: ArrayBuffer | null) { 43 | if (bytes === null) { 44 | await Input.read(Format.toByteArray(data)); 45 | } else { 46 | await Input.read(bytes); 47 | } 48 | }, 49 | /* 50 | * If getFridaTerminalMode returns "raw", then frida-inject will set the 51 | * console mode to RAW 52 | */ 53 | getFridaTerminalMode() { 54 | if (Version.VERSION >= Version.BINARY_MODE_MIN_VERSION) { 55 | return 'binary'; 56 | } else { 57 | return 'raw'; 58 | } 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /src/io/char.ts: -------------------------------------------------------------------------------- 1 | export class CharCode { 2 | public static readonly ESC: number = 0x1b; 3 | public static readonly CSI: number = CharCode.from('['); 4 | 5 | public static readonly CUP: number = CharCode.from('H'); 6 | 7 | public static readonly EL: number = CharCode.from('K'); 8 | public static readonly EL_BACK = CharCode.from('0'); 9 | public static readonly EL_FORWARD = CharCode.from('1'); 10 | public static readonly EL_ALL = CharCode.from('2'); 11 | 12 | public static readonly ED: number = CharCode.from('J'); 13 | public static readonly ED_BACK = CharCode.from('0'); 14 | public static readonly ED_FORWARD = CharCode.from('1'); 15 | public static readonly ED_ALL = CharCode.from('2'); 16 | 17 | public static readonly BS: number = CharCode.from('\b'); 18 | public static readonly DEL: number = 0x7f; 19 | public static readonly CR: number = CharCode.from('\r'); 20 | public static readonly TAB: number = 0x09; 21 | public static readonly LEFT: number = 0x44; 22 | public static readonly RIGHT: number = 0x43; 23 | public static readonly UP: number = 0x41; 24 | public static readonly DOWN: number = 0x42; 25 | public static readonly HOME: number = 0x48; 26 | public static readonly END: number = 0x46; 27 | public static readonly FF: number = 0x0c; 28 | public static readonly VT = CharCode.from('~'); 29 | public static readonly SGR = CharCode.from('m'); 30 | public static readonly SEPARATOR = CharCode.from(';'); 31 | 32 | public static readonly CLEAR_SCREEN: string = String.fromCharCode( 33 | CharCode.ESC, 34 | CharCode.CSI, 35 | CharCode.ED_ALL, 36 | CharCode.ED, 37 | ); 38 | public static readonly CURSOR_TOP_LEFT: string = String.fromCharCode( 39 | CharCode.ESC, 40 | CharCode.CSI, 41 | CharCode.CUP, 42 | ); 43 | public static readonly ERASE_LINE: string = String.fromCharCode( 44 | CharCode.ESC, 45 | CharCode.CSI, 46 | CharCode.EL_ALL, 47 | CharCode.EL, 48 | ); 49 | 50 | public static readonly RESET: string = String.fromCharCode( 51 | CharCode.ESC, 52 | CharCode.CSI, 53 | CharCode.from('0'), 54 | CharCode.SGR, 55 | ); 56 | 57 | public static readonly BOLD: string = String.fromCharCode( 58 | CharCode.ESC, 59 | CharCode.CSI, 60 | CharCode.from('1'), 61 | CharCode.SGR, 62 | ); 63 | 64 | public static readonly GREEN: string = String.fromCharCode( 65 | CharCode.ESC, 66 | CharCode.CSI, 67 | CharCode.from('0'), 68 | CharCode.SEPARATOR, 69 | CharCode.from('3'), 70 | CharCode.from('2'), 71 | CharCode.SGR, 72 | ); 73 | 74 | public static readonly YELLOW: string = String.fromCharCode( 75 | CharCode.ESC, 76 | CharCode.CSI, 77 | CharCode.from('0'), 78 | CharCode.SEPARATOR, 79 | CharCode.from('3'), 80 | CharCode.from('3'), 81 | CharCode.SGR, 82 | ); 83 | 84 | public static readonly BLUE: string = String.fromCharCode( 85 | CharCode.ESC, 86 | CharCode.CSI, 87 | CharCode.from('0'), 88 | CharCode.SEPARATOR, 89 | CharCode.from('9'), 90 | CharCode.from('4'), 91 | CharCode.SGR, 92 | ); 93 | 94 | public static readonly RED: string = String.fromCharCode( 95 | CharCode.ESC, 96 | CharCode.CSI, 97 | CharCode.from('0'), 98 | CharCode.SEPARATOR, 99 | CharCode.from('3'), 100 | CharCode.from('1'), 101 | CharCode.SGR, 102 | ); 103 | 104 | public static from(val: string): number { 105 | if (val.length !== 1) { 106 | throw new Error(`invalid escape char ${val}`); 107 | } 108 | return val.charCodeAt(0); 109 | } 110 | } 111 | 112 | enum VtModifierCode { 113 | Shift = 1, 114 | Alt = 2, 115 | Ctrl = 4, 116 | Meta = 8, 117 | } 118 | 119 | export class Vt { 120 | public static readonly HOME = CharCode.from('1'); 121 | public static readonly INSERT = CharCode.from('2'); 122 | public static readonly DELETE = CharCode.from('3'); 123 | public static readonly END = CharCode.from('4'); 124 | public static readonly PG_UP = CharCode.from('5'); 125 | public static readonly PG_DOWN = CharCode.from('6'); 126 | public static readonly UP = CharCode.from('A'); 127 | public static readonly DOWN = CharCode.from('B'); 128 | public static readonly RIGHT = CharCode.from('C'); 129 | public static readonly LEFT = CharCode.from('D'); 130 | public static readonly SEPARATOR = CharCode.from(';'); 131 | 132 | public static wordLeft(): string { 133 | const vt = new Vt() 134 | .add(this.SEPARATOR) 135 | .addModifier(VtModifierCode.Ctrl) 136 | .add(this.LEFT); 137 | return vt.toString(); 138 | } 139 | 140 | public static wordRight(): string { 141 | const vt = new Vt() 142 | .add(this.SEPARATOR) 143 | .addModifier(VtModifierCode.Ctrl) 144 | .add(this.RIGHT); 145 | return vt.toString(); 146 | } 147 | 148 | private val: string = ''; 149 | 150 | private constructor() {} 151 | 152 | private addModifier(...modifiers: VtModifierCode[]): Vt { 153 | let val = 1; 154 | modifiers.forEach(m => { 155 | val |= m; 156 | }); 157 | this.val = this.val.concat(String(val)); 158 | return this; 159 | } 160 | 161 | private add(...val: number[]): Vt { 162 | this.val = this.val.concat(String.fromCharCode(...val)); 163 | return this; 164 | } 165 | 166 | private toString() { 167 | return this.val; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/io/parser.ts: -------------------------------------------------------------------------------- 1 | import { Token } from './token.js'; 2 | 3 | export class Parser { 4 | private line: string; 5 | private inEscape = false; 6 | 7 | public constructor(line: string) { 8 | this.line = line; 9 | } 10 | 11 | public tokenize(): Token[] { 12 | const tokens: Token[] = []; 13 | let inQuoteString = false; 14 | let inWord = false; 15 | let current = ''; 16 | 17 | while (true) { 18 | const c = this.popEscaped(); 19 | if (c === null) break; 20 | 21 | switch (c) { 22 | case '\\': 23 | if (!inQuoteString) 24 | throw new Error('\\ outside quoted string is illegal'); 25 | this.inEscape = true; 26 | break; 27 | case ' ': 28 | if (inQuoteString) { 29 | current += c; 30 | } else if (inWord) { 31 | inWord = false; 32 | tokens.push(new Token(current)); 33 | current = ''; 34 | } 35 | break; 36 | case '"': 37 | if (inQuoteString) { 38 | inQuoteString = false; 39 | tokens.push(new Token(`"${current}"`)); 40 | current = ''; 41 | } else { 42 | inQuoteString = true; 43 | if (current.length !== 0) { 44 | tokens.push(new Token(current)); 45 | current = ''; 46 | } 47 | } 48 | break; 49 | default: 50 | current += c; 51 | if (!inQuoteString) { 52 | inWord = true; 53 | } 54 | break; 55 | } 56 | } 57 | if (inQuoteString) { 58 | throw new Error('unescaped quotation'); 59 | } 60 | 61 | if (current.length !== 0) { 62 | tokens.push(new Token(current)); 63 | } 64 | return tokens; 65 | } 66 | 67 | private popEscaped(): string | null { 68 | const c = this.pop(); 69 | if (!this.inEscape) { 70 | return c; 71 | } 72 | switch (c) { 73 | case 'n': 74 | return '\n'; 75 | case 't': 76 | return '\t'; 77 | default: 78 | return c; 79 | } 80 | } 81 | 82 | private pop(): string | null { 83 | if (this.line.length === 0) return null; 84 | const c = this.line[0] as string; 85 | this.line = this.line.slice(1); 86 | return c; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/io/token.ts: -------------------------------------------------------------------------------- 1 | import { Vars } from '../vars/vars.js'; 2 | import { Numeric } from '../misc/numeric.js'; 3 | import { Var } from '../vars/var.js'; 4 | import { Regs } from '../breakpoints/regs.js'; 5 | 6 | export class Token { 7 | private static isQuotedString(s: string): boolean { 8 | return ( 9 | s.length > 1 && 10 | s.startsWith('"') && 11 | s.endsWith('"') && 12 | s.slice(1, s.length - 1).indexOf('"') === -1 13 | ); 14 | } 15 | 16 | private readonly value: string; 17 | 18 | public constructor(value: string) { 19 | this.value = value; 20 | } 21 | 22 | public getLiteral(): string { 23 | return this.value; 24 | } 25 | 26 | public getString(): string { 27 | if (Token.isQuotedString(this.value)) 28 | return this.value.slice(1, this.value.length - 1); 29 | 30 | const v = Vars.get(this.value); 31 | if (v !== null) { 32 | const value = v.getLiteral(); 33 | if (Token.isQuotedString(value)) { 34 | return value.slice(1, value.length - 1); 35 | } else { 36 | return v.getLiteral(); 37 | } 38 | } 39 | 40 | return this.value; 41 | } 42 | 43 | public toVar(): Var | null { 44 | if (Token.isQuotedString(this.value)) 45 | return new Var(this.value.slice(1, this.value.length - 1), this.value); 46 | 47 | const num = Numeric.parse(this.value); 48 | if (num !== null) return new Var(num, this.value); 49 | 50 | if (this.value.charAt(0) === '$') return Regs.get(this.value.slice(1)); 51 | 52 | const v = Vars.get(this.value); 53 | if (v !== null) return v; 54 | 55 | const address = Module.findExportByName(null, this.value); 56 | if (address !== null) { 57 | return new Var(uint64(address.toString()), this.value); 58 | } 59 | 60 | const param = DebugSymbol.fromName(this.value); 61 | if (!param.address.isNull()) 62 | return new Var(uint64(param.address.toString()), this.value); 63 | 64 | return null; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/io/zmodem/block.ts: -------------------------------------------------------------------------------- 1 | import { Chars, Escape, Zdle } from './constants.js'; 2 | import { Crc16 } from './crc.js'; 3 | 4 | export class Block { 5 | public static readonly MAX_BLOCK_SIZE: number = 1024; 6 | 7 | private static lastSent = 0; 8 | 9 | private bytes: number[] = []; 10 | 11 | private checkByte(b: number) { 12 | if (b < 0 || b > 0xff) throw new Error(`byte: ${b} out of range`); 13 | } 14 | 15 | private shouldEscape(b: number): Escape { 16 | this.checkByte(b); 17 | if ((b & 0x60) != 0) { 18 | return Escape.ESCAPE_NEVER; 19 | } 20 | 21 | /* 22 | * ZMODEM software escapes ZDLE, 020, 0220, 021, 0221, 023, and 0223. 23 | * If preceded by 0100 or 0300 (@), 015 and 0215 are also escaped to protect the Telenet command escape CR-@-CR. 24 | * The receiver ignores 021, 0221, 023, and 0223 characters in the data stream. 25 | */ 26 | 27 | /* 28 | * ZMODEM software escapes 24 (CAN - ZDLE), 16 (DLE), 17 (DC1 - XON), 19 (DC3 - XOFF) (and 144, 145, 147) 29 | * If preceded by 64 (or 192) (@), 13 (CR) (and 141) are also escaped to protect the Telenet command escape CR-@-CR. 30 | * The receiver ignores 021, 0221, 023, and 0223 characters in the data stream. 31 | */ 32 | switch (b) { 33 | case Chars.ZDLE: 34 | case Chars.DLE: 35 | case Chars.DLE | 0x80: 36 | case Chars.XON: 37 | case Chars.XON | 0x80: 38 | case Chars.XOFF: 39 | case Chars.XOFF | 0x80: 40 | return Escape.ESCAPE_ALWAYS; 41 | case Chars.CR: 42 | case Chars.CR | 0x80: 43 | return Escape.ESCAPE_AFTER_AT; 44 | default: 45 | return Escape.ESCAPE_NEVER; 46 | } 47 | } 48 | 49 | private writeByte(b: number) { 50 | this.checkByte(b); 51 | const esc = this.shouldEscape(b); 52 | if ( 53 | esc === Escape.ESCAPE_ALWAYS || 54 | (esc === Escape.ESCAPE_AFTER_AT && Block.lastSent === Chars.AT) 55 | ) { 56 | this.bytes.push(Chars.ZDLE); 57 | this.bytes.push(b ^ 0x40); 58 | Block.lastSent = b ^ 0x40; 59 | } else { 60 | this.bytes.push(b); 61 | Block.lastSent = b; 62 | } 63 | } 64 | 65 | public constructor(data: ArrayBuffer | null, frameEnd: Zdle) { 66 | const crc = new Crc16(); 67 | if (data !== null) { 68 | const buffer = new Uint8Array(data); 69 | buffer.forEach(b => { 70 | this.writeByte(b); 71 | crc.update(b); 72 | }); 73 | } 74 | 75 | this.bytes.push(Chars.ZDLE); 76 | this.bytes.push(frameEnd); 77 | crc.update(frameEnd); 78 | 79 | crc.update(0); 80 | crc.update(0); 81 | 82 | this.writeByte(crc.value >> 8); 83 | this.writeByte(crc.value & 0xff); 84 | 85 | if (frameEnd === Zdle.ZCRCW) { 86 | this.writeByte(Chars.XON); 87 | } 88 | } 89 | 90 | public get data(): ArrayBuffer { 91 | return new Uint8Array(this.bytes).buffer as ArrayBuffer; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/io/zmodem/constants.ts: -------------------------------------------------------------------------------- 1 | export enum Escape { 2 | ESCAPE_ALWAYS = 'always', 3 | ESCAPE_NEVER = 'never', 4 | ESCAPE_AFTER_AT = 'after', 5 | } 6 | 7 | export enum Chars { 8 | NUL = 0x00, 9 | LF = 0x0a, 10 | CR = 0x0d, 11 | DLE = 0x10, 12 | XON = 0x11, 13 | XOFF = 0x13, 14 | ZDLE = 0x18, 15 | ZPAD = 0x2a, 16 | AT = 0x40, 17 | } 18 | 19 | export enum Header { 20 | ZBIN = 0x41, 21 | ZHEX = 0x42, 22 | ZBIN32 = 0x43, 23 | } 24 | 25 | export enum Conv { 26 | ZCNONE = 0, 27 | ZCBIN = 1, 28 | ZCNL = 2, 29 | ZCRESUM = 3, 30 | } 31 | 32 | export enum FileOpts { 33 | NONE = 0, 34 | ZMKSNOLOC = 0x80, 35 | ZMNEWL = 1, 36 | ZMCRC = 2, 37 | ZMAPND = 3, 38 | ZMCLOB = 4, 39 | ZMNEW = 5, 40 | ZMDIFF = 6, 41 | ZMPROT = 7, 42 | ZMCHNG = 8, 43 | } 44 | 45 | export enum Trans { 46 | ZTNONE = 0, 47 | ZTLZW = 1, 48 | ZTCRYPT = 2, 49 | ZTRLE = 3, 50 | } 51 | 52 | export enum Ext { 53 | ZXNONE = 0, 54 | ZXSPARS = 64, 55 | } 56 | 57 | export enum FrameType { 58 | ZRQINIT = 0, 59 | ZRINIT = 1, 60 | SZINIT = 2, 61 | ZACK = 3, 62 | ZFILE = 4, 63 | ZSKIP = 5, 64 | ZNAK = 6, 65 | ZABORT = 7, 66 | ZFIN = 8, 67 | ZRPOS = 9, 68 | ZDATA = 10, 69 | ZEOF = 11, 70 | ZFERR = 12, 71 | ZCRC = 13, 72 | ZCHALLENGE = 14, 73 | ZCOMPL = 15, 74 | ZCAN = 16, 75 | ZFREECNT = 17, 76 | ZCOMMAND = 18, 77 | ZSTDERR = 19, 78 | } 79 | 80 | export enum Zdle { 81 | ZCRCE = 0x68, 82 | ZCRCG = 0x69, 83 | ZCRCQ = 0x6a, 84 | ZCRCW = 0x6b, 85 | ZRUB0 = 0x6c, 86 | ZRUB1 = 0x6d, 87 | } 88 | 89 | export enum ZrinitFlags { 90 | CANFDX = 1 /* Rx can send and receive true FDX */, 91 | CANOVIO = 2 /* Rx can receive data during disk I/O */, 92 | CANBRK = 4 /* Rx can send a break signal */, 93 | CANCRY = 8 /* Receiver can decrypt */, 94 | CANLZW = 0x10 /* Receiver can uncompress */, 95 | CANFC32 = 0x20 /* Receiver can use 32 bit Frame Check */, 96 | ESCCTL = 0x40 /* Receiver expects ctl chars to be escaped */, 97 | ESC8 = 0x80, 98 | } 99 | -------------------------------------------------------------------------------- /src/io/zmodem/crc.ts: -------------------------------------------------------------------------------- 1 | export class Crc16 { 2 | public value: number = 0; 3 | 4 | private static readonly CRC_TABLE: number[] = [ 5 | 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 6 | 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 7 | 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, 8 | 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, 9 | 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 10 | 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 11 | 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 12 | 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 13 | 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 14 | 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 15 | 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, 16 | 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, 17 | 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 18 | 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 19 | 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 20 | 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 21 | 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, 22 | 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 23 | 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0x34e2, 0x24c3, 0x14a0, 24 | 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, 25 | 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 26 | 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 27 | 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 28 | 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 29 | 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, 30 | 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 31 | 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, 32 | 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, 33 | 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, 34 | ]; 35 | 36 | private checkCrc() { 37 | if (this.value < 0 || this.value > 0xfffff) 38 | throw new Error(`crc: ${this.value} out of range`); 39 | } 40 | 41 | private checkByte(b: number) { 42 | if (b < 0 || b > 0xff) throw new Error(`byte: ${b} out of range`); 43 | } 44 | 45 | public update(b: number): void { 46 | this.checkCrc(); 47 | this.checkByte(b); 48 | const idx = (this.value >> 8) & 0xff; 49 | const val = (Crc16.CRC_TABLE[idx] as number) ^ (this.value << 8) ^ b; 50 | this.value = val & 0xffff; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/io/zmodem/input.ts: -------------------------------------------------------------------------------- 1 | import { InputInterceptRaw } from '../input.js'; 2 | 3 | export class InputBuffer implements InputInterceptRaw { 4 | public static readonly BLOCK_SIZE: number = 1024; 5 | private static readonly DELAY: number = 50; // milliseconds 6 | 7 | private bytesPromise: Promise; 8 | private bytesReceived!: () => void; 9 | 10 | private inputBuffer: ArrayBuffer = new ArrayBuffer(0); 11 | private debug: (msg: string) => void; 12 | 13 | public constructor(debug: (msg: string) => void) { 14 | this.debug = debug; 15 | this.bytesPromise = new Promise(resolve => { 16 | this.bytesReceived = resolve; 17 | }); 18 | } 19 | 20 | addRaw(bytes: ArrayBuffer): void { 21 | if (bytes === null) { 22 | return; 23 | } 24 | 25 | const buffer = new Uint8Array(bytes); 26 | const msg = Array.from(buffer) 27 | .map(b => `0x${b.toString(16).padStart(2, '0')},`) 28 | .join(' '); 29 | this.debug(`- IN: ${msg}`); 30 | 31 | const newBuffer = new Uint8Array( 32 | this.inputBuffer.byteLength + bytes.byteLength, 33 | ); 34 | newBuffer.set(new Uint8Array(this.inputBuffer), 0); 35 | newBuffer.set(new Uint8Array(bytes), this.inputBuffer.byteLength); 36 | this.inputBuffer = newBuffer.buffer as ArrayBuffer; 37 | this.bytesReceived(); 38 | } 39 | 40 | abortRaw(): void {} 41 | 42 | public async read(count: number, timeout: number): Promise { 43 | const startTime = Date.now(); 44 | do { 45 | if (this.inputBuffer.byteLength < count) { 46 | if (timeout === 0) { 47 | break; 48 | } 49 | 50 | const timeoutPromise = new Promise(resolve => { 51 | setTimeout(() => { 52 | resolve(); 53 | }, InputBuffer.DELAY); 54 | }); 55 | 56 | await Promise.race([this.bytesPromise, timeoutPromise]); 57 | 58 | this.bytesPromise = new Promise(resolve => { 59 | this.bytesReceived = resolve; 60 | }); 61 | continue; 62 | } 63 | 64 | const result = this.inputBuffer.slice(0, count); 65 | const buffer = new Uint8Array(result); 66 | const msg = Array.from(buffer) 67 | .map(b => `0x${b.toString(16).padStart(2, '0')},`) 68 | .join(' '); 69 | this.inputBuffer = this.inputBuffer.slice(count); 70 | this.debug(`- RCV: ${msg} REMAIN: ${this.inputBuffer.byteLength}`); 71 | return result; 72 | } while (Date.now() - startTime < timeout); 73 | 74 | throw new Error( 75 | `timed out waiting for ${count} bytes, only got ${this.inputBuffer.byteLength}`, 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/io/zmodem/output.ts: -------------------------------------------------------------------------------- 1 | import { Output } from '../output.js'; 2 | 3 | export class OutputBuffer { 4 | public static readonly BLOCK_SIZE: number = 1024; 5 | 6 | private debug: (msg: string) => void; 7 | 8 | public constructor(debug: (msg: string) => void) { 9 | this.debug = debug; 10 | } 11 | 12 | public write(bytes: ArrayBuffer): void { 13 | const buffer = new Uint8Array(bytes); 14 | const msg = Array.from(buffer) 15 | .map(b => `0x${b.toString(16).padStart(2, '0')},`) 16 | .join(' '); 17 | this.debug(`- OUT: ${msg}`); 18 | Output.writeRaw(bytes); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/io/zmodem/zmodem.ts: -------------------------------------------------------------------------------- 1 | import { Format } from '../../misc/format.js'; 2 | import { Block } from './block.js'; 3 | import { InputBuffer } from './input.js'; 4 | import { OutputBuffer } from './output.js'; 5 | import { Sz } from './sz.js'; 6 | 7 | export class Zmodem { 8 | private input: InputBuffer; 9 | private output: OutputBuffer; 10 | private debug: (msg: string) => void; 11 | 12 | public constructor( 13 | input: InputBuffer, 14 | output: OutputBuffer, 15 | debug: (msg: string) => void, 16 | ) { 17 | this.input = input; 18 | this.output = output; 19 | this.debug = debug; 20 | } 21 | 22 | public async send(filePath: string) { 23 | const file = new File(filePath, 'rb'); 24 | file.seek(0, File.SEEK_END); 25 | const size = file.tell(); 26 | file.seek(0, File.SEEK_SET); 27 | this.debug(`File Size: ${Format.toSize(size)}`); 28 | 29 | const sz = new Sz(this.input, this.output, this.debug); 30 | const fileName = filePath.substring(filePath.lastIndexOf('/') + 1); 31 | await sz.start(fileName); 32 | 33 | let written = 0; 34 | while (true) { 35 | this.debug(`Wrote: ${Format.toSize(written)}`); 36 | const block = file.readBytes(Block.MAX_BLOCK_SIZE); 37 | if (block.byteLength === 0) { 38 | this.debug('EOF'); 39 | break; 40 | } 41 | written += block.byteLength; 42 | await sz.write(block); 43 | } 44 | this.debug('Transmission complete'); 45 | 46 | await sz.end(written); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/macros/macros.ts: -------------------------------------------------------------------------------- 1 | import { Output } from '../io/output.js'; 2 | 3 | export class Macro { 4 | private readonly _name: string; 5 | private readonly _commands: string[] = []; 6 | 7 | constructor(name: string, commands: string[]) { 8 | this._name = name; 9 | this._commands = commands; 10 | } 11 | 12 | public get name(): string { 13 | return this._name; 14 | } 15 | 16 | public get commands(): string[] { 17 | return this._commands; 18 | } 19 | 20 | public toString(): string { 21 | return this._commands 22 | .map(l => Output.writeln(` - ${Output.yellow(l)}`)) 23 | .join('\n'); 24 | } 25 | } 26 | 27 | export class Macros { 28 | private static map: Map = new Map(); 29 | 30 | public static get(name: string): Macro | null { 31 | return this.map.get(name) ?? null; 32 | } 33 | 34 | public static set(macro: Macro) { 35 | this.map.set(macro.name, macro); 36 | } 37 | 38 | public static delete(name: string): Macro | null { 39 | const macro = this.map.get(name); 40 | if (macro === undefined) return null; 41 | this.map.delete(name); 42 | return macro; 43 | } 44 | 45 | public static all(): Macro[] { 46 | return Array.from(this.map.entries()) 47 | .sort(([k1, _v1], [k2, _v2]) => k1.localeCompare(k2)) 48 | .map(([_k, v]) => v); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/memory/mem.ts: -------------------------------------------------------------------------------- 1 | import { BpMemory } from '../breakpoints/memory.js'; 2 | import { Overlay } from './overlay.js'; 3 | import { Format } from '../misc/format.js'; 4 | 5 | export class Mem { 6 | public static readBytes(address: NativePointer, length: number): Uint8Array { 7 | BpMemory.disableAll(); 8 | try { 9 | const data = address.readByteArray(length); 10 | if (data === null) 11 | throw new Error( 12 | `failed to read ${Format.toHexString(length)} bytes from ${Format.toHexString(address)}`, 13 | ); 14 | const buffer = new Uint8Array(data); 15 | Overlay.fix(address, buffer); 16 | return buffer; 17 | } finally { 18 | BpMemory.enableAll(); 19 | } 20 | } 21 | 22 | public static writeBytes(address: NativePointer, data: Uint8Array) { 23 | BpMemory.disableAll(); 24 | try { 25 | if (Overlay.overlaps(address, data.length)) { 26 | throw new Error( 27 | `failed to write ${Format.toHexString(data.length)} bytes to ${Format.toHexString(address)} as the address has been modified (check for breakpoints)`, 28 | ); 29 | } 30 | this.modifyMemory(address, data); 31 | } finally { 32 | BpMemory.enableAll(); 33 | } 34 | } 35 | 36 | public static modifyMemory(address: NativePointer, data: Uint8Array) { 37 | const alignStart = this.pageAlignDown(address); 38 | const alignEnd = this.pageAlignUp(address.add(data.length)); 39 | const pageShift = Math.log2(Process.pageSize); 40 | const numPages = alignEnd.sub(alignStart).shr(pageShift).toInt32(); 41 | const pageAddresses = Array.from({ length: numPages }, (_, i) => 42 | alignStart.add(i * Process.pageSize), 43 | ); 44 | const exitingProtections = pageAddresses.map(a => { 45 | return { address: a, protection: Memory.queryProtection(a) }; 46 | }); 47 | const hasNonExec = exitingProtections.some( 48 | pp => pp.protection.charAt(2) !== 'x', 49 | ); 50 | 51 | if (hasNonExec) { 52 | const newProtections = exitingProtections.map(pp => { 53 | const newProtection = `${pp.protection.charAt(0)}w${pp.protection.charAt(2)}`; 54 | return { 55 | address: pp.address, 56 | oldProtection: pp.protection, 57 | newProtection: newProtection, 58 | }; 59 | }); 60 | 61 | newProtections 62 | .filter(np => np.oldProtection !== np.newProtection) 63 | .forEach(p => { 64 | Memory.protect(p.address, Process.pageSize, p.newProtection); 65 | }); 66 | 67 | address.writeByteArray(data.buffer as ArrayBuffer); 68 | 69 | newProtections 70 | .filter(np => np.oldProtection !== np.newProtection) 71 | .forEach(p => { 72 | Memory.protect(p.address, Process.pageSize, p.oldProtection); 73 | }); 74 | } else { 75 | Memory.patchCode(address, data.length, ptr => 76 | ptr.writeByteArray(data.buffer as ArrayBuffer), 77 | ); 78 | } 79 | } 80 | 81 | public static pageAlignDown(addr: NativePointer): NativePointer { 82 | const pageMask = ptr(Process.pageSize).sub(1).not(); 83 | return addr.and(pageMask); 84 | } 85 | 86 | public static pageAlignUp(addr: NativePointer): NativePointer { 87 | return this.pageAlignDown(addr.add(Process.pageSize - 1)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/memory/overlay.ts: -------------------------------------------------------------------------------- 1 | import { Base64 } from '../misc/base64.js'; 2 | import { Mem } from './mem.js'; 3 | 4 | export class Overlay { 5 | public static overlays: [string, Overlay][] = []; 6 | private static readonly KEY_LENGTH: number = 32; 7 | 8 | private readonly address: NativePointer; 9 | private readonly data: Uint8Array; 10 | 11 | public constructor(address: NativePointer, length: number) { 12 | const data = Mem.readBytes(address, length); 13 | this.address = address; 14 | this.data = data; 15 | } 16 | 17 | public static add(address: NativePointer, length: number): string { 18 | const key = this.generateKey(); 19 | this.overlays.unshift([key, new Overlay(address, length)]); 20 | return key; 21 | } 22 | 23 | private static generateKey(): string { 24 | const numbers = Array.from({ length: this.KEY_LENGTH }, () => 25 | Math.floor(Math.random() * 256), 26 | ); 27 | const bytes = new Uint8Array(numbers); 28 | const key = Base64.encode(bytes); 29 | return key; 30 | } 31 | 32 | public static remove(key: string) { 33 | const index = this.overlays.findIndex(([k, _v]) => k === key); 34 | if (index === -1) throw new Error(`failed to find overlay key: ${key}`); 35 | this.overlays.splice(index, 1); 36 | } 37 | 38 | public static overlaps(address: NativePointer, length: number) { 39 | return this.overlays.some(([_, v]) => v.overlaps(address, length)); 40 | } 41 | 42 | public static fix(address: NativePointer, data: Uint8Array) { 43 | for (const [_, o] of this.overlays) { 44 | o.fix(address, data); 45 | } 46 | } 47 | 48 | private fix(addr: NativePointer, data: Uint8Array): void { 49 | if (!this.overlaps(addr, data.length)) return; 50 | 51 | const thisEnd = this.address.add(this.data.length); 52 | const otherEnd = addr.add(data.length); 53 | 54 | const overlapStart = Overlay.maxPtr(addr, this.address); 55 | const overlapEnd = Overlay.minPtr(otherEnd, thisEnd); 56 | 57 | const thisOverlapOffset = overlapStart.sub(this.address).toUInt32(); 58 | const otherOverlapOffset = overlapStart.sub(addr).toUInt32(); 59 | 60 | const overlapLength = overlapEnd.sub(overlapStart).toUInt32(); 61 | const thisOverlapData = this.data.subarray( 62 | thisOverlapOffset, 63 | thisOverlapOffset + overlapLength, 64 | ); 65 | 66 | data.set(thisOverlapData, otherOverlapOffset); 67 | } 68 | 69 | public static maxPtr(a: NativePointer, b: NativePointer): NativePointer { 70 | if (a.compare(b) > 0) return a; 71 | else return b; 72 | } 73 | 74 | public static minPtr(a: NativePointer, b: NativePointer): NativePointer { 75 | if (a.compare(b) < 0) return a; 76 | else return b; 77 | } 78 | 79 | private overlaps(addr: NativePointer, length: number): boolean { 80 | if (this.address.add(this.data.length).compare(addr) <= 0) return false; 81 | if (this.address.compare(addr.add(length)) >= 0) return false; 82 | return true; 83 | } 84 | 85 | public static all(): Overlay[] { 86 | return Overlay.overlays.map(([_, overlay]) => overlay); 87 | } 88 | 89 | public revert() { 90 | Mem.modifyMemory(this.address, this.data); 91 | } 92 | 93 | public toString(): string { 94 | return `addr: ${this.address}, length: ${this.data.length}`; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/misc/base64.ts: -------------------------------------------------------------------------------- 1 | export class Base64 { 2 | private static readonly BASE64_CHARS: string = 3 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; 4 | 5 | public static encode(input: Uint8Array): string { 6 | let output: string = ''; 7 | 8 | while (input.length !== 0) { 9 | const buffer = input.slice(0, 3); 10 | input = input.slice(3); 11 | 12 | const [b1, b2, b3] = buffer; 13 | const [c1, c2, c3] = [b1 as number, b2 as number, b3 as number]; 14 | 15 | const bits: number = (c1 << 16) | (c2 << 8) | c3; 16 | const e1: number = (bits >> 18) & 0x3f; 17 | const e2: number = (bits >> 12) & 0x3f; 18 | const e3: number = isNaN(c2) ? 64 : (bits >> 6) & 0x3f; 19 | const e4: number = isNaN(c3) ? 64 : bits & 0x3f; 20 | 21 | output += 22 | this.BASE64_CHARS.charAt(e1) + 23 | this.BASE64_CHARS.charAt(e2) + 24 | (isNaN(c2) ? '=' : this.BASE64_CHARS.charAt(e3)) + 25 | (isNaN(c3) ? '=' : this.BASE64_CHARS.charAt(e4)); 26 | } 27 | 28 | return output; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/misc/exception.ts: -------------------------------------------------------------------------------- 1 | import { BpMemory } from '../breakpoints/memory.js'; 2 | import { Regs } from '../breakpoints/regs.js'; 3 | import { Output } from '../io/output.js'; 4 | import { Format } from './format.js'; 5 | 6 | export enum BacktraceType { 7 | Accurate = 'accurate', 8 | Fuzzy = 'fuzzy', 9 | } 10 | 11 | export class Exception { 12 | public static exceptionHandler(details: ExceptionDetails): boolean { 13 | if (details.type === 'access-violation') { 14 | const address = details.memory?.address; 15 | if (address !== undefined) { 16 | if (BpMemory.addressHasBreakpoint(address)) { 17 | /* 18 | * Return false since we want to allow the memory breakpoint handler 19 | * to fire 20 | */ 21 | return false; 22 | } 23 | } 24 | } 25 | Output.writeln(); 26 | Output.writeln(`${Output.bold(Output.red('*** EXCEPTION ***'))}`); 27 | Output.writeln(); 28 | Output.writeln(`${Output.bold('type: ')} ${details.type}`); 29 | Output.writeln( 30 | `${Output.bold('address:')} ${Format.toHexString(details.address)}`, 31 | ); 32 | if (details.memory !== undefined) { 33 | Output.writeln( 34 | `${Output.bold('memory: ')} ${Format.toHexString(details.memory.address)} [${details.memory.operation}]`, 35 | ); 36 | } 37 | Output.writeln(); 38 | const regs = Regs.getRegs(details.context); 39 | Output.writeln(Output.bold('Registers:')); 40 | for (const [key, value] of regs) { 41 | Output.writeln(`${Output.bold(key.padEnd(4, ' '))}: ${value.toString()}`); 42 | } 43 | Output.writeln(); 44 | 45 | Exception.printBacktrace(details.context, BacktraceType.Accurate); 46 | Output.writeln(`${Output.bold(Output.red('*****************'))}`); 47 | Thread.sleep(1); 48 | return true; 49 | } 50 | 51 | public static printBacktrace(ctx: CpuContext, backtracer: BacktraceType) { 52 | Output.writeln(Output.blue(`${backtracer} backtrace:`)); 53 | Thread.backtrace( 54 | ctx, 55 | backtracer === BacktraceType.Accurate 56 | ? Backtracer.ACCURATE 57 | : Backtracer.FUZZY, 58 | ) 59 | .map(DebugSymbol.fromAddress) 60 | .forEach(s => { 61 | const prefix = s.moduleName === null ? '' : `${s.moduleName}!`; 62 | const name = `${prefix}${s.name}`; 63 | let fileInfo = ''; 64 | if (s.fileName !== null && s.lineNumber !== null) { 65 | if (s.fileName.length !== 0 && s.lineNumber !== 0) { 66 | fileInfo = `\t${Output.blue(s.fileName)}:${Output.blue(s.lineNumber.toString())}`; 67 | } 68 | } 69 | Output.writeln( 70 | [ 71 | Output.green(name.padEnd(40, '.')), 72 | Output.yellow(Format.toHexString(s.address)), 73 | fileInfo, 74 | ].join(' '), 75 | true, 76 | ); 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/misc/files.ts: -------------------------------------------------------------------------------- 1 | export class Files { 2 | private static getRandomString(length: number): string { 3 | let output: string = ''; 4 | const lookup = 'abcdefghijklmnopqrstuvwxyz0123456789'; 5 | for (let i = 0; i < length; i++) { 6 | const idx = Math.floor(Math.random() * lookup.length); 7 | const value = lookup[idx]; 8 | output += value; 9 | } 10 | return output; 11 | } 12 | 13 | public static getRandomFileName(extension: string): string { 14 | const rand = Files.getRandomString(16); 15 | const filename = `/tmp/${rand}.${extension}`; 16 | return filename; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/misc/format.ts: -------------------------------------------------------------------------------- 1 | export class Format { 2 | public static toHexString( 3 | ptr: NativePointer | UInt64 | number | null, 4 | ): string { 5 | if (ptr === null) return '[UNDEFINED]'; 6 | let hex = ptr.toString(16).padStart(this.is64bit() ? 16 : 8, '0'); 7 | if (this.is64bit()) { 8 | hex = [hex.slice(0, 8), '`', hex.slice(8)].join(''); 9 | } 10 | return `0x${hex}`; 11 | } 12 | 13 | private static is64bit(): boolean { 14 | switch (Process.arch) { 15 | case 'x64': 16 | case 'arm64': 17 | return true; 18 | default: 19 | return false; 20 | } 21 | } 22 | 23 | public static toDecString( 24 | ptr: NativePointer | UInt64 | number | null, 25 | ): string { 26 | if (ptr === null) return '[UNDEFINED]'; 27 | return ptr.toString(10).replace(/\B(?=(\d{3})+(?!\d))/g, ','); 28 | } 29 | 30 | public static toSize(ptr: NativePointer | UInt64 | number | null): string { 31 | if (ptr === null) return '[UNDEFINED]'; 32 | const val = uint64(ptr.toString()); 33 | const gb = uint64(1).shl(30); 34 | const mb = uint64(1).shl(20); 35 | const kb = uint64(1).shl(10); 36 | 37 | if (val > gb) { 38 | return `${val.shr(30).toString().padStart(4, ' ')} GB`; 39 | } else if (val > mb) { 40 | return `${val.shr(20).toString().padStart(4, ' ')} MB`; 41 | } else if (val > kb) { 42 | return `${val.shr(10).toString().padStart(4, ' ')} KB`; 43 | } else { 44 | return `${val.toString().padStart(4, ' ')} B`; 45 | } 46 | } 47 | 48 | public static toTextString(bytes: ArrayBuffer): string { 49 | return String.fromCharCode(...new Uint8Array(bytes)); 50 | } 51 | 52 | public static toByteArray(str: string): ArrayBuffer { 53 | return new Uint8Array(str.split('').map(c => c.charCodeAt(0))) 54 | .buffer as ArrayBuffer; 55 | } 56 | 57 | public static removeColours(input: string): string { 58 | // eslint-disable-next-line no-control-regex 59 | return input.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ''); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/misc/numeric.ts: -------------------------------------------------------------------------------- 1 | enum Base { 2 | Octal, 3 | Decimal, 4 | Hexadecimal, 5 | Invalid, 6 | } 7 | 8 | export class Numeric { 9 | private static getBase(val: string): Base { 10 | const hexWithPrefix = '^0[xX][0-9a-fA-F]+$'; 11 | if (val.match(hexWithPrefix) !== null) return Base.Hexadecimal; 12 | 13 | const decimalWithPrefix = '^0[dD][0-9]+'; 14 | if (val.match(decimalWithPrefix) !== null) return Base.Decimal; 15 | 16 | const octalWithPrefix = '^0[oO]?[0-7]+$'; 17 | if (val.match(octalWithPrefix) !== null) return Base.Octal; 18 | 19 | const decimalWithoutPrefix = '^[0-9]+$'; 20 | if (val.match(decimalWithoutPrefix) !== null) return Base.Decimal; 21 | 22 | const hexWithoutPrefix = '^[0-9a-fA-F]+$'; 23 | if (val.match(hexWithoutPrefix) !== null) return Base.Hexadecimal; 24 | 25 | return Base.Invalid; 26 | } 27 | 28 | private static getBareString(val: string): string { 29 | const prefixRegex = '^0[oOdDxX]'; 30 | if (val.match(prefixRegex) !== null) { 31 | return val.slice(2); 32 | } else { 33 | return val; 34 | } 35 | } 36 | 37 | public static parse(val: string): UInt64 | null { 38 | const stripped = val.split('`').join(''); 39 | const bare = this.getBareString(stripped); 40 | 41 | const base = this.getBase(stripped); 42 | switch (base) { 43 | case Base.Octal: 44 | if (isNaN(parseInt(bare, 8))) return null; 45 | break; 46 | case Base.Decimal: 47 | if (isNaN(parseInt(bare, 10))) return null; 48 | break; 49 | case Base.Hexadecimal: 50 | if (isNaN(parseInt(bare, 16))) return null; 51 | break; 52 | case Base.Invalid: 53 | return null; 54 | } 55 | 56 | switch (base) { 57 | case Base.Octal: { 58 | const num = parseInt(bare, 8); 59 | if (!Number.isSafeInteger(num)) return null; 60 | return uint64(num); 61 | } 62 | case Base.Decimal: 63 | return uint64(bare); 64 | case Base.Hexadecimal: 65 | return uint64(`0x${bare}`); 66 | } 67 | 68 | return null; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/misc/regex.ts: -------------------------------------------------------------------------------- 1 | export class Regex { 2 | public static readonly MatchAll: RegExp = /^.*$/; 3 | 4 | public static isGlob(input: string): boolean { 5 | const globRegex = /[[\]?!*]/; 6 | return globRegex.test(input); 7 | } 8 | 9 | public static globToRegex(input: string): RegExp | null { 10 | if (!Regex.isGlob(input)) return new RegExp(`^${input}$`); 11 | 12 | const escaped = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 13 | const regex = escaped 14 | .replace(/\\\*/g, '.*') 15 | .replace(/\\\?/g, '.') 16 | .replace(/\\\[/g, '[') 17 | .replace(/\\\]/g, ']') 18 | .replace(/\[!(.*?)]/g, (match, chars) => { 19 | return '[^' + chars + ']'; 20 | }); 21 | return new RegExp(`^${regex}$`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/misc/version.ts: -------------------------------------------------------------------------------- 1 | export class Version { 2 | private readonly major: number; 3 | private readonly minor: number; 4 | private readonly patch: number; 5 | 6 | public static readonly BINARY_MODE_MIN_VERSION = new Version('1.7.6'); 7 | public static readonly VERSION = new Version(Frida.version); 8 | 9 | public constructor(version: string) { 10 | const [major, minor, patch] = version.split('.'); 11 | this.major = Number(major); 12 | this.minor = Number(minor); 13 | this.patch = Number(patch); 14 | } 15 | 16 | public compareTo(other: Version): number { 17 | if (this.major !== other.major) { 18 | return this.major > other.major ? 1 : -1; 19 | } else if (this.minor !== other.minor) { 20 | return this.minor > other.minor ? 1 : -1; 21 | } else { 22 | return this.patch > other.patch ? 1 : -1; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/terminal/history.ts: -------------------------------------------------------------------------------- 1 | import { Line } from './line.js'; 2 | import { Command } from '../commands/command.js'; 3 | import { Output } from '../io/output.js'; 4 | import { Parser } from '../io/parser.js'; 5 | import { CmdLets } from '../commands/cmdlets.js'; 6 | import { Token } from '../io/token.js'; 7 | import { Var } from '../vars/var.js'; 8 | import { Input } from '../io/input.js'; 9 | 10 | export class History { 11 | private static readonly MAX_HISTORY: number = 100; 12 | 13 | private static line: Line = new Line(); 14 | private static history: string[] = []; 15 | private static index: number = -1; 16 | 17 | private constructor() {} 18 | 19 | public static getCurrent(): Line { 20 | return History.line; 21 | } 22 | 23 | public static async rerun(idx: number): Promise { 24 | if (idx >= this.history.length) 25 | throw new Error(`invalid history index: ${idx}`); 26 | const str = this.history[idx] as string; 27 | this.line = new Line(str); 28 | Input.prompt(); 29 | return this.run(); 30 | } 31 | 32 | public static async run(): Promise { 33 | const parser = new Parser(this.line.toString()); 34 | const tokens = parser.tokenize(); 35 | 36 | if (tokens.length !== 0) { 37 | const t0 = tokens[0] as Token; 38 | const isHistoryCommand = this.isHistoryCommand(t0); 39 | 40 | /* If our command isn't already top-most */ 41 | if (this.line.toString() !== this.history[0] && !isHistoryCommand) { 42 | this.history.unshift(this.line.toString()); 43 | if (this.history.length >= this.MAX_HISTORY) this.history.pop(); 44 | } 45 | } 46 | 47 | Output.writeln(); 48 | const ret = Command.run(tokens); 49 | return ret; 50 | } 51 | 52 | private static isHistoryCommand(token: Token): boolean { 53 | const cmdlet = CmdLets.getByName(token.getLiteral()); 54 | if (cmdlet === null) return false; 55 | 56 | if (cmdlet.name !== 'h') return false; 57 | 58 | return true; 59 | } 60 | 61 | public static up() { 62 | if (this.index >= this.history.length - 1) return; 63 | this.index++; 64 | this.line = new Line(this.history[this.index]); 65 | } 66 | 67 | public static down() { 68 | if (this.index === -1) return; 69 | this.index--; 70 | this.line = new Line(this.history[this.index]); 71 | } 72 | 73 | public static clearLine() { 74 | this.index = -1; 75 | this.line = new Line(); 76 | } 77 | 78 | public static all(): string[] { 79 | return this.history; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/terminal/line.ts: -------------------------------------------------------------------------------- 1 | export class Line { 2 | private line: string; 3 | private pos: number; 4 | 5 | public constructor(val: string | null = null) { 6 | if (val === null) { 7 | this.line = ''; 8 | this.pos = 0; 9 | } else { 10 | this.line = val; 11 | this.pos = val.length; 12 | } 13 | } 14 | 15 | public toString(): string { 16 | return this.line; 17 | } 18 | 19 | public getLength(): number { 20 | return this.line.length; 21 | } 22 | 23 | public getPosition(): number { 24 | return this.pos; 25 | } 26 | 27 | public peek(len: number): string { 28 | return this.line.slice(0, len); 29 | } 30 | 31 | public push(char: number) { 32 | this.line = [ 33 | this.line.slice(0, this.pos), 34 | String.fromCharCode(char), 35 | this.line.slice(this.pos), 36 | ].join(''); 37 | this.pos++; 38 | } 39 | 40 | public backspace() { 41 | if (this.pos === 0) return; 42 | 43 | this.pos--; 44 | this.line = [ 45 | this.line.slice(0, this.pos), 46 | this.line.slice(this.pos + 1), 47 | ].join(''); 48 | } 49 | 50 | public left() { 51 | if (this.pos > 0) this.pos--; 52 | } 53 | 54 | public right() { 55 | if (this.pos < this.line.length) this.pos++; 56 | } 57 | 58 | public home() { 59 | this.pos = 0; 60 | } 61 | 62 | public end() { 63 | this.pos = this.line.length; 64 | } 65 | 66 | public del() { 67 | if (this.pos >= this.line.length) return; 68 | 69 | this.line = [ 70 | this.line.slice(0, this.pos), 71 | this.line.slice(this.pos + 1), 72 | ].join(''); 73 | } 74 | 75 | public wordLeft() { 76 | if (this.pos === 0) return; 77 | 78 | this.pos--; 79 | while (this.pos !== 0) { 80 | if (!this.isAlpha(this.pos - 1) && this.isAlpha(this.pos)) { 81 | break; 82 | } 83 | this.pos--; 84 | } 85 | } 86 | 87 | public wordRight() { 88 | if (this.pos === this.line.length) return; 89 | 90 | this.pos++; 91 | while (this.pos < this.line.length) { 92 | if (!this.isAlpha(this.pos) && this.isAlpha(this.pos - 1)) { 93 | break; 94 | } 95 | this.pos++; 96 | } 97 | } 98 | 99 | private isAlpha(idx: number): boolean { 100 | if (idx >= this.line.length) return false; 101 | const line = this.line[idx] as string; 102 | return /^[a-zA-Z0-9]$/.test(line); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/tls/arm.ts: -------------------------------------------------------------------------------- 1 | export class TlsArm { 2 | private static readonly SHELL_CODE: Uint8Array = new Uint8Array([ 3 | /* mrc p15,0x0,r0,cr13,cr0,0x3 */ 0x70, 0x0f, 0x1d, 0xee, /* bx lr */ 0x1e, 4 | 0xff, 0x2f, 0xe1, 5 | ]); 6 | 7 | private readonly code: NativePointer; 8 | private readonly fn: NativeFunction; 9 | 10 | public constructor() { 11 | this.code = Memory.alloc(Process.pageSize); 12 | this.code.writeByteArray(TlsArm.SHELL_CODE.buffer as ArrayBuffer); 13 | Memory.protect(this.code, Process.pageSize, 'r-x'); 14 | this.fn = new NativeFunction(this.code, 'pointer', []); 15 | } 16 | 17 | public getTls(): NativePointer { 18 | const seg = this.fn(); 19 | return seg; 20 | } 21 | 22 | public static getTls(): NativePointer { 23 | const tls = new TlsArm(); 24 | const ptr = tls.getTls(); 25 | return ptr; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/tls/arm64.ts: -------------------------------------------------------------------------------- 1 | export class TlsAarch64 { 2 | private static readonly SHELL_CODE: Uint8Array = new Uint8Array([ 3 | /* mrs x0, tpidr_el0 */ 0x40, 0xd0, 0x3b, 0xd5, /* ret */ 0xc0, 0x03, 0x5f, 4 | 0xd6, 5 | ]); 6 | 7 | private readonly code: NativePointer; 8 | private readonly fn: NativeFunction; 9 | 10 | public constructor() { 11 | this.code = Memory.alloc(Process.pageSize); 12 | this.code.writeByteArray(TlsAarch64.SHELL_CODE.buffer as ArrayBuffer); 13 | Memory.protect(this.code, Process.pageSize, 'r-x'); 14 | this.fn = new NativeFunction(this.code, 'pointer', []); 15 | } 16 | 17 | public getTls(): NativePointer { 18 | const seg = this.fn(); 19 | return seg; 20 | } 21 | 22 | public static getTls(): NativePointer { 23 | const tls = new TlsAarch64(); 24 | const ptr = tls.getTls(); 25 | return ptr; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/tls/ia32.ts: -------------------------------------------------------------------------------- 1 | import { Output } from '../io/output.js'; 2 | 3 | class Selector { 4 | private readonly value: number; 5 | 6 | constructor(value: number) { 7 | this.value = value; 8 | } 9 | 10 | public getRpl(): number { 11 | return this.value & 0x3; 12 | } 13 | 14 | public getTi(): number { 15 | return (this.value >> 2) & 0x1; 16 | } 17 | 18 | public getIndex(): number { 19 | return (this.value >> 3) & 0x1fff; 20 | } 21 | 22 | public toString(): string { 23 | return [ 24 | `RPL ${this.getRpl()}`, 25 | `TI ${this.getTi()}`, 26 | `INDEX ${this.getIndex()}`, 27 | ].join(' '); 28 | } 29 | } 30 | 31 | /* 32 | * entry_number: int, 33 | * base: void* 34 | * limit: void* 35 | * flags: int 36 | */ 37 | class ThreadArea { 38 | private static readonly INT_SIZE = 4; 39 | private static readonly SIZE: number = 40 | Process.pointerSize * 2 + ThreadArea.INT_SIZE * 2; 41 | private readonly buff: NativePointer; 42 | 43 | constructor(segment: Selector) { 44 | this.buff = Memory.alloc(ThreadArea.SIZE); 45 | this.buff.writeInt(segment.getIndex()); 46 | } 47 | 48 | public ptr(): NativePointer { 49 | return this.buff; 50 | } 51 | 52 | public getBase(): NativePointer { 53 | return this.buff.add(Process.pointerSize).readPointer(); 54 | } 55 | 56 | public getLimit(): NativePointer { 57 | return this.buff.add(Process.pointerSize * 2).readPointer(); 58 | } 59 | 60 | public getFlags(): number { 61 | return this.buff.add(Process.pointerSize * 3).readInt(); 62 | } 63 | 64 | public toString(): string { 65 | return [ 66 | `IDX: ${this.buff.readInt()}`, 67 | `BASE: ${this.getBase().toString(16)}`, 68 | `LIMIT: ${this.getLimit().toString(16)}`, 69 | `FLAGS: ${this.getFlags().toString(16)}`, 70 | ].join(' '); 71 | } 72 | } 73 | 74 | export class TlsIa32 { 75 | private static readonly SYS_get_thread_area: number = 0xf4; 76 | private static readonly SHELL_CODE: Uint8Array = new Uint8Array([ 77 | /* xor eax, eax */ 0x31, 0xc0, /* mov ax, gs */ 0x66, 0x8c, 0xe8, 78 | /* ret */ 0xc3, 79 | ]); 80 | 81 | private fnSyscall: SystemFunction; 82 | private readonly codeGetSeg: NativePointer; 83 | private readonly fnGetSeg: NativeFunction; 84 | 85 | constructor() { 86 | const pSyscall = Module.findExportByName(null, 'syscall'); 87 | if (pSyscall === null) throw new Error('failed to find syscall'); 88 | 89 | this.fnSyscall = new SystemFunction(pSyscall, 'int', ['size_t', 'pointer']); 90 | 91 | this.codeGetSeg = Memory.alloc(Process.pageSize); 92 | this.codeGetSeg.writeByteArray(TlsIa32.SHELL_CODE.buffer as ArrayBuffer); 93 | Memory.protect(this.codeGetSeg, Process.pageSize, 'r-x'); 94 | this.fnGetSeg = new NativeFunction(this.codeGetSeg, 'int', []); 95 | } 96 | 97 | public getSegment(): Selector { 98 | const seg = this.fnGetSeg(); 99 | return new Selector(seg); 100 | } 101 | 102 | public getTls(): NativePointer { 103 | const seg = this.getSegment(); 104 | Output.debug(`tls segment: ${seg.toString()}`); 105 | 106 | if (seg.getRpl() !== 3) { 107 | throw new Error('tls segment is not in user space'); 108 | } 109 | 110 | if (seg.getTi() !== 0) { 111 | throw new Error('tls segment is in LDT, expected GDT entry'); 112 | } 113 | 114 | const user_desc = new ThreadArea(seg); 115 | 116 | const ret = this.fnSyscall( 117 | TlsIa32.SYS_get_thread_area, 118 | user_desc.ptr(), 119 | ) as UnixSystemFunctionResult; 120 | if (ret.value !== 0) throw new Error(`syscall failed, errno: ${ret.errno}`); 121 | 122 | Output.debug(`thread area: ${user_desc.toString()}`); 123 | 124 | const tls = user_desc.getBase(); 125 | return tls; 126 | } 127 | 128 | public static getTls(): NativePointer { 129 | const tls = new TlsIa32(); 130 | const ptr = tls.getTls(); 131 | return ptr; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/tls/tls.ts: -------------------------------------------------------------------------------- 1 | import { TlsArm } from './arm.js'; 2 | import { TlsX64 } from './x64.js'; 3 | import { TlsIa32 } from './ia32.js'; 4 | import { TlsAarch64 as TlsArm64 } from './arm64.js'; 5 | 6 | export class Tls { 7 | public static getTls(): NativePointer { 8 | switch (Process.arch) { 9 | case 'x64': 10 | return TlsX64.getTls(); 11 | case 'ia32': 12 | return TlsIa32.getTls(); 13 | case 'arm': 14 | return TlsArm.getTls(); 15 | case 'arm64': 16 | return TlsArm64.getTls(); 17 | default: 18 | return ptr(0); 19 | } 20 | } 21 | 22 | public static isSupported(): boolean { 23 | switch (Process.arch) { 24 | case 'x64': 25 | case 'ia32': 26 | case 'arm': 27 | case 'arm64': 28 | return true; 29 | default: 30 | return false; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/tls/x64.ts: -------------------------------------------------------------------------------- 1 | export class TlsX64 { 2 | private static readonly ARCH_GET_FS: number = 0x1003; 3 | private readonly arch_prctl: SystemFunction; 4 | 5 | constructor() { 6 | const pArchPrctl = Module.findExportByName(null, 'arch_prctl'); 7 | if (pArchPrctl === null) throw new Error('failed to find arch_prctl'); 8 | this.arch_prctl = new SystemFunction(pArchPrctl, 'int', ['int', 'pointer']); 9 | } 10 | 11 | public getTls(): NativePointer { 12 | const tls = Memory.alloc(Process.pointerSize); 13 | 14 | const ret = this.arch_prctl( 15 | TlsX64.ARCH_GET_FS, 16 | tls, 17 | ) as UnixSystemFunctionResult; 18 | if (ret.value !== 0) 19 | throw new Error(`arch_prctl failed, errno: ${ret.errno}`); 20 | 21 | return tls.readPointer(); 22 | } 23 | 24 | public static getTls(): NativePointer { 25 | const tls = new TlsX64(); 26 | const ptr = tls.getTls(); 27 | return ptr; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/traces/block.ts: -------------------------------------------------------------------------------- 1 | import { Output } from '../io/output.js'; 2 | import { Format } from '../misc/format.js'; 3 | import { TraceBase, TraceData, TraceElement, Traces } from './trace.js'; 4 | 5 | class BlockTraceData extends TraceData { 6 | private trace: ArrayBuffer = new ArrayBuffer(0); 7 | private depth: number; 8 | 9 | public constructor(depth: number) { 10 | super(); 11 | this.depth = depth; 12 | } 13 | 14 | public append(events: ArrayBuffer) { 15 | const newBuffer = new Uint8Array(this.trace.byteLength + events.byteLength); 16 | newBuffer.set(new Uint8Array(this.trace), 0); 17 | newBuffer.set(new Uint8Array(events), this.trace.byteLength); 18 | this.trace = newBuffer.buffer as ArrayBuffer; 19 | } 20 | 21 | public lines(): string[] { 22 | Output.debug(Output.yellow('parsing...')); 23 | const events = Stalker.parse(this.trace, { 24 | annotate: true, 25 | stringify: false, 26 | }) as StalkerEventFull[]; 27 | 28 | Output.debug(Output.yellow('filtering events...')); 29 | const filtered = this.filterEvents(events, (e: StalkerEventFull) => { 30 | if (e.length !== 3) return null; 31 | const [kind, start, _end] = e; 32 | if (kind !== 'block' && kind !== 'compile') return null; 33 | return start as NativePointer; 34 | }); 35 | 36 | Output.debug(Output.yellow('calculating depths...')); 37 | /* Assign a depth to each event */ 38 | const depths: { 39 | depth: number; 40 | events: TraceElement[]; 41 | } = filtered.reduce<{ 42 | depth: number; 43 | events: TraceElement[]; 44 | }>( 45 | (acc, event) => { 46 | switch (event.length) { 47 | case 3: { 48 | const [kind, start, _end] = event; 49 | const startPtr = start as NativePointer; 50 | switch (kind) { 51 | case 'block': 52 | case 'compile': { 53 | acc.events.push({ addr: startPtr, depth: acc.depth }); 54 | break; 55 | } 56 | } 57 | break; 58 | } 59 | case 4: { 60 | const [kind, _from, _to, _depth] = event; 61 | switch (kind) { 62 | case 'call': { 63 | acc.depth += 1; 64 | break; 65 | } 66 | case 'ret': { 67 | acc.depth -= 1; 68 | break; 69 | } 70 | } 71 | break; 72 | } 73 | } 74 | return acc; 75 | }, 76 | { depth: 0, events: [] }, 77 | ); 78 | 79 | Output.debug(Output.yellow('filtering depths...')); 80 | const elements = this.filterElements(depths.events, this.depth); 81 | Output.debug(Output.yellow('finding symbols...')); 82 | const named = this.nameElements(elements); 83 | Output.debug(Output.yellow('formatting...')); 84 | const strings = this.elementsToStrings(named); 85 | return strings; 86 | } 87 | 88 | public details(): string { 89 | return [ 90 | 'depth:', 91 | Output.blue(this.depth.toString()), 92 | 'size:', 93 | Output.blue(Format.toSize(this.trace.byteLength)), 94 | ].join(' '); 95 | } 96 | } 97 | 98 | export class BlockTrace extends TraceBase { 99 | private constructor(threadId: ThreadId, depth: number, unique: boolean) { 100 | const trace = new BlockTraceData(depth); 101 | super([threadId], trace); 102 | Stalker.follow(threadId, { 103 | events: { 104 | call: true, 105 | ret: true, 106 | block: !unique, 107 | compile: unique, 108 | }, 109 | onReceive: (events: ArrayBuffer) => { 110 | if (this.trace !== null) { 111 | this.trace.append(events); 112 | } 113 | }, 114 | }); 115 | } 116 | 117 | public static create( 118 | threadId: ThreadId, 119 | depth: number, 120 | unique: boolean, 121 | ): BlockTrace { 122 | if (Traces.has(threadId)) { 123 | throw new Error(`trace already exists for threadId: ${threadId}`); 124 | } 125 | 126 | const trace = new BlockTrace(threadId, depth, unique); 127 | Traces.set(threadId, trace); 128 | return trace; 129 | } 130 | 131 | protected doStop() { 132 | const threadId = this.threadIds[0] as number; 133 | Stalker.unfollow(threadId); 134 | Stalker.flush(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/traces/call.ts: -------------------------------------------------------------------------------- 1 | import { Output } from '../io/output.js'; 2 | import { Format } from '../misc/format.js'; 3 | import { TraceBase, TraceData, TraceElement, Traces } from './trace.js'; 4 | 5 | class CallTraceData extends TraceData { 6 | private trace: ArrayBuffer = new ArrayBuffer(0); 7 | private depth: number; 8 | 9 | public constructor(depth: number) { 10 | super(); 11 | this.depth = depth; 12 | } 13 | 14 | public append(events: ArrayBuffer) { 15 | const newBuffer = new Uint8Array(this.trace.byteLength + events.byteLength); 16 | newBuffer.set(new Uint8Array(this.trace), 0); 17 | newBuffer.set(new Uint8Array(events), this.trace.byteLength); 18 | this.trace = newBuffer.buffer as ArrayBuffer; 19 | } 20 | 21 | public lines(): string[] { 22 | Output.debug(Output.yellow('parsing...')); 23 | const events = Stalker.parse(this.trace, { 24 | annotate: true, 25 | stringify: false, 26 | }) as StalkerEventFull[]; 27 | 28 | Output.debug(Output.yellow('filtering events...')); 29 | /* Filter events for which we have no module information */ 30 | const filtered = this.filterEvents(events, (e: StalkerEventFull) => { 31 | if (e.length !== 4) return null; 32 | const [kind, _from, to, _depth] = e; 33 | if (kind !== 'call') return null; 34 | return to as NativePointer; 35 | }); 36 | 37 | Output.debug(Output.yellow('calculating depths...')); 38 | /* Assign a depth to each event */ 39 | const depths: { 40 | first: boolean; 41 | depth: number; 42 | events: TraceElement[]; 43 | } = filtered.reduce<{ 44 | first: boolean; 45 | depth: number; 46 | events: TraceElement[]; 47 | }>( 48 | (acc, event) => { 49 | if (event.length !== 4) return acc; 50 | const [kind, from, to, _depth] = event; 51 | const [fromPtr, toPtr] = [from as NativePointer, to as NativePointer]; 52 | switch (kind) { 53 | case 'call': { 54 | if (acc.first) { 55 | acc.events.push({ addr: fromPtr, depth: acc.depth }); 56 | acc.first = false; 57 | } 58 | acc.depth += 1; 59 | acc.events.push({ addr: toPtr, depth: acc.depth }); 60 | break; 61 | } 62 | case 'ret': { 63 | acc.depth -= 1; 64 | break; 65 | } 66 | } 67 | return acc; 68 | }, 69 | { first: true, depth: 0, events: [] }, 70 | ); 71 | 72 | Output.debug(Output.yellow('filtering depths...')); 73 | const elements = this.filterElements(depths.events, this.depth); 74 | Output.debug(Output.yellow('finding symbols...')); 75 | const named = this.nameElements(elements); 76 | Output.debug(Output.yellow('formatting...')); 77 | const strings = this.elementsToStrings(named); 78 | return strings; 79 | } 80 | 81 | public details(): string { 82 | return [ 83 | 'depth:', 84 | Output.blue(this.depth.toString()), 85 | 'size:', 86 | Output.blue(Format.toSize(this.trace.byteLength)), 87 | ].join(' '); 88 | } 89 | } 90 | 91 | export class CallTrace extends TraceBase { 92 | private constructor(threadId: ThreadId, depth: number) { 93 | const trace = new CallTraceData(depth); 94 | super([threadId], trace); 95 | Stalker.follow(threadId, { 96 | events: { 97 | call: true, 98 | ret: true, 99 | }, 100 | onReceive: (events: ArrayBuffer) => { 101 | if (this.trace !== null) { 102 | this.trace.append(events); 103 | } 104 | }, 105 | }); 106 | } 107 | 108 | public static create(threadId: ThreadId, depth: number): CallTrace { 109 | if (Traces.has(threadId)) { 110 | throw new Error(`trace already exists for threadId: ${threadId}`); 111 | } 112 | 113 | const trace = new CallTrace(threadId, depth); 114 | Traces.set(threadId, trace); 115 | return trace; 116 | } 117 | 118 | protected doStop() { 119 | const threadId = this.threadIds[0] as number; 120 | Stalker.unfollow(threadId); 121 | Stalker.flush(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/traces/coverage/trace.ts: -------------------------------------------------------------------------------- 1 | import { Output } from '../../io/output.js'; 2 | import { Files } from '../../misc/files.js'; 3 | import { Format } from '../../misc/format.js'; 4 | import { TraceBase, TraceData, Traces } from '../trace.js'; 5 | import { Coverage, CoverageSession } from './coverage.js'; 6 | 7 | class CoverageTraceData extends TraceData { 8 | private threadIds: ThreadId[]; 9 | private filename: string; 10 | private file: File; 11 | private size: number = 0; 12 | 13 | public constructor(threadIds: ThreadId[], filename: string | null) { 14 | super(); 15 | this.threadIds = threadIds; 16 | this.filename = filename ?? Files.getRandomFileName('cov'); 17 | this.file = new File(this.filename, 'wb+'); 18 | } 19 | 20 | public append(events: ArrayBuffer) { 21 | this.file.write(events); 22 | this.size += events.byteLength; 23 | } 24 | 25 | public stop(): void { 26 | this.file.close(); 27 | } 28 | 29 | public lines(): string[] { 30 | if (this.threadIds.length === 1) { 31 | const threadId = this.threadIds[0] as number; 32 | return [ 33 | [ 34 | 'Wrote coverage for thread:', 35 | Output.yellow(threadId.toString()), 36 | 'to:', 37 | Output.green(this.filename), 38 | ].join(' '), 39 | ]; 40 | } else { 41 | return [ 42 | [ 43 | 'Wrote coverage multiple threads sto:', 44 | Output.green(this.filename), 45 | ].join(' '), 46 | ]; 47 | } 48 | } 49 | 50 | public details(): string { 51 | return ['file:', Output.blue(this.filename)].join(' '); 52 | } 53 | 54 | public getFileName(): string { 55 | return this.filename; 56 | } 57 | 58 | public getSize(): number { 59 | return this.size; 60 | } 61 | } 62 | export class CoverageTrace extends TraceBase { 63 | private coverage: CoverageSession; 64 | 65 | private constructor( 66 | threadIds: ThreadId[], 67 | filename: string | null, 68 | modulePath: string | null, 69 | suppressed: boolean, 70 | ) { 71 | const trace = new CoverageTraceData(threadIds, filename); 72 | super(threadIds, trace); 73 | 74 | let moduleFilter = Coverage.allModules; 75 | if (modulePath !== null) { 76 | moduleFilter = m => m.path === modulePath; 77 | } 78 | 79 | this.coverage = Coverage.start({ 80 | moduleFilter: moduleFilter, 81 | onCoverage: (coverageData, isHeader) => { 82 | this.trace.append(coverageData); 83 | if (isHeader) 84 | Output.write(Output.blue(Format.toTextString(coverageData))); 85 | }, 86 | threadFilter: t => threadIds.includes(t.id), 87 | suppressed, 88 | }); 89 | } 90 | 91 | public static create( 92 | threadId: ThreadId, 93 | filename: string | null, 94 | modulePath: string | null, 95 | suppressed: boolean, 96 | ): CoverageTrace { 97 | const threadIds = 98 | threadId === -1 ? Process.enumerateThreads().map(t => t.id) : [threadId]; 99 | 100 | if (threadIds.length === 0) 101 | throw new Error('failed to find any threads to trace'); 102 | 103 | const traced = threadIds.filter(t => Traces.has(t)); 104 | 105 | if (traced.length !== 0) 106 | throw new Error(`trace already exists for threadIds: ${traced}`); 107 | 108 | const trace = new CoverageTrace( 109 | threadIds, 110 | filename, 111 | modulePath, 112 | suppressed, 113 | ); 114 | threadIds.forEach(t => Traces.set(t, trace)); 115 | return trace; 116 | } 117 | 118 | protected doStop() { 119 | this.coverage.stop(); 120 | this.trace.stop(); 121 | } 122 | 123 | public isSuppressed(): boolean { 124 | return this.coverage.isSuppressed(); 125 | } 126 | 127 | public unsuppress() { 128 | this.coverage.unsuppress(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/vars/var.ts: -------------------------------------------------------------------------------- 1 | import { Format } from '../misc/format.js'; 2 | 3 | export class Var { 4 | public static fromId(id: number): Var { 5 | return new Var(uint64(id.toString()), `#${id}`); 6 | } 7 | 8 | private val: string | UInt64; 9 | private p: NativePointer; 10 | private readonly literal: string; 11 | 12 | public constructor(val: string | UInt64, literal: string | null = null) { 13 | this.val = val; 14 | if (this.val instanceof UInt64) this.p = ptr(val.toString()); 15 | else this.p = Memory.allocUtf8String(val as string); 16 | this.literal = literal ?? this.val.toString(); 17 | } 18 | 19 | public getLiteral(): string { 20 | return this.literal; 21 | } 22 | 23 | public toPointer(): NativePointer { 24 | return this.p; 25 | } 26 | 27 | public toU64(): UInt64 { 28 | if (this.val instanceof UInt64) return this.val as UInt64; 29 | else return uint64(this.p.toString()); 30 | } 31 | 32 | public compare(other: Var): number { 33 | return this.toU64().compare(other.toU64()); 34 | } 35 | 36 | public isNull(): boolean { 37 | return this.toU64().compare(uint64(0)) === 0; 38 | } 39 | 40 | public toString(): string { 41 | if (this.val instanceof UInt64) 42 | return `${Format.toHexString(this.val)} ${Format.toDecString(this.val)}`; 43 | else return `${Format.toHexString(this.p)} "${this.val}"`; 44 | } 45 | 46 | public static ZERO: Var = new Var(uint64(0), 'ZERO'); 47 | } 48 | -------------------------------------------------------------------------------- /src/vars/vars.ts: -------------------------------------------------------------------------------- 1 | import { Var } from './var.js'; 2 | 3 | export class Vars { 4 | public static readonly RET_NAME = 'ret'; 5 | 6 | private static map: Map = new Map(); 7 | 8 | private constructor() {} 9 | 10 | public static set(name: string, val: Var) { 11 | if (!this.isNameValid(name)) 12 | throw new Error(`variable name '${name}' is invalid`); 13 | this.map.set(name, val); 14 | } 15 | 16 | private static isNameValid(name: string): boolean { 17 | const nameRegex = '^[a-zA-Z_][a-zA-Z0-9_]*$'; 18 | if (name.match(nameRegex) === null) { 19 | return false; 20 | } 21 | return true; 22 | } 23 | 24 | public static get(name: string): Var | null { 25 | if (!this.isNameValid(name)) return null; 26 | return this.map.get(name) ?? null; 27 | } 28 | 29 | public static delete(name: string): Var | null { 30 | if (!this.map.has(name)) return null; 31 | const val = this.map.get(name) as Var; 32 | this.map.delete(name); 33 | return val; 34 | } 35 | 36 | public static getRet(): Var { 37 | return this.get(this.RET_NAME) ?? Var.ZERO; 38 | } 39 | 40 | public static setRet(val: Var) { 41 | this.set(this.RET_NAME, val); 42 | } 43 | 44 | public static all(): [string, Var][] { 45 | return Array.from(this.map.entries()) 46 | .filter(([k, _]) => k !== this.RET_NAME) 47 | .sort(([k1, _v1], [k2, _v2]) => k1.localeCompare(k2)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": [ 5 | "es2020" 6 | ], 7 | "allowJs": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "declaration": true, 11 | "newLine": "lf", 12 | "stripInternal": true, 13 | "strict": true, 14 | "noImplicitReturns": true, 15 | "noImplicitOverride": true, 16 | "noUnusedLocals": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noUncheckedIndexedAccess": true, 19 | "noPropertyAccessFromIndexSignature": true, 20 | "noEmitOnError": true, 21 | "useDefineForClassFields": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "skipLibCheck": true 24 | } 25 | } -------------------------------------------------------------------------------- /version.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const packageJsonPath = path.resolve(__dirname, 'package.json'); 6 | const { version } = require(packageJsonPath); 7 | 8 | let commitHash = 'unknown'; 9 | try { 10 | commitHash = execSync('git rev-parse HEAD').toString().trim(); 11 | } catch (err) { 12 | console.error('Failed to get Git commit hash:', err.message); 13 | } 14 | 15 | // Write the commit hash to a file 16 | const outputPath = path.resolve('.', 'src', 'version.ts'); 17 | const content = ` 18 | export const GIT_COMMIT_HASH = '${commitHash}'; 19 | export const APP_VERSION = '${version}'; 20 | `; 21 | 22 | fs.writeFileSync(outputPath, content, { encoding: 'utf8' }); 23 | 24 | console.log(`Git commit hash written to ${outputPath}`); 25 | --------------------------------------------------------------------------------