weakptr 发表于 2021-9-15 20:52

学习用crackme,自己写自己破,带自学笔记和源码

刚注册没几天的萌新,自己折腾来自学。想好写完一时不知道这该发哪儿。总之,crackme源码、程序、还有拿来自学的笔记(博客)在这里都放出来了。

因为代码是自己写的,大概分析的时候会有点先入为主,汇编也是现学现卖,发出来虚心接受大佬批评。

另外爆破追码也不知道是指什么,看别的帖子经常提及,咱这里就随意啦...用什么方法都行。

## 得有个前言

总之上一个 (https://nnnewb.github.io/blog/p/crackme-01/) 还过得去,稍微加强一点。

## 0x01 源码

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

size_t getline(char **lineptr, size_t *n, FILE *stream) {
char *bufptr = NULL;
char *p = bufptr;
size_t size;
int c;

if (lineptr == NULL) {
    return -1;
}
if (stream == NULL) {
    return -1;
}
if (n == NULL) {
    return -1;
}
bufptr = *lineptr;
size = *n;

c = fgetc(stream);
if (c == EOF) {
    return -1;
}
if (bufptr == NULL) {
    bufptr = malloc(128);
    if (bufptr == NULL) {
      return -1;
    }
    size = 128;
}
p = bufptr;
while (c != EOF) {
    if ((p - bufptr) > (size - 1)) {
      size = size + 128;
      bufptr = realloc(bufptr, size);
      if (bufptr == NULL) {
      return -1;
      }
    }
    *p++ = c;
    if (c == '\n') {
      break;
    }
    c = fgetc(stream);
}

*p++ = '\0';
*lineptr = bufptr;
*n = size;

return p - bufptr - 1;
}

size_t r_trim(char *str, size_t len) {
size_t slen = strnlen(str, len);
for (size_t i = slen - 1; i >= 0; i++) {
    if (str == ' ' || str == '\n' || str == '\r') {
      str = '\0';
    } else {
      break;
    }
}
return strnlen(str, len);
}

char *calculate(char *username, const size_t username_len) {
// 初始化固定8字节计算密钥的空间
const size_t input_buf_len = 8;
char *input_buf = malloc(input_buf_len);
for (size_t i = 0; i < input_buf_len; i++) {
    input_buf = 0x52 + i;
}
// 用用户输入替换初始化的数据
memcpy_s(input_buf, input_buf_len, username, username_len);

// 异或处理
for (size_t i = 0; i < input_buf_len; i++) {
    input_buf ^= 0x25;
}

// 初始化 Hex 输出
const size_t output_buf_len = 17;
char *output_buf = malloc(output_buf_len);

// 转为可读字符串
for (size_t i = 0; i < input_buf_len; i++) {
    sprintf(&output_buf, "%02x", input_buf);
}

output_buf = 0;
free(input_buf);
return output_buf;
}

int main() {
while (1) {
    char *username = NULL;
    size_t username_len = 0;
    char *serial = NULL;
    size_t serial_len = 0;
    size_t linesize = 0;

    printf("username:");
    linesize = getline(&username, &username_len, stdin);
    username_len = r_trim(username, linesize);
    if (username_len > 8) {
      free(username);
      puts("username less than 8 letter");
      continue;
    } else if (username_len == 0) {
      free(username);
      continue;
    }

    printf("serial:");
    linesize = getline(&serial, &serial_len, stdin);
    serial_len = r_trim(serial, linesize);
    if (serial_len != 16) {
      free(username);
      free(serial);
      puts("serial has 16 letters");
      continue;
    }

    char *correct = calculate(username, username_len);
    int rc = strncmp(serial, correct, 16);
    if (rc == 0) {
      free(correct);
      puts("Good job!");
      break;
    } else {
      puts("wrong pwd!");
    }
    free(username);
    free(serial);
    free(correct);
}
return 0;
}
```

编译方式是

```batch
clang main.c -o cm02-easy.exe -Wall -m32 -O0
clang main.c -o cm02-normal.exe -Wall -m32 -O1
clang main.c -o cm02-hard.exe -Wall -m32 -O2
```

## 0x02 观察

启动后观察行为(不截图了。)

```plaintext
weakptr in cm02 ❯ .\cm02-easy.exe
username:abc
serial:123456
serial 长度为16
username:abc
serial:123456789012345
wrong pwd!
username:
serial:
serial 长度为16
username:abc
serial:aaaaaaaaaaaaaaa
wrong pwd!
```

这次的目标是:

1. 得到某个用户名对应的序列号(`serial`)。
2. 破解,总是正确或对任何输入都提示正确。
3. 注册机。

## 0x03 静态分析 - easy

### 3.1 主循环

在公司没IDA,用 (https://cutter.re/) 打开,在上方输入框输入 `main` 跳转到 `main` 函数。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-1.png)

然后点击 *图表(main)* 进入类似 IDA 的控制流视图。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-2.png)

之后就能看到下面的控制流了。

![](https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-3.png)

easy难度下没有开启任何编译器优化,控制流和原始代码能直接对应上。瞧着困难很多对吧?

先简单扫一眼,会发现很多分支直接跳回了`0x0040139d`,也就是从上往下数第二个代码块,基本每个跳转都是下一个块或跳回这个块。按照 (https://nnnewb.github.io/blog/p/crackme-01/)的经验,我们先找到关键的一跳。可以直接搜索字符串引用(`wrong pwd!`),也可以逐个代码块看下去。

很快,右下角的关键跳出现在眼前。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-4.png)

接着回头看跳转条件。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-5.png)

虽然没有名字,但`fcn.00403ef4` 是老熟人了。三个参数,`ecx`、`eax`、`0x10`,返回结果和`0`做比较,`jne`条件跳转。

- `cmp`指令,操作数相减(`dest`-`src`),结果存入标志位 `SF`和`ZF`。
- 结果是负数(`dest`<`src`),`SF`也就是结果符号位设置为1。
- 结果是正数(`dest`>`src`),`SF`也就是结果符号位设置为0。
- 结果是0(`dest`=`src`),`ZF`设置为1。
- `jne`或`jnz`指令,非零跳转。`ZF`标志位为`1`时跳转。

猜测这个函数应该是`strncmp`。继续往回看参数是怎么来的。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-6.png)

`eax`来自`sub.02x_40298c`这个函数,后面两个脱裤子放屁的`mov`忽略。`ecx`则来来自`mov ecx,dword `这一行。

先不着急分析函数,继续往回找,找到``的来源。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-7.png)

在入口点附近,看到``被初始化成了0。

因为没有很明确的路径,手动计算栈上偏移又非常麻烦,这里本应该掏出调试器——但出于学习练手的目的,还是先尝试计算下。首先回顾下简化的栈内存布局,从上往下增长,如图。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-8.png)

接下来从`mov ebp,esp`开始,往下列出所有函数调用,捋一捋逻辑。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-9.png)

第一个框,`` 计算结果是 ``,也就是栈顶,栈顶设置为字符串 `username:`,接着调用一个未知函数。从参数判断我们先认为是一个输出字符串的函数。

再看第二个框,`acrt_iob_func`,百度一下就会发现,`__acrt_iob_func`函数是定义于 c 运行库里的函数,作用是返回 `stdin/stdout/stderr` 。栈顶设置为0,所以获得的是 `stdin`。

再看第三个框,`edx`和`ecx`赋值为栈上两个变量的地址,再为参数。按顺序就是`f(edx,ecx,stdin)`。暂时不明。函数返回值被赋值回了``。

第四个框,从第三个框得到的返回值被当参数传给一个未知函数。`f(, )`,返回值被赋值回 ``。

结合最后的 `cmp` 和 `jbe` 指令分析,人肉反编译后用伪代码表示,就是下面这样。`jbe`指令只在`cmp`左操作数小于等于右操作数时执行跳转(`CF`标志位和`ZF`标志位其中一个为1时)。

```python
var var_8 # 偏移值 ebp-8h
var var_0C # 偏移值 ebp-0Ch
var var_18 # 偏移值 ebp-18h

print("username:")
var_18 = unknown_func1(&var_8,&var_0c,stdin)
var_0c = unknown_func2(var_8, var_18)
if var_0c <= 8:
    ... # jbe 跳转执行
```

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-10.png)

可以看出,当 `var_0c` 小于 8 时,提示 `username less than 8 letter` 。因此可以确定 `` 这个变量就是 `username` 字符串的长度,上一个函数会计算字符串长度返回。我们再根据这个发现修改下伪代码。

```python
var var_8 # 偏移值 ebp-8h
var username_len # 偏移值 ebp-0Ch
var var_18 # 偏移值 ebp-18h

print("username:")
var_18 = unknown_func1(&var_8,&username_len,stdin) # var_8 可能是 username 指针
username_len = unknown_func2(var_8, var_18) # 计算字符串长度
if username_len <= 8:
    ... # jbe 跳转执行
else:
    # jmp 到开头

```

第一个未知函数看起来已经呼之欲出了,`stdin`和`&username_len`作为参数,`var_8` 有极大可能就是`username`字符串指针。不过在进入调试器前,还不能马上下结论,继续看正确跳转的代码。

```asm
cmp , 0
jnz ...
```

这次是比较用户名长度和0,非0跳转。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-11.png)

可以看到为零时,经过一个未知函数 `sub_4036FC(var_8)` 后,跳回开头。

继续看正确流程,`jmp $+5` ,`$` 表示当前正在执行的代码在代码段内的偏移量,+5就是从当前代码开始往后跳过5个字节,我们直接看IDA分析好的跳转位置。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-12.png)

又是非常熟悉的代码,和读取 `username` 的分析方式相同,以相同的顺序调用相同的函数,可以得到`var_14`是`serial_len`,`Str1`可能是`serial`字符串指针。不做重复分析,继续往下看。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-13.png)

右边的代码块是关于长度的判断,分析方法不再重复。左侧代码就是我们的关键跳转了,其中出现两个函数调用。

```asm
mov   eax,
mov   ecx,
mov   , ecx ; void *
mov   , eax ; size_t
call    sub_401250
mov   , eax
```

`var_c`先前被判断是`username_len`,`Block`就是`var_8`,先前被怀疑是用户键入的用户名字符串指针。未知函数的返回值保存在 ``中。

这个`1c`在随后的代码中立刻被用到。

```asm
mov   eax,
mov   ecx,
mov   , ecx ; Str1
mov   , eax ; Str2
mov   , 10h ; MaxCount
call    _strncmp
mov   , eax
```

`Str1`在`serial`输入这一步被怀疑是用户输入的序列号字符串指针,它和上一个函数调用返回的`var_1c`被作为参数传递给`strncmp`,字符串长度最大16字节。由此可见,`var_1c`基本可以确定是正确序列号的指针,之前的未知函数可能就是生成序列号的函数。

下一步分析序列号生成函数。

### 3.2 生成序列号

先看下控制流全览,能依稀分辨出三个循环。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-14.png)

自动分析出的变量表

```asm
; var uint32_t var_1ch @ ebp-0x1c
; var int32_t var_18h @ ebp-0x18
; var int32_t var_14h @ ebp-0x14
; var uint32_t var_10h @ ebp-0x10
; var uint32_t var_ch @ ebp-0xc
; var int32_t var_8h @ ebp-0x8
; var int32_t var_4h @ ebp-0x4
; arg uint32_t arg_8h @ ebp+0x8
; arg int32_t arg_ch @ ebp+0xc
; var int32_t var_sp_4h @ esp+0x4
; var int32_t var_sp_8h @ esp+0x8
; var int32_t var_sp_ch @ esp+0xc
```

先看循环外的代码,简单按用途划一下分隔线。

```asm
0x004071f0      push    ebp
0x004071f1      mov   ebp, esp
0x004071f3      sub   esp, 0x2c
; ---
0x004071f6      mov   eax, dword
0x004071f9      mov   eax, dword
; ---
0x004071fc      mov   dword , 8
; ---
0x00407203      mov   dword , 8
0x0040720a      call    fcn.00401302
0x0040720f      mov   dword , eax
; ---
0x00407212      mov   dword , 0
```

开头是惯例的两句栈帧准备动作,随后开辟 0x2c 大小的栈空间。

两个没用的 `mov eax,...`,之后是``设置为8,再把8作为参数调用了一个未知函数,返回值赋值给``,再初始化``为 0。伪代码表示就是下面这样。

```c
int var_4h, var_8h, var_ch; // ebp-4h, ebp-8h, ebp-ch
var_4h = 0x8;
var_8h = unknown_func(0x8);
var_ch = 0x0;
```

然后是一个条件跳转。

```asm
0x00407219      cmp   dword , 8
0x0040721d      jae   0x407242
```

学习下`jae`指令。`jae`指令和`jnc`指令相同,`CF=0`则跳转。`jae` 可以看作 *Jump if above or equals*。上一句 `cmp` 计算 `var_ch - 0x8` ,对相关标志位赋值。`jae`指令根据`CF`标志位判断,由于`cmp`指令是减法,所以判断的是减法中有没有出现 *借位* 。

简单的描述就是,`cmp ax, bx`,如果`ax < bx` 则 `CF=1`,如果 `ax >= bx` 则 `CF=0`。

因为我们知道 `var_ch` 刚被初始化成了0,不成立,继续看不成立的分支。

```asm
0x00407223      mov   eax, dword
0x00407226      add   eax, 0x52; 82
0x00407229      mov   dl, al
; ---
0x0040722b      mov   eax, dword
0x0040722e      mov   ecx, dword
0x00407231      mov   byte , dl
; ---
0x00407234      mov   eax, dword
0x00407237      add   eax, 1
0x0040723a      mov   dword , eax
0x0040723d      jmp   0x407219
```

把`var_ch`移入寄存器`eax`后,加上`0x52`,又移动`al`到`dl`。后续`eax`被用作别的用途,这一番操作其实就是给`dl`赋值了一个`(int16_t)0x52+var_ch`。

随后把`var_8h`和`var_ch`相加后的地址赋值 `dl`,也就是`0x52`。

接着`var_ch`自增1,跳回 `jae`判断前的 `cmp`,形成循环,我们用伪代码表示。

```c
int var_4h, var_8h, var_ch; // ebp-4h, ebp-8h, ebp-ch
var_4h = 0x8;
var_8h = unknown_func(0x8);
var_ch = 0x0;
while(var_ch < 8) {
    *(var_8h + var_ch) = 0x52 + var_ch;
    var_ch++;
}
```

从结构上看,是一个典型的 for 循环。 `var_8h` 是一个未知函数返回的指针。我们稍微改下伪代码。

```c
int var_4h, var_8h; // ebp-4h, ebp-8h
var_4h = 0x8;
var_8h = unknown_func(0x8);

for (int var_ch=0; var_ch < 8; var_ch++) { // var_ch -> ebp-ch
    var_8h = 0x52 + var_ch;
}
```

接着继续看循环结束后的代码。

```asm
0x00407242      mov   eax, dword ; ebp+ch 函数右往左数第二个入参
0x00407245      mov   ecx, dword ; ebp+8h 函数右往左数第一个入参
0x00407248      mov   edx, dword ; ebp-8h
; ---
0x0040724b      mov   dword , edx
0x0040724e      mov   dword , 8
0x00407256      mov   dword , ecx
0x0040725a      mov   dword , eax
0x0040725e      call    fcn.00407310
0x00407263      mov   dword , 0
```

从之前分析主循环的代码,我们可以发现 `arg_8h` 其实是用户名字符串指针,`arg_ch`是用户名字符串长度。

接着这两个入参,和 `var_8h`,也就是之前得到指针,传入一个未知函数,随后再初始化了一个变量 `var_10h`。

伪代码如下。

```c
unknown_func(var_8h, 0x8, username, username_len); // 猜测的函数签名 func(void*, int, void*, int)
int var_10h = 0;
```

接着又是一个条件跳转。

```asm
0x0040726a      cmp   dword , 8
0x0040726e      jae   0x407292
```

和先前的循环相同,不作重复分析,直接进入循环体。

```asm
0x00407274      mov   eax, dword
0x00407277      mov   ecx, dword
0x0040727a      movsx   edx, byte
0x0040727e      xor   edx, 0x25; 37
0x00407281      mov   byte , dl
; ---
0x00407284      mov   eax, dword
0x00407287      add   eax, 1
0x0040728a      mov   dword , eax
; ---
0x0040728d      jmp   0x40726a
```

前两条指令没什么可说的,`movsx`还是第一次见,学习下。

`movsx` 从来源取数,不足的部分用来源的符号位填充,这里取的是`var_8h`,一字节,到 `edx` 寄存器。`movsx`的好处是可以保留符号位,加载不同大小的数据时(比如来源是 `word`,目标是 `dword`),如果来源是负数,则填充符号位可以正确表示补码形式表示的负数。

从`var_8h`取数移入`edx` 后,之后是一句简单的 `xor`,逻辑异或运算。之后将`xor`运算结果取低位1字节(`dl`寄存器)移回`var_8h`。

之后自增,跳转循环,和之前的循环一样。将分析过的部分用伪代码表示如下。

```c
int var_4h, var_8h; // ebp-4h, ebp-8h
var_4h = 0x8;
var_8h = unknown_func(0x8);

for (int var_ch=0; var_ch < 8; var_ch++) { // var_ch -> ebp-ch
    var_8h = 0x52 + var_ch;
}

unknown_func(var_8h, 0x8, username, username_len); // 猜测的函数签名 func(void*, int, void*, int)
for(int var_10h=0; var_10h < 8; var_10h++) { // var_10h -> ebp-10h
    var_8h ^= 0x25;
}
```

继续看循环结束后的动作。

```asm
0x00407292      mov   dword , 0x11 ; 17
0x00407299      mov   dword , 0x11 ; =-1 ; 17
0x004072a0      call    fcn.00401302
0x004072a5      mov   dword , eax
0x004072a8      mov   dword , 0
```

调用一个函数,返回值赋值给`var_18h`,同时初始化`var_1ch`为 0。伪代码表示如下。

```c
int var_14h = 0x11;
var_18h = unknown_func(0x11);
int var_1ch = 0x0;
```

接下来又是一个循环。

```asm
0x004072af      cmp   dword , 8
0x004072b3      jae   0x4072f2
```

不重复分析,进入循环体。

```asm
0x004072b9      mov   eax, dword
0x004072bc      mov   ecx, dword
0x004072bf      movsx   eax, byte
0x004072c3      mov   edx, dword
0x004072c6      mov   ecx, dword
; ---
0x004072c9      shl   ecx, 1
0x004072cc      add   edx, ecx
; ---
0x004072ce      lea   ecx, str.02x ; 0x45de50,内容是 %02x
; ---
0x004072d4      mov   dword , edx
0x004072d7      mov   dword , ecx
0x004072db      mov   dword , eax
0x004072df      call    fcn.00403dcd
; ---
0x004072e4      mov   eax, dword
0x004072e7      add   eax, 1
0x004072ea      mov   dword , eax
0x004072ed      jmp   0x4072af
```

依然是从 `var_8h` 取数,之后把 `var_18h` 和 `var_1ch` 也取数,分别放到 `eax`、`edx`、`ecx`。

接着是一个没见过的命令,`shl`,学习下。

`shl`是逻辑左移,和 c 中的 `<<` 运算符一样,两个操作数,命令格式`shl 寄存器,立即数`。

这里做的就是 `ecx`,也就是 `var_1ch` 的值左移1位,众所周知左移n位可以看作乘上2^n^ ,所以这句 `shl` 其实就是 `var_1ch*2`。左移后结果加到了`edx`,`edx`是`var_18h`。

之后是一个`lea`,加载地址,内容是常量字符串 `%02x`,看起来是一个 c 格式化字符串。

接着压栈传参,调用未知函数,结果忽略。伪代码表示如下。

```c
unknown_func(var_18h + var_1ch * 2, "%02x", var_8h);
```

随后是变量自增,跳转回循环开头。

我们把分析出来的伪代码再合并下。

```c
int var_4h, var_8h; // ebp-4h, ebp-8h
var_4h = 0x8;
var_8h = unknown_func(0x8);

for (int var_ch=0; var_ch < 8; var_ch++) { // var_ch -> ebp-ch
    var_8h = 0x52 + var_ch;
}

unknown_func(var_8h, 0x8, username, username_len); // 猜测的函数签名 func(void*, int, void*, int)
for(int var_10h=0; var_10h < 8; var_10h++) { // var_10h -> ebp-10h
    var_8h ^= 0x25;
}

int var_14h = 0x11;
var_18h = unknown_func(0x11);
for(int var_1ch = 0x0; var_1ch < 8; var_1ch++) {
    unknown_func(var_18h + var_1ch * 2, "%02x", var_8h);
}
```

最后是循环结束后的代码。

```asm
0x004072f2      mov   eax, dword
0x004072f5      mov   byte , 0
; ---
0x004072f9      mov   eax, dword
0x004072fc      mov   dword , eax
0x004072ff      call    fcn.00402a36
; ---
0x00407304      mov   eax, dword
; ---
0x00407307      add   esp, 0x2c
0x0040730a      pop   ebp
0x0040730b      ret
```

首先是把`var_18h` 的值设为0。

接着`var_8h`做参数调未知函数。

把`var_18h`移到`eax`,也就是`cdecl`约定下的返回值位置。

最后平栈,恢复`ebp`,返回,函数结束。我们把所有内容的伪代码合并起来。

```c
int var_4h = 0x8; // ebp-4h
void* var_8h = unknown_func(0x8); // ebp-8h

for (int var_ch=0; var_ch < 8; var_ch++) { // var_ch -> ebp-ch
    var_8h = 0x52 + var_ch;
}

unknown_func(var_8h, 0x8, username, username_len); // 猜测的函数签名 func(void*, int, void*, int)
for(int var_10h=0; var_10h < 8; var_10h++) { // var_10h -> ebp-10h
    var_8h ^= 0x25;
}

int var_14h = 0x11;
var_18h = unknown_func(0x11);
for(int var_1ch = 0x0; var_1ch < 8; var_1ch++) {
    unknown_func(var_18h + var_1ch * 2, "%02x", var_8h);
}
var_18h = 0;

unknown_func(var_8h);
return var_18h;
```

从这我们已经能看出具体算法了,未知函数可以猜测调试看看。

## 0x04 调试器 - easy

调试的目标是确认生成序列号的算法,把分析出的伪代码中还不清楚用途的未知函数,分析出作用。

### 4.1 x32dbg

打开调试器后,先找到关键跳,在工具栏点击字符串工具图标,在下方搜索栏输入`wrong pwd!`

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-15.png)

跳到引用位置。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-18.png)

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-19.png)

之后可以按g,进入控制流视图,不过这个控制流视图有点不好看,我们也可以直接参考静态分析中的汇编,直接找到函数,并在入口下断点。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-20.png)

尝试随便输入一点内容,调试器命中。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-21.png)

接下来就可以用左上角的单步调试了。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-22.png)

不做更多介绍,汇编的分析已经进行过一次。这次我们找到对输入 "abc" 的正确序列号,完成一次解密。

只需要在断点处点击!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-23.png)按钮,然后观察`eax`寄存器。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-24.png)

抄出来(居然不能右键复制后面的字符串),内容是`4447467073727d7c`。

接着继续运行,再把抄出来的答案复制进去看看。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-25.png)

到这里,我们拿到了一个可以用的序列号。

## 0x05 注册机

### 5.1 Python 脚本注册机

先把前面的伪代码贴一下。

```c
int var_4h = 0x8; // ebp-4h
void* var_8h = unknown_func(0x8); // ebp-8h

for (int var_ch=0; var_ch < 8; var_ch++) { // var_ch -> ebp-ch
    var_8h = 0x52 + var_ch;
}

unknown_func(var_8h, 0x8, username, username_len); // 猜测的函数签名 func(void*, int, void*, int)
for(int var_10h=0; var_10h < 8; var_10h++) { // var_10h -> ebp-10h
    var_8h ^= 0x25;
}

int var_14h = 0x11;
var_18h = unknown_func(0x11);
for(int var_1ch = 0x0; var_1ch < 8; var_1ch++) {
    unknown_func(var_18h + var_1ch * 2, "%02x", var_8h);
}
var_18h = 0;

unknown_func(var_8h);
return var_18h;
```

里面的未知函数(失策,clang默认静态链接了libcmt,很多库函数在x32dbg里认不出来)猜一猜吧。

```python
username = input('username:').encode()
username_len = len(username)

var_4h = 8
var_8h = bytearray(8)

for i in range(8):
    var_8h = 0x52 + i

# 这里的未知函数通过调试器可以看出,把入参复制到了 var_8h 里
var_8h[:username_len] = username

for i in range(8):
    var_8h ^= 0x25

# for(int var_1ch = 0x0; var_1ch < 8; var_1ch++) {
#   unknown_func(var_18h + var_1ch * 2, "%02x", var_8h);
# }
#
# 最后的那个循环中,函数判断为 sprintf 或其他啥,格式化明确是2位小写16进制数
# 前面的计算看作是算偏移,一个 var_8h 的字节对应 2 字节16进制表示,所以 var_18h 加上 NUL 一共是 0x11 也就是 17 个字节
# 循环的作用是把 var_8h 这个字节数组转换成16进制表示的字符串。
#
# 在 python 里用 hex() 就行了。
print(var_8h.hex())
```

运行脚本,输入`abc`,输出`4447467073727d7c`,确认注册机可以生成序列号。

## 0x06 修改 exe

### 6.1 x32dbg 修改关键跳

用调试器打开后找到决定serial是否正确的关键跳转,右键二进制选择用NOP填充,确认即可。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-26.png)

修改后效果如图。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-27.png)

接着把修改后的exe保存下来,在文件菜单里选择补丁。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-28.png)

全选,点修补文件,选择路径保存。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-29.png)

我保存在`cm02-easy-patched.exe`,接着我们试试运行。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-30.png)

遗憾的是被x32dbg补丁功能导出的文件需要管理员权限运行,为了能截到图,图中用了名为`sudo`的工具命令,可以用`scoop install sudo`来安装`sudo`,点击去(https://scoop.sh)。

### 6.2 反编译器修改关键跳

以Cutter为例,找到`jne`指令后,右键修改为`nop`即可。记得先备份。

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-31.png)

!(https://nnnewb.github.io/blog/image/crackme-02/cm02-easy-32.png)

修改后也能实现和x32导出一样的效果,而且不用管理员权限。

## 结论

总得有个结论。

这次逆向应该能帮助学到下面的东西:

- 栈帧结构和函数调用
- `cmp`指令
- `jne`、`jbe`、`jnz`、`jae`指令
- `movsx`指令
- `shl`指令

库函数因为静态链接的缘故已经变成了文中的未知函数,造成了分析上的障碍。老实说如果不是自己写的源码,能不能这么顺利逆向出注册机还真不好说。

开启优化的 *normal* 和 *hard* 难度就不进一步分析了,有兴趣可以看看。

weakptr 发表于 2021-9-16 18:55

jianga13458 发表于 2021-9-16 13:15
天哦 看的晕乎乎的 兴趣全没了 我这没基础的新手可咋整啊

不好意思,我这都是按自己的情况来自学的,主要目的是学汇编,写的也不好。

没基础可以看看yyhd大佬的的教程,如果还不了解的话推荐一下

uav 发表于 2021-9-16 00:04

这个太强大了,C语言的 crackme,自带分析

traviswen 发表于 2021-9-16 02:18

自己靠自己 自己动手丰衣足食

xiaohong 发表于 2021-9-16 06:31

好厉害,虚心学习

ongp1347 发表于 2021-9-16 08:28

楼主强 给个{:1_921:} 谢谢分享

swz02 发表于 2021-9-16 08:34

学习 谢谢分享

meichangsu 发表于 2021-9-16 11:14

谢谢楼主分享

小马奔腾2 发表于 2021-9-16 11:37

感谢分享

jianga13458 发表于 2021-9-16 13:15

天哦 看的晕乎乎的 兴趣全没了 我这没基础的新手可咋整啊

DarklyBoy 发表于 2021-9-16 18:49

牛啊牛啊。飞来学习学习。
页: [1] 2 3
查看完整版本: 学习用crackme,自己写自己破,带自学笔记和源码