#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include "dynso.h" #include "dynso_internal.h" #define BREAK() asm volatile("int3":::"memory") /*static void print_link_map(struct link_map* glob) { printf("[*] link map:\n"); for (size_t i = 0; glob; glob = glob->l_next, ++i) { printf("- [%zu] %p -> '%s'\n", i, (void*)glob->l_addr, glob->l_name); } }*/ static ptrdiff_t get_priv1_priv2_offset(void) { // the offset between the priv1 and priv2 parts of a link_map is a // constant but only determinted at runtime, as we don't know with which // glibc version we're running (nor do we want to hardcode stuff) // link_map of the main executable always comes first (... I hope) struct link_map_priv1* exe_map = (struct link_map_priv1*)_r_debug.r_map; ElfW(Ehdr)* exe_ehdr = (ElfW(Ehdr)*)exe_map->l_addr; // priv2 contains a field with the entrypoint of an ELF ElfW(Addr) entry = exe_ehdr->e_entry + exe_map->l_addr; // time for crimes const ElfW(Addr)* crimes = (const ElfW(Addr)*)&exe_map->l_info[0]; for (size_t iter = 0; *crimes != entry && iter < 4096/sizeof(*crimes); ++crimes, ++iter) ; if (*crimes != entry) { //printf("[!] E: can't get priv2 offset, bailing out...\n"); return -1;//exit(1); } // crimes is now &priv2->l_entry; ptrdiff_t r = (ptrdiff_t)crimes - (ptrdiff_t)exe_map - offsetof(struct link_map_priv2, l_entry); //printf("[-] offset of priv2 is %zd\n", r); return r; } static ptrdiff_t get_priv2_priv3_offset(void) { struct link_map_priv1* exe_map = (struct link_map_priv1*)_r_debug.r_map; struct link_map_priv2* exe2 = priv1_to_priv2(exe_map); // priv3 has a 'versyms' field, which we can easily fetch from the dyn // table for comparison. otherwise, same method as above ElfW(Versym)* versym_good = NULL; for (size_t i = 0; _DYNAMIC[i].d_tag != DT_NULL && i < 128; ++i) { if (_DYNAMIC[i].d_tag == DT_VERSYM) { versym_good = (ElfW(Versym*))_DYNAMIC[i].d_un.d_ptr; break; } } if (versym_good == NULL) { //printf("[!] E: can't find VERSYM, bailing out...\n"); return -1;//exit(1); } const ElfW(Versym)** crimes = (const ElfW(Versym)**)&exe2->l_direct_opencount; for (size_t iter = 0; *crimes != versym_good && iter < 4096/sizeof(*crimes); ++crimes, ++iter) ; if (*crimes != versym_good) { //printf("[!] E: can't get priv3 offset, bailing out...\n"); return -1;//exit(1); } // crimes is now &priv3->l_versyms; ptrdiff_t r = (ptrdiff_t)crimes - (ptrdiff_t)exe2 - offsetof(struct link_map_priv3, l_versyms); //printf("[-] offset of priv3 is %zd\n", r); return r; } static struct link_map_priv2* priv1_to_priv2(struct link_map_priv1* lm) { if (!lm) return NULL; static ptrdiff_t d = -1; if (d < 0) { d = get_priv1_priv2_offset(); if (d < 0) return NULL; } return (struct link_map_priv2*)((ptrdiff_t)lm + d); } static struct link_map_priv3* priv2_to_priv3(struct link_map_priv2* l2) { if (!l2) return NULL; static ptrdiff_t d = -1; if (d < 0) { d = get_priv2_priv3_offset(); if (d < 0) return NULL; } return (struct link_map_priv3*)((ptrdiff_t)l2 + d); } static enum dynso_err get_rtld_symtab(struct rtldinfo* ret) { ElfW(Addr) ldso_base = (ElfW(Addr))getauxval(AT_BASE); //printf("[-] getauxval(AT_BASE) = %p\n", (void*)ldso_base); //printf("[-] getauxval(AT_SYSINFO_EHDR) = %p\n", (void*)getauxval(AT_SYSINFO_EHDR)); struct link_map* glob = _r_debug.r_map; //print_link_map(glob); for (size_t i = 0; glob; glob = glob->l_next, ++i) { if (glob->l_addr != ldso_base) continue; //printf("[*] [%zu] %p:%s is ld.so!\n", i, (void*)glob->l_addr, glob->l_name); ElfW(Sym)* symtab = NULL; char* strtab = NULL; size_t strsz = 0; size_t shsz = ~(size_t)0; ptrdiff_t syment = 0; bool sym_malloc = false, str_malloc = false; // gives us .dynsym, not .symtab -> kinda useless ElfW(Dyn)* dyn = glob->l_ld; for (size_t j = 0; dyn->d_tag != DT_NULL; ++dyn, ++j) { if (dyn->d_tag == DT_STRTAB) { strtab = (char*)(/*glob->l_addr +*/ dyn->d_un.d_ptr); //printf(" - [%zu] strtab\n", j); } if (dyn->d_tag == DT_SYMTAB) { symtab = (ElfW(Sym) *)(/*glob->l_addr +*/ dyn->d_un.d_ptr); //printf(" - [%zu] symtab\n", j); } if (dyn->d_tag == DT_STRSZ) { strsz = dyn->d_un.d_val; //printf(" - [%zu] strsz\n", j); } if (dyn->d_tag == DT_SYMENT) { syment = dyn->d_un.d_val; //printf(" - [%zu] syment\n", j); } } if (!memcmp((const void*)ldso_base, ELFMAG, SELFMAG)) { //printf("[-] ldso is ELF\n"); FILE* fff = fopen(glob->l_name, "rb"); // TODO: use l_origin instead? if (!fff) goto nah; ElfW(Ehdr)* ldso = (ElfW(Ehdr)*)ldso_base; if (ldso->e_shoff && ldso->e_shnum && ldso->e_shentsize) { //printf(" - shoff=0x%zx shnum=0x%zx\n", ldso->e_shoff, ldso->e_shnum); enum dynso_err errr = dynso_ok; if (fseek(fff, ldso->e_shoff, SEEK_SET) < 0) errr = dynso_ldso_ioerr; uint8_t shdr_blob[ldso->e_shentsize]; ElfW(Shdr)* shdr = (ElfW(Shdr)*)shdr_blob; if (fseek(fff, ldso->e_shoff + ldso->e_shstrndx*ldso->e_shentsize, SEEK_SET) < 0) errr = dynso_ldso_ioerr; if (fread(shdr_blob, 1, sizeof(shdr_blob), fff) < 0) errr = dynso_ldso_ioerr; char* shstrtab = (char*)malloc(shdr->sh_size); if (shstrtab == NULL) errr = dynso_oom; if (fseek(fff, shdr->sh_offset, SEEK_SET) < 0) errr = dynso_ldso_ioerr; if (fread(shstrtab, 1, shdr->sh_size, fff) < 0) errr = dynso_ldso_ioerr; for (size_t j = 0; j < ldso->e_shnum; ++j) { if (fseek(fff, ldso->e_shoff + j*ldso->e_shentsize, SEEK_SET) < 0) errr = dynso_ldso_ioerr; if (fread(shdr_blob, 1, sizeof(shdr_blob), fff) < 0) errr = dynso_ldso_ioerr; //printf(" [-] shdr [%zx] -> %p, type=0x%zx\n", j, shdr, shdr->sh_type); if (shdr->sh_type == SHT_SYMTAB) { //printf(" [-] using .symtab instead of .dynsym at offset 0x%zx\n", shdr->sh_offset); ////symtab = (ElfW(Sym)*)(ldso_base + shdr->sh_offset); // not mapped in phdrs... sym_malloc = true; symtab = (ElfW(Sym)*)malloc(shdr->sh_size); if (symtab == NULL) errr = dynso_oom; if (fseek(fff, shdr->sh_offset, SEEK_SET) < 0) errr = dynso_ldso_ioerr; if (fread(symtab, 1, shdr->sh_size, fff) < 0) errr = dynso_ldso_ioerr; shsz = shdr->sh_size; syment = shdr->sh_entsize; // not *technically* needed, but doing this for completeness' sake } if (shdr->sh_type == SHT_STRTAB && !strcmp(&shstrtab[shdr->sh_name], ".strtab")) { //printf(" [-] using .strtab instead of .dynstr at offset 0x%zx\n", shdr->sh_offset); str_malloc = true; strtab = (char*)malloc(shdr->sh_size); if (strtab == NULL) errr = dynso_oom; if (fseek(fff, shdr->sh_offset, SEEK_SET) < 0) errr = dynso_ldso_ioerr; if (fread(strtab, 1, shdr->sh_size, fff) < 0) errr = dynso_ldso_ioerr; strsz = shdr->sh_size; } } free(shstrtab); if (errr != dynso_ok) { if (fff) fclose(fff); if (str_malloc && strtab) free(strtab); if (sym_malloc && symtab) free(symtab); return errr; } } //else printf("wut\n"); } nah: if (symtab == NULL || strtab == NULL) { return dynso_ldso_stripped; } //printf(" [-] symtab=%p\tstrtab=%p\tstrsz=0x%zx\tsyment=0x%zx\n", // symtab, strtab, strsz, syment); //printf("[-] resolving hidden needed symbols\n"); ret->base = ldso_base; ret->symtab = symtab; ret->strtab = strtab; ret->strsz = strsz; ret->shsz = shsz; ret->syment = syment; ret->sym_malloc = sym_malloc; ret->str_malloc = str_malloc; return dynso_ok; } //printf("wtf\n"); return dynso_ldso_not_found;//exit(1); } typedef void* (*gnu_ifunc_f)(void); static void* sym2addr(ElfW(Addr) ldso_base, ElfW(Sym)* sym) { void* r = (void*)(ldso_base + sym->st_value); if (ELF64_ST_TYPE(sym->st_info) == STT_GNU_IFUNC) { // sigh //printf("[-] W: IFUNC. not tested.\n"); return ((gnu_ifunc_f)(r))(); } else return r; } static enum dynso_err link_private_rtld_syms(struct dynso_lib* l) { struct rtldinfo rtldinf; enum dynso_err r = get_rtld_symtab(&rtldinf); if (r != dynso_ok) return r; ElfW(Sym)* sym = rtldinf.symtab; for (size_t j = 0; j*rtldinf.syment < rtldinf.shsz; ++j, sym = (ElfW(Sym*))((ptrdiff_t)sym + rtldinf.syment)) { if (ELF64_ST_BIND(sym->st_info) != STB_LOCAL) continue; if (sym->st_value == 0) continue; if (sym->st_name >= rtldinf.strsz) continue; const char* name = &rtldinf.strtab[sym->st_name]; if (!*name) continue; if (!strcmp(name, "_dl_new_object")) { //printf("- [%zu] -> info=0x%x other=0x%x value=0x%zx\t'%s'\n", j, // sym->st_info, sym->st_other, sym->st_value, name); l->_dl_new_object = (dl_new_object_f)sym2addr(rtldinf.base, sym); } if (!strcmp(name, "_dl_add_to_namespace_list")) { //printf("- [%zu] -> info=0x%x other=0x%x value=0x%zx\t'%s'\n", j, // sym->st_info, sym->st_other, sym->st_value, name); l->_dl_add_to_namespace_list = (dl_add_to_namespace_list_f)sym2addr(rtldinf.base, sym); } if (!strcmp(name, "_dl_setup_hash")) { //printf("- [%zu] -> info=0x%x other=0x%x value=0x%zx\t'%s'\n", j, // sym->st_info, sym->st_other, sym->st_value, name); l->_dl_setup_hash = (dl_setup_hash_f)sym2addr(rtldinf.base, sym); } } if (rtldinf.sym_malloc) free(rtldinf.symtab); if (rtldinf.str_malloc) free(rtldinf.strtab); //printf("[*] _dl_new_object=%p\n", _dl_new_object); //printf("[*] _dl_add_to_namespace_list=%p\n", _dl_add_to_namespace_list); //printf("[*] _dl_setup_hash=%p\n", _dl_setup_hash); if (l->_dl_new_object && l->_dl_add_to_namespace_list && l->_dl_setup_hash) return dynso_ok; else return dynso_no_ldso_syms; } static enum dynso_err inject_in_scope_of(struct inject_undo_info* r, struct link_map_priv3* vic3, struct link_map_priv1* add) { r->alloced = NULL; struct r_scope_elem* oldscope = vic3->l_scope[0]; struct r_scope_elem* scope = NULL; scope = (struct r_scope_elem*)calloc(1, sizeof(*scope) + sizeof(struct link_map_priv1*)*(oldscope->r_nlist+1)); if (scope == NULL) return dynso_oom; r->alloced = scope; struct link_map_priv1** rlist = (struct link_map_priv1**)( (size_t)scope + sizeof(*scope)); for (size_t i = 0; i < oldscope->r_nlist; ++i) rlist[i] = oldscope->r_list[i]; rlist[oldscope->r_nlist] = add; scope->r_list = rlist; scope->r_nlist = oldscope->r_nlist + 1; // now apply the new scope vic3->l_scope[0] = scope; r->dest = &vic3->l_scope[0]; r->old = oldscope; //r->rlist = rlist; return dynso_ok; } static void inject_undo(const struct inject_undo_info* inf) { *inf->dest = inf->old; if (inf->alloced) free(inf->alloced); //free(inf->rlist); } enum dynso_err dynso_create(struct dynso_lib** ll, size_t address, char* realname, const char* libname, struct link_map* calling, Lmid_t ns) { if (!ll) return dynso_argument; if (calling == NULL) calling = _r_debug.r_map; struct dynso_lib* r = (struct dynso_lib*)calloc(1, sizeof(struct dynso_lib) + 16*sizeof(struct dynso_sym)); if (r == NULL) return dynso_oom; enum dynso_err e = link_private_rtld_syms(r); if (e != dynso_ok) { free(r); return e; } struct link_map_priv1* lm = r->_dl_new_object(realname, libname, lt_loaded, calling, 0, ns); if (!lm) { free(r); return dynso_no_newobj; } struct link_map_priv2* l2 = priv1_to_priv2(lm); struct link_map_priv3* l3 = priv2_to_priv3(l2); if (!l2) { free(r); return dynso_no_priv2; } if (!l3) { free(r); return dynso_no_priv3; } l2->l_faked = 1; l2->l_nbuckets = 0; // disables symbol lookup l2->l_gnu_bitmask = NULL; // disable GNU hashing, use SysV hashing l3->l_initfini = NULL; lm->l_addr = address; r->ns = ns; r->caller = (struct link_map_priv1*)calling; r->fake1 = lm; r->fake2 = l2; r->fake3 = l3; r->address = address; r->syms = (struct dynso_sym*)((ptrdiff_t)r + sizeof(*r)); r->nsyms = 0; r->syms_cap = 16; *ll = r; return dynso_ok; } enum dynso_err dynso_add_sym_ex(struct dynso_lib* f, const char* name, void* value, int type, size_t size) { if (!f || f->added || f->removed) return dynso_argument; if (f->nsyms == f->syms_cap) { f->syms_cap <<= 1; if (f->alloc_syms) { f->syms = (struct dynso_sym*)realloc( f->syms, f->syms_cap * sizeof(struct dynso_sym)); if (f->syms == NULL) return dynso_oom; } else { struct dynso_sym* nsym = (struct dynso_sym*)calloc(f->syms_cap, sizeof(struct dynso_sym)); if (nsym == NULL) return dynso_oom; memcpy(nsym, f->syms, sizeof(struct dynso_sym)*f->nsyms); f->syms = nsym; f->alloc_syms = true; } } f->syms[f->nsyms].name = name ; f->syms[f->nsyms].value = value; f->syms[f->nsyms].type = type; f->syms[f->nsyms].size = size; ++f->nsyms; return dynso_ok; } enum dynso_err dynso_bind(struct dynso_lib* l) { if (!l || l->removed || l->added) return dynso_argument; struct link_map_priv1* lm = l->fake1; struct link_map_priv2* l2 = l->fake2; struct link_map_priv3* l3 = l->fake3; lm->l_addr = l->address; size_t strsz = 2; // beginning and ending null terminators for (size_t i = 0; i < l->nsyms; ++i) { strsz += strlen(l->syms[i].name) + 1; // include null terminator } ElfW(Dyn)* dyn = (ElfW(Dyn)*)calloc(5, sizeof(ElfW(Dyn))); if (!dyn) return dynso_oom; Elf_Symndx* idx = (Elf_Symndx*)calloc(l->nsyms + 2, sizeof(Elf_Symndx)); if (!idx) { free(dyn); return dynso_oom; } ElfW(Sym)* sym = (ElfW(Sym)*)calloc(l->nsyms + 2, sizeof(ElfW(Sym))); if (!sym) { free(dyn); free(idx); return dynso_oom; } char* str = (char*)calloc(strsz, 1); if (!str) { free(dyn); free(idx); free(sym); return dynso_oom; } // NOTE: should we give it a phdr? seems to work without for now... str[0] = 0; //BREAK(); //sym[0] = ((ElfW(Sym)){.st_name=0, .st_info=0, .st_other=0, .st_shndx=0, .st_value=0, .st_size=0}; for (size_t i = 0, strind = 1; i < l->nsyms; ++i) { int typ = l->syms[i].type & 0xf; if (typ == 0) typ = STT_FUNC; sym[i+1].st_name = strind; sym[i+1].st_info = typ | (STB_GLOBAL << 4); sym[i+1].st_other = STV_DEFAULT; sym[i+1].st_shndx = 0; sym[i+1].st_value = (ElfW(Addr))l->syms[i].value - l->address; sym[i+1].st_size = 0; strcpy(&str[strind], l->syms[i].name); strind += strlen(l->syms[i].name) + 1; } //sym[l->nsyms+1] = ((ElfW(Sym)){.st_name=0, .st_info=0, .st_other=0, .st_shndx=0, .st_value=0, .st_size=0}; str[strsz-1] = 0; for (size_t i = 0; i < l->nsyms; ++i) idx[i] = i + 1; idx[l->nsyms+0] = STN_UNDEF; idx[l->nsyms+1] = SHN_UNDEF; dyn[0].d_tag = DT_SYMTAB; dyn[0].d_un.d_ptr = (ElfW(Addr))sym; dyn[1].d_tag = DT_STRTAB; dyn[1].d_un.d_ptr = (ElfW(Addr))str; // these two aren't strictly needed, but adding them for completeness' sake dyn[2].d_tag = DT_STRSZ; dyn[2].d_un.d_val = strsz; dyn[3].d_tag = DT_SYMENT; dyn[3].d_un.d_val = sizeof(ElfW(Sym)); dyn[4].d_tag = DT_NULL; lm->l_ld = dyn; size_t ldnum = 0; for (size_t i = 0; dyn[i].d_tag != DT_NULL; ++i, ++ldnum) lm->l_info[dyn[i].d_tag] = &dyn[i]; l2->l_ldnum = ldnum; // lm->l_origin = "something"; // maybe? eh. l->_dl_setup_hash(lm); l2->l_relocated = 1; l3->l_used = 1; l2->l_global = 1; l2->l_faked = 1; //l2->l_type = lt_loaded; l2->l_nbuckets = 1; l2->l_buckets = idx; // TODO l2->l_chain = idx; // TODO l2->l_gnu_bitmask = NULL; // disable GNU hashing, use SysV legacy hashing (was easier to 'implement' on my end) l3->l_initfini = NULL; l->_dl_add_to_namespace_list(l->fake1, l->ns); enum dynso_err r = inject_in_scope_of(&l->undoinf, priv1_to_priv3(l->caller), lm); if (r == dynso_ok) { l->dyn = dyn; l->sym = sym; l->idx = idx; l->str = str; l->added = true; return dynso_ok; } free(dyn); free(idx); free(sym); free(str); return r; } void dynso_remove(struct dynso_lib* l) { if (!l) return; if (l->added) { inject_undo(&l->undoinf); for (size_t i = 0; l->dyn[i].d_tag != DT_NULL; ++i) l->fake1->l_info[l->dyn[i].d_tag] = NULL; l->fake1->l_ld = NULL; l->fake2->l_ldnum = 0; free(l->dyn); free(l->sym); free(l->idx); free(l->str); } if (l->alloc_syms) free(l->syms); // TODO: free dyn, symtab, strtab, idxtab l->fake2->l_removed = 1; l->fake3->l_used = 0; l->fake2->l_global = 0; l->fake2->l_nbuckets = 0; l->removed = true; free(l); }