以下の内容はhttps://nekochansecurity555.hatenablog.com/entry/2024/08/25/010211より取得しました。


【crackmes.one】「crackme dady」攻略

はじめに

crackmeの攻略の備忘録になります(攻略までの考え方、ツールの使い方をまとめるのを目的としています)。
静的解析・リバースエンジニアリングの練習としてツールを使いこなせるようにすることを目指しています。

crackmeとは

リバースエンジニアリングスキルをテストするために設計されたプログラムとなります。そのプログラムの挙動・仕様から解析し、攻略しているものになります。

crackmes.oneとは

リバース エンジニアリング スキルを向上させるためにcrackmeのプログラムをダウンロードできるサイトになります。

crackmes.one

攻略

対象ファイル「crackme dady」

今回対象とするファイルは以下のものとなります。

Crackmes

this crackme can be considered as a level 2 difficulty, you have 3 guesses,the password will completely change when entering a wrong pass, however if you take a look at the right place in the right time you will find the answer :), sometimes you gonna hate the litter b, lol

デコンパイル

本来、基本的な攻略としては実際にファイルを起動させてからどのようなプログラム確認してからディスアセンブルされたコードやデコンパイルされたコードを読むほうがいいとは思いますが、
今回Crackmeをやっている目的が静的解析のスキルを伸ばすことを目的としているので、できる限りディスアセンブル/デコンパイルされたコードから内容を把握してから攻略することを基本方針としています。

ツールはGhidraを利用しています。
Ghidra

デコンパイル/ディスアセンブル結果

デコンパイルされたコードは下記の通りとなる。

int __cdecl _main(int _Argc,char **_Argv,char **_Env)

{
  int iVar1;
  int iVar2;
  time_t tVar3;
  char local_45 [20];
  char local_31 [13];
  int local_24;
  int local_20;
  char local_1a;
  char local_19;
  int local_18;
  int counter;
  
  ___main();
  tVar3 = _time((time_t *)0x0);
  _srand((uint)tVar3);
  _puts("Press enter to start");
  _getchar();
  _puts("\t\t##################");
  _puts("\t\t# CRACK ME DADDY #");
  _puts("\t\t##################");
  _putchar(10);
  local_18 = 1;
  while( true ) {
    if (3 < local_18) {
      return 0;
    }
    for (counter = 0; counter < 4; counter = counter + 1) {
      iVar2 = counter * 3;
      iVar1 = _rand();
      local_31[iVar2] = (char)iVar1 + (char)(iVar1 / 10) * -10 + '0';
      iVar2 = _rand();
      local_19 = (char)iVar2 + (char)(iVar2 / 0x1a) * -0x1a + 'A';
      local_31[counter * 3 + 1] = local_19;
      iVar2 = _rand();
      local_1a = (char)iVar2 + (char)(iVar2 / 0x1a) * -0x1a + 'a';
      local_31[counter * 3 + 2] = local_1a;
    }
    local_31[counter * 3] = '\0';
    _puts("------------------------------------------------------------------------");
    _printf("what is the password ::");
    _scanf("%[^\n]%*c",local_45);
    local_20 = _strcmp(local_45,local_31);
    if (local_20 == 0) break;
    if (local_20 != 0) {
      _puts("\t  FAILD!!");
      local_24 = 3 - local_18;
      if (local_24 == 0) {
        _putchar(10);
      }
      else {
        _puts("the password has changed");
      }
      _printf("you have %d guessess left \n",local_24);
    }
    _puts("\n");
    local_18 = local_18 + 1;
  }
  _printf("congrats you got it!!, the password was really %s",local_31);
  return 0;
}

コードを見ると、下記の箇所で入力した文字列(local_45)とある文字列(local_31)が比較されている(strcmp)ことがわかる。
この文字列が一致していた場合(local_20 == 0)の処理としてループが抜けることがわかり、一致していない場合「the password has changed」の文字列と残り回数が表示される。

    _printf("what is the password ::");
    _scanf("%[^\n]%*c",local_45);
    local_20 = _strcmp(local_45,local_31);
    if (local_20 == 0) break;
    if (local_20 != 0) {
      _puts("\t  FAILD!!");
      local_24 = 3 - local_18;
      if (local_24 == 0) {
        _putchar(10);
      }
      else {
        _puts("the password has changed");
      }
      _printf("you have %d guessess left \n",local_24);
    }

文字列が一致していた場合、「congrats you got it!!, the password was really」の文字と、末尾にパスワードが表示される仕様のようである。

入力した文字列(local_45)と比較していることから、「local_31」に代入されている値がパスワードと推測される。
では、「local_31」には何が入っているのかを見てみると、下記for文の中で作成されていることがわかる。

    for (counter = 0; counter < 4; counter = counter + 1) {
      iVar2 = counter * 3;
      iVar1 = _rand();
      local_31[iVar2] = (char)iVar1 + (char)(iVar1 / 10) * -10 + '0';
      iVar2 = _rand();
      local_19 = (char)iVar2 + (char)(iVar2 / 0x1a) * -0x1a + 'A';
      local_31[counter * 3 + 1] = local_19;
      iVar2 = _rand();
      local_1a = (char)iVar2 + (char)(iVar2 / 0x1a) * -0x1a + 'a';
      local_31[counter * 3 + 2] = local_1a;
    }
    local_31[counter * 3] = '\0';

しかもrand関数を利用していることからわかる通り、疑似乱数を使用しているため実行ごとに異なる文字列を作っていることがわかる。
しかしながら、文字数については12文字であることがわかる。

実行

デコンパイルの結果から大まかな処理内容と求めるべきパスワード(の生成方法)について分かったため、
実際に実行してみることにする。
下図の通り、Ghidraでデコンパイルされたコードから推測した通りの挙動であることがわかる。

crackme実行結果

デバック

パスワードは、実行するたびに疑似乱数を利用して生成されることが分かっているのでGhidraからの解析はいったん諦め、
デバッカーを利用した解析をすることとした。
デバッカーにはx32dbgを利用した。
「congrats you got it!!」を表示される方法はいくつかあるとは推測されるが(Jmp命令の書き換えなど)今回は設定されたパスワードを探し当て入力することにした。

x32dbg実行結果

まず、アドレス「0x4015df」(scanf関数部分)にブレイクポイントを設定し、当該箇所まで実行する。
Ghidraでディスアセンブルのコードを見た際に、「local_31」は「esp+2f」のアドレスを参照していることを確認していたため、当該箇所をデバッカーでも確認する(アドレス「0x4015b3」)。
現時点でのespレジスタの値を確認する(0x61FED0)。
espの値から0x2f番目のアドレス(0x61feff)から12バイトの値を確認する。
すると値が「7Rj6Pj9Ct0Bg」であることがわかる。

esp+2f

「7Rj6Pj9Ct0Bg」を入力してい見ると、「congrats you got it!!, the password was really」が表示され確かに、この実行タイミングでのパスワードであったことがわかる。

攻略完了

「crackme dady」攻略完了




以上の内容はhttps://nekochansecurity555.hatenablog.com/entry/2024/08/25/010211より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14