Search code examples
c++linuxcode-injectionerrnoptrace

ptrace - Input/Output Error (errno 5) on 32 bits only


I was trying to inject a shared library in another process and I managed to get it working on x64. Though, when I tried using it for 32bits, something weird is happening: ptrace is not being able to execute properly due to an Input/Output error (errno 5). I don't know what to do, since this same code worked for x64. Then, I tried to make a smaller example using a function that I called test_ptrace. Surprisingly, the error doesn't happen there, though it is doing essentially the same thing (allocate memory on target process, inject a payload, set registers to match the payload, run the payload). When I saw the error was not happening, I tried again injecting the shared library with ptrace using a function called load_library. But unfortunately, there the error was again.

//this is the function that is NOT working, 'load_library'
void* load_library(pid_t pid, std::string path, int mode)
{
    int status;
    struct user_regs_struct old_regs, regs;
    void* dlopen_ex = (void*)0xf7c29700; //I disabled ASLR, so this address does not change
    void* handle_ex = (void*)-1;

    unsigned char inj_buf[] =
    {
        0x51,       //push ecx
        0x53,       //push ebx
        0xFF, 0xD0, //call eax
        0xCC,       //int3 (SIGTRAP)
    };

    size_t path_size = path.size();
    size_t inj_size  = sizeof(inj_buf) + path_size;
    void*  inj_addr  = allocate_memory(pid, inj_size, PROT_EXEC | PROT_READ | PROT_WRITE);
    void*  path_addr = (void*)((uintptr_t)inj_addr + sizeof(inj_buf));
    write_memory(pid, inj_addr, (void*)inj_buf, sizeof(inj_buf));
    write_memory(pid, path_addr, (void*)path.c_str(), path_size);

    if(ptrace(PTRACE_ATTACH, pid, NULL, NULL))
    {
        perror("PTRACE_ATTACH");
        std::cout << "Errno: " << errno << std::endl;
        return handle_ex;
    }
    wait(&status);
    if(ptrace(PTRACE_GETREGS, pid, NULL, &old_regs) == -1)
    {
        perror("PTRACE_GETREGS");
        std::cout << "Errno: " << errno << std::endl;
        return handle_ex;
    }

    regs.eax = (unsigned long)dlopen_ex;
    regs.ebx = (unsigned long)path_addr;
    regs.ecx = (unsigned long)mode;
    regs.eip = (unsigned long)inj_addr;

    if(ptrace(PTRACE_SETREGS, pid, NULL, &regs) == -1)
    {
        perror("PTRACE_SETREGS");
        std::cout << "Errno: " << errno << std::endl;
        return handle_ex;
    }
    
    if(ptrace(PTRACE_CONT, pid, NULL, NULL) == -1)
    {
        perror("PTRACE_CONT");
        std::cout << "Errno: " << errno << std::endl;
        return handle_ex;
    }

    waitpid(pid, &status, WSTOPPED);
    if(ptrace(PTRACE_GETREGS, pid, NULL, &regs) == -1)
    {
        perror("PTRACE_GETREGS");
        std::cout << "Errno: " << errno << std::endl;
        return handle_ex;
    }

    handle_ex = (void*)old_regs.eax;

    if(ptrace(PTRACE_SETREGS, pid, NULL, &old_regs) == -1)
    {
        perror("PTRACE_SETREGS");
        std::cout << "Errno: " << errno << std::endl;
        return handle_ex;
    }

    if(ptrace(PTRACE_DETACH, pid, NULL, NULL) == -1)
    {
        perror("PTRACE_DETACH");
        std::cout << "Errno: " << errno << std::endl;
        return handle_ex;
    }

    deallocate_memory(pid, inj_addr, inj_size);

    return handle_ex;
}
//this one, though, is working, but it is very similar to the function 
//above (except it doesn't restore the execution, but the code of the 
//other function doesn't even get there anyway.
void test_ptrace(pid_t pid)
{
    int status;
    struct user_regs_struct regs;
    unsigned char inj_buf[] =
    {
        0xCD, 0x80,               //int80 (syscall)
        0xCC,                     //int3  (SIGTRAP)
    };

    void* inj_addr = allocate_memory(pid, sizeof(inj_buf), PROT_EXEC | PROT_READ | PROT_WRITE);
    write_memory(pid, inj_addr, inj_buf, sizeof(inj_buf));

    std::cout << "--ptrace test started--" << std::endl;

    if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1)
    {
        perror("PTRACE_ATTACH");
        std::cout << "Errno: " << errno << std::endl;
        return;
    }

    wait(&status);

    if(ptrace(PTRACE_GETREGS, pid, NULL, &regs) == -1)
    {
        perror("PTRACE_GETREGS");
        std::cout << "Errno: " << errno << std::endl;
        return;
    }

    regs.eax = __NR_exit;
    regs.ebx = 222;
    regs.eip = (unsigned long)inj_addr;

    if(ptrace(PTRACE_SETREGS, pid, NULL, &regs) == -1)
    {
        perror("PTRACE_SETREGS");
        std::cout << "Errno: " << errno << std::endl;
        return;
    }

    if(ptrace(PTRACE_DETACH, pid, NULL, NULL) == -1)
    {
        perror("PTRACE_DETACH");
        std::cout << "Errno: " << errno << std::endl;
        return;
    }

    std::cout << "--ptrace test ended--" << std::endl;
}

