나도 해본다, 리버싱!(feat.Defcon2016 Step)

By choirish | January 11, 2017

나도 해본다, 리버싱! (feat. Defcon2016 Step)



짠! 리버싱도전기2를 갖고 돌아온 choirish입니다 ㅎㅎ

지난번에 옛 기억을 되살리며 isdebuggerpresent 함수와 관련된 핵 쉬운 리버싱 문제(feat. CSAW CTF 2013)를 풀어놓더니 갑자기 난이도 껑충 올라간 문제를 줍줍하여 돌아오게 되었네요 ㅋ.ㅋ




DEF CON CTF 2016 Qualifier Step

오늘 풀어볼 문제는 바로바로 데프콘 2016 CTF 예선에 출제되었던 “Step”이라는 문제입니다. 꺄륵!

대회 출제 당시 제공되었던 정보는 다음과 같습니다.


Step by step.

Running at step_8330232df7a7e389a20dd37eb55dfc13.quals.shallweplayaga.me:2345


문제 바이너리와 서버 접속 주소가 주어졌습니다.

주어진 바이너리를 분석하여 key1과 key2를 구하고, 서버에 접속해서 알맞은 key1, key2를 입력하면 flag 파일을 읽을 수 있는 시스템이었습니다 ㅎㅎ




※ 당 부 말 씀 ※

  • 현재는 문제 서버가 열려있지 않아 flag 파일을 직접 읽을 수 없었습니다. ‘로컬에서 임의로 flag 파일을 생성해놓고 그냥 풀면 되지 않나?’라는 의문을 가질 수 도 있겠으나…. 문제의 특성상…. 바이너리를 일반적으로 실행하면 key1을 입력한 후 sigtrap이 발생하여 프로그램이 종료되어버…..린다능 또륵
  • 실제 문제 서버에서는 이를 적절히 처리해주는 장치가 있었거나, sigtrap 이후의 부분이 제대로 패치된 바이너리를 실행시켜놓은 것이 아닐까… 살포시 생각해봅니다.
  • 그리하여 본 포스팅에서는 문제 바이너리를 분석하여 key1과 key2를 구해내는 과정까지 알려드리는 걸로! 사실상 key1, key2 구하면 flag 얻을 수 있는 거니까! ㅎㅎ




문제 바이너리는 이 GitHub repository에서 다운로드하실 수 있습니다! 친절 상세한 롸업도 제공되어있어요! ㅎㅎ 저도 많이 참고했다능(소곤소곤) 하지만!! 저는 여기 롸업에 자세히 안나와있는 부분을 보충하여 생동감 넘치는 스샷과 함께 한글로 문제 풀이를 제공할 예정이랍니다 후후훟!

그럼 본격적인 롸업의 길로 기기~




[ 들어가기 ] 바이너리를 열어보자!

먼저 바이너리의 정보를 file 커맨드로 알아보면 쁍! ELF 64-bit 파일이네염!



실행시켜보면? key1을 입력하라고 하고선 틀리면 Failed 쁍! 엔터도 안쳐주고 양아치임………ㅋㅋ



그럼 key1이 뭣인가 알아보기위해 ida를 켜자!!

ida로 열면 맨 처음에 쁍!



start 부분에서 main을 클릭하여 들어가면 쁍!



분석 내용 정리

  • “Key1: “ 을 출력한다.
  • fgets로 사용자의 입력을 받는다.
  • 요로코롬 인자 4개를 받는 sub_400D63 함수를 호출한다.
  • sub_400D63(특정부분의 offset, 특정값(length), 사용자의 입력값, 특정값(checksum))
  • sub_400D63 함수를 호출한 이후에 word_400E0E 부분을 호출한다.


sub_400D63 함수가 xor연산을 통해 특정 부분(0x400E0E)을 복구하는 함수라고 하였는데!
sub_400D63 함수를 분석하여 0x400E0E를 복구하는 것이 바로 첫 번째 과제입니다 ㅎㅎㅎ

문제 이름이 왜!! Step!!!!??????인지 알 수 있게 되는 순간입니다. ㅎㅎ
앞으로 복구를 크게 3번!이나 해야 하고! key1도 구해야 되고! key2도 구해야합니다 ㅎㅎ
한 단계, 단계를 차근차근 해결해 나가야하는 것이죠!

구분하기 나름이지만 저는 총 5단계로 나눠보았습니다.


step 1

  • sub_400D63 함수를 분석하여 Key 1 구하기

step 2

  • sub_400D63 함수의 알고리즘을 적용하여 sub_400E0E 복구하기

step 3

  • sub_400D63 함수의 알고리즘을 적용하여 sub_400936 복구/분석하기

※ step 4 ※

  • sub_400936 함수의 알고리즘을 적용하여 0x400E96 부터 instruction 한 줄씩 복구하기
  • 0x400936~0x40103D에 포함된 함수들을 모두 복구해 줘야함!!
  • 그러면 main이 복구되면서 “call sub_400E0E” 다음에 “call sub_400EAC”가 등장함!
    • sub_400EAC와, 내부에서 호출하는 함수들까지도 위 범위에 포함된다면 다 복구해 줘야함!!

step 5

  • sub_400EAC 함수에서 호출하는 sub_400A22와 sub_400C31을 분석하여 Key 2 구하기


