如何打破这个逆转练习

逆向工程 快手
2021-06-12 10:07:06

我已经反转了这个简单的crackme(更像是reverseme :))的代码,但我不明白如何为算法创建有效的密码。这是反向代码:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[]) {
    if(argc<2)
        return -1;

    char *original = argv[1]; 
    char *password = strdup(original);
    int success = 0xFD0970E7;
    int i, j;
    for (i = random() & 0xFF; i > 0; i--) {
        for (j = 0; j < (int)strlen(original); j++) {
            password[j] = password[j] ^ random();
        }
    }

    i = 0x1337;
    for (j = strlen(original)-1; j >= 0; j--) {
        i = i * password[j] + 0x31337;
    }

    if (i == success) {
        printf("SUCCESS\n");
        return 0;
    }

    printf("WRONG\n");
    return -1;
}

我知道因为 random() 不是种子,所以我可以控制进入程序后期阶段的输入,但我不知道如何使用它来解决它:(

3个回答

我们可以在 SMT 语言中定义递归函数(例如 with define-fun-rec),但是一些流行的求解器(例如z3)目前还不能处理它们(我不知道有什么可以支持);所以在这样的求解器中编码循环不是直接的。

但是我们可以使用一个技巧,即通过自动生成 SMT 公式来展开循环(然后它仍然必须测试几个长度的密码)。例如,以下程序为每个密码长度生成一个 SMT 公式:

#include <stdlib.h>
#include <stdio.h>

#define PRE_VAL_NUM 0xfff
int ran_vals[PRE_VAL_NUM];

void gen_pre_vals()
{
  for (unsigned int i = 0; i < PRE_VAL_NUM; ++i) {
    ran_vals[i] = random();
  }
  return;
}

int main(int argc, char* argv[])
{
  if (argc != 2) {
    printf("please run as keygen length_of_password\n");
    return 0;
  }

  gen_pre_vals();

  FILE* smt_file = fopen("reverseme.smt2", "w+");

  fprintf(smt_file, "(set-logic QF_BV)\n");
  fprintf(smt_file, "(set-info :smt-lib-version 2.0)\n");

  int passwd_len = strtol(argv[1], NULL, 0);

  fprintf(smt_file, "\n");
  for (int i = 0; i < passwd_len; ++i) {
    fprintf(smt_file, "(declare-fun pw%d () (_ BitVec 8))\n", i);
  }
  fprintf(smt_file, "\n");
  fprintf(smt_file, "(define-fun prev_i ((i (_ BitVec 32)) (pw_i (_ BitVec 8))) (_ BitVec 32)\n");
  fprintf(smt_file, "(let ((pw_i_ext ((_ sign_extend 24) pw_i)))\n");
  fprintf(smt_file, "(bvadd (bvmul i pw_i_ext) #x00031337)))\n");

  fprintf(smt_file, "\n");
  for (int i = 0; i < passwd_len; ++i) {
    fprintf(smt_file, "(assert (and (bvuge pw%d #x21) (bvule pw%d #x7e)))\n", i, i);
  }

  fprintf(smt_file, "\n");
  fprintf(smt_file, "(assert\n");
  fprintf(smt_file, "(let (\n");
  unsigned int acc_ran;
  for (int i = 0; i < passwd_len; ++i) {
    acc_ran = 0x00;

    for (int j = ran_vals[0] & 0xff; j > 0; j--) {
      acc_ran ^= ran_vals[1 + i + (j - 1) * passwd_len];
    }

    fprintf(smt_file, "(pwn%d (bvxor pw%d #x%x))\n", i, i, (acc_ran & 0xff));
  }
  fprintf(smt_file, ")\n");
  fprintf(smt_file, "(let ((i%d (prev_i #x1337 pwn%d)))\n", passwd_len - 2, passwd_len - 1);
  for (int i = passwd_len - 2; i >= 1; --i) {
    fprintf(smt_file, "(let ((i%d (prev_i i%d pwn%d)))\n", i - 1, i, i);
  }
  fprintf(smt_file, "(let ((i (prev_i i0 pwn0)))\n");
  fprintf(smt_file, "(= i #xfd0970e7))\n");
  for (int i = 0; i < passwd_len; ++i) fprintf(smt_file, ")");
  fprintf(smt_file, ")\n");

  fprintf(smt_file, "\n");
  fprintf(smt_file, "(check-sat)\n");
  fprintf(smt_file, "(get-value (\n");
  for (int i = 0; i < passwd_len; ++i) fprintf(smt_file, "pw%d\n", i);
  fprintf(smt_file, "))\n");

  fclose(smt_file);

  printf("output smt file: reverseme.smt2\n");

  return 1;
}

它会产生一个SMT文件,命名reverseme.smt2为密码的每个长度(如长度生成的SMT文件6在这里),那么我们可以输入:z3 reverseme.smt2获得有效的密码。

我已经测试了2, 3, 4, 5(长度1显然是不可能的)的长度在我的机器上,z3每次测试大约需要 1-5 秒,并为每个测试提供UNSAT第一个SAT结果“6`SHQe”(ASCII 代码:0x36, 0x60, 0x53, 0x48, 0x51, 0x65找到长度为6。我不检查是否存在一些长度大于6虽然的有效密码

在 C/C++ 中,rand() 的默认种子为 1。

考虑以下:

#include <stdlib.h>
#include <stdio.h>

int main(int argc, char const* argv[])
{
  srand(1);
  int foo = rand();
  printf("%d\n", foo);
  return 0;
}

这将始终为您提供相同的结果

#include <stdlib.h>
#include <stdio.h>

int main(int argc, char const* argv[])
{
  int foo = rand();
  printf("%d\n", foo);
  return 0;
}

如果您将 srand() 中的值更改为不同的值,您将看到您将获得不同的数字。

因此,在您的破解版的解决方案中,您可以简单地使用 rand() 而不使用 srand(),或者您可以在 rand() 之前使用 srand(1) 因为未播种的 rand() (或 rand() 之前有一个调用to srand(1)) 将始终产生相同的结果。

与所有方程一样,从最后开始求解。您可能需要根据原始密码字符串的长度进行多次迭代。

从假设长度等于 1 开始,解决最后一个循环(密码需要是什么才能找到正确答案?)然后根据您对 rand() 的了解解决第一个循环。如果结果看起来合理(例如,它的长度正好是您假设的 1),请检查并停止。如果不是 - 假设长度等于 2 并执行相同的操作。等等,直到适合 int 的字符串长度。我只想到 8,为什么要计算那个长度。

PS您发布的代码中没有递归,不确定您的意思。