最近我对Linux感兴趣,并且我正在尝试创建一个能够在Windows下注入共享对象(即.so文件,'动态可加载库',“DLL”)的程序。我知道这可以是通过设置环境变量来完成,但我想在已经运行的进程上进行。
我已经知道如何在Windows下执行此操作。有几种方法,但一般来说,您可以通过使用CreateRemoteThread()创建远程线程来调用LoadLibrary()。当然,您需要远程进程中LoadLibrary的地址,但(根据我的经验),每个进程的偏移量始终相同。
我已经做过一些关于如何在Linux下完成这项工作的研究。例如,Phrack 59中一个有趣的article显示了如何做到这一点。这篇文章还附有一个源代码,但由于对目标进程做了一些假设并且它是32位,我无法让它工作。我碰到的其他一些事情:a codeproject article,但是这个只解释了如何在gdb中做到这一点。 (我会发布更多链接,但网站限制我2: - /。)
首先,我想获取远程进程中dlopen()函数的地址。为此,我发现我必须获取进程的ELF头并遍历符号表。实际上,我设法通过以下方式做到了这一点:
1)获取ELF标头(根据我的经验,存储在0x400000下的64位以下)。
2)在标题为DYNAMIC的程序标题中找到全局偏移表。
3)通过访问全局偏移表中的第二个条目来检索第一个link_map。
4)迭代link_map链的动态部分,从而获得字符串表,符号表和哈希表的地址(* Hash_Table + 0x4保存符号表中的条目数量。)
5)通过符号表循环
我程序的一些示例输出:
** looking at lib "" **
Trying to find symbol main in symbol table... numentries: 49
index 1 name: val: 0
...
index 49 name: memcpy val: 0
symbol not found.
** looking at lib "" **
Trying to find symbol main in symbol table... numentries: 11
index 1 name: val: 0
...
index 11 name: __vdso_time val: 0xffffffffff700a80
symbol not found.
** looking at lib "/lib/x86_64-linux-gnu/libc.so.6" **
Trying to find symbol main in symbol table... numentries: 2190
index 1 name: val: 0
...
index 2190 name: wcpcpy val: 0xa3570
symbol not found.
但是,我无法找到dlopen的有效地址! (或者甚至是main的地址,就此而言!)为了测试目的,我让程序分析自己,所以我肯定知道main存在。我还尝试了readelf -s来查看符号表,它显示:
Symbol table '.symtab' contains 151 entries:
Num: Value Size Type Bind Vis Ndx Name
...
149: 0000000000401880 216 FUNC GLOBAL DEFAULT 13 main
所以,不知怎的,readelf设法找到了主要的,而我却做不到。我还看了一下libelf库,但它依赖于从应用程序文件中读取,而不是访问进程的内存(即在进程运行时无法使用它。)有没有人知道我如何找到它dlopen()在远程进程中,甚至是主要的,对于那个问题?
我正在运行Ubuntu 12.04 64位。
答案 0 :(得分:1)
首先,关于主要地址:
似乎必须使用Section Headers来找出main的地址。仅仅使用动态部分这样做似乎是不可能的。运行readelf -D -s
和readelf -D --dyn-sym
也不会提供主要地址。
现在,关于找到dlopen
的地址。事实证明我正在从哈希表中读取错误数量的符号表条目。有两种类型的哈希表(我到目前为止遇到过):DT_HASH
表和DT_GNU_HASH
表。前者具有hash_table_addr + 4
(source)的条目数,后者没有明确指定哈希表的数量。人们需要通过迭代哈希表的桶表来获得这个数量。除此之外,我的方法很好,现在我能够找到dlopen
,malloc
等的地址。
要从哈希表中获取符号表的条目数(即大小),可以使用(C):
ssize_t ReadData(int pid, void* buffer, const void* source, ssize_t size)
{
// Under Ubuntu and other distros with a 'hardened kernel', processes using this function
// should be run as root.
// See https://wiki.ubuntu.com/SecurityTeam/Roadmap/KernelHardening#ptrace_Protection
iovec local_vec;
local_vec.iov_base = Buffer;
local_vec.iov_len = Size;
iovec remote_vec;
remote_vec.iov_base = Address;
remote_vec.iov_len = Size;
return process_vm_readv(pid, &local_vec, 1, &remote_vec, 1, 0);
}
unsigned long FindNumEntriesHashTable(int pid, void* TablePtr, const void* TableLibAddr)
{
// Check if TablePtr is smaller than 0.
unsigned long pointer = ((long)TablePtr < 0) ? (unsigned long)TablePtr + (unsigned long)TableLibAddr : (unsigned long)TablePtr;
unsigned long ret = 0;
ReadData(pid, &ret, (void*)(pointer + sizeof(Elf_Word)), sizeof(Elf_Word));
return ret;
}
unsigned long FindNumEntriesGnuHashTable(int pid, void *TablePtr, const remote_voidptr TableLibAddr)
{
unsigned long pointer = ((long)TablePtr < 0) ? (unsigned long)TablePtr + (unsigned long)TableLibAddr : (unsigned long)TablePtr;
// Read in required info on the gnu_hash table
unsigned long nbuckets = 0;
unsigned long symndx = 0;
unsigned long maskwords = 0;
ReadData(pid, &nbuckets, (const remote_voidptr)pointer, sizeof(Elf_Word));
ReadData(pid, &symndx, (const remote_voidptr)(pointer + sizeof(Elf_Word)), sizeof(Elf_Word));
ReadData(pid, &maskwords, (const remote_voidptr)(pointer + 2 * sizeof(Elf_Word)), sizeof(Elf_Word));
// Calculate the offset to the bucket table. The size of the maskwords entries is 4 under 32 bit, 8 under 64 bit.
unsigned long masktab_size = (ENV_NUMBITS == 32) ? 4 * maskwords : 8 * maskwords;
unsigned long buckettab_offs = 4 * sizeof(Elf_Word) + masktab_size;
// Read in the bucket table
Elf_Word buckettab[nbuckets];
ReadData(pid, &buckettab, (const remote_voidptr)(pointer + buckettab_offs), nbuckets * sizeof(Elf_Word));
// Loop through the bucket table. If the given index is larger than the already known index, update.
unsigned long num_entries = 0;
for (size_t i = 0; i < nbuckets; i++)
{
if (num_entries == 0 || buckettab[i] > num_entries)
{
num_entries = buckettab[i];
}
}
if (num_entries == 0)
{
return 0;
}
// Add one, since the first entry is always NULL.
return num_entries++;
}
答案 1 :(得分:1)
你的问题有很多不好的假设和误解,我不知道从哪里开始。
使用CreateRemoteThread()
这个Windows功能没有Linux等价物(可能是错误的功能)。
获取ELF标题(根据我的经验,存储在0x400000下的64位以下)。
这是非PIE可执行文件的默认位置。 绝不是保证。
我想获取远程进程中的dlopen()函数的地址
您假设远程进程中 dlopen,但只有远程进程二进制文件与libdl
链接时才会出现这种情况。这是所有Linux二进制文件的一小部分。
某种程度上,readelf设法找到了主要的,而我却做不到。
您不了解动态和静态ELF符号表之间的区别。运行nm a.out
和nm -D a.out
并比较结果将是说明性的。你会在一个中找到main
,而在另一个中找不到。
注意:静态符号表在运行时不需要,可以被剥离。
答案 2 :(得分:0)
您可能不应该为主可执行文件使用硬编码的基址(因为它不必是0x400000)并迭代在第二个GOT条目中找到的link_map结构(因为动态链接器想要查找的任何内容可能在那里而不是)。
更好的解决方案是:
_dl_open
函数指针,您可能想要调用它。这是libdl的作用(使用ld.so中的_dl_open):
dlopen_doit (void *a)
{
struct dlopen_args *args = (struct dlopen_args *) a;
if (args->mode & ~(RTLD_BINDING_MASK | RTLD_NOLOAD | RTLD_DEEPBIND
| RTLD_GLOBAL | RTLD_LOCAL | RTLD_NODELETE
| __RTLD_SPROF))
GLRO(dl_signal_error) (0, NULL, NULL, _("invalid mode parameter"));
args->new = GLRO(dl_open) (args->file ?: "", args->mode | __RTLD_DLOPEN,
args->caller,
args->file == NULL ? LM_ID_BASE : NS,
__dlfcn_argc, __dlfcn_argv, __environ);
}
使用:
# define GLRO(name) _rtld_local_ro._##name
和
struct rtld_global_ro {
[...]
void *(*_dl_open) (const char *file, int mode, const void *caller_dlopen,
Lmid_t nsid, int argc, char *argv[], char *env[]);
[...]
};