본문 바로가기
Rev/Write-up

justCTF - debug me if you can

by zooonique 2021. 3. 10.
반응형

이 문제는 ptrace 개념을 익히는데 도움이 되는 문제이다. (Feat, club)

일단 ptrace는 리눅스에서 지원하는 시스템 콜이다.

 

 

더보기

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

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

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

 

 

_ptrace_request [출처 : it-diary.tistory.com/9]

 

일단 문제파일은 crackme.enc flag.png.enc supervisor 세 개의 파일이다.

 

일단 supervisor를 분석해보자.

 

supervisor - main

일단 fork함수가 활용된다. fork를 호출하는 프로세스는 부모 프로세스가 되고, 새롭게 생성되는 프로세스는 자식프로세스가 된다.

 

fork함수 반환값은 부모프로세스에서는 자식 프로세스의  PID값을 반환받고,

자식 프로세스에서는 0을 반환한다.(실패시 -1반환)

 

즉 v4가 부모프로세스에서는 0보다 크고, 자식 프로세스에서는 0이나 -1를 반환하니까

 

if절은 부모 프로세스 else절은 자식프로세스가 실행한다.

 

sub_1E99인 부모 프로세스를 분석하자.

 

저기서 v14가 0x1337BABE이고 v15는 0x55CFADC477DD이다. 하지만 이 주소는 supervisor에서 찾을 수가 없다.

 

그러면 어디서 0x1337BABE를 가져온것인가 했는데, 정답은 자식 프로세스에서 실행한 crackme.enc이다.

 

여기에서도 0x55CFADC477DD는 없다. 근데 libcbase처럼 0x55CFADC4?000이 base주소이고, 여기서 7DD만큼 떨어진 부분에 0x1337BABE가 있지않을까하고 끝이 7DD인 부분을 찾아보니 0x1337BABE를 찾을 수 있었다.

 

그렇게 v13에 crackme.enc의 데이터를 가져온다.

그 후, 저 for문에서 뭐라도 하는줄 알았는데, 저 if문 안으로 들어가지도 않는다.

0x55EE75C50E부터 쭈욱 00 으로 채워져있어서, v13이랑 같아지지가 않는다.

 

위 코드는 v11에 0xFEEDC0DE가 있는 crackme.enc의 주소, v12에 0xDEADC0DE가 있는 crackme.enc의 주소가 저장된다.

 

이 과정에서 00으로 채워져있던 unk_557EE75C50E0가 다른 값들로 채워진다.

 

sub_557EE75C5D60

그 다음 부분을 살펴보면, sub_557EE75C1C4B함수를 활용하여 crackme.enc의 코드를 복호화한다.

 

peektext로 데이터를 가져와서 poketext로 특정 주소의 데이터를 바꿔 준다. 

 

RDX 주소에 RCX를 넣어준다. 즉, 부모프로세스에서 crackme.enc의 주소(RDX)에 RCX를 치환시킨다는 의미이다.

 

RDX와 RCX를 일일이 디버깅해서 찾을 수도 있겠지만, LD_PRELOAD HOOKING을 이용하여 추출할 수 있다.

 

특정 함수(ptrace)의 인자값이 PTRACE_SETREGS일때, RCX와 RDX를 가져오는 원리이다.

 

#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");
}

// club

gcc -shared -fPIC -ldl ptrace_hook.c -o ptrace_hook.so

LD_PRELOAD=$PWD/ptrace_hook.so ./supervisor

 

 

 

이렇게 패치할 곳들을 찾아냈으면 crackme.enc에서 실제 패치를 해본다. 패치는 IDA Python을 활용한다.

 

import ida_bytes

patches_1 = [
    (0x1800 , 0x45c748fffff84be8),
    (0x1871 , 0x89e0458b48000000),
    (0x18e5 , 0x1ebfffff7b5e8c7),
    (0x1838 , 0x8948d8458b48c289),
    (0x18a8 , 0x775fff883fffffd)
]

patches_2 = [
    (0x16db , 0xe8c78948000009ab),
    (0x174b , 0x8348008b48d8458b),
    (0x17bd , 0x1ebfffff93de8c7),
    (0x1712 , 0xe8c7894800000000),
    (0x1781 , 0xf975e8c78948f845)
]

# Patch the irrelevant 0xCC bytes
rip = [0x17f9, 0x16d4]
CC = [0x17dc, 0x16b7]

