Convert bash script to a compiled standalone binary executable, not text

As jksoegaard already posted, there is the command shc. Below is an example of how to install and use this command. I tested this answer using macOS Big Sur.

Here are the steps I used to install shc.

  1. Open Terminal by pressing command+space, then type terminal and hit Enter key.

  2. Install homebrew first.

    ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" < /dev/null 2> /dev/null
    
  3. Install shc.

    brew install shc
    

Below is a listing of the test script file named hello.sh.

#!/bin/bash
osascript -e 'display dialog "Hi There"' >/dev/null'

Here are the steps I used to create and test the executable.

  1. Enter the following command to compile the script using shc.

    shc -f hello.sh
    
  2. Enter the following command to rename the executable.

    mv hello.sh.x hello
    
  3. Enter the command to test.

    ./hello
    

    Below is the resulting dialog box.


Appendix

Below is the output from running the commands.

Last login: Thu Oct  8 06:36:48 on console
dma@dmas-Mac-mini ~ % ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" < /dev/null 2> /dev/null 
Password:
==> You are using macOS 11.0.
==> We do not provide support for this pre-release version.
This installation may not succeed.
After installation, you will encounter build failures with some formulae.
Please create pull requests instead of asking for help on Homebrew\'s GitHub,
Discourse, Twitter or IRC. You are responsible for resolving any issues you
experience while you are running this pre-release version.

==> This script will install:
/usr/local/bin/brew
/usr/local/share/doc/homebrew
/usr/local/share/man/man1/brew.1
/usr/local/share/zsh/site-functions/_brew
/usr/local/etc/bash_completion.d/brew
/usr/local/Homebrew
==> The following existing directories will be made group writable:
/usr/local/bin
==> The following existing directories will have their owner set to dma:
/usr/local/bin
==> The following existing directories will have their group set to admin:
/usr/local/bin
==> The following new directories will be created:
/usr/local/etc
/usr/local/include
/usr/local/lib
/usr/local/sbin
/usr/local/share
/usr/local/var
/usr/local/opt
/usr/local/share/zsh
/usr/local/share/zsh/site-functions
/usr/local/var/homebrew
/usr/local/var/homebrew/linked
/usr/local/Cellar
/usr/local/Caskroom
/usr/local/Homebrew
/usr/local/Frameworks
==> The Xcode Command Line Tools will be installed.
==> /usr/bin/sudo /bin/chmod u+rwx /usr/local/bin
==> /usr/bin/sudo /bin/chmod g+rwx /usr/local/bin
==> /usr/bin/sudo /usr/sbin/chown dma /usr/local/bin
==> /usr/bin/sudo /usr/bin/chgrp admin /usr/local/bin
==> /usr/bin/sudo /bin/mkdir -p /usr/local/etc /usr/local/include /usr/local/lib /usr/local/sbin /usr/local/share /usr/local/var /usr/local/opt /usr/local/share/zsh /usr/local/share/zsh/site-functions /usr/local/var/homebrew /usr/local/var/homebrew/linked /usr/local/Cellar /usr/local/Caskroom /usr/local/Homebrew /usr/local/Frameworks
==> /usr/bin/sudo /bin/chmod g+rwx /usr/local/etc /usr/local/include /usr/local/lib /usr/local/sbin /usr/local/share /usr/local/var /usr/local/opt /usr/local/share/zsh /usr/local/share/zsh/site-functions /usr/local/var/homebrew /usr/local/var/homebrew/linked /usr/local/Cellar /usr/local/Caskroom /usr/local/Homebrew /usr/local/Frameworks
==> /usr/bin/sudo /usr/sbin/chown dma /usr/local/etc /usr/local/include /usr/local/lib /usr/local/sbin /usr/local/share /usr/local/var /usr/local/opt /usr/local/share/zsh /usr/local/share/zsh/site-functions /usr/local/var/homebrew /usr/local/var/homebrew/linked /usr/local/Cellar /usr/local/Caskroom /usr/local/Homebrew /usr/local/Frameworks
==> /usr/bin/sudo /usr/bin/chgrp admin /usr/local/etc /usr/local/include /usr/local/lib /usr/local/sbin /usr/local/share /usr/local/var /usr/local/opt /usr/local/share/zsh /usr/local/share/zsh/site-functions /usr/local/var/homebrew /usr/local/var/homebrew/linked /usr/local/Cellar /usr/local/Caskroom /usr/local/Homebrew /usr/local/Frameworks
==> /usr/bin/sudo /bin/mkdir -p /Users/dma/Library/Caches/Homebrew
==> /usr/bin/sudo /bin/chmod g+rwx /Users/dma/Library/Caches/Homebrew
==> /usr/bin/sudo /usr/sbin/chown dma /Users/dma/Library/Caches/Homebrew
==> Searching online for the Command Line Tools
==> /usr/bin/sudo /usr/bin/touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress
==> Installing Command Line Tools for Xcode-12.0
==> /usr/bin/sudo /usr/sbin/softwareupdate -i Command\ Line\ Tools\ for\ Xcode-12.0
Software Update Tool