핳… 참 많네요 ㅎㅎㅎ 1단계부터 차례차례 단계를 정복해봅시다 ‘ㅅ’!!




[ Step 1 ] sub_400D63 분석 및 Key 1 구하기

sub_400D63 함수부터 어떻게 생긴 놈인가 직접 살펴봅시다 ㅎㅎ (F5가 눌러져요!)
(보기 쉽게 “a1 = offset, a2 = length, a3 = input, a4 = check” 로 rename하였습니다.)


__int64 __fastcall sub_400D63(_BYTE *offset, int length, __int64 input, int check)
{
  __int64 result; // rax@4
  unsigned __int8 v5; // [sp+23h] [bp-Dh]@1
  unsigned int v6; // [sp+24h] [bp-Ch]@1
  _BYTE *i; // [sp+28h] [bp-8h]@1

  v5 = 0;
  v6 = 0;
  for ( i = offset; &offset[length] > i; ++i )
  {
    *i ^= *(_BYTE *)(v5 + input);
    v6 += *i;
    v5 = (char)(v5 + 1) % 4;
  }
  result = v6;
  if ( v6 != check )
  {
    printf("Failed");
    exit(0);
  }
  return result;
}


분석 내용 정리

  • 특정 부분의 1byte와 input의 1byte를 xor한다.
  • 특정 부분의 offset부터 length의 길이만큼 xor 연산을 진행한다.
  • input에서 xor 연산에 사용되는 부분은 첫 4byte뿐이다.
  • v5 = (char)(v5+1) % 4; 에서 알 수 있듯이 input은 첫 4byte만 반복되며 xor 연산에 사용된다.
  • 즉… input은 4자리만 입력하면 된다는 것! 그게 과연 무엇일까?
  • xor 연산한 1byte 값을 각각 모두 더해 v6에 저장하고, v6의 값이 check와 일치하지 않으면 Fail!!


그럼! input은 과연 무엇이어야 할까?

짐작해 볼 수 있건대… (사실 미리 말해버리긴 했지만 ㅋ)
여기서 sub_400D63은, 본래 함수인데 망가져서 알아보기 힘든 0x400E0E 요부분을 xor 연산을 통해 decrypt하고! 제대로 된 함수로 작동할 수 있도록 복구시켜주는 녀석입니다.

그렇다면….. 0x400E0E가 함수라고 했잖아요!!
그럼 함수의 첫 시작은 보~~~통 요로코롬 생겼다는 걸 여러분 이미 수도 없이 봐서 알고 계시죠?




여기를 hex값으로 보면!



여기의 opcode가 쫜 0x55 0x48 0x89 0xE5 입니다! 딱 4byte!!

그럼 이제 아시겠나요? 제가 뭘 하려는지? 예상컨대 xor 연산 후 0x400E0E 부분의 첫 4byte는 저 opcode와 일치할 거라는 삘이 팍팍! 심하게 옵니다 ㅎㅎ (이러한 사고의 흐름 배워갑니다 쥽쥽)


즉, (복구 전 0x400E0E의 첫 4byte)^(input의 첫 4byte) = 0x554889E5이니까

결론적으로 input(4byte) = (복구 전 0x400E0E의 첫 4byte)^(0x554889E5) 라는 말씀!!


그럼 복구 전 상태의 0x400E0E를 찾아가서! (괴랄…)



여기를 hex값으로 보면!



4byte가 쫜 0x07 0x27 0xFD 0xA8 입니다! 다 찾앗다능! key1 코앞이라능!

ida를 켜면 제일 하단에 “Python” 명령어를 칠 수 있어여! (이번 기회에 활용해봄 꺄륵)

여기에다가 요로코롬 ‘(복구 전 0x400E0E의 첫 4byte)^(함수 시작 opcode : 0x554889e5)를 계산해서 chr 형태로 보여줬!’하고 입력하면 뜐!!



R!!o!!t!!M!! RotM!!!!!!!!! 우리가 찾던 input(key1)이 RotM이라고 알려 줍니다 ㅎㅎ

Key 2로 넘어가진 않지만…. 리눅스에서 실행해보면!!



fail은 안 뜨네요 꺄륵! Key 1 잘 찾음! ㅎㅎ




[ Step 2 ] sub_400E0E 복구하기

자 이제 복구 알고리즘(sub_400D63)을 알았으니, sub_400E0E를 직접 복구해봅시다!
ida로 분석할 때는 이걸 실행시켜서 자동으로 복구되게 못하니까..(내가 못하는 걸 수도)
애증의 idapython을 이용해 해당 부분을 고쳐봅시다 ㅎㅎ (idapython 활용법 배워갑니다 쥽쥽)

우리가 앞서 분석한 것을 바탕으로, idaapi를 이용해 바이트패치를 할 수 있습니다! 코드 뜐!


# python script를 따로 작성하여 Alt+f7로 돌릴 때 필요한 import 입니다. 
# import idaapi

def repeating_key_xor(start_address, buffer_len, key):
    for i in xrange(buffer_len):
        c = idaapi.get_byte(start_address + i) ^ ord(key[(i % len(key))])
        idaapi.patch_byte(start_address + i, c) 
    return


이 함수를 복사하여 ida 하단의 python 창에 입력하면 함수 등록 완료!
그리고 우리가 복구할 부분은 sub_400E0E에서 길이 0x9E만큼이므로 (key1은 RotM이라는 거 이미 앎!)


