hans7 发表于 2022-7-27 02:11

【CTF reverse】SMC好题赏析——网鼎杯2020青龙组jocker、SCUx401CTF2021-RE2-pixpix

本帖最后由 hans7 于 2022-7-27 02:18 编辑

SMC,即self modifying code,自修改代码。

为什么是好题呢?~~因为我刚好都会写~~!

本文52pojie:https://www.52pojie.cn/thread-1667202-1-1.html

本文juejin:https://juejin.cn/post/7124745598271488030/

**作者:(https://blog.csdn.net/hans774882968)以及(https://juejin.cn/user/1464964842528888)**

## 【网鼎杯2020青龙组】jocker

传送门:在buuoj上找qwq。

### 依赖

IDA版本为7.7。

### 分析

32位,`Section: [.text], EP: 0x000008E0`故无壳。用IDA打开,一进来就是main函数。

IDA7.7相比于6.6强大了许多,终于`positive sp`的代码也可以反编译了,感动!

```c
// positive sp value has been detected, the output may be wrong!
int __cdecl main(int argc, const char **argv, const char **envp)
{
char Str; // BYREF
char Destination; // BYREF
DWORD flOldProtect; // BYREF
size_t v7; //
int i; //

__main();
puts("please input you flag:");
if ( !VirtualProtect(encrypt, 0xC8u, 4u, &flOldProtect) )
    exit(1);
scanf("%40s", Str);
v7 = strlen(Str);
if ( v7 != 24 )
{
    puts("Wrong!");
    exit(0);
}
strcpy(Destination, Str);
wrong(Str);
omg(Str);
for ( i = 0; i <= 186; ++i )
    *((_BYTE *)encrypt + i) ^= 0x41u;
if ( encrypt(Destination) )
    finally(Destination);
return 0;
}
```

可以发现`encrypt`是SMC(self modifying code),写一个IDAPython脚本:

```python
import idc
addr = 0x401500# encrypt函数的地址
for i in range(187):
    b = get_bytes(addr + i, 1)
    idc.patch_byte(addr + i, ord(b) ^ 0x41)
```

运行IDAPython脚本:选择`File -> Script File…`。

接下来,需要先右键undefine encrypt的函数定义,键盘不断按c,把所有孤立的`db xx`的数据全部强转为汇编代码,保证`db xx`不再出现。然后`Edit -> functions -> Create function...`重新创建函数。这样就能成功得到`encrypt`函数。

```c
int __cdecl encrypt(char *a1)
{
int v2; // BYREF
int v3; //
int i; //

v3 = 1;
qmemcpy(v2, &unk_403040, sizeof(v2));
for ( i = 0; i <= 18; ++i )
{
    if ( (char)(a1 ^ Buffer) != v2 )
    {
      puts("wrong ~");
      v3 = 0;
      exit(0);
    }
}
puts("come here");
return v3;
}
```

我们写脚本,执行以后,可以看到没有拿到完整flag。接下来看finally函数。finally函数也被smc影响了,所以同样需要进行encrypt函数的流程,才能拿到代码。

```c
int __cdecl finally(char *a1)
{
unsigned int v1; // eax
char v3; // BYREF
int v4; //

strcpy(v3, "%tp&:");
v1 = time(0);
srand(v1);
v4 = rand() % 100;
v3 = 0;
*(_WORD *)&v3 = 0;
if ( (v3[(unsigned __int8)v3] != a1[(unsigned __int8)v3]) == v4 )
    return puts("Really??? Did you find it?OMG!!!");
else
    return puts("I hide the last part, you will not succeed!!!");
}
```

这里我认为是无解的了。查了答案发现,是社工,假设你已经知道:

- 对常量串`"%tp&:"`的操作和前面一样,是xor。
- 最后一个字符是`'}'`。

于是可以解出最后5个字符。

### 代码

```python
buffer = 'hahahaha_do_you_find_me?'
v2 = [
    0x0E, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x09, 0x00,
    0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00,
    0x05, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x56, 0x00,
    0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00,
    0x0C, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x1F, 0x00,
    0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00,
    0x6B, 0x00, 0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x59, 0x00,
    0x00, 0x00, 0x0D, 0x00, 0x00, 0x00
]
ans = ''
for i in range(0, len(v2), 4):
    ans += chr(ord(buffer) ^ v2)
# 最后5位
v3 = '%tp&:'
xor_v = ord(v3[-1]) ^ ord('}')
for c in v3:
    ans += chr(xor_v ^ ord(c))
print(ans)
```

## SCUx401CTF2021-RE2-pixpix

[传送门](https://github.com/bluesadi/SCUCTF-Backup)

### 依赖

IDA7.7

### 分析

32位,`Section: [.text], EP: 0x000007AD`故无壳。

打开IDA,立刻到了`main函数`:

```c
int __cdecl main(int argc, const char **argv, const char **envp)
{
HDC DC; // eax
COLORREF Pixel; // eax
unsigned int v5; // edi
unsigned int v6; // kr00_4
unsigned int v7; // esi
int v9; //
DWORD flOldProtect; // BYREF

DC = GetDC(0);
Pixel = GetPixel(DC, 401, 401);
word_9A3384 = Pixel;
byte_9A3386 = BYTE2(Pixel);
VirtualProtect(sub_9A1050, (char *)nullsub_1 - (char *)sub_9A1050, 0x40u, &flOldProtect);
v5 = 0;
if ( (char *)nullsub_1 != (char *)sub_9A1050 )
{
    do
    {
      v6 = v5;
      v7 = v5++;
      *((_BYTE *)sub_9A1050 + v7) ^= *((_BYTE *)&word_9A3384 + v6 % 3);
    }
    while ( v5 < (char *)nullsub_1 - (char *)sub_9A1050 );
}
VirtualProtect(sub_9A1050, (char *)nullsub_1 - (char *)sub_9A1050, flOldProtect, &flOldProtect);
sub_9A1050(v9);
return 0;
}
```

`nullsub_1`的地址是`9A10B0`,所以进行运行时解密的范围是`9A1050 ~ 9A10B0`。而运行时解密仅仅是一个简单的异或。

`word_9A3384`相当于一个长为3的数组,数组内容是`(401,401)`处的像素值。因为动态调试可以修改任意处内存的值,所以也就**相当于你输入的内容**。

我们知道,c语言程序生成的x86 exe,其各函数头是固定的,目的是处理栈帧。具体是什么呢?查看`main`函数开头的汇编:

```assembly
.text:009A10C0 55                            push    ebp
.text:009A10C1 8B EC                         mov   ebp, esp
```

另外,`0x9a1050`处的值为`61 BB DD`。所以我们求出`0x34, 0x30, 0x31`为所求数组。

#### 法一

那么我们写一段IDAPython脚本:

```python
import idc
st_addr = 0x9a1050
ed_addr = 0x9a10b0
pix =
for i in range(st_addr, ed_addr):
    b = get_bytes(i, 1)
    idc.patch_byte(i, ord(b) ^ pix[(i - st_addr) % 3])
```

脚本运行方法和运行后的undefine+重新创建函数的操作同jocker那题。

于是我们拿到

```c
int sub_9A1050()
{
char v1; // BYREF
int v2; //

strcpy(v1, "scuctf{pixel!pixel!pixel!}");
v1 = 0;
v2 = 0;
return ((_DWORD (__cdecl *)(char *))sub_9A1010)(v1);
}

int sub_9A1010(char *Format, ...)
{
unsigned __int64 *v1; // eax
FILE *v3; //
va_list va; // BYREF

va_start(va, Format);
v3 = _acrt_iob_func(1u);
v1 = (unsigned __int64 *)sub_9A1000();
return _stdio_common_vfprintf(*v1, v3, Format, 0, va);
}
```

答案很明显了。

#### 法二

当然,你也可以用x64dbg动态调试的方式,在`GetPixel()`执行完后修改`word_9A3384`处的内容,然后让它正常执行直接输出flag。动态调试做法的好处是:

- 不需要写idc脚本。
- 也许不需要知道代码写的啥。

WuJ1n9 发表于 2022-7-27 11:09

jocker最后5个字符是真的恶心,当年耗了半天在这儿,这种与misc的结合挺没意思的

hans7 发表于 2022-7-27 23:01

WuJ1n9 发表于 2022-7-27 11:09
jocker最后5个字符是真的恶心,当年耗了半天在这儿,这种与misc的结合挺没意思的

我也觉得这个答案挺牵强的{:1_908:}

LuHaHa627 发表于 2022-7-27 11:19

可以,很6

silencespy 发表于 2022-7-27 11:35

感谢分享 ,牛逼

XgbbtmjLnn 发表于 2022-7-27 11:38

感谢分享 ,牛逼

BMK 发表于 2022-7-27 12:50

学习了!!

lanlinux 发表于 2022-7-27 13:49

学习了 牛皮

zohuan 发表于 2022-7-27 17:53

IDA7.7哪下的...

qq420984 发表于 2022-7-27 19:06


感谢分享 ,牛逼
页: [1] 2 3
查看完整版本: 【CTF reverse】SMC好题赏析——网鼎杯2020青龙组jocker、SCUx401CTF2021-RE2-pixpix