selenium을 이용할 때면, webdriver로 chromedriver를 많이 사용하곤 했다. GUI가 필요없을 때에도 headless로 사용했으니 대부분이라 말할수도 있을 것 같다.

하지만 어느 순간부터 selenium을 이용하여 Google login을 자동화하는 부분을 작성하다 보면, 아래와 같은 page를 확인할 수 있다.


문제 파악

email 입력 부분을 채우고 다음 버튼을 누르게 되면, 위와 같은 브라우저 또는 앱이 안전하지 않을 수 있습니다. 라면서 더 이상의 login을 할 수 없도록 한다.

Google 입장에서는 비정상적인 login 시도 행위를 막는 것은 당연한 일이지만, 자동화가 필요한 입장에서는 여간 불편한 것은 사실이다.

조금만 더 생각해 보면 Google에서 만든 chromedriver을 가지고 비정상 행위를 한다고 했을 때, 이를 탐지하는 방법 역시 쉬울테니 언젠가는 막히리라 생각하곤 했으나 실제로 막히니 난감하기는 했다.

그렇다면 Google은 어떻게 chromedriver을 이용한 login automation을 탐지하는 것이며, undetected_chromedriver은 어떻게 이를 우회하는 것일까? 쉬운 방법으로는 firefox와 같은 다른 브라우저의 드라이버를 이용하는 것이나 궁금증을 해결하기 위하여 undetected_chromedriver에 대하여 분석해 봤다.


우회법 분석

Google이 chromedriver을 이용하는 것인지에 대하여 알 방법은 명확하지 않기에 후자인 undetected_chromedriver가 어떻게 login automation을 가능하게 해주는지에 대하여 분석해 본다.

제일 먼저 간단한 binary diffing부터 진행한다.

(.venv) onsoim@MacBook-Pro undetected_chromedriver % ./chromedriver --version
ChromeDriver 98.0.4758.80 (7f0488e8ba0d8e019187c6325a16c29d9b7f4989-refs/branch-heads/4758@{#972})
(.venv) onsoim@MacBook-Pro undetected_chromedriver % md5 chromedriver
MD5 (chromedriver) = 3e5b46c2463efef54a6a8ecfd61b455a
#################################################################################
(.venv) onsoim@MacBook-Pro Safari % ./chromedriver --version
ChromeDriver 98.0.4758.80 (7f0488e8ba0d8e019187c6325a16c29d9b7f4989-refs/branch-heads/4758@{#972})
(.venv) onsoim@MacBook-Pro Safari % md5 chromedriver
MD5 (chromedriver) = 3f71959e5d8644780ee3dc353ffbdc70

기본적으로 undetected_chromedriver에서 설치되는 chromedriver와 https://chromedriver.storage.googleapis.com에서 받은 chromedriver를 비교해 보았다.

Google APIs에서 받은 chromdriver의 cdc_adoQpoasnfa76pfcZLmcfl가 undetected_chromedriver에서 설치되는 chromedriver에서는 xyx_bakbtmdhmelauoizUBpwcp 로 바뀐 것을 확인할 수 있다.

이 부분이 무엇인지 알기 위해서는 binary의 해당 부분을 직접 확인해 보면 명확해 진다.

아래는 위의 문자열이 사용된 함수의 일부를 발췌한 것이다.

function getPageCache(opt_doc, opt_w3c) {
  var doc = opt_doc || document;
  var w3c = opt_w3c || false;
  // |key| is a long random string, unlikely to conflict with anything else.
  var key = '$cdc_adoQpoasnfa76pfcZLmcfl_';
  if (w3c) {
    if (!(key in doc))
      doc[key] = new CacheWithUUID();
    return doc[key];
  } else {
    if (!(key in doc))
      doc[key] = new Cache();
    return doc[key];
  }
}

cdc_adoQpoasnfa76pfcZLmcfl는 window 객체의 여러 값을 가지고 있는 변수명들의 prefix로 사용되는데, 이 prefix는 각 build version마다 고유한 값을 가지고 있기 때문에 Google에서는 이를 이용하여 chromedriver 임을 쉽게 탐지할 수 있는 것 같다.


undetected_chromedriver

실제로 project에서 binary를 patch하는 부분의 코드는 아래와 같다.

@staticmethod
def gen_random_cdc():
    cdc = random.choices(string.ascii_lowercase, k=26)
    cdc[-6:-4] = map(str.upper, cdc[-6:-4])
    cdc[2] = cdc[0]
    cdc[3] = "_"
    return "".join(cdc).encode()

def patch_exe(self):
    """
    Patches the ChromeDriver binary
    :return: False on failure, binary name on success
    """
    logger.info("patching driver executable %s" % self.executable_path)

    linect = 0
    replacement = self.gen_random_cdc()
    with io.open(self.executable_path, "r+b") as fh:
        for line in iter(lambda: fh.readline(), b""):
        if b"cdc_" in line:
            fh.seek(-len(line), 1)
            newline = re.sub(b"cdc_.{22}", replacement, line)
            fh.write(newline)
            linect += 1
            return linect

Google에서는 prefix를 이용하여 chromedriver 임을 탐지하기 때문에 이를 다른 random한 값으로 바꿔주는 방식으로 탐지를 우회한다.

이를 이용하여 Google login을 automation하는 것은 간단하기에 따로 code를 올려두지는 않겠지만 필요하시면 요청해주세요!

  • __ROR__ : ROtate Right (circular rotation to the right)
    • __ROR1__ : uint8
    • __ROR2__ : uint16
    • __ROR3__ : uint24
    • __ROR4__ : uint32
  • __ROL__ : ROtate Left (circular rotation to the left)
    • __ROL1__ : uint8
    • __ROL2__ : uint16
    • __ROL3__ : uint24
    • __ROL4__ : uint32

def __ROL__(num, count, bits=8):
    return ((num << count) | (num >> (bits - count))) & ((0b1<<bits) - 1)

def __ROR__(num, count, bits=8):
    return ((num >> count) | (num << (bits - count))) & ((0b1<<bits) - 1)

Ref : https://www.geeksforgeeks.org/rotate-bits-of-an-integer/

readme.txt

readme.txt 파일을 읽어보면, file 을 복호화하는 것이 목표이며 그 암호화된 파일은 EXE 인 것 같습니다.

 

unpack UPX

암호화하는 프로그램으로 추정되는 run.exe 의 정보를 확인해 보면, upx 로 압축되어 있습니다.

따라서 이를 먼저 풀어줍니다.

 

anti Hexray

run.exe 프로그램을 보면 중간중간 분석을 방해하기 위해서 특정한 목적없이 무의미한 명령어를 반복하는 부분이 있는데, 이로 인하여 디컴파일 도구에서 제대로 분석하지 못하도록 방해하고 있습니다. 따라서 이것들을 먼저 제거해 줍니다.

 

바이트 코드를 확인해 보면 60 61 90 50 58 53 5B 가 반복이 되는데, 이를 010 editor 같은 도구를 이용하여 90 * 7 으로 치환합니다.

 

90 치환 시작 주소 90 치환 끝 주소 offset 실제 offset
0x406 0x129CD 0x125C7 0x125C4
0x129E9 0x24FB0 0x125C7 0x125C4
0x24FD7 0x3759E 0x125C7 0x125C4
0x375AD 0x49B74 0x125C7 0x125C4

이제 90으로 채워져서 NOP 으로 채워져 있지만, 디컴파일에 시간을 잡아먹기 때문에 이 NOP 공간을 점프하는 명령어를 넣습니다.

JMP 할 공간의 offset 은 0x125C7 로 동일하며, 명령어에 사용되는 5바이트를 빼주고 90 의 끝에서 다음 주소를 가리켜야 하기 때문에 1을 더해줍니다.

따라서 E9 C3 25 01 00 을 90 공간의 맨 앞부분에 넣어줍니다.

 

그러면 위와 같이 중간에 NOP 코드를 생략하는 것을 확인할 수 있습니다. 근데 위의 코드도 자세히 보면 레지스터 값을 넣고, 그대로 빼는 의미없는 함수입니다.

 

실제로 이 함수를 여러번 호출하는데, 이를 통해 디컴파일을 방해하려는 목적인 것 같습니다.

 

복호화 루틴

sub_41355e0 함수를 보면, 이 함수에서 복호화가 진행되는 것을 알 수 있습니다.

 

복호화에 사용할 키를 0x44d370 에 입력받습니다.

 

입력으로 들어온 Key 의 길이를 구합니다.

 

그 후 file 이라는 파일을 열고,

 

파일의 크기를 구하고, 파일 포인터를 처음으로 돌려 놓습니다.

 

그 후 파일의 내용을 받아서, dword [ebp - 0x8] + 0x5415b8 영역에 올립니다.

 

그 후 key 와 파일의 내용을 1바이트 씩 가져와서 xor 연산을 수행한 후, 그 값에 0xff 를 xor 즉 ~ 연산을 수행합니다.

 

그 후 그 내용을 file 에 다시 씁니다.

 

복호화 하기

파일의 마지막 부분을 보면, 평소 0으로 채워져 있어야 할 영역을 확인할 수 있습니다.

특정 문자열이 13개씩 반복되는 것으로 보아, 키는 13자리인 것을 추측할 수 있습니다.

또한 exe 라면 dos stub 에 위치한 This program cannot be run in DOS mode 를 이용하여 키를 구해보겠습니다.

 

위에서 찾은 키를 이용하여, 암호화된 파일을 복호화하겠습니다.

 

복호화 key 는 letsplaychess 이며, 그 키를 이용하여 file 을 복호화하였습니다.

 

복호화가 된 파일을 확인해 보면, upx 로 압축된 pe32 파일임을 알 수 있습니다.

 

Flag

복호화한 EXE 에서 플래그를 찾아보겠습니다.

 

먼저 UPX로 압축이 되어 있기 때문에, 이를 먼저 풀어줍니다.

 

그 후 출력하는 부분을 확인해 보면, Key -> Colle System 라는 문자열을 출력합니다.

이 부분외에는 출력하는 부분이 없는 것으로 보아, Colle System 이 플래그인 것 같습니다.

 

인증

'0x00 Reversing > 0x01 Reversing.kr' 카테고리의 다른 글

Reversing.kr :: Easy_ELF  (0) 2019.10.05
Reversing.kr :: Easy_Unpack  (0) 2019.03.01
Reversing.kr :: Easy_Keygen  (0) 2019.02.26
Reversing.kr :: Easy_Crack  (0) 2019.02.24

문제 소개

상용 안드로이드 어플리케이션을 개발했지만, 불행히도 개발은 멈췄고 출시를 하지 못하게 되었습니다. 하지만 유효한 키를 찾아서 프리미엄 기능을 열어달라고 합니다.

 

APK 분석

apk 를 제공해 주므로, apktool 을 이용하여 디컴파일을 진행합니다.

.
├── THC
│   ├── AndroidManifest.xml
│   ├── apktool.yml
│   ├── original
│   ├── res
│   └── smali
└── THC.apk

apk 를 디컴파일해보면, 여러 구성 요소들이 보입니다.

 

AndroidManifest.xml 파일을 확인해 보면, com.thc.bestpig.serial.MainActivity 가 메인 엑티비티 인 것을 알 수 있습니다.

 

MainActiviy->onCreate() 부터 살펴 봅니다. onCreate() 에서 하는 게 많지만, 핵심은 마지막 부분에 위치합니다.

Id가 0x7f070023 View 를 가져와서 버튼인지 확인을 하고, MainActivity$3의 <init> 함수를 호출하고 버튼에 Click Listener 을 설정합니다.

 

<public type="id" name="buttonActivate" id="0x7f070023" />

Id 가 0x7f070023 인 것을 확인해 보면, buttonActivate 라는 이름을 확인할 수 있습니다.

 

더 확인해보면, 텍스트로 "Activate Software" 를 가진 버튼임을 확인할 수 있습니다.

 

다시 MainActivity$3 으로 돌아와서, 버튼을 click 하게 되면 위의 method 가 호출됩니다.

입력한 값을 가져와서 문자열로 바꾸고, 이 문자열을 MainActivity의 checkPassword() 메소드의 인자로 하여 호출합니다.

 

checkPassword 메소드에서는 이 문자열을 다시 validateSerial 메소드의 인자로 하여 호출을 하고, 결과값이 1이 아니면 인증 실패 루틴으로 이동하게 됩니다. 따라서 validateSerial 메소드에서 1을 반환할 조건을 분석해 봅니다.

 

validateSerial 메소드에서는 v0 의 값을 return 하게 되는데, 이 값이 1이 되는 경우는 아래에 더 존재하는 모든 조건을 충족하는 경우만 있습니다. 따라서 이 조건들을 정리해 보면 아래와 같습니다.

 

.line 18    len(flag) == 0x13
.line 20    flag[0x04] == flag[0x09] == flag[0x0e] == 0x2d
.line 22    flag[0x05] == flag[0x06] + 0x01
.line 25    flag[0x05] == flag[0x12]
.line 28    flag[0x01] == (flag[0x12] % 0x04) * 0x16
.line 30    flag[0x0a] == flag[0x03] * flag[0x0f] / flag[0x11] - 0x01
.line 32    flag[0x01] == flag[0x0a]
.line 34    flag[0x0d] == flag[0x0a] + 0x05
.line 36    flag[0x0a] == flag[0x05] - 0x09
.line 38    0x5a0 == (flag[0x00] % flag[0x07]) * flag[0x0b]
.line 40    flag[0x02] - flag[0x08] + flag[0x0c] == flag[0x0a] - 0x09
.line 42    (flag[0x03] + flag[0x0c]) / 0x02 == flag[0x10]
.line 44    flag[0x00] - flag[0x02] + flag[0x03] == flag[0x0c] + 0x0f
.line 46    flag[0x03] == flag[0x0d]
.line 48    flag[0x10] == flag[0x00]
.line 50    flag[0x07] + 0x01 == flag[0x02]
.line 52    flag[0x0f] + 0x01 == flag[0x0b]
.line 54    flag[0x0b] + 0x03 == flag[0x11]
.line 56    flag[0x07] + 0x14 == flag[0x06]

이 조건들로 시리얼을 직접 구할 수도 있지만, SMT solver 중 하나인 Z3 를 이용하여 solver 를 만듭니다.

 

Serial 구하기

위에서 찾은 식을 조건에 추가하고, 조건을 충족하는 결과 값을 출력합니다.

 

위에서 구한 serial 값을 입력하면, 활성화에 성공한 것을 확인할 수 있습니다.

'0x00 Reversing > 0x02 CTF' 카테고리의 다른 글

InCTF 2019 :: cliche_crackme  (0) 2019.10.05

_start 함수

_start 함수를 보면 main 함수를 호출한다.

 

main 함수

main 함수를 보면 eax 값이 1이 아니면 Wrong 이라는 문자열로 보아 sub_804851 의 return 값이 1이 되어야 한다.

 

sub_8048451 함수

함수의 return 값이 1이 되어야 하는데, 위에서 return 을 할 수 있는 루틴은 맨 왼쪽으로 진행되는 루틴 이외에는 존재하지 않는다. 따라서 해당 루틴으로 갈 수 있는 조건들을 확인해 본다.

 

data_804a021 == 0x31
data_804a024 == 0x7c
data_804a025 == 0x00
data_804a022 == 0x7c
data_804a020 == 0x78
data_804a023 == 0xdd

위에서 부터 위의 조건들을 모두 성립해야 return 1을 한다.

data_804a020 == 0x78
data_804a021 == 0x31
data_804a022 == 0x7c
data_804a023 == 0xdd
data_804a024 == 0x7c
data_804a025 == 0x00

이를 순서대로 정리해 보면 아래와 같이 된다.

하지만 중간에 위치한 xor 도 있기 때문에 이를 적용해 본다.

data_804a020 ^= 0x34
data_804a020 == 0x78
data_804a021 == 0x31
data_804a022 ^= 0x32
data_804a022 == 0x7c
data_804a023 ^= -0x78
data_804a023 == 0xdd
data_804a024 == 0x7c
data_804a025 == 0x00

이를 최종적으로 정리해 보면 아래와 같은 조건들을 만족해야 한다.

data_804a020 == 0x78 ^ 0x34        // L
data_804a021 == 0x31                    // 1
data_804a022 == 0x7c ^ 0x32        // N
data_804a023 == 0xdd ^ -0x78        // U
data_804a024 == 0x7c                    // X
data_804a025 == 0x00                    // End of String

그러면 data_804a020 부터 있는 문자열은 어디서 오는지 확인을 해본다.

 

data_804a020 데이터

아까 main 함수에서 sub_8048451 함수를 호출하기 전에 sub_8048434 함수를 호출한다.

이를 확인해 보면, scanf 함수를 호출하는데 입력받은 값을 data_804a020 에 넣는다.

따라서 data_804a020 는 사용자가 입력한 값이며, 이 값이 위의 조건문을 성립할 경우에만 Correct! 라는 문자열을 확인할 수 있다.

 

정답

'0x00 Reversing > 0x01 Reversing.kr' 카테고리의 다른 글

Reversing.kr :: Ransomware  (0) 2019.12.09
Reversing.kr :: Easy_Unpack  (0) 2019.03.01
Reversing.kr :: Easy_Keygen  (0) 2019.02.26
Reversing.kr :: Easy_Crack  (0) 2019.02.24

문제 소개

설명을 보면 뉴비를 위한 뻔한 crackme 문제라고 한다. 바이러니의 checksum 비교를 위하여 hash 값을 비교해 본다.

 

hash 값은 동일하며, 맥에서 사용되는 Mach-0 파일임을 알 수 있다.

 

함수 분석

__int64 start()
{
  void *v0; // ST30_8
  __int64 result; // rax
  char v2; // [rsp+40h] [rbp-30h]
  __int64 v3; // [rsp+68h] [rbp-8h]

  v0 = calloc(0x29BuLL, 4uLL);
  scanf("%s", &v2);
  result = sub_100000DB0(sub_100000D00, sub_100000C80, sub_100000BA0, sub_100000C10, &v2, v0);
  if ( __stack_chk_guard == v3 )
    result = 0LL;
  return result;
}

ida 로 열어보면, start 함수에서는 v0 에 0x29B 만큼 calloc 해주고 v2 에 입력값을 받는다.

그 후 sub_100000DB0 함수에서 여러 인자들로 result 값을 만든다.

 

__int64 __fastcall sub_100000DB0(
  void (__fastcall *a1)(__int64, __int64),          // sub_100000D00
  unsigned int (__fastcall *a2)(__int64, void *),   // sub_100000C80
  unsigned int (__fastcall *a3)(__int64),           // sub_100000BA0
  unsigned int (__fastcall *a4)(__int64),           // sub_100000C10
  __int64 a5,                                       // scanf address
  __int64 a6)                                       // calloc(0x29BuLL, 4uLL)
{
  unsigned int (__fastcall *v6)(__int64);           // sub_100000BA0
  __int64 v7;                                       // scanf address
  __int64 v9;                                       // calloc(0x29BuLL, 4uLL)
  unsigned int (__fastcall *v10)(__int64);          // sub_100000C10

  v6 = a3;
  v10 = a4;
  v7 = a5;
  v9 = a6;
  a1(a6, a5);
  if ( v6(v7) && v10(v9) && a2(v9, &unk_100001040) )
    printf("Congratz - You have earned it!");
  else
    printf("%s", "You've got to do better");
  return 0LL;
} 

sub_100000DB0 함수로 들어가 보면, 인자들을 로컬변수에 넣고 그 값들을 이용하여 입력값이 올바른지 확인한다.

이를 요약하면 아래의 함수들로 정리할 수 있다.

  • al(a6,a5) => sub_100000D00(v0,&v2)
  • v6(v7) => sub_100000BA0(&v2)
  • v10(v9) => sub_100000C10(v0)
  • a2(v9, &unk_100001040) => sub_100000C80(v0,&unk_100001040)

 

__int64 __fastcall sub_100000D00(__int64 a1, const char *a2)
{
  __int64 result; // rax
  int j; // [rsp+0h] [rbp-20h]
  int i; // [rsp+4h] [rbp-1Ch]
  int v5; // [rsp+8h] [rbp-18h]
  int v6; // [rsp+Ch] [rbp-14h]

  v6 = strlen(a2);
  v5 = 0;
  for ( i = 0; i < v6; ++i )
  {
    for ( j = i + 1; j < v6; ++j )
      *(_DWORD *)(a1 + 4LL * v5++) = a2[j] + a2[i];
  }
  result = a1;
  *(_DWORD *)(a1 + 2668) = 0;
  return result;
}

sub_100000D00 함수에서는 a1 을 이용하여 a2 값을 생성한다.

 

_BOOL8 __fastcall sub_100000BA0(__int64 a1)
{
  signed int i; // [rsp+4h] [rbp-18h]
  int v3; // [rsp+8h] [rbp-14h]

  v3 = 0;
  for ( i = 0; i < 37; ++i )
    v3 += *(char *)(a1 + i);
  return v3 == 3504;
}

sub_100000BA0 함수에서는 a1의 배열 값들을 더해서 3504 가 맞으면 True 값을 리턴해준다.

 

_BOOL8 __fastcall sub_100000C10(__int64 a1)
{
  signed int i; // [rsp+4h] [rbp-18h]
  int v3; // [rsp+8h] [rbp-14h]

  v3 = 0;
  for ( i = 0; i < 667; ++i )
    v3 += *(_DWORD *)(a1 + 4LL * i);
  return v3 == 126144;
}

sub_100000C10 함수에서는 a1의 배열 값들을 더해서 126144 가 맞으면 True 값을 리턴해준다.

 

_BOOL8 __fastcall sub_100000C80(__int64 a1, __int64 a2)
{
  signed int v3; // [rsp+0h] [rbp-1Ch]

  v3 = 0;
  while ( v3 < 666 )
  {
    if ( *(_DWORD *)(a1 + 4LL * v3) == *(_DWORD *)(a2 + 4LL * v3) )
      ++v3;
  }
  return v3 >= 666;
}

마지막으로 sub_100000C80 함수에서는 a1의 값과 a2의 값이 같은지 확인하는데, 그 길이가 666인지 확인한다.

여기서 a1은 입력값을 기반으로 만든 배열로 a2의 길이가 길이가 666일 경우, a1의 길이가 37인 것을 알 수 있다.

 

위의 함수들을 정리해 보면

  • 입력값의 길이는 37
  • 입력값을 이용하여 만든 배열을 바이너리에 있는 값과 비교
  • 입력값의 문자열들의 총합은 3504

인 것을 알 수 있다.

이제 이를 구하는 간단한 파이썬 코드를 작성한다.

 

flag 구하기

with open('cliche_crackme','rb') as rb:
    rb.seek(0x1040)
    data = rb.read(666*4)
    for i in range(0x21,0x80):
        try:
            rev = chr(i)
            for j in range(0,len(data),4):
                rev += chr(data[j]-ord(rev[0+(len(rev)-1)//37]))
        except:
            a5,chk = rev[:37],0
            for j in range(len(a5)): chk += ord(a5[j])
            if chk == 3504: print('input :',a5)

플래그를 구할때는 거꾸로, 바이너리에서 데이터를 읽어오고 그 값에서 임의의 값을 빼서 문자열을 추측해 본다.

임의의 값을 기반으로 생성한 문자열의 값들을 합한 값이 3504 인지 확인하고, 맞을 경우 이를 출력해 주는 코드이다.

 

숫자, 대문자, 소문자를 거쳐 i로 시작하는 경우에만 그 문자열의 합이 3504이며 플래그를 확인할 수 있다.

'0x00 Reversing > 0x02 CTF' 카테고리의 다른 글

THC CTF 2018 :: Android Serial (with Z3 solver)  (0) 2019.10.13

0x0 Packer

PE 파일을 unpack 하기 전에 packer 에 대하여 먼저 알아본다.
packer은 사용 용도에 따라 크게 2가지로 나눌 수 있다.

0x01 Compression

  • PE 파일의 용량을 줄이기 위한 용도로 활용

0x02 Protector

  • 리버싱을 방해하기 위하여 사용
  • ex) 카카오톡, 악성코드 등

0x1 Manual Unpacking

_start 함수를 따라가다 보면, 여러 반복문들을 확인할 수 있다.

처음에 kernel32.dll 을 로드하고, 0x409000 에서 0x4994ee 까지 xor 을 하여 복호화를 진행한다.

그 후 VirtualProtect 함수를 이용하여 메모리 영역에서의 실행 권한을 변경한다. 즉 0x405000 부터 0x1000 만큼 변경이 된다.

그리고 0x401000 에서 0x405000 까지 xor 을 하여 복호화를 진행한다.

마지막으로는 0x406000 에서 0x409000 까지 xor 을 하여 복호화를 진행한다.
전반적인 복호화가 끝나고, 0x401150 으로 넘어가게 되고, packing 이 풀어진 코드를 실행한다.

0x2 PEiD

PE 에 대한 정보를 보여주는 프로그램 PEiD 를 이용하여 unpacking 을 해본다.
PEiD 에서 다운로드 받을 수 있다.

PEiD 에서 지원하는 플러그인 중에서 Generic OEP Finder 라는 이름의 플러그인이 존재한다.

해당 플러그인을 이용하면 손쉽게 OEP 를 구할 수 있다.

플러그인 분석

PEiD 의 설치 폴더에 가보면 위와 같이 플러그인을 확인할 수 있다.
이 중에서 GenOEP.dll 라이브러리를 분석해 본다.
[추후 추가 예정]

0x3 Reference

'0x00 Reversing > 0x01 Reversing.kr' 카테고리의 다른 글

Reversing.kr :: Ransomware  (0) 2019.12.09
Reversing.kr :: Easy_ELF  (0) 2019.10.05
Reversing.kr :: Easy_Keygen  (0) 2019.02.26
Reversing.kr :: Easy_Crack  (0) 2019.02.24

루틴 분석


_start 의 마지막 루틴 부분에 보면, __return_addr, var_4, var_8_2 를 인자로 하여 sub_401000 함수를 호출한다.

sub_401000

sub_401000 함수는 크게 4가지로 나눌 수 있다.


Input Name이라는 문자열을 출력하며, 이름을 입력 받는다.


그 후 이름을 이용하여, 어떠한 문자열들을 생성한다.


그리고 Input Serial: 라는 문자열을 출력하고, 시리얼을 입력 받는다.


마지막으로는 이름을 기반으로 생성한 문자열과 시리얼을 비교하고, 이에 대한 결과를 출력한다.
따라서 2번째 과정인 이름을 기반으로 시리얼을 생성하는 과정을 분석해 본다.

시리얼 생성


생성 루틴의 처음을 보면, esi 와 0x3 을 비교한 후 작을 경우에 xor을 생략한다.
처음 루틴에 들어왔을 경우에는 esi 가 0으로 초기화되었기 때문에, xor 루틴을 생략된다. 그리고 esi 는 0 이기에 esp+0xc 를 ecx에 넣는다.


여기서 esp+0xc 에는 0x10이 들어가 있다. 이 값은 esi 의 증가에 따라서 0x10, 0x20, 0x30 으로 변화할 것이다.
그 다음으로는 esp+ebp+0x10의 바이트를 edx에 넣는다. 마찬가지로 ebp 는 0으로 초기화 되어있기에 esp+0x10 의 바이트가 들어갈 것이고, esp+0x10에는 입력받은 문자열의 시작 주소를 가리키고 있다.
따라서 ebp 가 증가함에 따라 edx 에는 입력받은 문자열에서 순서대로 하나씩 문자가 들어갈 것이다.
def name2serial(name):
    xorKey = ["10", "20", "30"]
    serial = []
    for i in range(len(name)):
        ecx = int(xorKey[i%3],16)
        edx = int(name[i].encode('hex'),16)
        serial.append(format(ecx ^ edx,'x'))
    return "".join(serial)
분석한 생성 과정을 바탕으로, 이름에서 시리얼을 생성하는 파이썬 코드는 위와 같다.

이름 복구

생성 방식이 단순히 xor 만을 이용하기에, 시리얼에서 이름을 구하는 코드도 비슷하다.
def serial2name(serial):
    xorKey = ["10", "20", "30"]
    name = []
    for i in range(0,len(serial)/2):
        ecx = int(xorKey[i%3],16)
        edx = int(serial[i*2:i*2+2],16)
        name.append(chr(ecx ^ edx))
    return "".join(name)
시리얼을 문자열 형태로 받고, 이를 이용하여 이름을 복구(?!)한다.


이를 이용하여 이름을 구하면 "K3yg3nm3" 라는 이름으로 나온다.


확인 결과 이름과 시리얼이 일치하는 것을 확인할 수 있다.




'0x00 Reversing > 0x01 Reversing.kr' 카테고리의 다른 글

Reversing.kr :: Ransomware  (0) 2019.12.09
Reversing.kr :: Easy_ELF  (0) 2019.10.05
Reversing.kr :: Easy_Unpack  (0) 2019.03.01
Reversing.kr :: Easy_Crack  (0) 2019.02.24

+ Recent posts