repeating_key_xor(0x400E0E, 0x9E, "RotM")  


요로코롬 뙇! python 창에 입력하면!

0x0727로 시작하던 요부분이



0x5548로 시작하는 녀석으로 쁍! 바뀝니다 ㅎㅎ (xor 연산이 진행된 것이죠!)



word_400E0E 부분을 클릭하고 키보드 자판에서 c(=MakeCode)를 누르면!!



요로코롬 확인 창이 뜨겠지만 당황 ㄴㄴ하고 Yes 눌러주세요.
그럼 요로코롬 가지런하고 바른 instruction 으로 변합니다 예에에에에~



※ 참고할 TIP ※

  • c를 누르고 나면 함수 이름이 우선 loc_400E0E로 나타납니다.
  • 여기서 p를 한 번 더 누르면 함수로 인식하게 하면서 sub_400E0E가 됩니다.
  • p를 눌러서 함수로 인식해야만! 복구한 instruction에 F5를 적용할 수 있다는 핵꿀팁!드립니다 ㅎㅎ




[ Step 3 ] sub_400936 복구/분석하기


sub_400E0E 함수를 무사히 복구하고 나서 어셈 코드의 앞부분을 보니…



반가운 sub_400D63 함수가 앞서 봤던 것과 매우 유사한 형태로 또다시 등장했어요 ㅎㅎ 아까는 분석하느라 귀찮았지만! 우린 이제 다 아니까~ 훗