Finding available software

Downloading Command Line Tools for Xcode
Downloaded Command Line Tools for Xcode
Installing Command Line Tools for Xcode
Done with Command Line Tools for Xcode
Done.
==> /usr/bin/sudo /bin/rm -f /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress
Password:
==> /usr/bin/sudo /usr/bin/xcode-select --switch /Library/Developer/CommandLineTools
==> Downloading and installing Homebrew...
HEAD is now at f84b9a027 Merge pull request #8878 from reitermarkus/upgrade-casks
==> Homebrew is run entirely by unpaid volunteers. Please consider donating:
  https://github.com/Homebrew/brew#donations
Already up-to-date.
==> Installation successful!

==> Homebrew has enabled anonymous aggregate formulae and cask analytics.
Read the analytics documentation (and how to opt-out) here:
  https://docs.brew.sh/Analytics
No analytics data has been sent yet (or will be during this `install` run).

==> Homebrew is run entirely by unpaid volunteers. Please consider donating:
  https://github.com/Homebrew/brew#donations

==> Next steps:
- Run `brew help` to get started
- Further documentation: 
    https://docs.brew.sh
dma@dmas-Mac-mini ~ % brew install shc
Updating Homebrew...
==> Auto-updated Homebrew!
Updated 1 tap (homebrew/core).
==> Updated Formulae
Updated 4 formulae.

Warning: You are using macOS 11.0.
We do not provide support for this pre-release version.
You will encounter build failures with some formulae.
Please create pull requests instead of asking for help on Homebrew's GitHub,
Discourse, Twitter or IRC. You are responsible for resolving any issues you
experience while you are running this pre-release version.

==> Downloading https://homebrew.bintray.com/bottles/shc-4.0.3.catalina.bottle.tar.gz
######################################################################## 100.0%
==> Pouring shc-4.0.3.catalina.bottle.tar.gz
🍺  /usr/local/Cellar/shc/4.0.3: 17 files, 101.2KB
dma@dmas-Mac-mini ~ % pwd
/Users/dma
dma@dmas-Mac-mini ~ % cd Documents 
dma@dmas-Mac-mini Documents % shc -f hello.sh
dma@dmas-Mac-mini Documents % mv hello.sh.x hello
dma@dmas-Mac-mini Documents % ./hello

I would take a look at a project named shc (Shell script compiler).

https://github.com/neurobin/shc

It takes a shell script and compiles it into C source code. The C source code can then be compiled with a standard C compiler into a binary executable.

This allows you to run the script you've made without the contents of the shell script being immediately revealed in clear text. It would be a binary executable like so many others.

It might take a bit of trial and error to get your specific script parsed by shc depending on how advanced features you're using in your bash script, but this should take you most of the way there.

Note that the program really just encrypts your shell script and integrates it within the binary. When the binary is executed, the script is decrypted and executed using ordinary shell again. For programmers it would be trivial to break this to reveal the contents of your script, but for hiding the script contents from the average home user it would perhaps be sufficient.

If you have HomeBrew installed, you can install shc by running the following command in the Terminal:

brew install shc

Then it's just a matter of running shc to convert your script into a binary:

shc -U -f myscript.sh -o mybinary

The mybinary command is then the finished product.


Bash is technically an interpreted language, it's not compiled; compiling it is not possible.

Bash is the shell, or command language interpreter, for the GNU operating system.

However, it is possible to obfuscate it so prying eyes cannot look at your script.

Use openssl to encode the file.

I created a simple Zsh script file called arraytest.sh that will execute in Zsh (it's what I had available at the moment). I'm not going to post the actual shell script, instead, I am going to post the base64 encoding.

On my computer, I run the following command on my script and get the following results:

% openssl base64 < arraytest.sh

IyEvYmluL3pzaAoKZGVjbGFyZSAtQSBwcm9ncz0oW2dzXT0iR2hvc3RzY3JpcHQi
IFtic109IkJ1bGxzaGl0IiBbYWJdPSJWb2RrYSIpCgoKbGV0IGFzaXplPSR7I3By
b2dzW0BdfQpsZXQgYnNpemU9JGFzaXplLTEKbGV0IGk9MQpmb3IgeCBpbiAiJHtw
cm9nc1tAXX0iOwpkbwogIGlmIFsgJGkgLWVxICRic2l6ZSBdICAgCiAgdGhlbgog
ICBwcmludGYgIiR4IGFuZCAiCiAgZWxpZiBbICRpIC1sZSAkYnNpemUgXSAmJiBb
ICRpIC1nZSAxIF0KICB0aGVuCiAgICBwcmludGYgIiR4LCAiCiAgZWxzZQogICAg
cHJpbnRmICIkeCIKICBmaQogICgoaSsrKSkKZG9uZQoKcHJpbnRmICIuXG4iCmVj
aG8gRG9uZS4KCg==

That output needs to be saved to a file. Let's called it myencodedscript. You can copy and paste it into a new file. You don't have to make it executable. Once you've copied that script into your clipboard, issue the command

% pbpaste > myencodedscript

Now, you should have a file called myencodedscript. Next, to run it, you need to feed that encoding back through openssl to decode it and then pipe it to your shell.

% cat myencodedscript | openssl base64 -d | zsh

It should run the the script and generate output from an associative array.

So, how can you make this self contained?

#!/bin/zsh
#
#Script Name:  encoded_script.sh

code="IyEvYmluL3pzaAoKZGVjbGFyZSAtQSBwcm9ncz0oW2dzXT0iR2hvc3RzY3JpcHQi
IFtic109IkJ1bGxzaGl0IiBbYWJdPSJWb2RrYSIpCgoKbGV0IGFzaXplPSR7I3By
b2dzW0BdfQpsZXQgYnNpemU9JGFzaXplLTEKbGV0IGk9MQpmb3IgeCBpbiAiJHtw
cm9nc1tAXX0iOwpkbwogIGlmIFsgJGkgLWVxICRic2l6ZSBdICAgCiAgdGhlbgog
ICBwcmludGYgIiR4IGFuZCAiCiAgZWxpZiBbICRpIC1sZSAkYnNpemUgXSAmJiBb
ICRpIC1nZSAxIF0KICB0aGVuCiAgICBwcmludGYgIiR4LCAiCiAgZWxzZQogICAg
cHJpbnRmICIkeCIKICBmaQogICgoaSsrKSkKZG9uZQoKcHJpbnRmICIuXG4iCmVj
aG8gRG9uZS4KCg=="

echo "$code" | openssl base64 -d | zsh

Save that script as encoded_script.sh, make it executable and then run it with the command ./encoded_script.sh

Bonus...

(This assumes a scenario like an admin needing to run a script discretely on 1 or more machines in his care.)

If you want to truly hide this from "prying eyes", use this single line command:

% openssl base64 < script.sh | ssh [email protected] ' openssl base64 -d | bash'

This will encode the script, pipe it through an SSH connection where it decodes the script and passes that to the shell interpreter.

green check Obfuscates code
green check No third party tools
green check Cross platform (tested from macOS host to FreeBSD remote)
green check No need for executable bit set (chmod +x)
green check Gatekeeper on macOS is rendered moot as it not an app that must be notarized or explicitly allowed by the user.

Obviously you need an account on the remote and while nice, but not required, SSH Keys enabled so you don't have to enter a password, especially if you're running this on a batch of machines.


Note: This was done in Zsh with an associative array. It may work in Bash but you'll need at least Bash version 4 to operate properly. Also there's nothing nefarious going on with the script. It will print 3 words and then, on a new line, the word "Done" to indicate the script is finished.

If you want to verify that the code is actually from me, create a file called myencodedscript in vi, paste the code (no quotes), be sure to press "I" for "Insert" before pasting, save it and issue the command and check the hash below

% shasum -a 256 myencodedscript
d4fc6ce35480b0ace6ed0e1c910f1f40079a106b1914d767fa17ada02a422f88  myencodedscript

It will only work if you use the same filename. I tested this cross platform on macOS and on FreeBSD. If you don't get the same hash, let me know.


As it is not completely clear what it is you are actually after, what I'm going to suggest does the following:

  • Creates from a bash script an application bundle structure with a single binary executable which does not contain in human readable form any of the script code used to create it.
  • Executes like an ordinary macOS application bundle running as the name of application bundle.
  • Does not open Terminal during its execution.

That said, it still may be interpreted because if you look at the Open Files and Ports in Activity Monitor for the e.g. name.app application bundle, /bin/bash is listed.

There is a GitHub project named bashapp which states:

bashapp takes as input a bash script and generates a binary executable and OS X application directory structure. This allows developers to provide Finder clickable bash scripts without terminals popping up, etc. Useful for launch, service scripts, etc.

It also provides simple source encryption as a means to obfuscate the bash script. You can specify your own key, or let bashapp generate a randomly sized random key for you, no fewer than 32 bytes long.

You will need to have Command Line Tools for Xcode installed in order to compile the bashapp executable that creates the application bundle. See: How to Install Command Line Tools in Mac OS X (Without Xcode) Or in Terminal run: xcode-select --install

That said, there was one annoying thing in that the Dock Tile for the application bounces in the Dock while it's being executed. A workaround for this is to not have it show in the Dock by running the following in Terminal:

defaults write '/path/to/$name.app/Contents/Info.plist' LSUIElement -bool yes

Changing /path/to/$name.app as appropriate.