【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脚本。
- 也许不需要知道代码写的啥。 jocker最后5个字符是真的恶心,当年耗了半天在这儿,这种与misc的结合挺没意思的 WuJ1n9 发表于 2022-7-27 11:09
jocker最后5个字符是真的恶心,当年耗了半天在这儿,这种与misc的结合挺没意思的
我也觉得这个答案挺牵强的{:1_908:} 可以,很6 感谢分享 ,牛逼 感谢分享 ,牛逼
学习了!! 学习了 牛皮 IDA7.7哪下的...
感谢分享 ,牛逼