복구할 부분의 offset(0x400936)과 복구할 이(0xEC)만 다르지 복구 방법은 같으니까 아까 등록해 둔repeating_key_xor` 함수를 요로코롬 똑같은 방법으로 사용하면 됩니다.


repeating_key_xor(0x400936, 0xEC, "RotM")
MakeCode(0x400936)


헤헿 MakeCode( )는 우리가 손으로 키보드에 c를 누르는 것과 똑같은 기능을 합니다.
저렇게 입력하면 sub_400936 함수가 곧장 어셈코드로 복구된다능! p는 따로 또 눌러주긔!

그럼 sub_400936 함수를 분석하기 앞서 이 함수의 리턴값이 어디에 사용되는 녀석인지, 먼저 파악해두기 위해 sub_400E0E 함수를 c코드로 분석해봅시다.


void __usercall sub_400E0E(__int64 _RBX@<rbx>, __int64 a2@<rdi>, long double a3@<st0>)
{
  unsigned __int64 v3; // rt0@2
  long double v4; // fst7@2
  __int64 (__fastcall *v6)(); // [sp+10h] [bp-A0h]@1
  __int64 v7; // [sp+18h] [bp-98h]@1
  int v8; // [sp+98h] [bp-18h]@1
  __int64 v9; // [sp+A8h] [bp-8h]@1

  v9 = *MK_FP(__FS__, 40LL);
  sub_400D63(sub_400936, 236LL, a2, 18883LL);
  v6 = sub_400936;
  v8 = 4;
  sigfillset((sigset_t *)&v7);
  sigaction(5, (const struct sigaction *)&v6, 0LL);
  v3 = __readeflags();
  __writeeflags(v3 | 0x100);
  v4 = a3 * (long double)*(signed __int16 *)(_RBX + 1224669253);
  _EAX = v28 ^ (v3 | 0x100);
  __asm { xlat }
  JUMPOUT(__CS__, *(_QWORD *)(_RBX + 104));


분석 내용 정리

  • sub_400D63 함수를 이용해 sub_400936 함수를 복구한다.
  • sub_400936 함수의 리턴값이 v6에 저장되어 sigaction의 두 번째 인자(*act)로 사용된다.
  • sigaction 형태 : int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • sigaction(5, (const struct sigaction *)&v6, 0LL);
  • sigaction을 부르기 전에 sigfillset을 실행한다.
  • sigfillset 형태: int sigfillset(sigset_t *set);
  • sigfillset은 시그널 집합 변수인 set에 모든 시그널을 추가하는 일을 한다.


sigaction에 대한 내용을 처음 접하셨다면 넘나 머리가 복잡하고 이해가 안될 수 있는데, 가장 중요한 sigaction(5, (const struct sigaction *)&v6, 0LL); 요부분만 일단 알고 가시면 됩니다.

이 부분을 다시 해석하면 SIGTRAP!이 발생했을 때, v6! 즉 sub_400936 함수를! SIGTRAP 시그널에 대한 signal handler!로 사용한다!는 것입니다.


※ 미리 말씀드리자면, SIGTRAP은 sigaction을 부른 후 fimul word ptr [rbx+48FEF845h] 요부분에서 발생한다는 것을 gdb를 통해 알 수 있습니다!! 그리고 대충 딱 봐도 저기 코드 이상하게 생겨서 sub_400936 함수로 디코딩해줘야 될 삘이 남!


그럼 sub_400936 함수를 분석해볼까요!


__int64 __fastcall sub_400936(__int64 a1, __int64 a2, __int64 a3)
{
  __int64 v4; // [sp+48h] [bp-8h]@1

  v4 = *MK_FP(__FS__, 40LL);
  qword_6020C8 = qword_6020C0;
  qword_6020C0 = *(_QWORD *)(a3 + 168);
  if ( (unsigned __int64)qword_6020C0 > 0x400935 && (unsigned __int64)qword_6020C0 <= 0x40103D )
  {
    if ( (unsigned __int64)qword_6020C8 > 0x400935 && (unsigned __int64)qword_6020C8 <= 0x40103D )
      *(_BYTE *)qword_6020C8 ^= qword_6020C8;
    *(_BYTE *)qword_6020C0 ^= qword_6020C0;
  }
  return *MK_FP(__FS__, 40LL) ^ v4;
}


분석 내용 정리

  • qword_6020C0 = *(_QWORD *)(a3 + 168);
  • a3 : sub_400936의 세 번째 인자. sub_400936은 SIGTRAP의 signal handler!라고 앞서 언급하였다. 이 때, signal handler의 세 번째 인자는 void* ucontext_t이다.
  • qword_6020C0값이 0x400936부터 0x40103D 안에 포함될 때 qword_6020C0 주소에 들어있는 값을, xor 연산을 통해 변환(복구)한다.


자…… 그래서 가장 중요한 게 “qword_6020C0이 도대체 무엇의 주소값을 가리키냐?”는 것이다. a3가 ucontext_t 구조체의 pointer라는 것을 알게 되었으니 이제 “a3에서 168byte 만큼 떨어진 곳에 무엇이 있냐”를 알아내면 된다! ucontext_t 구조체를 좀 더 살펴보자!


ucontext_t* (pointer to ucontext_t)                    ; 8byte
sigset_t (structure or integer, usually unsigned long) ; 8byte
stack_t (structure)
    void *ss_sp                                        ; 8byte
    size_t ss_size                                     ; 8byte
    int ss_flags                                       ; 8byte

mcontext_t (structure)
    gregset_t gregs;
    fpregset_t fpregs;
    unsigned long __reserved1 [8];


으어으어 언제 끝냐냐 힘듬잼(갑툭튀한 나의 의식… 다시 들어가ㅠ)
여러분도… 힘내고 차근히 따라와요 다 와가요 ㅎㅎ

무튼 앞부분에서 40byte를 차지한다는 걸 알게 되었습니다!
그럼 우리가 찾는 것은 mcontext_t 구조체의 128byte 위치에 있을 것이고! mcontext_t가 어떻게 구성되어있나! gregset_t를 검색해봤더니 쁍!
gregset_t 배열에 23개의 레지스터가 들어있다고 합니다….오오…..?
64bit linux에서 레지스터 하나의 크기는 8byte이므로 23개의 레지터 중 17번째 레지스터!!!!!!! ( ∵ 128=8*16 )

바로바로 RIP!!!!!!!!!!!가 mcontext_t 구조체에서 offset 128위치에, ucontext_t 구조체로부터 offset 168위치에 있다는 것을!! 알게 되었습니다… (요로코롬 하나씩 끈질기게 타고, 타고 들어가는 법을 배워갑니다 쥽쥽)


/* Type for general register.  */
typedef long int greg_t;

/* Number of general registers.  */
#define NGREG   23

/* Container for all general registers.  */
typedef greg_t gregset_t[NGREG];

#ifdef __USE_GNU
/* Number of each register in the `gregset_t' array.  */
enum
{
  REG_R8 = 0,
# define REG_R8 REG_R8
  REG_R9,
# define REG_R9 REG_R9
  REG_R10,
# define REG_R10    REG_R10
  REG_R11,
# define REG_R11    REG_R11
  REG_R12,
# define REG_R12    REG_R12
  REG_R13,
# define REG_R13    REG_R13
  REG_R14,
# define REG_R14    REG_R14
  REG_R15,
# define REG_R15    REG_R15
  REG_RDI,
# define REG_RDI    REG_RDI
  REG_RSI,
# define REG_RSI    REG_RSI
  REG_RBP,
# define REG_RBP    REG_RBP
  REG_RBX,
# define REG_RBX    REG_RBX
  REG_RDX,
# define REG_RDX    REG_RDX
  REG_RAX,
# define REG_RAX    REG_RAX
  REG_RCX,
# define REG_RCX    REG_RCX
  REG_RSP,
# define REG_RSP    REG_RSP
  REG_RIP,                      -----★★★
# define REG_RIP    REG_RIP
  REG_EFL,
# define REG_EFL    REG_EFL
  REG_CSGSFS,   /* Actually short cs, gs, fs, __pad0.  */
# define REG_CSGSFS REG_CSGSFS
  REG_ERR,
# define REG_ERR    REG_ERR
  REG_TRAPNO,
# define REG_TRAPNO REG_TRAPNO
  REG_OLDMASK,
# define REG_OLDMASK    REG_OLDMASK
  REG_CR2
# define REG_CR2    REG_CR2
};
#endif


으어으어…….. smokeleeteveryday의 롸업에서는 이 부분이 명료하고 간단히 잘 설명되어있긴 하지만… 너무 요점만 짚어놔서… 저는 일일이 하나씩 다 찾아가며 이해하느라 시간이 좀 걸렸어요! 제 설명은 그에 비해 좀 난잡할 수는 있지만…! 처음 보는 여러분도 함께 따라오며 정확히 이해하는 데 도움이 되었길 바랍니다!

제가 RIP를 알아내기 위해 참고한 주소를 한 번에 뙇 모아서 적어드릴게요.



