mumble/plugins/mumble_plugin_linux.h
Davide Beatrici 5df2bb2c0b FEAT(client): add getExportedSymbol() function for positional audio plugins
This function is of critical importance for the Source Engine plugin, let me elaborate on why.

Most games consist in an executable and maybe one or more libraries, but they don't have particular exported symbols we can use to easily access (existing) resources inside the process, which means we have to rely on hardcoded offsets and hex pattern scanning.

Source Engine games are special: the executable is nothing more than a manager that takes care of loading the core libraries, which are engine(.dll|.so) and client(.dll|.so) (or server(.dll|.so) in the case of a dedicated server).

Those libraries have a common exported symbol, which is CreateInterface(). The function takes the interface's name as argument (char *), creates the interface object if it doesn't exist yet and returns a pointer (void *) to it. The interfaces objects are stored in a list called s_pInterfaceList, which is usually an exported symbol on Linux.

getExportedSymbol() allows us to get the address to CreateInterface() (different for each loaded library) inside the process and read the function's assembly code in order to reach s_pInterfaceList.

If s_pInterfaceList is exported we can get its address with getExportedSymbol(), without the need of CreateInterface().
2020-09-27 20:12:02 +02:00

375 lines
8.1 KiB
C++

// Copyright 2005-2020 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
#ifndef MUMBLE_PLUGIN_LINUX_H_
#define MUMBLE_PLUGIN_LINUX_H_
#ifndef MUMBLE_PLUGIN_MAIN_H_
# error "Include mumble_plugin_main.h instead of mumble_plugin_linux.h"
#endif
#include <cstring>
#include <elf.h>
#include <fstream>
#include <iostream>
#include <math.h>
#include <sstream>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <sys/uio.h>
static inline std::string readAll(const std::string &fn) {
std::ifstream ifs;
ifs.open(fn.c_str(), std::ifstream::binary);
std::string content;
char buf[256];
while (ifs.good()) {
ifs.read(&buf[0], sizeof(buf));
size_t nread = ifs.gcount();
if (nread > 0) {
content.append(&buf[0], nread);
}
}
return content;
}
// This function returns:
// -1 in case of failure.
// 0 if the process is 32-bit.
// 1 if the process is 64-bit.
static inline int isProcess64Bit(const procptr_t &baseAddress) {
if (isWin32) {
return isWin32Process64Bit(baseAddress);
}
// We can know the process architecture by looking at its ELF header.
uint8_t elf[5];
peekProc(baseAddress, elf, sizeof(elf));
// The first 4 bytes constitute the magical number in ASCII: 0x7F 45 4c 46.
if (!(elf[0] == 0x7f && elf[1] == 'E' && elf[2] == 'L' && elf[3] == 'F')) {
return -1;
}
// The fifth byte is 1 in case the process is 32-bit or 2 in case it's 64-bit.
return elf[4] != 1;
}
// This function returns:
// -1 in case of failure.
// 0 if the process is not running through Wine.
// 1 if the process is running through Wine.
static inline int8_t isProcessWin32(const procid_t &pid) {
std::stringstream ss;
ss << "/proc/";
ss << static_cast< unsigned long >(pid);
ss << "/exe";
char *path = realpath(ss.str().c_str(), nullptr);
if (!path) {
return -1;
}
const char *filename = basename(path);
if (strcmp(filename, "wine-preloader") == 0 || strcmp(filename, "wine64-preloader") == 0) {
free(path);
return 1;
}
// basename() returns a pointer to the basename's position in the string passed as argument.
// For that reason we cannot free 'path' before the if statement.
free(path);
return 0;
}
static inline procptr_t getModuleAddr(const procid_t &pid, const wchar_t *modname) {
std::wstring modnameWide(modname);
std::string modnameNonWide(modnameWide.begin(), modnameWide.end());
std::stringstream ss;
ss << std::string("/proc/");
ss << static_cast< unsigned long >(pid);
ss << std::string("/maps");
std::string mapsFn = ss.str();
std::string maps = readAll(mapsFn);
if (maps.size() == 0) {
return 0;
}
std::stringstream ssPath(maps);
while (ssPath.good()) {
std::string baseaddr;
int ch;
while (1) {
ch = ssPath.get();
if (ch == '-') {
break;
} else if (ch == EOF) {
return 0;
}
baseaddr.push_back(static_cast< char >(ch));
}
// seek to perms
do {
ch = ssPath.get();
if (ch == EOF) {
return 0;
}
} while (ch != ' ');
// seek to offset
do {
ch = ssPath.get();
if (ch == EOF) {
return 0;
}
} while (ch != ' ');
// seek to dev
do {
ch = ssPath.get();
if (ch == EOF) {
return 0;
}
} while (ch != ' ');
// seek to inode
do {
ch = ssPath.get();
if (ch == EOF) {
return 0;
}
} while (ch != ' ');
// seek to pathname
do {
ch = ssPath.get();
if (ch == EOF) {
return 0;
}
} while (ch != ' ');
// eat spaces until we're at the beginning of pathname.
while (ch == ' ') {
if (ch == EOF) {
return 0;
}
ch = ssPath.get();
}
ssPath.unget();
std::string pathname;
while (1) {
ch = ssPath.get();
if (ch == '\n') {
break;
} else if (ch == EOF) {
return 0;
}
pathname.push_back(static_cast< char >(ch));
};
// OK, we found 'em!
// Only treat path as a real path if it starts with /.
if (pathname.size() > 0 && pathname.at(0) == '/') {
// Find the basename.
size_t lastSlash = pathname.find_last_of('/');
if (pathname.size() > lastSlash + 1) {
std::string basename = pathname.substr(lastSlash + 1);
if (basename == modnameNonWide) {
unsigned long addr = strtoul(baseaddr.c_str(), nullptr, 16);
return addr;
}
}
}
}
return 0;
}
static inline bool peekProc(const procptr_t &addr, void *dest, const size_t &len) {
struct iovec in;
in.iov_base = reinterpret_cast< void * >(addr); // Address from target process
in.iov_len = len; // Length
struct iovec out;
out.iov_base = dest;
out.iov_len = len;
ssize_t nread = process_vm_readv(pPid, &out, 1, &in, 1, 0);
return (nread != -1 && static_cast< size_t >(nread) == in.iov_len);
}
static inline procptr_t getExportedSymbol(const std::string &symbol, const procptr_t module) {
if (isWin32) {
return getWin32ExportedSymbol(symbol, module);
}
procptr_t hashTable = 0;
procptr_t strTable = 0;
procptr_t symTable = 0;
if (is64Bit) {
const auto ehdr = peekProc< Elf64_Ehdr >(module);
const auto phdr = peekProcVector< Elf64_Phdr >(module + ehdr.e_phoff, ehdr.e_phnum);
for (size_t i = 0; i < phdr.size(); ++i) {
if (phdr[i].p_type == PT_DYNAMIC) {
const auto dyn =
peekProcVector< Elf64_Dyn >(module + phdr[i].p_vaddr, phdr[i].p_memsz / sizeof(Elf64_Dyn));
for (size_t j = 0; j < dyn.size(); ++j) {
switch (dyn[j].d_tag) {
case DT_HASH:
hashTable = dyn[j].d_un.d_ptr;
break;
case DT_STRTAB:
strTable = dyn[j].d_un.d_ptr;
break;
case DT_SYMTAB:
symTable = dyn[j].d_un.d_ptr;
break;
}
if (hashTable && strTable && symTable) {
break;
}
}
break;
}
}
} else {
const auto ehdr = peekProc< Elf32_Ehdr >(module);
const auto phdr = peekProcVector< Elf32_Phdr >(module + ehdr.e_phoff, ehdr.e_phnum);
for (size_t i = 0; i < phdr.size(); ++i) {
if (phdr[i].p_type == PT_DYNAMIC) {
const auto dyn =
peekProcVector< Elf32_Dyn >(module + phdr[i].p_vaddr, phdr[i].p_memsz / sizeof(Elf32_Dyn));
for (size_t j = 0; j < dyn.size(); ++j) {
switch (dyn[j].d_tag) {
case DT_HASH:
hashTable = dyn[j].d_un.d_ptr;
break;
case DT_STRTAB:
strTable = dyn[j].d_un.d_ptr;
break;
case DT_SYMTAB:
symTable = dyn[j].d_un.d_ptr;
break;
}
if (hashTable && strTable && symTable) {
break;
}
}
break;
}
}
}
// Hash table pseudo-struct:
// uint32_t nBucket;
// uint32_t nChain;
// uint32_t bucket[nBucket];
// uint32_t chain[nChain];
const auto nChain = peekProc< uint32_t >(hashTable + sizeof(uint32_t));
if (is64Bit) {
for (uint32_t i = 0; i < nChain; ++i) {
const auto sym = peekProc< Elf64_Sym >(symTable + sizeof(Elf64_Sym) * i);
const auto name = peekProcString(strTable + sym.st_name, symbol.size());
if (name == symbol) {
return module + sym.st_value;
}
}
} else {
for (uint32_t i = 0; i < nChain; ++i) {
const auto sym = peekProc< Elf32_Sym >(symTable + sizeof(Elf32_Sym) * i);
const auto name = peekProcString(strTable + sym.st_name, symbol.size());
if (name == symbol) {
return module + sym.st_value;
}
}
}
return 0;
}
static void generic_unlock() {
pModule = 0;
pPid = 0;
}
static bool initialize(const std::multimap< std::wstring, unsigned long long int > &pids, const wchar_t *procname,
const wchar_t *modname = nullptr) {
pModule = 0;
if (!pids.empty()) {
auto iter = pids.find(std::wstring(procname));
if (iter != pids.end()) {
pPid = static_cast< procid_t >(iter->second);
} else {
pPid = 0;
}
} else {
pPid = 0;
}
if (!pPid) {
return false;
}
pModule = getModuleAddr(procname);
if (!pModule) {
pPid = 0;
return false;
}
int8_t ret = isProcessWin32(pPid);
if (ret == -1) {
generic_unlock();
return false;
}
isWin32 = ret;
ret = isProcess64Bit(pModule);
if (ret == -1) {
generic_unlock();
return false;
}
is64Bit = ret;
if (modname) {
pModule = getModuleAddr(modname);
if (!pModule) {
pPid = 0;
return false;
}
}
return true;
}
#endif