oss-sec mailing list archives

Re: binary-patching bash


From: Solar Designer <solar () openwall com>
Date: Mon, 29 Sep 2014 12:41:20 +0400

On Mon, Sep 29, 2014 at 04:44:05AM +0400, Solar Designer wrote:
I've just tweeted some crazy stuff, and it is even crazier to talk about
this on a mailing list focused on Open Source, but ...

<solardiz> cp -ip bash{,~} && env - perl -pe 's/\((\) {\0)/\0\1/g' bash > bash~ && test `cmp -l bash{,~} | wc -l` = 1 
&& ln bash{,-} && mv -v bash{~,}

I just did a Google web search for "bash binary patch" and found (on
page 2 of search results) that this very approach had been suggested
before, by Antti Louko:

https://www.schneier.com/blog/archives/2014/09/nasty_vulnerabi.html#c6679473

| alo  September 25, 2014 5:41 PM 
| 
| It is actually quite easy to binary patch bash. Open bash with eg. emacs
| and search for string "() {" and replace "(" with a null character. This
| disables the horrible "function definition from the environment" feature
| altogether.

https://www.schneier.com/blog/archives/2014/09/nasty_vulnerabi.html#c6679613

| alo  September 27, 2014 7:06 AM 
| 
| As I wrote earlier, simple binary patch removes whole "automagic
| function definitions from the environment". I made a simple Python
| script to make the patch. It simply finds null-ending string "() {" and
| writes null over first "(".
| 
| The script assumes that the first occurrence is the correct one. At
| least bash shells I have, that is the only and correct one. This can be
| also used to disable the feature in already patched vendor supplied bash
| binaries.

No analysis as to why this patch works was included in the comments,
though.  Also, patching the first occurrence is riskier than making sure
there's exactly one occurrence, as my one-liner does.  (For extra
safety, my one-liner can be further improved to check the actual output
from "cmp -l" with e.g. "egrep -c ' 50[[:space:]]+0$'" in place of
"wc -l", but that didn't fit in the tweet and is less portable.)

The Python script is:

http://alo.fi/bash/Patch-bash.py

SHA-256 of Patch-bash.py above as of the time of this writing:

4de321a4fb8c1787f983b754d434c1dde6fe58e3c76f2f6b3b77f10a3d0ea171  Patch-bash.py

Antti, you could want to enhance the Python script with an "exactly one
match" check, and post the new SHA-256 in here.  While not tweetable, I
do see some advantage in this being written in just one language, as
opposed to my mix of shell and Perl.

I thought of doing the same in Perl or sed alone, and it'd fit in a
tweet easily (e.g., using "perl -pi.orig -e"), but not with the kind of
safety I wanted to include - the "exactly one match" check, and creating
a backup and updating bash atomically ("perl -i" unfortunately writes
over the file, so it'd bump into "Text file busy" or have bash broken
for new invocations for a while, and would keep bash broken if patching
fails).  Hence the mix.

<solardiz> Previous tweet disables function imports in bash due to strncmp(..., 4). Tested on some Linux & FreeBSD, 
from bash & csh. At your own risk.
<solardiz> perl -pe 's/\(\) {\0/(){\0\0/g' followed by an "exactly one match" check may be safer e.g. for an 
Internet-wide scan^Wpatch. ;-) #shellshock
<solardiz> bash 1.14.7 and bash 4.3 (and all inbetween?) use STREQN ("() {", string, 4) and define STREQN via 
strncmp(). Allows portable binary patch.

The idea is that the length 4 STREQN() aka !strncmp() when invoked on a
shorter constant string will require that the entire env var value be
that string - that is, either empty (in my first tweet above) or a
3-char string (in my third tweet above).  Neither case leaves any room
for an attacker to provide arbitrary input to the parser via the former
function imports feature.

This dirty hack may be handy for patching otherwise unmaintained systems.

The primary risk I see here is that some build of bash might include
custom patches where this check had been changed to use something other
than (an equivalent of) strncmp().  I am not aware of any such cases.

Here's how to test that the feature is indeed disabled (or at least
broken, although that is an insufficient test for security).  Before the
binary patch:

$ testfunc() { echo test; }
$ export -f testfunc
$ bash -c testfunc
test

After the binary patch (first tweet):

$ testfunc() { echo test; }
$ export -f testfunc
$ bash -c testfunc
bash: testfunc: command not found

Alexander


Current thread: