SMC,即self modifying code,自修改代码。
为什么是好题呢?因为我刚好都会写!
本文52pojie:https://www.52pojie.cn/thread-1667202-1-1.html
本文juejin:https://juejin.cn/post/7124745598271488030/
作者:hans774882968以及hans774882968
【网鼎杯2020青龙组】jocker
传送门:在buuoj上找qwq。
依赖
IDA版本为7.7。
分析
32位,Section: [.text], EP: 0x000008E0
故无壳。用IDA打开,一进来就是main函数。
IDA7.7相比于6.6强大了许多,终于positive sp
的代码也可以反编译了,感动!
// positive sp value has been detected, the output may be wrong!
int __cdecl main(int argc, const char **argv, const char **envp)
{
char Str[50]; // [esp+12h] [ebp-96h] BYREF
char Destination[80]; // [esp+44h] [ebp-64h] BYREF
DWORD flOldProtect; // [esp+94h] [ebp-14h] BYREF
size_t v7; // [esp+98h] [ebp-10h]
int i; // [esp+9Ch] [ebp-Ch]
__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脚本:
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
函数。
int __cdecl encrypt(char *a1)
{
int v2[19]; // [esp+1Ch] [ebp-6Ch] BYREF
int v3; // [esp+68h] [ebp-20h]
int i; // [esp+6Ch] [ebp-1Ch]
v3 = 1;
qmemcpy(v2, &unk_403040, sizeof(v2));
for ( i = 0; i <= 18; ++i )
{
if ( (char)(a1[i] ^ Buffer[i]) != v2[i] )
{
puts("wrong ~");
v3 = 0;
exit(0);
}
}
puts("come here");
return v3;
}
我们写脚本,执行以后,可以看到没有拿到完整flag。接下来看finally函数。finally函数也被smc影响了,所以同样需要进行encrypt函数的流程,才能拿到代码。
int __cdecl finally(char *a1)
{
unsigned int v1; // eax
char v3[9]; // [esp+13h] [ebp-15h] BYREF
int v4; // [esp+1Ch] [ebp-Ch]
strcpy(v3, "%tp&:");
v1 = time(0);
srand(v1);
v4 = rand() % 100;
v3[6] = 0;
*(_WORD *)&v3[7] = 0;
if ( (v3[(unsigned __int8)v3[5]] != a1[(unsigned __int8)v3[5]]) == v4 )
return puts("Really??? Did you find it?OMG!!!");
else
return puts("I hide the last part, you will not succeed!!!");
}
这里我认为是无解的了。查了答案发现,是社工,假设你已经知道:
- 对常量串
"%tp&:"
的操作和前面一样,是xor。
- 最后一个字符是
'}'
。
于是可以解出最后5个字符。
代码
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[i // 4]) ^ v2[i])
# 最后5位
v3 = '%tp&:'
xor_v = ord(v3[-1]) ^ ord('}')
for c in v3:
ans += chr(xor_v ^ ord(c))
print(ans)
SCUx401CTF2021-RE2-pixpix
传送门
依赖
IDA7.7
分析
32位,Section: [.text], EP: 0x000007AD
故无壳。
打开IDA,立刻到了main函数
:
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; // [esp+0h] [ebp-Ch]
DWORD flOldProtect; // [esp+8h] [ebp-4h] 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
函数开头的汇编:
.text:009A10C0 55 push ebp
.text:009A10C1 8B EC mov ebp, esp
另外,0x9a1050
处的值为61 BB DD
。所以我们求出0x34, 0x30, 0x31
为所求数组。
法一
那么我们写一段IDAPython脚本:
import idc
st_addr = 0x9a1050
ed_addr = 0x9a10b0
pix = [0x34, 0x30, 0x31]
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那题。
于是我们拿到
int sub_9A1050()
{
char v1[28]; // [esp+0h] [ebp-20h] BYREF
int v2; // [esp+1Ch] [ebp-4h]
strcpy(v1, "scuctf{pixel!pixel!pixel!}");
v1[27] = 0;
v2 = 0;
return ((_DWORD (__cdecl *)(char *))sub_9A1010)(v1);
}
int sub_9A1010(char *Format, ...)
{
unsigned __int64 *v1; // eax
FILE *v3; // [esp-10h] [ebp-18h]
va_list va; // [esp+14h] [ebp+Ch] 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。动态调试做法的好处是: