/* * uninformed research * ------------------- * * elfcmp - Compare a binary image with a process image to verify * that it has not been tampered with. * * skape * mmiller@hick.org * 01/19/2003 */ #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct _elfcmp_ctx { // Process side int pid; // File side int fd; unsigned long base; char image[1024]; size_t imageSize; Elf32_Ehdr elfHeader; } ELFCMP_CTX; unsigned long elfcmpInitialize(ELFCMP_CTX *ctx); unsigned char *elfcmpGetMemory(ELFCMP_CTX *ctx, unsigned long addr, unsigned long size); unsigned long elfcmpDeinitialize(ELFCMP_CTX *ctx); unsigned long elfcmpPhaseCode(ELFCMP_CTX *ctx); unsigned long elfcmpPhaseReadonlyData(ELFCMP_CTX *ctx); unsigned long elfcmpPhaseLibraries(ELFCMP_CTX *ctx); struct _phases_st { const char *name; unsigned long (*func)(ELFCMP_CTX *ctx); } phases[] = { { "STATIC CODE", elfcmpPhaseCode }, { "READ-ONLY DATA", elfcmpPhaseReadonlyData }, { "LIBRARIES", elfcmpPhaseLibraries }, { NULL, NULL }, }; int main(int argc, char **argv) { unsigned long failure = 0, attempts = 0; ELFCMP_CTX ctx; int x; if (argc < 2) { fprintf(stdout, "Usage: %s pid [binary]\n", argv[0]); return 0; } memset(&ctx, 0, sizeof(ctx)); ctx.pid = atoi(argv[1]) & 0xFFFF; if (argc == 3) strncpy(ctx.image, argv[2], sizeof(ctx.image) - 1); else snprintf(ctx.image, sizeof(ctx.image) - 1, "/proc/%d/exe", ctx.pid); if (!elfcmpInitialize(&ctx)) { elfcmpDeinitialize(&ctx); fprintf(stdout, "elfcmp: failed to initialize.\n"); return 0; } fprintf(stdout, "Comparing process %d to binary '%s'...\n\n", ctx.pid, ctx.image); for (x = 0; phases[x].name ; x++) { fprintf(stdout, "Phase %d... (%s)\n", x, phases[x].name); if (!phases[x].func(&ctx)) fprintf(stdout, "Phase %d failed.\n\n", x), failure++; else fprintf(stdout, "Phase %d succeeded.\n\n", x); attempts++; } fprintf(stdout, "%lu phases tested, %lu phases passed. (%s)\n", attempts, attempts - failure, (!failure)?"OK":"FAIL"); elfcmpDeinitialize(&ctx); return 1; } unsigned long elfcmpInitialize(ELFCMP_CTX *ctx) { struct stat statbuf; int status = 0; // Ptrace if (ptrace(PTRACE_ATTACH, ctx->pid, 0, 0) == -1) return 0; while (1) { wait(&status); if (WIFSTOPPED(status)) break; } // File if (!(ctx->fd = open(ctx->image, O_RDONLY))) return 0; if (fstat(ctx->fd, &statbuf) != 0) return 0; if (!(ctx->base = (unsigned long)mmap(NULL, (ctx->imageSize = statbuf.st_size), PROT_READ, MAP_PRIVATE, ctx->fd, 0))) return 0; // Copy the elf header for easy access memcpy(&ctx->elfHeader, (void *)ctx->base, sizeof(Elf32_Ehdr)); return 1; } unsigned char *elfcmpGetMemory(ELFCMP_CTX *ctx, unsigned long addr, unsigned long size) { unsigned char *ret = (unsigned char *)malloc(size); unsigned long x = addr, end = addr + size, data, left = size, y, retPos = 0; if (!ret) return NULL; memset(ret, 0, size); for (; x < end; x += 4) { // If the first address is non-readable, break out and return null if (((data = ptrace(PTRACE_PEEKDATA, ctx->pid, x, 0)) == -1) && (errno == 5)) { if (x == addr) { free(ret); ret = NULL; break; } } for (y = 0; y < sizeof(unsigned long) && left != 0; y++) { ret[retPos++] = ((unsigned char *)&data)[y]; left--; } } return ret; } unsigned long elfcmpDeinitialize(ELFCMP_CTX *ctx) { // Ptrace ptrace(PTRACE_DETACH, ctx->pid, 0, 0); // File if (ctx->base) munmap((void *)ctx->base, ctx->imageSize); if (ctx->fd) close(ctx->fd); return 1; } unsigned long elfcmpPhaseCode(ELFCMP_CTX *ctx) { unsigned long ret = 1, x = 0; Elf32_Shdr *sections = (Elf32_Shdr *)((ctx->base + ctx->elfHeader.e_shoff)); unsigned char *strings = (unsigned char *)((ctx->base + sections[ctx->elfHeader.e_shstrndx].sh_offset)), *fileCode = NULL, *processCode = NULL; for (x = 0; x < ctx->elfHeader.e_shnum; x++) { // No progbits, no play. if (sections[x].sh_type != SHT_PROGBITS) continue; // We only want executable sections for now. if (!(sections[x].sh_flags & SHF_EXECINSTR)) continue; fprintf(stdout, "\t%s...", strings + sections[x].sh_name); fflush(stdout); fileCode = (unsigned char *)((ctx->base + sections[x].sh_offset)); processCode = (unsigned char *)elfcmpGetMemory(ctx, sections[x].sh_addr, sections[x].sh_size); if ((!fileCode) || (!processCode)) { fprintf(stdout, "MISSING.\n"); ret = 0; continue; } if (memcmp(fileCode, processCode, sections[x].sh_size)) { fprintf(stdout, "MISMATCHED\n"); ret = 0; } else fprintf(stdout, "OK\n"); free(processCode); } return ret; } unsigned long elfcmpPhaseReadonlyData(ELFCMP_CTX *ctx) { unsigned long ret = 1, x = 0; Elf32_Shdr *sections = (Elf32_Shdr *)((ctx->base + ctx->elfHeader.e_shoff)); unsigned char *strings = (unsigned char *)((ctx->base + sections[ctx->elfHeader.e_shstrndx].sh_offset)), *fileCode = NULL, *processCode = NULL; for (x = 0; x < ctx->elfHeader.e_shnum; x++) { // We want all read-only, non-executable, and allocated pages if ((sections[x].sh_flags & SHF_WRITE) || (sections[x].sh_flags & SHF_EXECINSTR) || (!((sections[x].sh_flags & (SHF_ALLOC))))) continue; fprintf(stdout, "\t%s...", strings + sections[x].sh_name); fflush(stdout); fileCode = (unsigned char *)((ctx->base + sections[x].sh_offset)); processCode = (unsigned char *)elfcmpGetMemory(ctx, sections[x].sh_addr, sections[x].sh_size); if ((!fileCode) || (!processCode)) { fprintf(stdout, "MISSING.\n"); ret = 0; continue; } if (memcmp(fileCode, processCode, sections[x].sh_size)) { fprintf(stdout, "MISMATCHED\n"); ret = 0; } else fprintf(stdout, "OK\n"); free(processCode); } return ret; } unsigned long elfcmpPhaseLibraries(ELFCMP_CTX *ctx) { Elf32_Phdr *programs = (Elf32_Phdr *)((ctx->base + ctx->elfHeader.e_phoff)); unsigned long interpBase = 0, dl_rtld_map = 0, interpVirtualBase = 0; struct link_map *curr = NULL, *localMap = NULL; unsigned long ret = 1, x = 0; const char *interp = NULL; struct stat statbuf; FILE *ldd = NULL; int fd; // Find the interpreter for (x = 0; x < ctx->elfHeader.e_phnum; x++) { if (programs[x].p_type != PT_INTERP) continue; interp = (const char *)((ctx->base + programs[x].p_offset)); } // Load it if (!interp || ((fd = open(interp, O_RDONLY)) <= 0)) { fprintf(stdout, "\tWARNING: Image has no interpreter.\n"); return 0; } // Locate the _dl_rtld_map symbol. do { Elf32_Ehdr *interpEhdr = NULL; Elf32_Shdr *interpSections = NULL; unsigned long currentSymbol, numSymbols; if (fstat(fd, &statbuf) != 0) break; if (!(interpBase = (unsigned long)mmap(NULL, statbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0))) break; interpEhdr = (Elf32_Ehdr *)((interpBase)); interpSections = (Elf32_Shdr *)((interpBase + interpEhdr->e_shoff)); for (x = 0; !dl_rtld_map && x < interpEhdr->e_shnum; x++) { Elf32_Sym *sym = NULL; const char *symStrings = NULL; if (interpSections[x].sh_type != SHT_SYMTAB) continue; numSymbols = interpSections[x].sh_size / interpSections[x].sh_entsize; sym = (Elf32_Sym *)((interpBase + interpSections[x].sh_offset)); symStrings = (const char *)((interpBase + interpSections[interpSections[x].sh_link].sh_offset)); for (currentSymbol = 0; !dl_rtld_map && currentSymbol < numSymbols; currentSymbol++) { if (!strcmp(symStrings + sym[currentSymbol].st_name, "_dl_rtld_map")) dl_rtld_map = sym[currentSymbol].st_value; } } } while (0); if (interpBase) munmap((void *)interpBase, statbuf.st_size); close(fd); if (!dl_rtld_map) { fprintf(stdout, "\tWARNING: Interpreter has no _dl_rtld_map symbol.\n"); return 0; } // Now we need to know where the interpreter is loaded at. do { char cmd[1024]; cmd[sizeof(cmd) - 1] = cmd[0] = 0; snprintf(cmd, sizeof(cmd) - 1, "ldd %s", ctx->image); if (!(ldd = popen(cmd, "r"))) break; while (!interpVirtualBase && fgets(cmd, sizeof(cmd) - 1, ldd)) { unsigned long addr = 0; char lib[1024], garbage[1024]; lib[sizeof(lib) - 1] = garbage[sizeof(garbage) - 1] = 0; cmd[strlen(cmd) - 1] = 0; sscanf(cmd, " %1023s => %1023s (%x)", garbage, lib, (unsigned int *)&addr); if (!strstr(lib, interp)) continue; interpVirtualBase = addr; } } while (0); if (ldd) pclose(ldd); if (!interpVirtualBase) { fprintf(stdout, "\tWARNING: Executable doesn't NEED interpreter.\n"); return 0; } // Okay, finally, we have the vma of the rtld_map. Now we can walk it. do { unsigned long currentLink = interpVirtualBase + dl_rtld_map; char *name = NULL; do { curr = (struct link_map *)elfcmpGetMemory(ctx, currentLink, sizeof(struct link_map)); name = NULL; if (curr->l_name) name = (char *)elfcmpGetMemory(ctx, (unsigned long)curr->l_name, 1024); currentLink = (unsigned long)curr->l_prev; // Change the addy of the name to curr->l_name = name; // Add it to our local copy if (localMap) { curr->l_next = localMap; localMap->l_prev = curr; } else curr->l_next = NULL; localMap = curr; } while (currentLink); } while (0); // Enumerate linked libraries, comparing their file system images with the mapped images. for (curr = localMap; curr; curr = curr->l_next) { Elf32_Ehdr *libraryHeader = NULL; Elf32_Shdr *librarySections = NULL; const char *libraryStrings = NULL; unsigned char *libMemory, *procMemory; unsigned long libBase = 0; struct stat libstat; int libfd = 0; if (!curr->l_name || !curr->l_name[0]) continue; do { if ((libfd = open(curr->l_name, O_RDONLY)) <= 0) { fprintf(stdout, "\tWARNING: Could not load linked library '%s'.\n", curr->l_name); break; } if (fstat(libfd, &libstat) != 0) { fprintf(stdout, "\tWARNING: Could not load linked library '%s'.\n", curr->l_name); break; } libBase = (unsigned long)mmap(NULL, libstat.st_size, PROT_READ, MAP_PRIVATE, libfd, 0); libraryHeader = (Elf32_Ehdr *)((libBase)); if (!libraryHeader) break; // Cool, we've got the library mapped. Now we must compare symbols/data. librarySections = (Elf32_Shdr *)((libBase + libraryHeader->e_shoff)); libraryStrings = (const char *)((libBase + librarySections[libraryHeader->e_shstrndx].sh_offset)); for (x = 0; x < libraryHeader->e_shnum; x++) { unsigned long passed = 0; // Executable? if (librarySections[x].sh_flags & SHF_EXECINSTR) passed = 1; // Read only data? if (!passed && (!(librarySections[x].sh_flags & SHF_WRITE) && (librarySections[x].sh_flags & SHF_ALLOC))) passed = 1; if (!passed) continue; libMemory = (unsigned char *)((libBase + librarySections[x].sh_offset)); procMemory = (unsigned char *)elfcmpGetMemory(ctx, curr->l_addr + librarySections[x].sh_addr, librarySections[x].sh_size); // It matched, all is well. if (procMemory && !memcmp(libMemory, procMemory, librarySections[x].sh_size)) { free(procMemory); continue; } // Well fuck me sideways, a section doesn't match. Good heavens. // Maybe we can find out what symbol was modified? fprintf(stdout, "\tWARNING: Section '%s' in library '%s' mismatches!\n", libraryStrings + librarySections[x].sh_name, curr->l_name); if (librarySections[x].sh_flags & SHF_EXECINSTR) { unsigned long y, end = librarySections[x].sh_addr + librarySections[x].sh_size; unsigned long currentSymbol, numSymbols; unsigned char *fileSymData = NULL, *procSymData = NULL; const char *symStrings; Elf32_Sym *sym = NULL; for (y = 0; y < libraryHeader->e_shnum; y++) { // We only want symbols. if (librarySections[y].sh_type != SHT_DYNSYM) continue; sym = (Elf32_Sym *)((libBase + librarySections[y].sh_offset)); symStrings = (const char *)((libBase + librarySections[librarySections[y].sh_link].sh_offset)); numSymbols = librarySections[y].sh_size / librarySections[y].sh_entsize; for (currentSymbol = 0; currentSymbol < numSymbols; currentSymbol++) { if ((sym[currentSymbol].st_value >= librarySections[x].sh_addr) && (sym[currentSymbol].st_value <= end)) { fileSymData = (unsigned char *)((libBase + librarySections[x].sh_addr + (sym[currentSymbol].st_value - librarySections[x].sh_addr))); procSymData = (unsigned char *)elfcmpGetMemory(ctx, curr->l_addr + sym[currentSymbol].st_value, sym[currentSymbol].st_size); if (procSymData && memcmp(fileSymData, procSymData, sym[currentSymbol].st_size)) fprintf(stdout, "\t\tSymbol '%s' mismatches.\n", symStrings + sym[currentSymbol].st_name); if (procSymData) free(procSymData); } } } } ret = 0; if (procMemory) free(procMemory); } } while (0); if (libBase) munmap((void *)libBase, libstat.st_size); if (libfd) close(libfd); } // Flush map linked list while (localMap) { curr = localMap->l_next; if (localMap->l_name) free(localMap->l_name); free(localMap); localMap = curr; } return ret; }