하… 우리가 왜 mcontext_t랑 이것저것 뒤져서 RIP를 찾아냈나!
너무 흘러와서 우리가 뭘 하고 있었는지 헷갈리실 분을 위해 분석한 내용 요약정리!


분석 내용 요약정리

  • 우리는 sub_400936 함수를 분석하던 중이었다.
  • qword_6020C0 값은 a3로부터 offset 168만큼 떨어진 값을 가리킨다.
  • 열심히 찾아본 바로, qword_6020C0은 RIP address를 뜻한다.
  • 결론 : sub_400936 함수에서는 0x400935 < RIP address <= 0x40103D 일 때, 해당 RIP address에 있는 값에 특정 xor 연산을 적용하여 instruction을 변환한다.


하핳… sub_400936에 있는 xor 연산 알고리즘을 idapython에 사용할 코드로 간단히 변환하면 다음과 같습니다 쁍!


def selfmod_decoder(rip_address):
    if ((rip_address > 0x400935) and (rip_address <= 0x40103D)):
        idaapi.patch_byte(rip_address, idaapi.get_byte(rip_address) ^ (rip_address & 0xFF))
    return


그럼… 드디어 이 함수를 이용해 마지막 패치를 시작해볼까요? (엄청난 수작업 대기잼)




[ Step 4 ] 0x400E96 부터 instruction 한 줄씩 복구하기


앞서…. 다섯 가지 step을 소개할 때… ※강조※ 했었는데 기억하실랑가 모르겠습니다…
이 디코딩은… 한 줄, 한 줄, 한 땀, 한 땀 해줘야합니다. python 명령어로 쁍쁍! 한 번에 되는 놈이 아닙니다…
왜냐면 instruction이 수정될 때마다 instuction 길이가 변경되고… 자동 스크립트로 짜기에는 뭔가 매우 복잡한 문제들이 많기 때문이죠… (사실 방법은 있을 거에요 얼마든지… 저도 그거 편케 해보려고 삽질하다가 하루 날렸다는 ㅂㄷㅂㄷ 언젠가 다시 할 거라능)

자 그럼 아까 말했듯이… 0x400E96 요부분부터 SIGTRAP이 터집니다!



그럼 여기서부터 sub_400936이 발동돼야 하는 것이겠죠? ida 코드를 정적으로 고쳐주기 위해선 앞서 짜둔 selfmod_decoder 함수가 필요합니다!

rip_address를 계속 바꿔서 손으로 입력해주기는 번거로우니까 저는 ScreenEA()를 사용했습니다. 이건 ida view screen에 클릭되어있는 주소(linear address)를 가져오는 녀석이에요! 그래서 저는 rip_address를 키보드로 수정하는 거 대신 screen에서 다음 주소를 일일이 하나씩 클릭하고 selfmod_decoder(ScreenEA()) 치고, 또 클릭하고 치고…… 요로코롬 했습니다. ㅎㅎ 이래나 저래나인데 이게 그나마 수월할 것이라능! 그리고 넘나 instruction 고칠 거 많아 보이겠지만 멍때리고(하지만 실수하면 재앙임! 어느 정도 정신은 차려야함) 기계적으로 하다보면 30분은 안걸린다능……….(위로인가과연)

아참!! 그리고 저 범위 안에 있는 코드를 한 줄 씩 변환하되, 진짜 모~든 주소를 바꾸는 게 아니라 rip가 가는 주소를 바꾸는 거니께 ㅎㅎ 각 함수가 끝나는 시점(leave ret 뙇!)까지만 해주시면 됩니다. ㅎㅎ

참고로 바꾸셔야 할 함수는….


  • main 뒷부분 나머지 (0x40101E 부터)
  • 0x400E0E 뒷부분 (0x400E96부터)
  • sub_400EAC 함수 (main의 뒷부분을 고치면 call 하는 것이 등장함)
  • sub_400A22 함수 (called by 0x400EAC)
  • sub_400C31 함수 (called by 0x400EAC)


예시로 살짝 보여드리면 요로코롬 selfmod_decoder 함수 정의한 다음, 요로코롬 하나씩 python 창에 selfmod_decoder(ScreenEA()) 명령을 치는 거고!



그럼 요로코롬 한 줄 씩 차근차근 바뀌는데(현재 0x400EA5까지 명령 친 상태)
빨강 줄은 무시해도 됨… 안 예쁘지만 지장 없음!



0x400EA5 에서 0xE8 부분처럼!!
원래 instuction 형태가 아닌 데이터 형태로 존재하던 것들은, decode 함수 돌린 후에 그 값 자체가 바뀌긴 하는데 instruction으로 자동 변환이 안됩니다!

그 때는 c를 살짜쿵 눌러줘서 요로코롬 바꿔주면 된다는 거!



하하하핳!! 으어으어!!

알려드렸으니 이제 한 땀 한 땀 화이팅!!



고생 끝에 모든 instruction을 패치하시면! 요러한 함수들이 복구되어 요로코롬 예쁜 c 코드를 만나실 수 있습니다.

main

