CTF

justCTF2020] debug_me_if_you_can ( LD_PRELOAD 후킹, ptrace )

wsoh9812 2021. 2. 1. 22:29

후 기 : 이번 대회로 ptrace개념을 정확히 알게되었다.!!!!!!!!

기 본 개 념


ptrace란??

부모 프로세스가 다른 프로세스의 실행을 관찰하거나 제어하는 수단을 제공한다. (자식 프로세스)

 

아래의 표는

ptrace함수의 첫번째 인자로 들어가는 request마다의 행동을 요약해 둔 것입니다.

TRACE_TRACEME

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

 

PTRACE_PEEKTEXT, PTRACE_PEEKDATA

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

 

PTRACE_POKETEXT, PTRACE_POKEDATA

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

 

PTRACE_PEEKUSER

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

PTRACE_GETREGS, PTRACE_GETFPREGS

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


PTRACE_POKEUSER

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

 

PTRACE_CONT

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

 

PTRACE_SETREGS, PTRACE_SETFPREGS

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

 

PTRACE_SYSCALL, PTRACE_SINGLESTEP

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

 

PTRACE_KILL

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

 

PTRACE_ATTACH

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

 

PTRACE_DETACH

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



출처 : it-diary.tistory.com/9

 

Shared Library Injection (3)

Shared Library Injection 세 번째 포스트입니다. 이전 글은 아래의 링크에서 확인하시기 바랍니다. 2016/07/18 - [Security] - Shared Library Injection (1) 2016/07/12 - [Security] - Shared Library Injectio..

it-diary.tistory.com

 


 

 

더보기

지문 : I bet you can't crack this binary protected with my custom bl33d1ng edg3 pr0t3c70r!!!111oneoneone

 

문제 파일

3개의 문제파일이 주어진다.

 

 

흐 름

"supervisor을 통해 자식프로세스가 생성되고, 자식 프로세스는 crackme.enc를 실행시킨다.

이후 flag.png.enc를 복호화 한다(Child). 여기서!!! 부모 프로세스인 supervisor은 자식프로세스를 컨트롤한다."

 

[참고] crackme.enc를 그냥 실행되지는 않는다. 뒤에 알게 되겠지만 crackme.enc가 패치 되어 실행되기 때문이다.

 

superviosr의 main 함수

 

 

자식프로세스부터 보자.


sub_557CBFCF527F

[중 요]

 자식프로세스가 생성되면, 빨간색박스로 인해 crackme.enc가 부모에 의해 attach된다.

, 부모 프로세스에 의해서 자식프로세스가 디버깅 된다.

이 말은 자식프로세스에 debuger가 붙어져 있기 때문에 또 다른 debuger가 붙을 수 없다.

그래서 이 문제의 이름처럼 자식프로세스에 디버거를 붙일 수 없다.

 

 

 

부모가 실행하는 코드


supervisor의 sub_557CBFC4E99

[중 요]

24 : 자식프로세스를 다시 실행시킨다.

30 : 자식프로세스의 레지스터를 모두 들고온다.

31 : 자식프로세스의 특정 주소에 값을 읽는다.

32 : 만약 그값이 0x1337BABE이 아니면 break; ( 그 값이 아래의 사진이다. )

 

crackme.enc

이 부분의 헥스값을 읽어 들인다고 볼 수 있다.!

 

supervisor의 sub_557CBFC4E99

다음 위의 코드를 실행하는데 무슨 의도로 이 코드를 작성했는지는 분석하지 못했다..

 

 

supervisor의 sub_557CBFC4E99

[중 요]

이 부분이 핵심코드이다. 특정영역의 데이터를 읽어와서 다른 값으로 치환한다.

즉 crackme.enc의 코드를 복호화하는 작업이라고 볼 수 있다.

peektext로 데이터를 가지고 와서 poketext을 이용하여 특정 주소의 데이터를 바꾸어 준다.

 

우리는 이 부분을 후킹하여 값들을 가지고 올 수 도있고, 손으로 직접 뽑아 낼 수 도있다.

 

POKETEXT 실행 후

[중 요]

RDX가 주소이고, RCX가 데이터이다.