for i in range(len(rip)):
    ida_bytes.patch_bytes(CC[i], '\x90'*(rip[i] - CC[i])

# Patch the encrypted bytes
def patch(patches):
    for i in patches:
        print(hex(i[0]))
        ida_bytes.patch_qword(i[0], i[1])

patch(patches_1)
patch(patches_2)

 

이 코드로 패치를 하면, 이런식으로 main함수를 볼 수 있다.

 

secret_key가 있다는 것도 여기서 처음 알았다.

 

저기서 loc_16A3으로 secret_key가 있는지 확인하고, loc_13D7로 secret_key가 맞는지 확인한다.

 

근데 IDA버전에 따라서 IDAPython이 적용되는 함수들이 미세하게 달라서 그런지 제대로 패치가 되지 않는다.

 

아래의 python파일을 IDA 7.0에서 File>ScriptFile에서 적용하여 패치를 한다.

 

 

더보기
import ida_bytes

patches_main = [
    (0x1800 , 0x45c748fffff84be8),
    (0x1871 , 0x89e0458b48000000),
    (0x18e5 , 0x1ebfffff7b5e8c7),
    (0x1838 , 0x8948d8458b48c289),
    (0x18a8 , 0x775fff883fffffd)
]

patches_read_file = [
    (0x16db , 0xe8c78948000009ab),
    (0x174b , 0x8348008b48d8458b),
    (0x17bd , 0x1ebfffff93de8c7),
    (0x1712 , 0xe8c7894800000000),
    (0x1781 , 0xf975e8c78948f845)
]

patches_check_key = [
    (0x140b , 0xc700000000f845c7),
    (0x1494 , 0xbaf0458b1c7501f8),
    (0x151f , 0x1eb9004ebffffff),
    (0x144f , 0xbe0fef458800b60f),
    (0x14d7 , 0xf44539c0b60f1004)
]

patches_compare_char = [
    (0x13b9 , 0xb04a5b749d359b75),
    (0x13bd , 0x28c197b658b3b38d),
    (0x13c4 , 0x1ebfc4589ffc1d0),
    (0x13ba , 0x3bc43e2f0001b807),
    (0x13be , 0xffffffb805eb0000)
]

patches_calculate = [
    (0x1373 , 0x17e27f613f63871),
    (0x1376 , 0x1ebfc453a63b257),
    (0x1372 , 0x89d001e4458bc289)
]

# Patch the irrelevant 0xCC bytes
rip = [0x17f9, 0x16d4, 0x17cb, 0x1404, 0x13b2, 0x13d2, 0x136b, 0x1384, 0x152d]
CC = [0x17dc, 0x16b7, 0x17c6, 0x13E7, 0x1399, 0x13cd, 0x1352, 0x137f, 0x1528]

for i in range(len(rip)):
    ida_bytes.patch_bytes(CC[i], '\x90'*(rip[i] - CC[i]))

# Patch the encrypted bytes
def patch(patches):
    for i in patches:
        print(hex(i[0]))
        ida_bytes.patch_qword(i[0], i[1])

patch(patches_main)
patch(patches_read_file)
patch(patches_check_key)
patch(patches_compare_char)
patch(patches_calculate)

 

그리고 나서 secret_key를 체크하는 함수를 봐야하는데, 어셈블리를 확인해보면

중간중간 깨진 부분이 존재해서 이를 손수 패치해줘야한다.

 

 

이런 48 8B 부분을 Options>General>Number of opcode bytes : 4로 변경해주고,

Edit>PatchProgram>Change_bytes로 바꿔준다.

 

그리고 회색부분의 부분을 단축키 d로 데이터화 시키고 c로 코드화 시키는 요령을 활용하자.

 

이제 sub_13D7를 분석해보자.

 

__int64 __fastcall sub_13D7(__int64 a1, unsigned __int64 a2)
{
  int v2; // eax
  char v4; // [rsp+1Fh] [rbp-11h]
  unsigned int j; // [rsp+20h] [rbp-10h]
  unsigned int i; // [rsp+24h] [rbp-Ch]
  int v7; // [rsp+28h] [rbp-8h]
  unsigned int v8; // [rsp+2Ch] [rbp-4h]

  v8 = 1;
  v7 = 0;
  for ( i = 1; i <= 0x7F; ++i )
  {
    for ( j = 0; ; j = sub_1345(j, 2LL, 2LL) )
    {
      while ( 1 )
      {
        if ( a2 <= v7 )
        {
          v8 = -1;
          goto LABEL_13;
        }
        v2 = v7++;
        v4 = *(v2 + a1);
        if ( sub_1389(v4, 48) != 1 )
          break;
        j = sub_1345(j, 2LL, 1LL);
      }
      if ( sub_1389(v4, 49) != 1 )
        break;
    }
    if ( sub_1389(v4, 63) == 1 )
    {
      if ( i != byte_40C0[j] )
        v8 = -1;
    }
    else
    {
      v8 = -1;
    }
LABEL_13:
    if ( v8 == -1 )
      break;
  }
  if ( a2 != v7 + 1 )
    v8 = -1;
  return v8;
}

 

byte_40C0을 활용한다.

 

 

sub_1389는 두 매개변수가 같으면 1을 반환 다르면 -1을 반환한다.

 

하.. 뭐 역연산해서 코드를 짜면 아래와 같다.

이걸 scret_key로 만들어주고 supervisor를 실행시키면...

 

드디어 Decoding done!

 

 

 

반응형

'Rev > Write-up' 카테고리의 다른 글

RedpwnCTF 2020 - i-wanna-find-the-flag  (0) 2021.03.09
Tenable CTF 2021 - Play Me  (0) 2021.02.24
Tenable CTF 2021 - Forwards from Grandma  (0) 2021.02.23
Trollcat CTF - Solver  (0) 2021.02.17
RaziCTF2020 - Protected Conditions  (0) 2021.01.29