__int64 __usercall main@<rax>(char **a1@<rsi>, char **a2@<rdx>, __int64 a3@<rbx>, long double a4@<st0>)
{
  __int64 result; // rax@1
  __int64 v5; // rcx@1
  char s; // [sp+0h] [bp-10h]@1
  char v7; // [sp+4h] [bp-Ch]@1
  __int64 v8; // [sp+8h] [bp-8h]@1

  v8 = *MK_FP(__FS__, 40LL);
  printf("Key1: ", a1, a2);
  fflush(stdout);
  fgets(&s, 6, stdin);
  v7 = 0;
  sub_400D63(sub_400E0E, 158, (__int64)&s, 18879);
  sub_400E0E(a3, (__int64)&s, a4);
  ((void (__fastcall *)(char *, signed __int64))((char *)&loc_400EAA + 2))(&s, 158LL);
  result = 0LL;
  v5 = *MK_FP(__FS__, 40LL) ^ v8;
  return result;
}


SUB_400E0E

void __usercall sub_400E0E(__int64 _RBX@<rbx>, __int64 a2@<rdi>, long double a3@<st0>)
{
  unsigned __int64 v3; // rt0@2
  __int64 (__fastcall *v4)(__int64, __int64, __int64); // [sp+10h] [bp-A0h]@1
  __int64 v5; // [sp+18h] [bp-98h]@1
  int v6; // [sp+98h] [bp-18h]@1
  __int64 v7; // [sp+A8h] [bp-8h]@1

  v7 = *MK_FP(__FS__, 40LL);
  sub_400D63(sub_400936, 236, a2, 18883);
  v4 = sub_400936;
  v6 = 4;
  sigfillset((sigset_t *)&v5);
  sigaction(5, (const struct sigaction *)&v4, 0LL);
  v3 = __readeflags();
  __writeeflags(v3 | 0x100);
  JUMPOUT(loc_400E9A);
}


SUB_400EAC

__int64 sub_400EAC()
{
  FILE *stream; // [rsp+8h] [rbp-58h]@1
  char filename; // [rsp+10h] [rbp-50h]@1
  char v3; // [rsp+11h] [rbp-4Fh]@1
  char v4; // [rsp+12h] [rbp-4Eh]@1
  char v5; // [rsp+13h] [rbp-4Dh]@1
  char v6; // [rsp+14h] [rbp-4Ch]@1
  char format; // [rsp+20h] [rbp-40h]@3
  char v8; // [rsp+21h] [rbp-3Fh]@3
  char v9; // [rsp+22h] [rbp-3Eh]@3
  char v10; // [rsp+23h] [rbp-3Dh]@3
  char v11; // [rsp+24h] [rbp-3Ch]@3
  char v12; // [rsp+25h] [rbp-3Bh]@3
  char v13; // [rsp+26h] [rbp-3Ah]@3
  char s; // [rsp+30h] [rbp-30h]@3
  char v15; // [rsp+4Fh] [rbp-11h]@3
  __int64 v16; // [rsp+58h] [rbp-8h]@1

  v16 = *MK_FP(__FS__, 40LL);
  filename = 'f';
  v3 = 'l';
  v4 = 'a';
  v5 = 'g';
  v6 = 0;
  stream = fopen(&filename, "r");
  if ( !stream )
    exit(1);
  fgets(::s, 64, stream);
  fclose(stream);
  format = 'K';
  v8 = 'e';
  v9 = 'y';
  v10 = '2';
  v11 = ':';
  v12 = ' ';
  v13 = '\0';
  printf(&format, 64LL);
  fflush(stdout);
  fgets(&s, 32, stdin);
  v15 = 0;
  sub_400A22(&s);
  sub_400C31(&s);
  return *MK_FP(__FS__, 40LL) ^ v16;
}


SUB_400A22

__int64 __fastcall sub_400A22(_QWORD *a1)
{
  unsigned __int8 i; // [rsp+1Fh] [rbp-31h]@1
  __int64 s; // [rsp+20h] [rbp-30h]@1
  __int64 v4; // [rsp+28h] [rbp-28h]@4
  __int64 v5; // [rsp+30h] [rbp-20h]@4
  __int64 v6; // [rsp+38h] [rbp-18h]@4
  __int64 v7; // [rsp+48h] [rbp-8h]@1

  v7 = *MK_FP(__FS__, 40LL);
  bzero(&s, 0x20uLL);
  for ( i = 0; i <= 0x1Fu; ++i )
  {
    *((_BYTE *)&s + i) |= *((_BYTE *)a1 + i) >> 7;
    *((_BYTE *)&s + i) |= (*((_BYTE *)a1 + i) & 0x40) >> 1;
    *((_BYTE *)&s + i) |= 2 * (*((_BYTE *)a1 + i) & 0x20);
    *((_BYTE *)&s + i) |= (*((_BYTE *)a1 + i) & 0x10) >> 3;
    *((_BYTE *)&s + i) |= 16 * (*((_BYTE *)a1 + i) & 8);
    *((_BYTE *)&s + i) |= 2 * (*((_BYTE *)a1 + i) & 4);
    *((_BYTE *)&s + i) |= 2 * (*((_BYTE *)a1 + i) & 2);
    *((_BYTE *)&s + i) |= 16 * (*((_BYTE *)a1 + i) & 1);
  }
  *a1 = s;
  a1[1] = v4;
  a1[2] = v5;
  a1[3] = v6;
  return *MK_FP(__FS__, 40LL) ^ v7;
}


참고!

