├── src ├── sys │ ├── sandbox_nop.c │ ├── README.md │ ├── watcher_nop.c │ ├── sandbox_seccomp.c │ └── watcher_inotify.c ├── palette.h ├── cmd.h ├── README.md ├── sandbox.h ├── watcher.h ├── render.h ├── Makefile.am ├── cmd.c ├── blindsight.h ├── render.c ├── palette.c └── blindsight.c ├── bootstrap ├── Makefile.am ├── .gitignore ├── examples ├── blindsight.c ├── README.md ├── dasm.c ├── bits.h ├── tlb.c ├── hexdumps.h └── summaries.h ├── pkg ├── archlinux │ └── PKGBUILD └── homebrew │ └── blindsight.rb ├── LICENSE ├── TODO ├── .travis.yml ├── README.md └── configure.ac /src/sys/sandbox_nop.c: -------------------------------------------------------------------------------- 1 | #include "sandbox.h" 2 | 3 | void sandbox_init() {} 4 | -------------------------------------------------------------------------------- /bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | autoreconf --install --verbose # && ./configure "$@" && make 3 | 4 | -------------------------------------------------------------------------------- /src/palette.h: -------------------------------------------------------------------------------- 1 | #ifndef PALETTE_H 2 | #define PALETTE_H 1 3 | 4 | palette palette_init(); 5 | 6 | // for when ^^^ inevitably breaks for mysterious reasons 7 | void palette_debug_dump(palette* pal); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | SUBDIRS = src 2 | EXTRA_DIST = autogen.sh README.md LICENSE examples/*.c 3 | dist_bin_SCRIPTS = blindsight # examples/blindsight.c for now 4 | 5 | ## autoreconf --install said to do this, so I obey: 6 | ACLOCAL_AMFLAGS = -I build-aux 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/sys/README.md: -------------------------------------------------------------------------------- 1 | File monitoring on Linux uses inotify. Could be implemented on OS X via 2 | FSEvents or a common wrapper thereof, if needed. 3 | 4 | The sandbox attempts to use seccomp, but is pretty ghetto and probably fragile 5 | to whims of glibc/ncursesw. 6 | -------------------------------------------------------------------------------- /src/cmd.h: -------------------------------------------------------------------------------- 1 | #ifndef CMD_H 2 | #define CMD_H 1 3 | 4 | typedef struct { 5 | const char* keys; // optional, no keys = treated as section header 6 | const char* desc; // mandatory until last entry 7 | } cmd; 8 | 9 | void cmd_render_help(cmd cs[]); 10 | void cmd_print_help(cmd cs[]); 11 | 12 | #endif 13 | 14 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | The API in [blindsight.h](blindsight.h) is stable bedrock where the 2 | incompatibility worm cannot go. Everywhere else, walk without rhythm 3 | or be eaten. 4 | 5 | Platform-specific code in [sys](sys) handles features with complex 6 | dependencies. If dependencies aren't detected during `configure`, 7 | [Makefile.am](Makefile.am) will build base functionality only. 8 | -------------------------------------------------------------------------------- /src/sys/watcher_nop.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "watcher.h" 4 | 5 | bool watcher_init(char* prog_name, bool blocking) { 6 | return false; 7 | } 8 | 9 | bool watcher_check() { 10 | return false; 11 | } 12 | 13 | void* watcher_reload(const char* sym, void* st, 14 | void error(void* st, const char* msg)) { 15 | errx(1, "watcher_reload() unsupported in current build"); 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Autotools stuff 2 | build-aux/ 3 | autom4te.cache/ 4 | aclocal.m4 5 | Makefile.in 6 | config.h.in 7 | configure 8 | confdefs.h 9 | 10 | # Configuration stuff 11 | stamp-h1 12 | config.log 13 | config.status 14 | Makefile 15 | src/config.h 16 | 17 | # Libtool and build artefacts 18 | .deps/ 19 | libtool 20 | .libs 21 | .dirstamp 22 | libblindsight?la* 23 | blindsight 24 | 25 | # Package build artefacts 26 | pkg/ 27 | 28 | # general dev stuff, may as well 29 | tags 30 | *.swp 31 | -------------------------------------------------------------------------------- /src/sandbox.h: -------------------------------------------------------------------------------- 1 | #ifndef SANDBOX_H 2 | #define SANDBOX_H 1 3 | 4 | /* After initialization, the viewer is likely limited to doing 5 | * terminal-ncurses-stuff, and maybe reading the input stream if I ever get 6 | * around to implementing something other than direct mmap. 7 | * 8 | * Dropping syscall privs via seccomp/pledge before parsing of the input file 9 | * starts seems like a low-effort no-brainer. The whole point of this tool is 10 | * writing quick-and-dirty parsing code and running it on whatever. While the 11 | * fixed-size window constraint limits bug classes that'll actually occur, 12 | * a ghetto BPF policy will limit future CTF fun. 13 | */ 14 | void sandbox_init(); 15 | 16 | #endif 17 | 18 | -------------------------------------------------------------------------------- /examples/blindsight.c: -------------------------------------------------------------------------------- 1 | #!/usr/bin/tcc -run -L/usr/local/lib -lblindsight 2 | #include 3 | #include 4 | #include 5 | #define _XOPEN_SOURCE_EXTENDED 6 | // ^ pray to mr skeltal and open source gods 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | view* default_views[] = { 14 | &entropy, 15 | &bytehist, 16 | &bytepix, 17 | &braille, 18 | &HexII_DW, 19 | &HexII, 20 | &xxd, 21 | &bits, 22 | 0 23 | }; 24 | 25 | int main(const int argc, char** argv) { 26 | return blindsight(argc, argv, default_views, "default_views"); 27 | } 28 | -------------------------------------------------------------------------------- /src/watcher.h: -------------------------------------------------------------------------------- 1 | #ifndef WATCHER_H 2 | #define WATCHER_H 1 3 | #include 4 | 5 | /* Singleton file-watcher-and-reloader. Assumes main program is a 6 | * #!/usr/bin/tcc -run shellscript. Returns false for everything otherwise. 7 | * 8 | * prog_name: program's argv[0], used to detect reload path 9 | * blocking: watcher_check() blocks until a write 10 | * 11 | * returns false if not a script running under /bin/tcc */ 12 | bool watcher_init(char* prog_name, bool blocking); 13 | 14 | /* true if file was written to since last call */ 15 | bool watcher_check(); 16 | 17 | /* reload file and return a single symbol from it */ 18 | void* watcher_reload(const char* sym, void* st, 19 | void error(void* st, const char* msg)); 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | [blindsight.c](blindsight.c) is a a general-purpose binary viewer. It's a prototype for what a standalone tool might look like, if it's ever stabilized and packaged with the library. The views it implements are arranged roughly by level-of-detail. Currently, half of them require a terminal and `ncurses` that supports >256 color pairs. 2 | 3 | Other example viewers are simple and single-purpose: 4 | 5 | - [dasm.c](dasm.c) - uses an external library (Capstone) for more info 6 | - [tlb.c](tlb.c) - human-readable ARM TLB dumps for a CTF challenge 7 | 8 | Individual views are provided as examples in [hexdumps.h](hexdumps.h), 9 | [summaries.h](summaries.h), and [bits.h](bits.h). You can copy-paste from them 10 | as starting points, or just `#include ` directly into your 11 | viewer. 12 | -------------------------------------------------------------------------------- /src/render.h: -------------------------------------------------------------------------------- 1 | #ifndef RENDER_H 2 | #define RENDER_H 1 3 | 4 | /* Experimental UI default */ 5 | extern const int default_ui_color; 6 | 7 | typedef struct {int y, x;} vec2; 8 | 9 | vec2 inline yx(int y, int x) { 10 | vec2 ret = {y, x}; 11 | return ret; /* Passing by value, as syntax+type sugar. */ 12 | } 13 | 14 | /* Render panels */ 15 | 16 | void render_col_addrs(vec2 base, const int dim_x, 17 | size_t bytes_per_row, view v); 18 | 19 | void render_row_addrs(vec2 base, vec2 dim, 20 | const size_t buf_sz, const size_t cursor, 21 | view v, size_t bytes_per_row); 22 | 23 | void render_grid(vec2 base, vec2 dim, 24 | const size_t buf_sz, const size_t cursor, 25 | unsigned char* buf, view v, size_t bytes_per_full_row); 26 | 27 | /* Utility */ 28 | 29 | uint32_t dlog2(uint32_t v); 30 | #endif 31 | -------------------------------------------------------------------------------- /pkg/archlinux/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: amtal 2 | pkgname=blindsight-git 3 | pkgver=autofilled 4 | pkgrel=1 5 | pkgdesc="High-density, terminal-based binary viewer for visual pattern matching" 6 | arch=('i686' 'x86_64') 7 | url="https://github.com/amtal/blindsight#readme" 8 | license=('MIT') 9 | groups=() 10 | depends=('tcc' 'ncurses') 11 | makedepends=('autoconf') 12 | optdepends=('libseccomp: sandboxing') 13 | provides=('blindsight') 14 | conflicts=('blindsight') 15 | replaces=('blindsight') 16 | backup=() 17 | source=("blindsight::git+https://github.com/amtal/blindsight.git") 18 | md5sums=('SKIP') 19 | 20 | pkgver() { 21 | cd "$srcdir/blindsight" 22 | git describe --long | sed 's/^v//;s/-/.r/;s/-/./' 23 | } 24 | 25 | build() { 26 | cd "$srcdir/blindsight" 27 | ./bootstrap 28 | ./configure --prefix=/usr 29 | make 30 | } 31 | 32 | package() { 33 | cd "$srcdir/blindsight" 34 | make DESTDIR="$pkgdir/" install 35 | install -Dm644 LICENSE "$pkgdir"/usr/share/licenses/$pkgname/LICENSE 36 | } 37 | 38 | -------------------------------------------------------------------------------- /pkg/homebrew/blindsight.rb: -------------------------------------------------------------------------------- 1 | class Blindsight < Formula 2 | desc "High-density, terminal-based binary viewer for visual pattern matching" 3 | homepage "https://github.com/amtal/blindsight#readme" 4 | url "https://github.com/amtal/blindsight/archive/v0.1.1.tar.gz" 5 | sha256 "0a82748207dd4157b79d2e373f9463af3fd5abc75472a6cd0a5c757eeaf526ad" 6 | 7 | head "https://github.com/amtal/blindsight.git" 8 | 9 | # see https://discourse.brew.sh/t/1008 for why autoconf is a direct dep. 10 | # "I had trouble finding the exact semantics of :build" loool brew #1 11 | depends_on "autoconf" => :build 12 | depends_on "automake" => :build 13 | depends_on "libtool" => :build 14 | 15 | # ncurses is built with --enable-widec (which we need for >256 color pairs) 16 | # header in ncursesw/ncurses.h, library in libncurses.dylib 17 | depends_on "ncurses" 18 | depends_on "tcc" 19 | 20 | def install 21 | system "./bootstrap && ./configure && make" 22 | system "make", "DESTDIR=#{prefix}", "install" 23 | end 24 | 25 | test do 26 | system "time gtimeout 1 blindsight `which blindsight` || true" 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | You are accessing a U.S. Government (USG) Information System (IS) 2 | that is provided for USG-authorized use only. By using this IS 3 | (which includes any device attached to this IS), you consent to the 4 | following conditions: 5 | 6 | - The USG routinely intercepts and monitors communications on this 7 | IS for purposes including, but not limited to, penetration 8 | testing, COMSEC monitoring, network operations and defense, 9 | personnel misconduct (PM), law enforcement (LE), and 10 | counterintelligence (CI) investigations. 11 | - At any time, the USG may inspect and seize data stored on this IS. 12 | - Communications using, or data stored on, this IS are not private, 13 | are subject to routine monitoring, interception, and search, and 14 | may be disclosed or used for any USG-authorized purpose. 15 | - This IS includes security measures (e.g., authentication and 16 | access controls) to protect USG interests--not for your personal 17 | benefit or privacy. 18 | - Notwithstanding the above, using this IS does not constitute 19 | consent to PM, LE or CI investigative searching or monitoring of 20 | the content of privileged communications, or work product, related 21 | to personal representation or services by attorneys, 22 | psychotherapists, or clergy, and their assistants. Such 23 | communications and work product are private and confidential. See 24 | User Agreement for details. 25 | -------------------------------------------------------------------------------- /examples/dasm.c: -------------------------------------------------------------------------------- 1 | #!/usr/bin/tcc -run -L/usr/local/lib -lblindsight -lcapstone 2 | #include "blindsight.h" 3 | #include 4 | #include 5 | #include 6 | 7 | /* Color potential ARM Cortex ROM/RAM pointers */ 8 | VIEW(dw_ptr, 9 | 4, /*=>*/ {1, 9} 10 | )(uint8_t* s, size_t n, /*=>*/ int y, int x) { 11 | uint32_t dw = *(uint32_t*)s; 12 | bool is_data = (dw & 3) == 0 && dw >= 0x20000000 && dw <= 0x3FFFffff; 13 | bool is_code = (dw & 3) == 1 && dw > 0 & dw <= 0x1FFFffff; 14 | mvprintw(y, x, "%08x", dw); 15 | mvchgat(y, x, 8, A_NORMAL, is_code ? 1 : (is_data ? 3 : 7), 0); 16 | } 17 | 18 | VIEW(dasm_thumb, 19 | 2, /*=>*/ {1, 5} 20 | )(uint8_t* s, size_t n, /*=>*/ int y, int x) { 21 | csh hnd; 22 | cs_insn *isn; 23 | 24 | cs_open(CS_ARCH_ARM, CS_MODE_THUMB, &hnd); 25 | size_t count = cs_disasm(hnd, s, 2, 0x8000000, 0, &isn); 26 | 27 | if (count) { 28 | mvprintw(y, x, isn[0].mnemonic); 29 | int color = strcmp(isn[0].mnemonic, "stm") ? 0 : 1; 30 | mvchgat(y, x, 5, A_NORMAL, color, NULL); 31 | 32 | cs_free(isn, count); 33 | } 34 | cs_close(&hnd); 35 | } 36 | 37 | view* views[] = {&dasm_thumb, &dw_ptr, 0}; 38 | 39 | int main(const int argc, char** argv) { 40 | return blindsight(argc, argv, views, "views"); 41 | } 42 | -------------------------------------------------------------------------------- /examples/bits.h: -------------------------------------------------------------------------------- 1 | /* Displays for spotting bit-level patterns. */ 2 | 3 | 4 | /* Print positions at which bits are set, with a nice gray sweep. */ 5 | VIEW(bits, 1, /*=>*/ {1, 8})(uint8_t* s, size_t n, /*=>*/ int y, int x) { 6 | // assumes 8 bits per char, please do not run on obscure DSPs 7 | assert(n <= 2 && "bits must fit into one hex char"); 8 | for(int i=0; i> (bit % 8)) & 1) { 11 | mvprintw(y, x+i, "%x", bit); 12 | mvchgat(y, x+i, 1, A_NORMAL, pal.gray[25-i*2], NULL); 13 | } 14 | } 15 | } 16 | 17 | /* Show bits via 8-dot unicode Braille. */ 18 | VIEW(braille, 19 | 1, /*=>*/ {1, 1, F_256C|F_UTF8} 20 | )(uint8_t* s, size_t n, /*=>*/ int y, int x) { 21 | assert(n <= 2 && "bits must fit into one hex char"); 22 | /* The Unicode braille symbols are ordered a little weird, since 23 | * there's 8 dots but Braille has 6. The following remaps: 24 | * 25 | * 01 08 01 02 26 | * 02 10 -\ 04 08 27 | * 04 20 -/ 10 20 28 | * 40 80 40 80 29 | */ 30 | uint8_t u = s[0]; 31 | u = u&0xe1 | (u&2)<<2 | (u&4)>>1 | (u&8)<<1 | (u&16)>>2; 32 | 33 | wchar_t byte[2] = {0x2800 + u, 0}; 34 | mvaddwstr(y, x, (wchar_t*)&byte); 35 | 36 | mvchgat(y, x, 1, A_NORMAL, pal.gray[8 + (s[0] >> 4)], NULL); 37 | } 38 | -------------------------------------------------------------------------------- /src/Makefile.am: -------------------------------------------------------------------------------- 1 | AM_CCFLAGS = -Wall -O3 2 | 3 | ## Linking clang/gcc code from TCC has some special requirements. 4 | if HAVE_TCC_BINARY 5 | ## TCC doesn't do stack alignment, and I've been returning laaarge structs on 6 | ## it. Calling into clang faults on movapd due to non-16byte aligned SP. 7 | ## 8 | ## Stack alignment for clang (-mstackrealign -mstack-alignment=4) don't seem to 9 | ## change behavior. Switching to GCC, where preferred-stack-boundary (argument 10 | ## in of bits to mask off to 0, unlike clang's stride) works. 11 | ## 12 | ## Can't use TCC as compiler for everything because its linker doesn't handle 13 | ## autotools default arguments. 14 | AM_CCFLAGS += -mpreferred-stack-boundary=2 15 | ##AM_CCFLAGS += -mstackrealign -mstack-alignment=4 16 | endif 17 | 18 | include_HEADERS = blindsight.h 19 | otherincludedir = $(includedir)/blindsight 20 | otherinclude_HEADERS = ../examples/*.h 21 | 22 | lib_LTLIBRARIES = libblindsight.la 23 | libblindsight_la_SOURCES = blindsight.c \ 24 | palette.c cmd.c render.c \ 25 | palette.h cmd.h render.h \ 26 | watcher.h sandbox.h 27 | libblindsight_la_LIBADD = 28 | 29 | if HAVE_TCC 30 | if SYS_LINUX 31 | libblindsight_la_SOURCES += sys/watcher_inotify.c 32 | libblindsight_la_LIBADD += -ltcc 33 | else # unknown platform 34 | libblindsight_la_SOURCES += sys/watcher_nop.c 35 | endif # SYS_* 36 | else 37 | libblindsight_la_SOURCES += sys/watcher_nop.c 38 | endif # HAVE_TCC 39 | 40 | if HAVE_SANDBOX 41 | libblindsight_la_SOURCES += sys/sandbox_seccomp.c 42 | libblindsight_la_LIBADD += -lseccomp 43 | else 44 | libblindsight_la_SOURCES += sys/sandbox_nop.c 45 | endif # SANDBOX 46 | 47 | libblindsight_la_CFLAGS = -Wall 48 | libblindsight_la_LDFLAGS = -export-symbols-regex '(blindsight|blindsight_version|pal)' # TODO drop 'pal' global once refactored 49 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | COULD-A 2 | ======= 3 | 4 | [ ] Check watcher polling performance, improve queuing if necessary. 5 | Current method is ghetto workaround for event drops by polling more often, 6 | probably breaks under load/uses more CPU than necessary/introduces user 7 | input latency, or all of the above. 8 | 9 | [ ] Macro-wrap a render_fun caller, so compiler can inline stuff? 10 | 11 | SHOULD-A 12 | ======== 13 | 14 | [x] scale log-scale on histogram to match palette depth based on buf_sz 15 | 16 | [x] figure out what entropy scaling should be depending on size 17 | 18 | [~] dwarf fortress depth-style "palettes" 19 | (entropy is 0123456789, rescaled up to limit) 20 | 21 | [~] composition of colour + contents! 22 | (colour by ascii, content by entropy) 23 | 24 | [ ] have render_fn return 'slide' flag, accumulate per-row 25 | if row is slid, render only 1x ***, star out address bar hd-style 26 | 27 | tradeoffs: 28 | + can use this to implement heapviz/pagetable viewers! 29 | + further compression, more effective bytes per page! 30 | - lose some sense of proportion 31 | 32 | + addr bar is handled in same loop; can probably do by tampering with 33 | row-loop variables, mutability ftw 34 | + can be toggled on/off, for any methods that support it 35 | (could do generic equality-collapse pass for things that don't!) 36 | - bpp no longer precise if slide occurs 37 | - needs precondition on render_fn to be called on sequential addrs 38 | 39 | ? rows get collapsed, but repetition is checked on cell level... weird? 40 | 41 | => need to evaluate heapviz/pagetableviz to determine usefulness 42 | 43 | might be worth doing a simple equality-compressor that can be toggled 44 | and is view-agnostic 45 | 46 | WOULD-A 47 | ======= 48 | 49 | [~] ubsan+asan+tsan clean lol (not msan) 50 | 51 | [!] style: sentence capitalized/punctuated // comments on separate lines before 52 | blocks, covering 'why' as per Modern C 53 | 54 | 55 | XXX IMPORTANT: write IRC client next 56 | -------------------------------------------------------------------------------- /examples/tlb.c: -------------------------------------------------------------------------------- 1 | #!/usr/bin/tcc -run -L/usr/local/lib -lblindsight 2 | #include "blindsight.h" 3 | #include 4 | 5 | /* QEMU Cortex-A15 first-level translation table rwx-markup. 6 | * 7 | * Port of equivalent Python script from Boston Key Party's 2017 barewithme 8 | * challenge. Usage from gdb: 9 | * 10 | * x/258 0x2a00000 11 | * dump binary tlb.bin 0x2a00000 0x2a00408 12 | * shell ./tlb.c tlb.bin 13 | */ 14 | VIEW(a15_TLB, 15 | 4, /*=>*/ {1, 23} 16 | )(uint8_t* s, size_t n, /*=>*/ int y, int x) { 17 | uint32_t w = *(uint32_t*)s; 18 | if (!w) return; // leave unused entries blank 19 | 20 | // Parse problem-relevant bits only: 21 | uint32_t addr = w & 0xFFFFf000; 22 | uint32_t mask = w & 0x00000fff; 23 | char* mode = "?? ??"; 24 | switch (mask & 0xc00) { 25 | case 0x000: mode = "-- --"; break; 26 | case 0x400: mode = "rw --"; break; 27 | case 0x800: mode = "rw r-"; break; 28 | case 0xc00: mode = "rw rw"; break; 29 | } 30 | char* exe = mask & 0x010 ? "-" : "x"; 31 | 32 | /* Print supervisor and user rw access, with a static-state hack to 33 | * only print info that changed. (Rendering is always sequential.) */ 34 | static uint32_t last = 0; 35 | char* fmt = mask == last ? "0x%08x * * * *" : "0x%08x %03x %s %s"; 36 | last = mask; 37 | 38 | // Highlight userland-reachable pages: 39 | int color = 0; 40 | switch (mask & 0xc00) { 41 | // rwx:red, r-x:green 42 | case 0xc00: color = mask & 0x010 ? 2 : 1; break; 43 | // r--:yellow 44 | case 0x800: color = 3; break; 45 | } 46 | 47 | mvprintw(y, x, fmt, addr, mask, mode, exe); 48 | mvchgat(y, x, 23, A_NORMAL, color, 0); 49 | /* 0x02c00000 802 rw r- x 0x02d00000 * * * * 50 | * 0x06000000 c03 rw rw x 0x06100000 * * * * 51 | * 0x06400000 c13 rw rw - 0x06500000 * * * */ 52 | } 53 | 54 | view* views[] = {&a15_TLB, 0}; 55 | 56 | int main(const int argc, char** argv) { 57 | return blindsight(argc, argv, views, "views"); 58 | } 59 | -------------------------------------------------------------------------------- /src/cmd.c: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | #ifdef HAVE_NCURSESW_NCURSES_H 3 | # include 4 | #else 5 | # include 6 | #endif 7 | #include 8 | #include 9 | #include "cmd.h" 10 | 11 | /* Printed Nethack-guidebook style in --help, 12 | * vim-cheatsheet style in help screen. */ 13 | 14 | #define KB_SZ 3 15 | // Try a help screen sort of like the vim cheatsheets, for better spacial 16 | // awareness of key layout. 17 | void cmd_render_help(cmd cs[]) { 18 | const char* const wasd_kb[KB_SZ] = { 19 | "qQ wW eE rR tT yY uU iI oO pP [{ ]} \\|", 20 | " aA sS dD fF gG hH jJ kK lL ;: '\"", 21 | " zZ xX cC vV bB nN mM ,< .> /?", 22 | }; 23 | 24 | erase(); 25 | attr_set(A_NORMAL, -1, NULL); 26 | 27 | for (int y=0; y*/ {1, 59} 7 | )(uint8_t* s, size_t n, /*=>*/ int y, int x) { 8 | for (int i=0, xi=x; i*/ {1, 3, F_256C} 41 | )(uint8_t* s, size_t n, /*=>*/ int y, int x) { 42 | for (int i=0, xi=x; i> 4)]; 72 | } 73 | mvprintw(y, xi, fmt, arg); 74 | mvchgat(y, xi, 2, A_NORMAL, clr, NULL); 75 | } 76 | } 77 | 78 | // slightly denser version: 79 | view HexII_DW = {"HexII_DW", HexII_B, 4, /*=>*/ {1, 9, F_256C}}; 80 | -------------------------------------------------------------------------------- /src/sys/sandbox_seccomp.c: -------------------------------------------------------------------------------- 1 | /* This sandbox is kind of sketch due to being bolted on. Not from a security 2 | * point of view, but a stability one. The only system it's been tested on 3 | * identified some edge cases that can't possibly be the full extent of what 4 | * ncursesw/libc will eventually call. 5 | * 6 | * Whatever script launches the program can mess with things by rebinding file 7 | * descriptors/mapping in shared memory to a vulnerable process or whatever. 8 | * Sandbox assumes a pretty minimal/sane situation, and is meant to protect 9 | * against lazy bad code, not cartoon villain bad code. 10 | */ 11 | #include 12 | #include 13 | #include 14 | #include "sandbox.h" 15 | 16 | #define ALLOW(ctx, sc) seccomp_rule_add(ctx, SCMP_ACT_ALLOW, sc, 0) 17 | 18 | /* Final tally and security argument: 19 | * 20 | * - Code can open + read whatever, and mmap + mprotect whatever. 21 | * - Code can only write to file descriptors 1 and 2. 22 | * 23 | * Therefore: 24 | * 25 | * - Confidentiality: attacks limited to covert channels and dumb stuff 26 | * like /dev/tcp, which thankfully is a bash-ism not a Linux-ism. 27 | * - Integrity: as long as there's no way to remap 1/2 to something other 28 | * than STDFOO, should be fine. I don't know of one on Linux. Some 29 | * privesc attack surface, but not that much - although everything 30 | * needed for rowhammer is there. :) 31 | * - Availability: memory/CPU DOS trivial, but lame. 32 | * 33 | */ 34 | void sandbox_init() { 35 | scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); 36 | if (!ctx) errx(1, "seccomp_init failed!"); 37 | 38 | int sc = 0; /* error accumulator */ 39 | 40 | /* normal I/O */ 41 | sc |= ALLOW(ctx, SCMP_SYS(read)); // STDIN || mapped file, allowing all 42 | sc |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1, 43 | SCMP_A0(SCMP_CMP_EQ, STDOUT_FILENO)); 44 | sc |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1, 45 | SCMP_A0(SCMP_CMP_EQ, STDERR_FILENO)); 46 | /* file polling */ 47 | sc |= ALLOW(ctx, SCMP_SYS(poll)); // fds is an array, can't inspect 48 | sc |= ALLOW(ctx, SCMP_SYS(gettimeofday)); 49 | sc |= ALLOW(ctx, SCMP_SYS(rt_sigaction)); //SIGTSTP 50 | 51 | /* iconv funtimes */ 52 | sc |= ALLOW(ctx, SCMP_SYS(open)); // O_RDONLY|O_CLOEXEC, but needed for TCC anyway 53 | sc |= ALLOW(ctx, SCMP_SYS(fstat64)); // on the above? 54 | sc |= ALLOW(ctx, SCMP_SYS(fstat)); 55 | sc |= ALLOW(ctx, SCMP_SYS(close)); 56 | 57 | /* memory management; my code is malloc-clean - dependencies aren't */ 58 | sc |= ALLOW(ctx, SCMP_SYS(brk)); 59 | 60 | sc |= ALLOW(ctx, SCMP_SYS(sigreturn)); // from SIGWINCH 61 | sc |= ALLOW(ctx, SCMP_SYS(rt_sigreturn)); 62 | 63 | /* clean exit */ 64 | sc |= ALLOW(ctx, SCMP_SYS(ioctl)); // SNDCTL_TMR_STOP or TCSETSW?? 65 | sc |= ALLOW(ctx, SCMP_SYS(munmap)); 66 | sc |= ALLOW(ctx, SCMP_SYS(exit_group)); 67 | sc |= ALLOW(ctx, SCMP_SYS(rt_sigprocmask)); // abort 68 | sc |= ALLOW(ctx, SCMP_SYS(exit)); // not used anymore on glibc/musl... 69 | 70 | /* live code reloads hit a bunch of stuff, of course */ 71 | sc |= ALLOW(ctx, SCMP_SYS(mmap2)); // only hit on TCC reloads 72 | sc |= ALLOW(ctx, SCMP_SYS(mmap)); // adding for portability? 73 | sc |= ALLOW(ctx, SCMP_SYS(_llseek)); 74 | sc |= ALLOW(ctx, SCMP_SYS(mprotect)); // yeahhhh... 75 | sc |= ALLOW(ctx, SCMP_SYS(select)); 76 | sc |= ALLOW(ctx, SCMP_SYS(openat)); 77 | /* it's tempting to disable this w/o TCC, but then I see a state 78 | * explosion on testing on the horizon... */ 79 | 80 | if (sc) errx(128+sc, "seccomp rule addition failed"); 81 | if (seccomp_load(ctx)) errx(1, "seccomp_load"); 82 | seccomp_release(ctx); 83 | } 84 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # It's slightly tempting to automate testing of TCC reload working, sandbox not 2 | # faulting, doing a matrix across the two features... 3 | # 4 | # The starting point, though, is validating that the autotools pain was at all 5 | # worth it. Does the base library build? Good. 6 | # 7 | # The optional features might flow down from Arch/Gentoo's nice build 8 | # environments in the future. 9 | language: c 10 | compiler: gcc 11 | 12 | matrix: 13 | allow_failures: 14 | - os: osx 15 | - dist: precise 16 | include: 17 | - os: linux 18 | env: "NOTE='Ubuntu Trusty: secondary target, basic features'" 19 | dist: trusty 20 | sudo: false # going to try the fancy Beta stuff 21 | addons: 22 | apt: 23 | packages: # tcc/seccomp don't seem to be in the whitelist, oh well! 24 | - libncursesw5-dev 25 | script: 26 | - echo "travis_fold:start:bootstrap_and_configure" 27 | - ./bootstrap 28 | - ./configure 29 | - echo "travis_fold:end:bootstrap_and_configure" 30 | - make 31 | - "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(pwd)/lib" 32 | - "PATH=$PATH:$(pwd)/bin" 33 | - "make DESTDIR=$(pwd) install" 34 | - echo "travis_fold:start:smoke_test_cannot_look" 35 | - time timeout 1 blindsight `which blindsight` || true 36 | - echo "travis_fold:end:smoke_test_cannot_look" 37 | - os: linux 38 | env: "NOTE='Arch Linux: main target platform, all bells and whistles'" 39 | sudo: required 40 | services: docker 41 | before_install: 42 | - "curl -s https://raw.githubusercontent.com/mikkeloscar/arch-travis/master/arch-travis.sh | bash" 43 | script: 44 | - echo "travis_fold:end:makepkg" 45 | - os: linux 46 | env: "NOTE='Ubuntu Precise: old automake incapable of clean builds'" 47 | dist: precise 48 | sudo: required 49 | before_install: 50 | - sudo apt-get update -qq # automake... I'm dependant on at least 1.13? Yuck. 51 | - automake --version # AC_CONFIG_MACRO_DIRS is showstopper, probably others too. 52 | - sudo apt-get install -qq libncursesw5-dev libseccomp-dev libtcc-dev tcc 53 | script: 54 | - echo "travis_fold:start:bootstrap_and_configure" 55 | - ./bootstrap && ./configure 56 | - echo "travis_fold:end:bootstrap_and_configure" 57 | - make 58 | - sudo make install 59 | - echo "travis_fold:start:smoke_test_cannot_look" 60 | - time timeout 1 blindsight `which blindsight` || true 61 | - echo "travis_fold:end:smoke_test_cannot_look" 62 | - os: osx 63 | env: "NOTE='OSX + Homebrew: never manually tested, but might work!'" 64 | before_install: 65 | - brew update # fixes some dependency check for ruby 2.x, sorry :( 66 | install: 67 | - echo "travis_fold:start:brew_setup" 68 | - mkdir Formula && git mv pkg/homebrew/blindsight.rb Formula/ 69 | - git -c user.name='t' -c user.email='t@t.com' commit -m 't' 70 | - mkdir -p `brew --repo`/Library/Taps/travis 71 | - ln -s `pwd` `brew --repo`/Library/Taps/travis/homebrew-btest 72 | - brew tap --repair 73 | - echo "travis_fold:end:brew_setup" 74 | script: 75 | - brew install --HEAD blindsight 76 | - echo "travis_fold:start:smoke_test_cannot_look" 77 | - "brew test blindsight" 78 | - "brew audit --strict blindsight || true" 79 | - "brew info blindsight" 80 | - echo "travis_fold:end:smoke_test_cannot_look" 81 | 82 | # parsed by arch-travis.sh, not travis-ci: 83 | arch: 84 | packages: 85 | - autoconf # build-time 86 | - tcc 87 | - ncurses 88 | - libseccomp # optional 89 | script: 90 | - echo "travis_fold:start:makepkg" 91 | - "(cd pkg/archlinux && makepkg -i --noconfirm)" 92 | - echo "travis_fold:start:smoke_test_cannot_look" 93 | - time timeout 1 blindsight `which blindsight` || true # TODO check $? 124 94 | - echo "travis_fold:end:smoke_test_cannot_look" 95 | 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | High-density hex viewer focused on visual pattern matching on <1MB binaries. 2 | 3 | [Binary Editor BZ](https://github.com/devil-tamachan/binaryeditorbz) has this 4 | covered on Windows. This is a Unix version you can quickly extend and hack 5 | on. Supports live code reload for smooth domain-specific prototyping. 6 | 7 | ![Quick usage demonstration on /bin/ls](https://i.imgur.com/lYa5KVX.png) 8 | 9 | # Installing 10 | 11 | On Arch, run `makepkg -si` in [pkg/archlinux/](pkg/archlinux/). On OSX, there's a [Homebrew formula](pkg/homebrew/blindsight.rb) you can add to your personal tap and `brew install`. 12 | 13 | Elsewhere, ensure that you have `ncursesw`, `autotools`, the Tiny C Compiler, and a GCC-based build environment. You may want to install TCC from source (it's small) to enable live code reload. As usual, `./bootstrap && ./configure && make` then `sudo make install`. 14 | 15 | [![Travis](https://img.shields.io/travis/amtal/blindsight.svg)](https://travis-ci.org/amtal/blindsight) [![Codacy grade](https://img.shields.io/codacy/grade/e8b2d157ee3448f4ac050e586aa085c4.svg)](https://www.codacy.com/app/amtal/blindsight/dashboard) [![license](https://img.shields.io/github/license/amtal/blindsight.svg)](LICENSE) 16 | 17 | # Using 18 | 19 | Run some examples (like [examples/bs.c](examples/bs.c#L242)) which are all C 20 | scripts executable via the Tiny C Compiler's `-run` option. For fast 21 | iteration, saved code will be automatically re-loaded by running hex viewers on 22 | supported platforms. 23 | 24 | ![Live code reload demonstration](https://i.imgur.com/XXob133.gif) 25 | 26 | You can write your own views that make use of C libraries, such as the 27 | Capstone-based disassembly view in [examples/dasm.c](examples/dasm.c). 28 | Copy-paste liberally from other examples, and see 29 | [src/blindsight.h](src/blindsight.h#L69) for the `VIEW(..)` struct + function 30 | definition macro and the API available to render functions. 31 | 32 | 33 | ```c 34 | #!/usr/bin/tcc -run -L/usr/local/lib -lblindsight 35 | #include "blindsight.h" 36 | #include 37 | 38 | /* vim's xxd.c in default mode */ 39 | VIEW(xxd, 40 | /* bytes y, x dimensions */ 41 | 16, /*=>*/ {1, 59} 42 | )(uint8_t* s, size_t n, /*=>*/ int y, int x) { 43 | for (int i=0, xi=x; i "Cause still unknown after several thousand engineering-hours of review. Now 67 | > parsing data with a hex editor to recover final milliseconds." 68 | 69 | Hexdumps are the lowest-level debug tool in software. They'll never go away, 70 | but as shown by [Corkami](https://github.com/corkami/pics/tree/master/binary)'s 71 | dissections they might get fancier. 72 | 73 | > "We're the goddamned Kalashnikovs of thinking meat." 74 | 75 | This is a hex *viewer*. It's here to encourage and assist the part of the brain 76 | that sees animals in clouds and patterns in bespoke ciphertexts. Use it to do 77 | initial reconnaissance, seed whimsical hypotheses, and test them swiftly. 78 | 79 | Stateful operations like edits on binary formats should be formalized in 80 | version controlled tools, not shot from the hip and left undocumented. Your end 81 | goal should be elegant domain-specific parsers and pretty-printers, not some 82 | kludge caged in a power-of-2 aligned grid. See [Scapy], [Nom], or [Parsec] and 83 | its [variations] as examples. 84 | 85 | [Scapy]: https://www.secdev.org/projects/scapy/ 86 | [Nom]: https://crates.io/crates/nom 87 | [Parsec]: https://wiki.haskell.org/Parsec 88 | [variations]: https://hackage.haskell.org/package/trifecta 89 | 90 | -------------------------------------------------------------------------------- /src/blindsight.h: -------------------------------------------------------------------------------- 1 | #ifndef BLINDSIGHT_H 2 | #define BLINDSIGHT_H 1 3 | #include 4 | #include // size_t 5 | 6 | /* Versioned according to Semantic Versioning 2.0.0, http://semver.org */ 7 | extern const char* blindsight_version; // "v.." 8 | 9 | /* Palettes set up for ncurses mvchgatt color pairs. */ 10 | typedef struct { 11 | short const gray[26]; 12 | short fg_gray[16][26]; 13 | short gray_gray[26][26]; 14 | } palette; // this struct is dependency hell :| 15 | extern palette pal; // make it global while I figure out a portable theme :/ 16 | 17 | typedef enum { 18 | F_256C = 1, // $TERM == xterm-256color or similar 19 | F_FG_GRAY = 2, // 16-bit color pair limit, requires ncursesw (wide) 20 | F_GRAY_GRAY = 4,// * 21 | F_UTF8 = 8, // locale should be en_US.UTF-8 or similar 22 | F_PIXELS = F_GRAY_GRAY|F_UTF8, // Unicode blocks + FGxBG monotone 23 | } feature; 24 | 25 | /* This library generalizes hexdumps as tables of identical cells. 26 | * 27 | * The table shows a sequential, uninterrupted window of buffer contents. The 28 | * address of each cell within it is the sum of the column address and row 29 | * address, which are shown in address bars if there's room. 30 | * 31 | * To keep addresses simple to read, the byte width for a single row is 32 | * constrained to powers of 2. 33 | * 34 | * A 'view' struct contains the information needed to render a single 35 | * standalone cell. This operation is mapped over the table for a full dump. 36 | * */ 37 | typedef struct { 38 | const char* name; 39 | /* The 'render' function is a mapping from a byte array domain to an 40 | * ncurses screen codomain. Specifically: 41 | * 42 | * - The domain is a fixed-width window into the overall binary being 43 | * displayed. 44 | * - The codomain is a y*x character rectangle, possibly with additional 45 | * degrees of freedom such as colors, blink attributes, or emoji. 46 | * 47 | * The dimensions and cardinalities of the domain and codomain are 48 | * useful for categorizing functions in terms of information density, 49 | * bijective versus lossy display, and terminal capabilities needed. 50 | * They're declared statically alongside the render function. 51 | * 52 | * Of course, the render function's specific output is an "image" :) */ 53 | void (*render)(uint8_t* s, size_t n, /*=>*/ int y, int x); 54 | /* s: content being drawn 55 | * n: view.dom passthrough 56 | * y, x: ncurses coordinate for the cell 57 | * 58 | * Oh yeah; currently v.render(..) will not be called on any tail 59 | * */ int y, int x); \ 85 | view name = { \ 86 | #name, fun_name, \ 87 | __VA_ARGS__ \ 88 | }; \ 89 | void fun_name 90 | 91 | 92 | /* Main executable 93 | * 94 | * argc, argv: passthrough from main 95 | * views: selection of view structs that will be rendered 96 | * reload_sym: name of the symbol defining the previous argument 97 | * (will be reloaded on save if live code reload is supported) 98 | */ 99 | int blindsight(const int argc, char** argv, const view** views, const char* reload_sym); 100 | 101 | #endif 102 | -------------------------------------------------------------------------------- /src/sys/watcher_inotify.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include // basename/dirname 10 | #include 11 | #include 12 | #include 13 | #include "watcher.h" 14 | 15 | static struct { 16 | /* public-ish: */ 17 | const char* file_name; 18 | const char* dir_name; 19 | /* internals: */ 20 | char real[PATH_MAX]; 21 | int inotify_fd; 22 | bool blocking_mode; 23 | } w = {0}; 24 | 25 | /* Just gonna assume PATH_MAX is enough, and to hell with anyone running on 26 | * filesystems where it's not. */ 27 | 28 | /* sanity check that we're under tcc, otherwise this is a dumb endeavor */ 29 | static bool am_a_script_running_in_tcc() { 30 | char exe[PATH_MAX]; 31 | ssize_t len = readlink("/proc/self/exe", exe, PATH_MAX); 32 | if (-1 == len) err(1, "readlink"); 33 | if (len > PATH_MAX - 1) { 34 | fprintf(stderr, "readlink memory corruption O_o\n"); 35 | abort(); 36 | } 37 | 38 | exe[len] = 0; 39 | return (0 != strstr(exe, "/bin/tcc")); 40 | } 41 | 42 | static void get_script_realpath(const char* argv_0, char* real) { 43 | /* best guess for abspath of script being run; do not run across trust 44 | * boundaries unless it would be funny */ 45 | char script[PATH_MAX]; 46 | { 47 | if (argv_0[0] == '/') { 48 | snprintf(script, PATH_MAX, "%s", argv_0); 49 | } else { 50 | char cwd[PATH_MAX]; 51 | if (!getcwd(cwd, PATH_MAX)) err(1, "getcwd"); 52 | snprintf(script, PATH_MAX, "%s/%s", cwd, argv_0); 53 | } 54 | } 55 | 56 | /* get to what inotify needs */ 57 | if (!realpath(script, real)) err(1, "realpath"); 58 | } 59 | 60 | bool watcher_init(char* prog_name, bool blocking) { 61 | if (!am_a_script_running_in_tcc()) return false; 62 | 63 | w.blocking_mode = blocking; 64 | 65 | get_script_realpath(prog_name, w.real); 66 | w.file_name = basename(w.real); 67 | w.dir_name = dirname(w.real); 68 | 69 | w.inotify_fd = inotify_init(); 70 | if (w.inotify_fd == -1) err(1, "inotify_init"); 71 | 72 | if (!blocking) { 73 | int flags = fcntl(w.inotify_fd, F_GETFL, 0); 74 | fcntl(w.inotify_fd, F_SETFL, flags | O_NONBLOCK); 75 | } 76 | 77 | /* Have to watch entire directory, since editors tend to delete files 78 | * as they save 'em */ 79 | int in_wd = inotify_add_watch(w.inotify_fd, w.dir_name, IN_MODIFY); 80 | if (in_wd == -1) err(1, "inotify_add_watch"); 81 | 82 | return true; 83 | } 84 | 85 | bool watcher_check() { 86 | char in_buf[sizeof(struct inotify_event) + NAME_MAX + 1]; 87 | bool write_event = false; 88 | 89 | do { 90 | if (-1 == read(w.inotify_fd, in_buf, sizeof(in_buf))) { 91 | if (EAGAIN == errno) { // queue exhausted, return 92 | errno = 0; 93 | break; 94 | } 95 | err(1, "inotify read"); 96 | } 97 | 98 | struct inotify_event* e = (struct inotify_event*)in_buf; 99 | 100 | if (e->mask & IN_MODIFY) { 101 | if (e->len && !strcmp(e->name, w.file_name)) { 102 | write_event = true; 103 | } 104 | } 105 | } while (!w.blocking_mode); // exhaust queue in non-block mode 106 | 107 | return write_event; 108 | } 109 | 110 | void* watcher_reload(const char* sym, void* st, 111 | void error(void* st, const char* msg)) { 112 | char fname[PATH_MAX]; 113 | snprintf(fname, sizeof(fname), "%s/%s", w.dir_name, w.file_name); 114 | 115 | static TCCState* tcc; 116 | 117 | if (tcc) { 118 | //tcc_delete(tcc); // uh, let's try memory leaking and see where it gets us! 119 | tcc = 0; 120 | } 121 | 122 | tcc = tcc_new(); 123 | if (!tcc) errx(1, "tcc_new() failed!\n"); 124 | 125 | tcc_set_error_func(tcc, st, error); 126 | tcc_set_output_type(tcc, TCC_OUTPUT_MEMORY); // is default, but still... 127 | 128 | if (-1 == tcc_add_library(tcc, "blindsight")) 129 | errx(1, "tcc_add_library(..) for self failed!\n"); 130 | if (-1 == tcc_add_file(tcc, fname)) 131 | errx(1, "tcc_add_file(..) failed!\n"); 132 | // TODO miscompilation probably shouldn't be fatal, just add error handling 133 | 134 | /* Recent versions of tcc_relocate add a 2nd argument. 135 | * 136 | * <=0.9.24 no argument, memory managed internally 137 | * ==0.9.25 pass 0 to return required size, pass buffer to fill it 138 | * >=0.9.26 pass 0 for required size, 1 for auto-alloc like in <=0.9.24 139 | * 140 | * Currently, arch + ubuntu don't have .25 so I'll ignore it. 141 | * Otherwise, need to add AC_COMPILE_IFELSE test to configure.ac 142 | * since tcc isn't in pkg-config for either test system. 143 | */ 144 | #ifdef TCC_RELOCATE_AUTO 145 | if (-1 == tcc_relocate(tcc, TCC_RELOCATE_AUTO)) 146 | errx(1, "tcc_relocate/1 failed!\n"); 147 | #else 148 | if (-1 == tcc_relocate(tcc)) 149 | errx(1, "tcc_relocate/2 failed!\n"); 150 | #endif 151 | 152 | return tcc_get_symbol(tcc, sym); 153 | } 154 | -------------------------------------------------------------------------------- /src/render.c: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | #ifdef HAVE_NCURSESW_NCURSES_H 3 | # include 4 | #else 5 | # include 6 | #endif 7 | #include "blindsight.h" 8 | #include "render.h" 9 | 10 | static const char log256[256] = { 11 | #define LT(n) n,n,n,n, n,n,n,n, n,n,n,n, n,n,n,n 12 | -1, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 13 | LT(4), LT(5), LT(5), LT(6), LT(6), LT(6), LT(6), 14 | LT(7), LT(7), LT(7), LT(7), LT(7), LT(7), LT(7), LT(7) 15 | }; // Bit Twiddling Hacks, by Sean Eron Anderson 16 | uint32_t dlog2(uint32_t v) { // working on 4+G files? use a GPU... 17 | register uint32_t t, tt; 18 | if ((tt = v >> 16)) { 19 | return (t = tt >> 8) ? 24 + log256[t] : 16 + log256[tt]; 20 | } else { 21 | return (t = v >> 8) ? 8 + log256[t] : log256[v]; 22 | } 23 | } 24 | 25 | const int default_ui_color = 6; // keep in basic colors, so it *always* works 26 | 27 | /* Column width in bytes is a power of 2; rendering offsets into it 28 | * requires numbers lower than it. In many cases view.x is going to be 29 | * smaller than this, so we can't render an address for each column. In 30 | * such cases, we'll render an address every nth column, but the 31 | * addresses will not be truncated in any way. */ 32 | void render_col_addrs(vec2 base, const int dim_x, size_t bytes_per_row, view v) { 33 | unsigned int addr_sz = dlog2(bytes_per_row) / 8 + 1; 34 | addr_sz *= 2; // printed in hex 35 | addr_sz += 1; // add a space? 36 | unsigned int skip = addr_sz / v.codom.x; // # of columns addr shown 37 | if (addr_sz % v.codom.x) skip++; // ceil(^) 38 | 39 | char fmt[32]; 40 | snprintf(fmt, sizeof(fmt), "%%0%dx ", addr_sz-1); 41 | 42 | size_t addr = 0; 43 | for (int x=base.x; x < base.x + dim_x; 44 | x+=v.codom.x * skip, addr+=v.dom * skip) { 45 | mvprintw(base.y, x, fmt, addr); 46 | mvchgat(base.y, x, addr_sz-1, A_NORMAL, default_ui_color, NULL); 47 | } 48 | } 49 | 50 | void render_row_addrs(vec2 base, vec2 dim, const size_t buf_sz, 51 | const size_t cursor, view v, size_t bytes_per_row) { 52 | // Don't print redundant 0s, as per HexII. 53 | char fmt[32]; 54 | snprintf(fmt, sizeof(fmt), "%%%dx", dim.x - 2); // do not count spaces 55 | size_t dy = cursor; 56 | // Do not print addrs beyond buffer: v 57 | for (int y=base.y, row=0; y 2 | #include "config.h" 3 | #ifdef HAVE_NCURSESW_NCURSES_H 4 | # include 5 | #else 6 | # include 7 | #endif 8 | #include "blindsight.h" 9 | #include "palette.h" 10 | /* The default xterm-256color palette, and hopefully others, goes: 11 | * 12 | * 0..7 8 dark [black RGYB magenta cyan white] \__prone to remapping! 13 | * 8..f 8 light [*] / 14 | * 10..e7 6x6x6 rgb cube (black:0x10, white:0xe7) 15 | * e8..ff 24-value gray sweep (doesn't reach rgb000/fff!) 16 | * 17 | * When using ncurses built with extra color pair support, we've got 18 | * 0x7fff fg-bg pairs to play with. Constructed ncurses palettes 19 | * probably fall into the following categories: 20 | * 21 | * [Core] 22 | * - Basic 16 colors. These retain any custom style, and support inversion. 23 | * Pro: maintain look and feel. Con: only useable for classification, 24 | * likely to run into invisible characters when using background as 25 | * an extra dimension. 26 | * - Worth mapping all 256 basic colors on a static background for 27 | * color debug purpose, anyway. That's 1/0x7f palette budget. 28 | * 29 | * [Sweeps] 30 | * - Square unicode (or DOS CP437 or w/e) pixels are handy for retaining 31 | * proportions. Full grayscale sweep of both takes 0x2a4. 32 | * - Cube helix sweep of hue to extend the grayscale sweep. Should add 33 | * an extra couple of bits of information per character, which is 34 | * clutch. 35 | * 36 | * [Classes overlaid on sweeps] 37 | * - Basic 16 colors on grayscale might work, regardless of themes. 38 | * - Otherwise, import some of the nicer ones (hot/cold delta) and 39 | * overlay. 40 | * 41 | * Shouldn't run out of budget until heavy use of cube helix on cube 42 | * helix. Probably don't need palette swapping, except maybe when 43 | * versioning stabilizes. (Since any external render funs will depend 44 | * on palettes.) 45 | */ 46 | 47 | palette palette_init() { 48 | palette pal = { 49 | .gray = { 50 | 0x10, // rgb:000 51 | 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 52 | 0xee, 0xef, 0xf0, 0xf1, 0xf2, 0xf3, 53 | 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 54 | 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, 55 | 0xe7 // rgb:fff 56 | }, 57 | .fg_gray = {{0}}, 58 | .gray_gray = {{0}}, 59 | }; 60 | 61 | assert(!start_color()); 62 | assert(has_colors()); 63 | 64 | use_default_colors(); // roll with defaults for -1... 65 | assume_default_colors(0x7,0x10); // can use this to remap dynamically, per-renderer palette load? 66 | 67 | 68 | short base = 0; 69 | for (short c=0; c<256; c++) init_pair(base+c, c, -1); 70 | base += 256; 71 | 72 | /* On systems with 256 max color pairs, don't call init_pair beyond 73 | * that range; it messes up what palette we do have. */ 74 | if (base >= COLOR_PAIRS) return pal; 75 | 76 | for (short fg=0; fg<16; fg++) { 77 | for (short bg=0; bg<26; bg++) { 78 | short pair = base + fg*26 + bg; // TODO running counter this, lol 79 | init_pair(pair, fg, pal.gray[bg]); 80 | pal.fg_gray[fg][bg] = pair; 81 | } 82 | } // experimental: background /w contrast 83 | base += 26*16; 84 | for (short fg=0; fg<26; fg++) { 85 | for (short bg=0; bg<26; bg++) { 86 | short pair = base + fg*26 + bg; 87 | bool palette_hack = false; // HACK: oookay this can't be portable 88 | // but on this version of ncurses one of the 89 | // high bits inverts the fg-bg colors, so while 90 | // building the palette I'm un-inverting it 91 | // 92 | // tracing down bug in source and avoiding the 93 | // problematic bit (and there's probably more) 94 | // will probably be more portable :) 95 | // 96 | // It gets weirder. That's nor normal; it just 97 | // somehow sometimes gets into that mode. Colors 98 | // inverted, except this small range. WAT. 99 | if (palette_hack && (fg<2 || (fg==2 && bg<19))) { 100 | init_pair(pair, pal.gray[bg], pal.gray[fg]); 101 | } else { 102 | init_pair(pair, pal.gray[bg], pal.gray[fg]); 103 | } 104 | pal.gray_gray[fg][bg] = pair; 105 | } 106 | } // tone-on-tone, for square unicode pixels 107 | 108 | /* technically unused, since V afaik doesn't work? 109 | * or at least .Xresources takes precedence */ 110 | assert(can_change_color()); 111 | //color_content(COLOR_BLACK, 0, 0, 0); 112 | //bkgdset(COLOR_PAIR(255 + 8*26) | '#'); 113 | return pal; 114 | } 115 | 116 | void palette_debug_dump(palette* pal) { // for when ^^^ inevitably breaks for mysterious reasons 117 | erase(); 118 | mvprintw(0, 8, "[linear dump, 0x%x colors, 0x%x pairs]", COLORS, COLOR_PAIRS); 119 | for (int x=0;x<128;x++) { 120 | mvprintw(1, x, "%x", x >> 4); 121 | mvprintw(2, x, "%x", x & 7); 122 | for (int y=0;y<12;y++) { 123 | mvprintw(3+y, x, "P"); 124 | mvchgat(3+y, x, 1, A_NORMAL, y*128+x, NULL); 125 | } 126 | } 127 | 128 | mvprintw(18, 8, "[sweeps]"); 129 | for (int y=0;y<16;y++) { 130 | for (int x=0;x<26;x++) { 131 | mvprintw(y+19, x, "#"); 132 | mvchgat(y+19, x, 1, A_NORMAL, 256+x+y*26, NULL); 133 | } 134 | } 135 | for (int y=0;y<26;y++) { 136 | for (int x=0;x<26;x++) { 137 | mvprintw(y+41, x, "$"); 138 | mvchgat(y+41, x, 1, A_NORMAL, pal->gray_gray[y][x], NULL); 139 | } 140 | } 141 | refresh(); 142 | getchar(); 143 | } 144 | 145 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | dnl The file watcher that uses TCC to recompile and reload render code when it 2 | dnl changes is some fragile, non-portable stuff. Testing dependencies and working 3 | dnl around API breakage/platform differences is the gnarly problem autotools 4 | dnl evolved for, so here it is. 5 | dnl 6 | dnl The fragile file-watcher-reloader code is the *only* reason autotools is in 7 | dnl use. All the other bells and whistles it adds may be cool, but aren't worth 8 | dnl the incidental complexity brought into a tool that's quite manageable as a 9 | dnl one-file bash/C polyglot. 10 | dnl 11 | dnl Without it, this could literally be a single C file you wget and tcc -run. 12 | dnl 13 | dnl Serious second thoughts about going down the path at all. 14 | AC_INIT([blindsight],[0.1.1],[amtal@github]) 15 | 16 | dnl ################################ 17 | dnl Automake dependencies and config 18 | dnl ################################ 19 | 20 | dnl Reduce the amount of clutter in project rootdir. 21 | AC_CONFIG_AUX_DIR([build-aux]) 22 | dnl ^^ automake 1.11 doesn't have this? It's an autoconf macro, though. 23 | dnl The build-aux directory isn't created if it's missing, either. Not sure 24 | dnl whether that's a new feature or just due to the macro being missing. 25 | dnl 26 | dnl Workaround: `make dist` on recent system and distribute to older ones, as 27 | dnl the build system was intended to do. 28 | AC_CONFIG_MACRO_DIRS([build-aux]) 29 | 30 | dnl The 'foreign' option avoids extra Gnu complaints. 31 | AM_INIT_AUTOMAKE([1.9 -Wall -Werror foreign subdir-objects]) 32 | 33 | dnl ######################### 34 | dnl Build System Dependencies 35 | dnl ######################### 36 | 37 | dnl There's a weird dep on GCC as compiler right now due to stack alignment 38 | dnl issues when called by TCC. One day... 39 | dnl AC_PROG_CC(clang gcc tcc) 40 | AC_PROG_CC(gcc) 41 | AC_PROG_CC_C99 42 | dnl /usr/share/automake-1.15/am/ltlibrary.am: 43 | dnl warning: 'libblindsight.la': linking libtool libraries using a non-POSIX 44 | m4_ifdef([AM_PROG_AR], [AM_PROG_AR]) # magic incantation from some github issue 45 | LT_INIT([shared]) 46 | AC_PROG_LIBTOOL 47 | 48 | dnl ################################# 49 | dnl Main Library and API Dependencies 50 | dnl ################################# 51 | 52 | AC_CONFIG_HEADERS([src/config.h]) # avoid -D clutter in build log 53 | 54 | AC_SEARCH_LIBS([log2], [m], , [ 55 | AC_MSG_ERROR([Need -lm mathlib.]) 56 | ]) 57 | 58 | AC_CHECK_FUNCS([err errx], [], [AC_MSG_ERROR([Missing BSD convenience wrapper.])]) 59 | 60 | dnl There's a pile of other linuxy stuff I don't think is portable, but eh, later. 61 | dnl 62 | dnl Is there any point to calling these if nothing checks their output? It seems 63 | dnl nice to have some output on build that indicates a dependency, but means 64 | dnl there's a pile of extra -DHAVE_WHATEVER passed around and this configure.ac 65 | dnl is longer than necessary. 66 | AC_CHECK_HEADERS([limits.h]) 67 | AC_TYPE_SIZE_T 68 | AC_FUNC_MMAP 69 | 70 | dnl Pin to at-least ncurses versions I've tested, for now. 71 | PKG_CHECK_MODULES([NCURSESW], [ncursesw >= 5.9], [], [ 72 | AC_MSG_WARN([Untested version of ncursesw.]) 73 | ]) 74 | AC_SEARCH_LIBS([mvaddwstr], [ncursesw ncurses], , [ # on osx libncurses is wide 75 | AC_MSG_ERROR([Need ncursesw, for wide char and color pair support.]) 76 | ]) 77 | AC_CHECK_HEADERS([ncursesw/ncurses.h]) # Ubuntu layout, needs ifdef 78 | 79 | 80 | dnl ##################### 81 | dnl Optional Dependencies 82 | dnl ##################### 83 | 84 | dnl Live code update: 85 | 86 | dnl Do an initial basic check for compiler binary, then a bunch of extra checks. 87 | dnl Once all checks done, create the conditional that will be used to tweak 88 | dnl generated Makefile. 89 | AC_CHECK_PROG([tcc],[tcc],[yes],[no]) 90 | AM_CONDITIONAL([HAVE_TCC_BINARY], [test "x$tcc" = xyes]) 91 | AC_CHECK_HEADERS([libtcc.h], , [tcc=no]) 92 | 93 | dnl libtcc depends on libdl, SEARCH_LIBS check will fail if it's not added. Could 94 | dnl add -ldl to arguments, or can search for [dl] first since autotools seems to 95 | dnl append a running list of -lfoo for every library found. Which breaks future 96 | dnl SEARCH_LIBS calls if you add -ldl to this SEARCH_LIBS arguments, because 97 | dnl future ones will have -ltcc but no -ldl. 98 | dnl Why am I doing this again? 99 | AC_SEARCH_LIBS([dlopen], [dl], , [tcc=no]) 100 | dnl Debian ships only libtcc.a, I want to build a .so, and am too lazy to figure 101 | dnl out the right way to do this. So, let's just make sure the system has a .so 102 | dnl by adding -shared to the build-test. 103 | AC_SEARCH_LIBS([tcc_new], [tcc], , [tcc=no], [-shared]) 104 | 105 | AC_MSG_CHECKING([if tcc_relocate is the okay one]) 106 | AC_COMPILE_IFELSE( 107 | [AC_LANG_PROGRAM( 108 | [[#include ]], 109 | [[TCCState* tcc; 110 | tcc_relocate(tcc, 0); 111 | #ifdef TCC_RELOCATE_AUTO 112 | #error auto-allocation re-added, everything is fine 113 | #endif 114 | ]] 115 | )], 116 | [AC_MSG_RESULT([no]) 117 | AC_MSG_WARN([Unsupported 0.9.25 tcc_relocate detected]) 118 | tcc=no 119 | ], 120 | [AC_MSG_RESULT([yes])] 121 | ) 122 | 123 | dnl Enough checks for a working TCC. 124 | AM_CONDITIONAL([HAVE_TCC], [test "x$tcc" = xyes]) 125 | AM_COND_IF([HAVE_TCC],,[ 126 | AC_MSG_WARN([Disabled live code reload due to missing TCC.]) 127 | ]) 128 | 129 | dnl Look for platform-specific directory/file watchers: 130 | 131 | AC_CHECK_HEADERS([sys/inotify.h], [linux=yes], [linux=no]) 132 | AM_CONDITIONAL([SYS_LINUX], [test "x$linux" = xyes]) 133 | 134 | dnl Sandbox, Linux-only currently: 135 | 136 | AC_ARG_ENABLE([sandbox], 137 | [AS_HELP_STRING([--enable-sandbox], [enforce sandbox where available])], 138 | [ 139 | case "${enableval}" in 140 | yes) sandbox=yes ;; 141 | no) sandbox=no ;; 142 | *) AC_MSG_ERROR([bad value ${enableval} for --enable-sandbox]) ;; 143 | esac 144 | 145 | ], [sandbox=yes]) 146 | 147 | dnl For some reason SEARCH_LIBS links -ltcc -lncursesw, which fails due to tcc 148 | dnl depending on dl. Guess I'll check for dl. 149 | AC_SEARCH_LIBS([seccomp_init], [seccomp], , [sandbox=no]) 150 | AC_CHECK_HEADERS([seccomp.h], , [sandbox=no]) 151 | 152 | dnl Until this feature proves stable across platforms, fail-silent for when it's 153 | dnl missing due to broken dependency. Warn when it's enabled to give a heads up 154 | dnl on imminent SIGSYS. 155 | AM_CONDITIONAL([HAVE_SANDBOX], [test "x$sandbox" = xyes]) 156 | AM_COND_IF([HAVE_SANDBOX],[ 157 | AC_MSG_WARN([Ghetto sandbox is enabled, get ready to strace crashes!]) 158 | ]) 159 | 160 | dnl For now, install blindsight.c as a standalone hex viewer. It's an 161 | dnl incomplete work-in-progress that ought to eventually get compiled rather 162 | dnl than #! -run, but for now... Rename it, then install via a rule in 163 | dnl Makefile.am 164 | dnl 165 | dnl Oh, this requires TCC to be installed on the system. Should probably 166 | dnl disable this if not present... 167 | AC_CONFIG_FILES([blindsight:examples/blindsight.c], [chmod +x blindsight]) 168 | 169 | dnl Cross fingers and generate makefiles. 170 | AC_CONFIG_FILES([Makefile 171 | src/Makefile]) 172 | AC_OUTPUT 173 | -------------------------------------------------------------------------------- /examples/summaries.h: -------------------------------------------------------------------------------- 1 | /* Information-dense, lossy views that summarize data. */ 2 | 3 | 4 | /* A histogram view shows symbol (byte) distribution inside an address window. 5 | * It's not very space-efficient to show, so there's a few ways to compress it: 6 | * 7 | * - Shannon entropy is a measure of the information represented by the 8 | * particular symbol distribution in a given address window. 9 | * - The chi-square test is a measure of how far a particular distribution is 10 | * from the expected distribution. (For us, the expected distribution is 11 | * probably a uniform one produced by encrypted data.) 12 | * 13 | * Neithers take symbol ordering into account, just their distribution in the 14 | * window. Shannon entropy is a little easier to display, so here it is. 15 | * */ 16 | static inline double shannon_entropy(uint16_t count[256], size_t window) { 17 | /* We're always dealing with 256 symbols, since we work in bytes. 18 | * Windows are often smaller than that, though; worst-case compression 19 | * for a 128-byte window would be 7 bits, 16-byte window 4 bits, etc. 20 | * 21 | * For such smaller windows, we'll re-scale the output to '8' bits to 22 | * keep the shading range looking consistent while switching scales. 23 | */ 24 | static double visual_scale; 25 | 26 | /* Memoize FP calc for small windows */ 27 | static double lut[512] = {0}; 28 | static size_t lut_for_sz = 0; 29 | if (lut_for_sz != window && window <= sizeof(lut)/sizeof(lut[0])) { 30 | lut_for_sz = window; 31 | for (int i=1; i= 256 ? 1.0 : (8.0 / log2(window)); 36 | } 37 | 38 | double entropy = 0.0; 39 | if (window <= sizeof(lut)/sizeof(lut[0])) { /* small windows */ 40 | for (int i=0;i<256;i++) entropy += lut[count[i]]; 41 | entropy *= visual_scale; 42 | } else { /* unmemoized */ 43 | for (int i=0;i<256;i++) { 44 | if (!count[i]) continue; 45 | double prob = (double)count[i] / window; 46 | entropy += prob * log2(1 / prob); 47 | } 48 | } 49 | return entropy; 50 | } 51 | 52 | VIEW(entropy, 53 | 32, /*=>*/ {1, 1, F_256C|F_FG_GRAY}, .zoom={16, 256} 54 | )(uint8_t* s, size_t n, /*=>*/ int y, int x) { 55 | uint16_t count[256] = {0}; // histogram of symbol occurances 56 | uint8_t classes = 0; // mask of bits seen, used to detect ascii 57 | for (int i=0;i= ' ' && classes <= 0x7f && !count[0x7f]) { 84 | mvprintw(y, x, "%01x", hi); // ascii 85 | color = pal.fg_gray[3][0]; 86 | } else if (ent <= 7.0) { 87 | mvprintw(y, x, "%01x", hi); // binary, low entropy 88 | color = pal.fg_gray[4][0]; 89 | } else { 90 | mvprintw(y, x, "%01x", hi); // binary, very high entropy 91 | // TODO ought to perceptual-brightness scale this 92 | int bg = lo ? (int)((float)lo / 10 * 24) : 25; 93 | color = pal.fg_gray[lo ? 4 : 5][bg]; 94 | } 95 | mvchgat(y, x, 1, A_NORMAL, color, NULL); 96 | } 97 | 98 | /* Grayscale byte occurance histogram, log-scaled to make low counts stand out. 99 | * (The log scale/palette isn't tuned to adjusting n yet, and this view 100 | * realllly needs a scaled legend.) 101 | * 102 | * Even consuming large byte chunks, this view is still space-inefficient. The 103 | * lack of a linear "sweep" is also discongruous - it's more detailed than an 104 | * entropy+class view, but less space-efficient. 105 | * 106 | * Using unicode for square "pixels" allows almost 2x the density of an ASCII 107 | * representation, and better addressing of individual bytes. However, the 108 | * ASCII representation does leave room for extra class/occurance count info. */ 109 | VIEW(bytehist, 110 | 1024, /*=>*/ {9, 17, F_256C|F_PIXELS}, .zoom={64, 4096} 111 | )(uint8_t* s, size_t n, /*=>*/ int y, int x) { 112 | // grid 113 | attr_set(A_NORMAL, pal.gray[6], NULL); // make grid fade into background 114 | mvprintw(y, x+1, "0123456789abcdef"); 115 | for (int row=1;row<9;row++) { 116 | mvprintw(y+row, x, "%x", 2 * (row - 1)); 117 | mvaddwstr(y+row, x+1, L"\u2580\u2580\u2580\u2580" // 16x line of 118 | L"\u2580\u2580\u2580\u2580" // square blocks 119 | L"\u2580\u2580\u2580\u2580" 120 | L"\u2580\u2580\u2580\u2580"); 121 | } 122 | attr_set(A_NORMAL, -1, NULL); 123 | 124 | assert(n <= 0xFFff && "uint16_t limit"); 125 | uint16_t count[256] = {0}; 126 | for (int i=0;i*/ {1, 65, F_256C|F_PIXELS} // errors on narrow screens 150 | )(uint8_t* s, size_t n, /*=>*/ int y, int x) { 151 | for (int col=0; col<(n/2); col++) { 152 | mvaddwstr(y, x+col, L"\u2580"); 153 | const unsigned char top = s[col]; 154 | const unsigned char bot = s[col + n/2]; 155 | mvchgat(y, x+col, 1, A_NORMAL, pal.gray_gray[bot>>4][top>>4], NULL); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/blindsight.c: -------------------------------------------------------------------------------- 1 | /* 2 | * "We're the goddamn Kalashnikovs of thinking meat." 3 | * - Echophraxia, by Peter Watts 4 | **/ 5 | #define _XOPEN_SOURCE_EXTENDED 6 | // ^ pray to mr skeltal and open source gods 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include "config.h" 18 | #ifdef HAVE_NCURSESW_NCURSES_H 19 | # include 20 | #else 21 | # include 22 | #endif 23 | #include "blindsight.h" 24 | #include "palette.h" 25 | #include "render.h" 26 | #include "cmd.h" 27 | #include "watcher.h" 28 | #include "sandbox.h" 29 | //#include 30 | /* TODO: purge all non-unsigned with righteous flame, except those mandated by ncurses I guess >_> 31 | uh, maybe ignore ncurses and just use typedefs to mark separate coord spaces? */ 32 | 33 | /* semver.org, major.minor.patch */ 34 | const char* blindsight_version = PACKAGE_VERSION; 35 | 36 | /* Architecture: 37 | 38 | Entry point is an executable that bootstraps the main library. 39 | Main library sets up viewport, 40 | mmaps file, 41 | loads shaders and 42 | starts rendering. 43 | It also watches the entry point for changes. 44 | Renderer library is a .so in entry point, re-loaded by renderer? 45 | 46 | 47 | Contemplations: 48 | Range-skips ala hd, worth implementing or no? Would be neat for heapviz. 49 | Nethack-style keyboard controls? :D 50 | */ 51 | 52 | 53 | /* Warn on missing stuff - has to run after curses is initialized */ 54 | void check_features_and_warn(const view** views, const char* locale) { 55 | feature present = 0, needed =0; 56 | 57 | if (COLORS >= 256) present |= F_256C; 58 | if (COLOR_PAIRS >= 0x7fff) present |= F_FG_GRAY|F_GRAY_GRAY; 59 | if (strstr(locale, "UTF-8")) present |= F_UTF8; 60 | 61 | for (const view** v=views; *v; v++) { 62 | if ((*v)->codom.bounds & ~present) { 63 | if (!needed) { 64 | def_prog_mode(); 65 | endwin(); // drop back to terminal briefly 66 | printf("Warning, these views require additional terminal capabilities:\n"); 67 | } 68 | 69 | printf("\t%s\n", (*v)->name); 70 | } 71 | needed |= (*v)->codom.bounds; 72 | } 73 | if (needed & F_256C) 74 | printf("Need 256+ colors. Set $TERM? Switch terminals?\n"); 75 | if (needed & (F_FG_GRAY|F_GRAY_GRAY)) 76 | printf("Need more color pairs. Set $TERM? Link ncursesw, not ncurses?\n"); 77 | if (needed & F_UTF8) 78 | printf("Need UTF-8 support. Set $TERM? Check locale?\n"); 79 | if (needed) { 80 | printf("Running 'export TERM=xterm-256color' will probably fix everything!\n"); 81 | // Take that, neckbeards! ^^^ This is for all the time I wasted 82 | // on Unix archeology research, trying to figure out how to 83 | // control terminal colors the "right" way. 84 | // 85 | // Turns out, there's more cruft and dirty hacks than you can 86 | // shake a stick at and nobody really cares all that much. Time 87 | // was not well-spent. 88 | reset_prog_mode(); 89 | } 90 | } 91 | 92 | palette screen_init() { 93 | initscr(); 94 | palette p = palette_init(); 95 | 96 | raw(); // do not buffer until line break 97 | keypad(stdscr, TRUE); // capture KEY_* function keys 98 | noecho(); // we control input 99 | nonl(); // not sure what this does 100 | curs_set(0); // do not show cursor, if supported 101 | 102 | return p; 103 | } 104 | 105 | /* unsafe type-generic macros, safety is for Rust */ 106 | #define MIN(a, b) ((a)>(b) ? (b) : (a)) 107 | #define MAX(a, b) ((a)>(b) ? (a) : (b)) 108 | 109 | /* At the end of this call is the first point where untrusted data is 110 | * introduced into the program. The sandbox is thus initialized here; call 111 | * should be pushed as far forward in the initialization process as possible. 112 | * 113 | * FIXME should probably make this stdin-friendly 114 | */ 115 | const char* mmap_buf(const char* fname, size_t* fsize) { 116 | int in_fd = open(fname, O_RDONLY); 117 | if (in_fd == -1) err(1, "open"); 118 | 119 | struct stat in_sb; 120 | if (fstat(in_fd, &in_sb)) err(1, "fstat"); 121 | *fsize = in_sb.st_size; 122 | 123 | const char* buf = mmap(0, in_sb.st_size, PROT_READ, MAP_PRIVATE, in_fd, 0); 124 | if (buf == MAP_FAILED) err(1, "mmap"); 125 | 126 | sandbox_init(); /* hope we're all done initializing */ 127 | 128 | return buf; 129 | } 130 | 131 | 132 | /* 'hex view' grid 133 | * cached parameters updated on screen re-size or view change 134 | * not the cursor though, this stays constant through cursor changes */ 135 | typedef struct { 136 | /* screen coordinates */ 137 | int max_y, max_x; // *screen* max, not just grid window's :| 138 | /* file coordinates */ 139 | size_t bytes_per_full_row; // grid window rows 140 | } viewport; 141 | 142 | viewport viewport_init(int non_grid_x, view v) { 143 | // Trying a thing where instead of declaring inputs/outputs by 144 | // reference as const, they're passed returned by value. This makes 145 | // effects 100% clear without relying on warnings or looking up decls. 146 | viewport vp = {0}; 147 | 148 | getmaxyx(stdscr, vp.max_y, vp.max_x); 149 | // this is a macro, args are written to :| 150 | 151 | vp.max_y--; // compensate for cursor dead space at the bottom of window 152 | // am I not initializing things correctly or something? 153 | 154 | vp.bytes_per_full_row = (vp.max_x - non_grid_x) / v.codom.x * v.dom; 155 | vp.bytes_per_full_row = 1 << dlog2(vp.bytes_per_full_row); 156 | 157 | return vp; 158 | } 159 | 160 | void reload_error(void* st, const char* msg) { 161 | // for now, dump syntax errors to stdout while dropped out of terminal 162 | printf("tcc: %s\n", msg); 163 | } 164 | 165 | /* Hot code reloads are watched on the input to the main render loop. However, 166 | * there are occasional other modes (help screen, debug palette dump, address 167 | * entry, etc) where a code reload isn't useful since it won't result in an 168 | * immediate re-render. 169 | * So, the main loop is polled with a timeout, but otherwise getch() is set to 170 | * block indefinitely. */ 171 | int key_poll(bool* trigger_reload) { 172 | timeout(10); // 200ms peak latency on live reloads should be fine 173 | // want to spend time blocked on user input, not polling 174 | // XXX overpolling 20x until I figure out what's up with watcher... 175 | // seems like it's dropping messages from a full queue or something 176 | int key; 177 | do { 178 | if (watcher_check()) { 179 | *trigger_reload = true; 180 | key = 0; // exit loop w/o user input 181 | break; 182 | } 183 | key = getch(); 184 | } while (key == ERR); 185 | 186 | timeout(-1); // return to blocking mode 187 | return key; 188 | } 189 | 190 | /* Major actions on the view that key presses can request. Bounds checked 191 | * later, closer to the state the bounds checking is actually performed on. 192 | * 193 | * Alternatively, just poke ncurses/globals directly. Nobody's watching! 194 | * 195 | * It's awfully tempting to generate a help screen with a horrific wrapper 196 | * macro. I shouldn't. */ 197 | typedef struct { 198 | intptr_t scroll;// relative scrolling 199 | size_t jump; // absolute, SIZE_MAX goes to 0 200 | signed flip; // view shift to adjacent ones 201 | signed zoom; // view byte zoom, if supported 202 | // window sizes? 203 | } input_req; 204 | 205 | cmd KEYS[] = { 206 | {"x", "Exit program."}, 207 | {"h?", "Hotkey cheatsheet and help screen."}, 208 | {.desc = "Cursor movement"}, 209 | {"wasd","Vertical scroll by row, horizontal by cell."}, 210 | {"WS", "Vertical scroll by page."}, 211 | {"g", "Go to address 0x0."}, 212 | {.desc = "View control"}, 213 | {"qe", "Flip through the view list. Acts like a \"zoom\"."}, 214 | {"QE", "Zoom bytes-per-cell scale on current view, if supported."}, 215 | {.desc = "Debug"}, 216 | {"p", "Palette dump screen, for triaging color issues."}, 217 | {"jJ", "Debug background color test."}, 218 | {0} 219 | }; 220 | /* Printed Nethack-guidebook style in --help, 221 | * vim-cheatsheet style in help screen. */ 222 | 223 | input_req key_actions(int key, viewport const vp, view const v, palette* pal) { 224 | input_req req = {0}; 225 | 226 | switch(key) { 227 | /* window resize (apparently not as portable as SIGWINCH, but clean) */ 228 | case KEY_RESIZE: endwin(); clear(); break; 229 | /* WASD-style scrolling */ 230 | case KEY_LEFT: 231 | case 'a': req.scroll = -v.dom; 232 | break; 233 | case KEY_RIGHT: 234 | case 'd': req.scroll = v.dom; 235 | break; 236 | case KEY_UP: 237 | case 'w': req.scroll = -vp.bytes_per_full_row; 238 | break; 239 | case KEY_DOWN: 240 | case 's': req.scroll = vp.bytes_per_full_row; 241 | break; 242 | case KEY_PPAGE: 243 | case 'W': req.scroll = -vp.bytes_per_full_row * vp.max_y; 244 | break; 245 | case KEY_NPAGE: 246 | case 'S': req.scroll = vp.bytes_per_full_row * vp.max_y; 247 | break; 248 | /* view/zoom control */ 249 | case 'e': req.flip = 1; break; 250 | case 'q': req.flip = -1; break; 251 | case 'E': req.zoom = 1; break; 252 | case 'Q': req.zoom = -1; break; 253 | /* jump back to zero */ 254 | case 'g': req.jump = SIZE_MAX; break; 255 | /* help screen */ 256 | case 'h': 257 | case '?': cmd_render_help(KEYS); break; 258 | /* debug palette dump to deal with weirdness */ 259 | case 'p': palette_debug_dump(pal); break; 260 | /* TEST TEST TEST DEBUG/IDEA STUFF */ 261 | case 'j': assume_default_colors(0xf,0x08); break; // okay, can dynamically change this nice :D 262 | case 'J': assume_default_colors(0x7,0x00); break; 263 | } 264 | 265 | return req; 266 | } 267 | 268 | // TODO: generalize in terms of dims, drop viewport dependency, move into render.c 269 | void render_scrollbar(const vec2 base, size_t buf_sz, size_t cursor, view v, viewport vp, const palette* pal) { 270 | // overview "scroll" address bar (not sure whether any address info will actually go there, yet) 271 | size_t bar_step = buf_sz / (vp.max_y + 1 - base.y); 272 | for (int y=base.y; y= bar && cursor <= bar_end) || 277 | (cur_end >= bar && cur_end <= bar_end)) { // segments overlap 278 | size_t overlap_lo = MAX(cursor, bar); 279 | size_t overlap_hi = MIN(cur_end, bar_end); 280 | uint8_t overlap = (uint8_t)((double)(overlap_hi - overlap_lo) / bar_step * 20); 281 | /* Currently requires 256-color palette. Just inverting 282 | * common UI color doesn't look quite as good as fading 283 | * in % of bar accessed. Hmm. */ 284 | mvprintw(y, base.x, "%x", overlap/2); 285 | mvchgat(y, base.x, 1, A_NORMAL, pal->gray[5 + overlap], NULL); 286 | } else if (cursor < bar && cur_end > bar_end) { // viewport covers bar step 287 | mvaddch(y, base.x, '#'); 288 | mvchgat(y, base.x, 1, A_NORMAL, pal->gray[21], NULL); 289 | } else { 290 | /* State of the unicodes: 291 | * 292 | * mvprintw prints UTF-8 encoded as byte sequences; but not wchars, at all, ever 293 | * mvaddwstr prints wchars fine, just gotta prefix with L 294 | * ACS_foo aren't properly configured in terminfo on my system, :doot: 295 | */ 296 | //mvprintw(y, 0, " %c ", y==0 ? ACS_UARROW : (y==max_y-0) ? ACS_DARROW : ACS_VLINE); 297 | //mvaddstr(y, 0, " ");//L"\x2551 "); // mvaddwstr for unicode char 298 | //mvprintw(y, 0, " \xe2\x9c\x93 "); 299 | //mvprintw(y, 0, "\xe2\x96\x80\xe2\x96\x84\xe2\x96\x81"); // upper lower upper 300 | mvchgat(y, base.x, 1, A_NORMAL, pal->gray[0], NULL); 301 | } 302 | } 303 | } 304 | 305 | void render_info(const vec2 base, const view v, const viewport vp) { 306 | attr_set(A_REVERSE, default_ui_color, NULL); 307 | // TODO only have one piece of UI so far; make it pretty with colours, put units into background 308 | mvprintw(base.y, base.x, " \"%s\" b/char:%.3g b/cell:0x%x b/page:0x%X ", 309 | // may need to compensate for top address line size, if later added V 310 | v.name, 311 | (float)v.dom / (v.codom.x * v.codom.y), 312 | v.dom, 313 | vp.bytes_per_full_row*vp.max_y/v.codom.y); 314 | /* For reference: BZ's recent versions top out the default binary view 315 | * at 10*12 pixels = 120 bytes/hex-char, or 960 bits/hex-char. A single 316 | * terminal character can carry up to ~8 bits of information in glyphs, 317 | * and ~8 more in clever fg+bg palette use... 318 | * However, BZ spends most screen estate on a classic hex+ascii dump, 319 | * so with an order of magnitude or so entropy+class display we can 320 | * still summarize files effectively. */ 321 | } 322 | 323 | /* TODO sort out a portable theme (may need to drop from 0x7Fff color pairs to 324 | * 256, possibly with remapping to still do cool stuff) then refactor palette 325 | * code heavily. Poorly named global for now. */ 326 | palette pal; 327 | 328 | int blindsight(const int argc, char** argv, const view** views, const char* reload_sym) { 329 | if (argc != 2) { 330 | printf(" _ _ _ _ _ _ _ \n" 331 | " | |_| |_|___ _| |___|_|___| |_| |_ \n" 332 | " | . | | | | . |_ -| | . | | _|\n" 333 | " |___|_|_|_|_|___|___|_|_ |_|_|_| \n" 334 | " |___| \n" 335 | "High-density binary viewer for visual pattern matching.\n" 336 | "\n" 337 | "Usage: %s \n" 338 | "\n", 339 | argv[0] 340 | ); 341 | cmd_print_help(KEYS); 342 | printf("\n" "v%s on %s\n", blindsight_version, curses_version()); 343 | return EXIT_FAILURE; 344 | } 345 | 346 | palette pal_tmp = screen_init(); 347 | memcpy(&pal, &pal_tmp, sizeof(pal)); 348 | check_features_and_warn(views, setlocale(LC_CTYPE, "")); 349 | 350 | 351 | /* Three coordinates are kept separate by name and type convention: 352 | * 353 | * file offsets: 354 | * size_t cursor, dy, dx, dtmp, ... 355 | * screen coordinates, curses-style: 356 | * int y, x; 357 | * addressed grid of cells: 358 | * int row, col; 359 | */ 360 | 361 | // TODO move definition/initialization somewhere appropriate (maybe group /w other state into struct?) 362 | bool trigger_reload = false; 363 | if (!watcher_init(argv[0], false)) { // non-blocking 364 | reload_sym = 0; // not running under tcc, don't poll inotify 365 | } 366 | 367 | size_t cursor = 0; 368 | signed view_ind = 0; // index into view array, kept as int for easy rolling 369 | signed view_num = 0; 370 | for (const view** v=views; *v; v++) {view_num++;} 371 | view v = *views[view_ind]; // starting view 372 | 373 | /* Going to keep the buffer as unsigned to make it clear it's data, not 374 | * strings. int8_t would also do, but would encourage UB casts. */ 375 | size_t buf_sz; 376 | unsigned char* buf = (unsigned char*)mmap_buf(argv[1], &buf_sz); // TODO move to struct return from arg pass? 377 | 378 | /* Window layout: there's three major vertical panes all stuffed 379 | * together as far left as possible. 380 | * 381 | * - A fixed-width scroll bar, followed by 382 | * - A variable-width address bar, followed by 383 | * - A mostly fixed-width fancy 'hexdump'. 384 | * 385 | * addr_width plus some spacing is the screen x-offset for the 386 | * 'hexdump' grid. 387 | */ 388 | int addr_width = (dlog2(buf_sz) + 1) / 4 + 1; // nibbles in max address 389 | viewport vp = viewport_init(addr_width + 2, v); 390 | 391 | int key_press = 0; 392 | do { // main loop 393 | /* handle inputs and update state */ 394 | if (trigger_reload) { 395 | // Print any possible errors to console, relying on 396 | // upcoming refresh() to restore terminal. 397 | def_prog_mode(); 398 | endwin(); 399 | 400 | printf("Re-loading %s... ", argv[0]); 401 | views = watcher_reload(reload_sym, 0, reload_error); 402 | v = *views[view_ind]; // that's what I get for doing stuff by-value... 403 | assert(views && "live code reload didn't return a symbol!"); 404 | trigger_reload = false; 405 | printf("ok\n"); 406 | 407 | } 408 | input_req req = key_actions(key_press, vp, v, &pal); 409 | 410 | /* Current theory is that scrolling should always be done in 411 | * predictable increments. 412 | * Jumping a portion of a cell down to 0 or up to the filesize 413 | * is a little disconcerting and might lose an interesting 414 | * location when zoomed out temporarily. So, never do less than 415 | * a requested scroll. 416 | */ 417 | assert(buf_sz < INTPTR_MAX && "cleaner code, but <2GB inputs (lol)"); 418 | if (req.scroll && buf_sz > (size_t)(cursor + req.scroll)) { 419 | cursor += req.scroll; 420 | } else if (req.jump) { 421 | cursor = req.jump == SIZE_MAX ? 0 : MIN(req.jump, buf_sz - 1); 422 | } else if (req.flip) { 423 | view_ind += req.flip; 424 | view_ind = MAX(0, view_ind); 425 | view_ind = MIN(view_ind, view_num - 1); 426 | v = *views[view_ind]; 427 | } else if (req.zoom && v.zoom.min && v.zoom.max) { 428 | v.dom = req.zoom < 0 ? v.dom >> 1 : v.dom << 1; 429 | v.dom = MAX(v.zoom.min, v.dom); 430 | v.dom = MIN(v.dom, v.zoom.max); 431 | } 432 | 433 | vp = viewport_init(addr_width + 2, v); // possible view changes,re-calc 434 | 435 | assert(1 << dlog2(v.dom) == v.dom && "viewport calc assumes v.dom is pow2"); 436 | const int x_width = vp.bytes_per_full_row * v.codom.x / v.dom; // ^ 437 | 438 | /* Writing view render functions is easier if you don't have to 439 | * fill 100% of the declared codomain area. Clear on redraw. */ 440 | attr_set(A_NORMAL, -1, NULL); 441 | erase(); 442 | // Minimal address bar, separated from main grid by a scrol bar. 443 | render_scrollbar(yx(1, 0), buf_sz, cursor, v, vp, &pal); 444 | render_row_addrs(yx(1, 0), yx(vp.max_y + 1, addr_width + 2), 445 | buf_sz, cursor, v, vp.bytes_per_full_row); 446 | // Column addresses aligned with main grid. 447 | render_col_addrs(yx(0, addr_width + 2), x_width, 448 | vp.bytes_per_full_row, v); 449 | render_grid(yx(1, addr_width + 2), yx(vp.max_y+1, x_width), 450 | buf_sz, cursor, buf, v, 451 | vp.bytes_per_full_row); 452 | // View-info window somewhere out of the way. 453 | render_info(yx(vp.max_y - 0, vp.max_x - 62), v, vp); 454 | refresh(); 455 | } while ((key_press = key_poll(&trigger_reload)) != 'x'); 456 | 457 | endwin(); 458 | return EXIT_SUCCESS; 459 | } 460 | --------------------------------------------------------------------------------