Program entry:

int main()
{
    pid_t pid = get_process_id("target");
    std::cout << "PID: " << pid << std::endl;
    std::string lib_path = "<my_path>/ptrace-test/libtest.so";
    load_library(pid, lib_path, RTLD_LAZY);
    return 0;
}

Output:

PID: 2383
PTRACE_SETREGS: Input/output error
Errno: 5

If you need the whole project as a 'minimal' reproducible example, here you go: https://github.com/rdbo/ptrace-test

The PID is correct, I'm running as root, both the tracer and the tracee are compiled with G++ on 32 bits. Running up-to-date Manjaro. Any ideas?


Solution

  • I fixed it. Don't ask me how, though, I have no clue. I followed the exact same logic and out of sudden it worked. I used the working test_ptrace and kept putting the load_library code on it line by line to see what could be causing the problem. Turns out, I got it fixed and still don't know what it was. Anyways, here's the code (it is a bit messed up, because I didn't expect it to work):

    void load_library(pid_t pid, std::string lib_path)
    {
        int status;
        struct user_regs_struct old_regs, regs;
        unsigned char inj_buf[] =
        {
            0x51,       //push ecx
            0x53,       //push ebx
            0xFF, 0xD0, //call eax
            0xCC,       //int3 (SIGTRAP)
        };
    
        size_t inj_size = sizeof(inj_buf) + lib_path.size();
        void* inj_addr = allocate_memory(pid, inj_size, PROT_EXEC | PROT_READ | PROT_WRITE);
        void* path_addr = (void*)((uintptr_t)inj_addr + sizeof(inj_buf));
        write_memory(pid, inj_addr, inj_buf, sizeof(inj_buf));
        write_memory(pid, path_addr, (void*)lib_path.c_str(), lib_path.size());
    
        std::cout << "--ptrace test started--" << std::endl;
    
        if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1)
        {
            perror("PTRACE_ATTACH");
            std::cout << "Errno: " << errno << std::endl;
            return;
        }
    
        wait(&status);
    
        if(ptrace(PTRACE_GETREGS, pid, NULL, &old_regs) == -1)
        {
            perror("PTRACE_GETREGS");
            std::cout << "Errno: " << errno << std::endl;
            return;
        }
    
        regs = old_regs;
    
        long dlopen_ex = 0xf7c28700;
    
        regs.eax = dlopen_ex;
        regs.ebx = (long)path_addr;
        regs.ecx = RTLD_LAZY;
        regs.eip = (unsigned long)inj_addr;
    
        if(ptrace(PTRACE_SETREGS, pid, NULL, &regs) == -1)
        {
            perror("PTRACE_SETREGS");
            std::cout << "Errno: " << errno << std::endl;
            return;
        }
    
        ptrace(PTRACE_CONT, pid, NULL, NULL);
        waitpid(pid, &status, WSTOPPED);
        ptrace(PTRACE_SETREGS, pid, NULL, &old_regs);
    
        if(ptrace(PTRACE_DETACH, pid, NULL, NULL) == -1)
        {
            perror("PTRACE_DETACH");
            std::cout << "Errno: " << errno << std::endl;
            return;
        }
    
        deallocate_memory(pid, inj_addr, inj_size);
    
        std::cout << "--ptrace test ended--" << std::endl;
    }
    

    Output:

    PID: 24615
    --ptrace test started--
    --ptrace test ended--
    

    Target Process:

    PID: 24615
    dlopen: 0xf7c28700
    Waiting...
    Injected!
    Waiting...
    Waiting...
    Waiting...
    Waiting...
    

    EDIT: I just remembered I ran the following command as root (could've been it, not sure though): echo 0 > /proc/sys/kernel/yama/ptrace_scope
    EDIT2: It was not it, the code is still working after a reboot. Also, I improved the code a bit on the GitHub repository.