저는 이 함수에서 p를 눌렀는데도 p가 제 기능을 제대로 못했어요!
그 때는 ida가 가장 하단 줄에다가 오른쪽 커서를 누르고 Reanalyze program을 클릭해주세요!
코드 스크린에서는 함수화시킬 함수를 클릭해두고요!! 이렇게 쁍! 그럼 다시 함수로 잘 인식해준답니다~


SUB_400C31

여기도 p 잘 안되니까 Reanalyze 기기!

__int64 __fastcall sub_400C31(const void *a1)
{
  char format; // [rsp+10h] [rbp-40h]@1
  char v3; // [rsp+11h] [rbp-3Fh]@1
  char v4; // [rsp+12h] [rbp-3Eh]@1
  char v5; // [rsp+13h] [rbp-3Dh]@1
  char v6; // [rsp+14h] [rbp-3Ch]@1
  char v7; // [rsp+15h] [rbp-3Bh]@1
  char s2; // [rsp+20h] [rbp-30h]@1
  char v9; // [rsp+21h] [rbp-2Fh]@1
  char v10; // [rsp+22h] [rbp-2Eh]@1
  char v11; // [rsp+23h] [rbp-2Dh]@1
  char v12; // [rsp+24h] [rbp-2Ch]@1
  char v13; // [rsp+25h] [rbp-2Bh]@1
  char v14; // [rsp+26h] [rbp-2Ah]@1
  char v15; // [rsp+27h] [rbp-29h]@1
  char v16; // [rsp+28h] [rbp-28h]@1
  char v17; // [rsp+29h] [rbp-27h]@1
  char v18; // [rsp+2Ah] [rbp-26h]@1
  char v19; // [rsp+2Bh] [rbp-25h]@1
  char v20; // [rsp+2Ch] [rbp-24h]@1
  char v21; // [rsp+2Dh] [rbp-23h]@1
  char v22; // [rsp+2Eh] [rbp-22h]@1
  char v23; // [rsp+2Fh] [rbp-21h]@1
  char v24; // [rsp+30h] [rbp-20h]@1
  char v25; // [rsp+31h] [rbp-1Fh]@1
  char v26; // [rsp+32h] [rbp-1Eh]@1
  char v27; // [rsp+33h] [rbp-1Dh]@1
  char v28; // [rsp+34h] [rbp-1Ch]@1
  char v29; // [rsp+35h] [rbp-1Bh]@1
  char v30; // [rsp+36h] [rbp-1Ah]@1
  char v31; // [rsp+37h] [rbp-19h]@1
  char v32; // [rsp+38h] [rbp-18h]@1
  char v33; // [rsp+39h] [rbp-17h]@1
  char v34; // [rsp+3Ah] [rbp-16h]@1
  char v35; // [rsp+3Bh] [rbp-15h]@1
  char v36; // [rsp+3Ch] [rbp-14h]@1
  char v37; // [rsp+3Dh] [rbp-13h]@1
  char v38; // [rsp+3Eh] [rbp-12h]@1
  char v39; // [rsp+3Fh] [rbp-11h]@1
  __int64 v40; // [rsp+48h] [rbp-8h]@1

  v40 = *MK_FP(__FS__, 40LL);
  format = 'n';
  v3 = 'o';
  v4 = 'p';
  v5 = 'e';
  v6 = 10;
  v7 = 0;
  s2 = 'P';
  v9 = 'l';
  v10 = 'e';
  v11 = 'a';
  v12 = 's';
  v13 = 'e';
  v14 = ',';
  v15 = ' ';
  v16 = 'm';
  v17 = 'a';
  v18 = 'y';
  v19 = ' ';
  v20 = 'I';
  v21 = ' ';
  v22 = 'h';
  v23 = 'a';
  v24 = 'v';
  v25 = 'e';
  v26 = ' ';
  v27 = 't';
  v28 = 'h';
  v29 = 'e';
  v30 = ' ';
  v31 = 'f';
  v32 = 'l';
  v33 = 'a';
  v34 = 'g';
  v35 = ' ';
  v36 = 'n';
  v37 = 'o';
  v38 = 'w';
  v39 = '\0';
  if ( !memcmp(a1, &s2, 0x20uLL) )
    puts(s);
  else
    printf(&format, &s2);
  return *MK_FP(__FS__, 40LL) ^ v40;
}


복구 끝! 저는 25분정도 걸린 듯합니다 ㅎㅎ 그냥 무념무상하면 재밌게 할 수 있다능…

그럼 하… 진!짜!마!지!막!
복구시킨 sub_400EAC / sub_400A22 함수 / sub_400C31 함수 분석하러 기기!




[ Step 5 ] sub_400A22 함수와 sub_400C31 함수를 분석하여 Key 2 구하기

위에서 이미 c코드를 공개했으니… 각 함수별로 요점 정리만 해보겠습니다 ㅎㅎ

1. sub_400EAC

“flag”라는 이름의 파일에서 내용(stream)을 읽어와 .bss 세그먼트(::s)에 저장한다. “Key 2 : “를 출력하고 사용자로부터 key2(&s에 저장)를 입력받는다. 이 때 flag가 저장된 버퍼와, 사용자의 입력이 저장된 버퍼가 모두 “s”로 표시되어 헷갈릴 수 있으나 절대 같은 값이 아니니! 더블 클릭하여 어느 부분에 저장된 값인지 잘 체크 할 것! 사용자의 입력값을 input이라 했을 때 sub_400A22(input), sub_400C31(input) 을 차례로 호출한다. sub_400A22에서 비트 치환 연산을 거친 후, 그 연산의 결과가 sub_400C31에서 비교하는 문자열과 일치해야 한다.