RDX라는 주소에 RCX값을 넣는다.

 

이것을 손으로 뽑는데 한계가 있어 LD_PRELOAD HOOKING을 이용하였다.

 

#define _GNU_SOURCE

#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <stdarg.h>
#include <sys/utsname.h>
#include <sys/stat.h>

long int ptrace(enum __ptrace_request __request, ...){
    pid_t caller = getpid();
    va_list list;
    va_start(list, __request);
    pid_t pid = va_arg(list, pid_t);
    void* addr = va_arg(list, void*);
    void* data = va_arg(list, void*);
    long int (*orig_ptrace)(enum __ptrace_request __request, pid_t pid, void *addr, void *data);
    orig_ptrace = dlsym(RTLD_NEXT, "ptrace");
    long int result = orig_ptrace(__request, pid, addr, data);
    if (__request == PTRACE_SETREGS){
        unsigned long rip = *((unsigned long*)data + 16) - 0x555555554000;
        printf("SETREGS: rip: 0x%lx\n", rip);  
    } else if (__request == PTRACE_POKETEXT){
        printf("POKETEXT: (addr , data) = (0x%lx , 0x%lx)\n", (unsigned long)addr - 0x555555554000, (unsigned long)data);
    }
    return result;
}

__attribute__((constructor)) static void setup(void) {
    fprintf(stderr, "called setup()\n");
}

gcc -shared -fPIC -ldl hook.c -o preload_test.so

LD_PRELOAD=$PWD/preload_test.so ./supervisor

 

LD_PRELOAD hooking result

이 결과를 바탕으로 어느 주소에서 어떤값으로 수정해주는지 알 수 있다.

 

 

좀 더 분석을 해보면 secret_key를 읽어서 password를 check한다는 것을 알 수 있었다.

secret_key를 만들어 준다음 후킹을 시도해 바뀌는 값들을 모두 찾아준다. 

 

 

이제 우리는 해당주소에가서 데이터를 수정해야주어야 한다.

 

 

idaPython을 이용하여 패치를 해주면 복구가 될 것이다.(코드는 길어서 생략)

주의해야할점이 있다. 

전체적인 패턴이 int3이후의 특정 주소에 rip가 설정된다.

그 int3~rip지점 까지 다 nop으로 처리 해주어야한다. 안그러면 코드가 깨져서보인다.

(코드를 복구하기 위해 아이다로 노가다를 좀 해본 결과로 알게 되었다.ㅠㅠ) 

 

key 검증 로직

key를 검증하는 로직이다. 

 

이를 역연산 해주면 되는데,,, 꽤나 오래걸렷다. 코드는 아래와 같다.

table = [27, 89, 41, 76, 61, 111, 34, 127, 38, 28, 44, 47, 7,
 78, 23, 30, 97, 10, 83, 16, 52, 101, 74, 66, 88, 8,
 29, 96, 51, 85, 55, 68, 82, 57, 46, 114, 15, 110, 126,
 63, 50, 71, 90, 19, 25, 6, 122, 81, 24, 26, 99, 72,
 2, 119, 62, 84, 53, 22, 4, 94, 79, 73, 48, 3, 21, 113,
 77, 17, 56, 18, 5, 69, 39, 104, 58, 117, 9, 32, 1, 64,
 105, 35, 106, 59, 65, 95, 123, 87, 60, 31, 102, 86,
 92, 12, 54, 115, 45, 103, 67, 93, 75, 40, 118, 120,
 125, 49, 109, 37, 20, 116, 91, 107, 13, 80, 112, 100,
 14, 98, 43, 11, 70, 42, 124, 121, 108, 36, 33]


key =''
def func(x):
    tmp=''
    while x > 0:
        if x&1 == 0:
            tmp +='1'
            x = (x-2) // 2
        else:
            tmp +='0'
            x = (x-1 )// 2
        #print tmp
    return tmp[::-1]

for i in range(len(table)):
    x = table.index(i+1)
    key +=func(x)+'?'
    
print (key)

이렇게 뽑아낸 키를 secret_key에 저장하고, 실행하면

 

 

flag