尤皮·埃克斯历险记(1)
本题考查 UPX 脱壳及反调试。
将 exe 拖入 IDA 之后发现主函数不见了,程序只有一两个函数,再用 DIE 或者 exeinfope 查壳,发现有 UPX 壳。
upx -d 可以脱壳。
脱壳后查看主函数:
cpp
int __fastcall main(int argc, const char **argv, const char **envp)
{
__int64 n34_1; // rax
std::ostream *v4; // rax
unsigned __int64 v5; // rax
std::ostream *v6; // rax
_BYTE v8[32]; // [rsp+20h] [rbp-60h] BYREF
_BYTE v9[2]; // [rsp+40h] [rbp-40h] BYREF
_BYTE v10[16]; // [rsp+50h] [rbp-30h] BYREF
_BYTE v11[22]; // [rsp+60h] [rbp-20h] BYREF
char v12; // [rsp+76h] [rbp-Ah]
char v13; // [rsp+77h] [rbp-9h]
__int64 n34; // [rsp+78h] [rbp-8h]
unsigned __int64 i; // [rsp+80h] [rbp+0h]
char v16; // [rsp+8Fh] [rbp+Fh]
_main();
qmemcpy(v8, "isfhGJ\tt~cU\ny\nuTjcj\tT~cjQdu~w{", 30);
v8[30] = 4;
v8[31] = 5;
qmemcpy(v9, "qA", sizeof(v9));
n34 = 34;
std::string::string((std::string *)v11);
std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "Enter your flag: ");
std::operator>><char>(refptr__ZSt3cin, (std::string *)v11);
encrypt((const std::string *)v10);
n34_1 = std::string::length((std::string *)v10);
if ( n34_1 == n34 )
{
v16 = 1;
for ( i = 0; ; ++i )
{
v5 = std::string::length((std::string *)v10);
if ( v5 <= i )
break;
if ( IsDebuggerPresent() )
{
v12 = *(_BYTE *)std::string::operator[](v10, i) ^ 0xC3;
if ( v8[i] != v12 )
{
v16 = 0;
break;
}
}
else
{
v13 = *(_BYTE *)std::string::operator[](v10, i) ^ 0x3C;
if ( v8[i] != v13 )
{
v16 = 0;
break;
}
}
}
if ( v16 )
v6 = (std::ostream *)std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "Right!");
else
v6 = (std::ostream *)std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "Wrong!");
refptr__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_(v6);
}
else
{
v4 = (std::ostream *)std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "Wrong!");
refptr__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_(v4);
}
std::string::~string((std::string *)v10);
std::string::~string((std::string *)v11);
return 0;
}发现给出了密文,还有一个 encrypt 函数:
cpp
__int64 __fastcall encrypt(__int64 a1, __int64 a2)
{
unsigned __int64 i_1; // rax
char v4; // [rsp+27h] [rbp-19h] BYREF
char *v5; // [rsp+28h] [rbp-18h]
char C; // [rsp+37h] [rbp-9h]
unsigned __int64 i; // [rsp+38h] [rbp-8h]
v5 = &v4;
std::string::basic_string<std::allocator<char>>(a1, &unk_1400C7000, &v4);
std::__new_allocator<char>::~__new_allocator(&v4);
for ( i = 0; ; ++i )
{
i_1 = std::string::length(a2);
if ( i >= i_1 )
break;
C = *(_BYTE *)std::string::operator[](a2, i);
if ( (unsigned int)(C - 48) > 9 )
{
if ( islower(C) || isupper(C) )
std::string::operator+=(a1, (unsigned int)(char)(0xBB - C));
else
std::string::operator+=(a1, (unsigned int)C);
}
else
{
std::string::operator+=(a1, (unsigned int)(char)(0x69 - C));
}
}
return a1;
}是一个凯撒密码的变体:
如果字符是数字,那么反转其在 0~9 之间的顺序(也就是
0x69-C),0 ↔ 9,1 ↔ 8,...如果是字母,反转其在字母表中的顺序并切换大小写(也就是
0xBB-C),A ↔ z,b ↔ Y,...
而主函数后面逻辑中的 IsDebuggerPresent 函数是反调试函数,当存在调试器时返回真,否则为假。
由于题目正常运行时肯定不存在调试器,所以这里一定会按假的分支走(也就是异或 0x3C),接下来动态调试,在 std::string::string((std::string *)v11); 这一行下断点,提取前面的密文69736668474A09747E63550A790A75546A636A09547E636A5164757E777B04057141。
在 cyberchef fromhex 后异或 0x3C 后得到 UOZT{v5HB_i6E6IhV_V5hB_VmXIBKG89M},然后编写一个简单的 python 脚本即可恢复 flag:
python
def func(s: str) -> str:
result = []
for c in s:
if '0' <= c <= '9':
transformed = chr(ord('9') - (ord(c) - ord('0')))
result.append(transformed)
elif c.isalpha():
transformed = chr(0xBB - ord(c))
result.append(transformed)
else:
result.append(c)
return ''.join(result)
original = "UOZT{v5HB_i6E6IhV_V5hB_VmXIBKG89M}"
dec = func(original)
print(dec)flag 为:flag{E4sy_R3v3rSe_e4Sy_eNcrypt10n}。