2. sub_400A22

사용자가 입력한 문자(31글자)를 비트 연산으로 치환한다. 입력한 문자를 한 글자(1byte = 8bit)씩 가져와 8bit 치환 연산을 진행한다. smokeleeteveryday의 롸업에서 이 부분이 넘나 명료하고 완벽하게! (이건진심 나무랄 데가 없었음) 정리되어 있으므로 그것을 참고하겠습니다! 함수의 비트 연산을 눈으로 보기 쉽게 정리하면? 쁍!

10000000 -> 00000001     1st bit >> 8th bit
01000000 -> 00100000     2nd bit >> 3rd bit
00100000 -> 01000000     3rd bit >> 2nd bit
00010000 -> 00000010     4th bit >> 7th bit
00001000 -> 10000000     5th bit >> 1st bit
00000100 -> 00001000     6th bit >> 5th bit
00000010 -> 00000100     7th bit >> 6th bit
00000001 -> 00010000     8th bit >> 4th bit


이 연산을 한 줄의 코드로 뙇 정리하면? 쁍!

(l[i] >> 7) | ((l[i] & 0x40) >> 1) | ((l[i] & 0x20) << 1) | ((l[i] & 0x10) >> 3) | ((l[i] & 8) << 4) | ((l[i] & 4) << 1) | ((l[i] & 2) << 1) | ((l[i] & 1) << 4)


간단하지요? ㅎㅎ


3. sub_400C31

  • s2 변수부터 차례로 한 글자씩 저장되어있는 것을 모아보면
    • → Please, may I have the flag now
  • sub_400A22를 거쳐서 넘어온 인자값(a1)을 위 문자열과 비교!(memcmp)한닷
    • 일치하면 flag 저장해뒀던 buffer의 값을 print!!! yeah >_ <


그렇다면 Key 2에는 무엇을 입력해야 하는가??

Please, may I have the flag now라는 그럴듯한 문장을 보고 이게 key2라고 착각하시면 안됨돠! 결론은! Please, may I have the flag now에 sub_400A22의 치환 알고리즘을 반대로 적용한 값을! key2에 넣어줘야겠지요 ㅎㅎㅎㅎㅎ

앞서 한 줄로 정리한 코드를 반대로 적용해 보면? 쁍!

((l[i] & 1) << 7) | ((l[i] & 0x20) << 1) | ((l[i] & 0x40) >> 1) | ((l[i] & 2) << 3) | ((l[i] & 0x80) >> 4) | ((l[i] & 8) >> 1) | ((l[i] & 4) >> 1) | ((l[i] & 0x10) >> 4)


이걸로 key2 값을 구하는 함수를 짜면!?

def inv_sbox(c):
    p = ''
    for i in xrange(len(c)):
        p += chr(((ord(c[i]) & 1) << 7) | ((ord(c[i]) & 0x20) << 1) | ((ord(c[i]) & 0x40) >> 1) | ((ord(c[i]) & 2) << 3) | ((ord(c[i]) & 0x80) >> 4) | ((ord(c[i]) & 8) >> 1) | ((ord(c[i]) & 4) >> 1) | ((ord(c[i]) & 0x10) >> 4))
    return p

key2 = inv_sbox("Please, may I have the flag now\x00")


꺄륵 넘나 다사다난했지만 key1 구했고, 복구 다 했고, 분석했고, key2도 구하고, 모두 완성! 함께하느라 고생하셨습니다… 한 땀 한 땀 수정하느라 고생하셨구요… (혹시 제가 이걸 성공하기 전에 한 번에 간지나게 복구하는 법을 알아내신 분은 제게도 살짝 귀띔… 부탁드려요 ㅎㅎ 배움잼)

저는 nc 접속 환경이 없는 상황에서 바이너리만 분석하였으므로 여기까지가 끝이고.. ㅎㅎ 지금까지 정리했던 내용을 모아 익스코드(간단함)를 작성하고 실제 대회 서버에서 flag를 얻는 내용까지 보고 싶으신 분은?? smokeleeteveryday의 롸업을 참고해주시길 바랍니다.

정리 끝!




[ 끄읕 ]

으어으어 기나긴 롸업을 끝내기 전 마지막으로…. 이 롸업을 쓰던 가장 초반에 끄적끄적 적어두었던 것을 살포시 남깁니다. ㅎㅎ


문제 풀이 point 및 꾸르팁

  • step by step
  • sigtrap
  • xor decode… 꼭 순서대로 해야함!!
    • ∵ decode할 때마다 instruction이 새로 세팅돼서 그 다음 instruction 시작 주소가 바뀌기 때문)
  • c를 눌러서 Makecode한 다음, 함수 시작 부분 클릭하고 p를 눌러서 함수로 인식시켜야 F5 눌러짐 ★★★
  • p가 잘 안먹으면 하단에 오른쪽 클릭하고 reanalyze 해주면 됨!


친절하고 상세한 롸업 남겨주신 smokeleeteveryday님(팀?)과, 헤매는 저를 싫은 소리 않고 지도해주신 kkokko님께 깊은 고마움 남깁니다 헿

그럼 이만 나는 진짜 뿅!

comments powered by Disqus