其他
看雪·深信服 2021 KCTF 春季赛 | 第六题设计思路及解析
本题我们跟随主人公飞停一起,寻找宝剑。本题耗时2天,吸引了超过2千人围观,最终11支战队夺旗成功!恭喜!
出题团队简介
崇文路大专是一个隶属于重庆邮电大学信息安全协会的CTF战队,其主要目标是培养对信息安全感兴趣的学弟学妹们,带着新入门的成员一起参加比赛,致力于夺得各大比赛签到题一血。同时也是重庆邮电大学信息安全协会旗下的0xFA战队的附属战队,0xFA战队曾多次进入XCTF线下总决赛、安洵杯线下总决赛、国赛总决赛。
专家点评
比赛第六题是一个有趣的42*42棋盘游戏,使用了自己开发的混淆器进行混淆处理。选手需要编写代码去除混淆拿到核心算法代码再进行逆向。题目难度中等偏上,需要考验选手的逆向和编写脚本的能力。
赛题设计思路
原创混淆器
本次比赛出题使用了我自己开发的“COP”混淆器,COP可以打开并解析一个PE文件,针对32/64位分别进行处理。 COP要求所给文件必须具有.text段、.reloc段,且不含有花指令。 COP使用Capstone解析出程序文件的所有汇编指令,将指令进行混淆,并且随机打乱,膨胀后的指令无法塞进原.text段,因此将多余的部分放入新的.cop段中。 顺序被完全打乱的指令需要用某种方式连接它们,让它们顺序执行,我将非跳转指令用“call+pop+add/xor+push+ret”的方式连接,将跳转指令进行了不同程度的变形。 此外,COP还将程序所有基于相对偏移的指令进行了修复,通过重定位表将所有基于绝对地址的命令进行了修复,对64位程序的基于rip寻址的指令进行了修复。 COP自行维护了一个独立的重定位表,混淆后仍可以开启地址随机化。 为了提高混淆强度,对64位程序还将清空其.pdata段的内容。 COP还支持嵌套混淆(简称套娃),进一步提高混淆强度。 用户可以选择保留.reloc段或者去除,去除以后会进一步提高混淆强度,但不支持继续嵌套混淆,且会关闭地址随机化。 我使用COP将编译出的程序进行了2次混淆,得到最终附件,考察了各位选手编写脚本的能力。
解题思路
拿到的附件使用IDA打开,发现有非常多的重复的代码,且动态调试的时候有效指令出现的频率极低,因此可以考虑使用脚本对文件进行初步处理。
赛题解析
本赛题解析由看雪论坛 KuCha128 给出:
0x00 简单分析
初步观察
好家伙!往EntryPoint上下翻翻都是清一色的代码块。 简单单步跟踪了一下,总结混淆方案共有4种。
混淆方案1:jmp指令简单变形
push rax
push rax
pushfq
call $+5
pop rax
xor rax,D1E15
mov qword ptr ss:[rsp+10],rax
popfq
pop rax
ret
混淆方案2:jmp指令复杂变形
push rax
push rax
pushfq
push rax
push rax
pushfq
call $+5
pop rax
xor rax, 0xE4EB3
mov qword ptr ss:[rsp+0x10], rax
popfq
pop rax
pop rax
add rax, 0x8242E
mov qword ptr ss:[rsp+0x10], rax
popfq
pop rax
ret
混淆方案3:垃圾指令push reg/pop reg
push rdx
push rdx
pop rdx
pop rdx
push rcx
push rdx
pop rdx
push rbx
pop rbx
pop rcx
混淆方案4:call指令变形
push rax
push rax
pushfq
push rax
push rax
pushfq
call $+5
pop rax
xor rax, 0xDC85F
mov qword ptr ss:[rsp+0x10], rax
popfq
pop rax
pop rax
add rax, 0x51438
mov qword ptr ss:[rsp+0x10], rax
popfq
pop rax
jmp 0x00000001400979D6
整理思路
如果想靠肉眼顶着混淆看代码,属实不太现实。
看来必须要先写代码对PE文件的混淆做优化,再来盘程序逻辑了。
0x01 优化题目的混淆
准备工作
针对题目里的4种混淆方案,需要给出对应的去混淆方法。
不需要优化得很彻底,只需要能不太费力地使用肉眼看代码逻辑即可。 干到这里留了个心眼。
既然咱们需要patch题目文件优化混淆,题目会不会有CRC之类的校验代码段,如果有的话还得先要patch代码校验。
先简单手动修改一处混淆(随便找了个混淆方案1的地方改成jmp指令),保存文件,运行。
yes,运行是没问题,看来可以放心patch题目文件了。 要patch文件,首先需要把文件读入到内存中来,Section展开不展开都行。
这里需要注意的是,题目的ImageBase是默认0x140000000,申请的地址要满足后面几位也最好都是0,要不然混淆方案中的xor计算会出错。
static uintptr_t g_nFileSize = 0;
static LPBYTE g_lpFileBuffer = 0;
void ReadPEFile()
{
HANDLE hFile = NULL;
HANDLE hMapFile = NULL;
LPBYTE lpFileBuffer = NULL;
hFile = CreateFileA("D:\\Projects\\KCTF2021Spring\\CrackNote\\6\\KCTF.exe", // open MYFILE.TXT
GENERIC_READ, // open for reading
FILE_SHARE_READ, // share for reading
NULL, // no security
OPEN_EXISTING, // existing file only
FILE_ATTRIBUTE_NORMAL, // normal file
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
std::cout << "Could not create file." << std::endl;
goto EXIT_PROC;
}
g_nFileSize = GetFileSize(hFile, NULL);
if (g_nFileSize == INVALID_FILE_SIZE)
{
std::cout << "GetFileSize error." << std::endl;
goto EXIT_PROC;
}
hMapFile = CreateFileMappingA(hFile, NULL, PAGE_READONLY,
0, 0, NULL);
if (hMapFile == NULL)
{
std::cout << "Could not create file-mapping object." << std::endl;
goto EXIT_PROC;
}
lpFileBuffer = (LPBYTE)MapViewOfFile(hMapFile, // Handle to mapping object.
FILE_MAP_READ, // Read/write permission
0, // Max. object size.
0, // Size of hFile.
0); // Map entire file.
if (lpFileBuffer == NULL)
{
std::cout << "MapViewOfFile error." << std::endl;
goto EXIT_PROC;
}
g_lpFileBuffer = new BYTE[g_nFileSize + 0x100000000];
g_lpFileBuffer = (LPBYTE)(((QWORD)g_lpFileBuffer & 0xFFFFFFFF00000000) + 0x100000000);
if (g_lpFileBuffer == NULL)
{
std::cout << "Alloc error." << std::endl;
goto EXIT_PROC;
}
memcpy(g_lpFileBuffer, lpFileBuffer, g_nFileSize);
EXIT_PROC:
if (lpFileBuffer != NULL)
{
UnmapViewOfFile(lpFileBuffer);
lpFileBuffer = NULL;
}
if (hMapFile != NULL)
{
CloseHandle(hMapFile);
hMapFile = NULL;
}
if (hFile != INVALID_HANDLE_VALUE)
{
CloseHandle(hFile);
hFile = INVALID_HANDLE_VALUE;
}
}
去混淆方案1
混淆方案1等价于jmp指令。
jmp到的地址等于call $+5下一行的地址异或上后面的异或值常量。
这种混淆方案在反汇编代码中随便拉都是,看来可以先进行一波特征搜索patch掉这些"肉眼可见"的jmp变异。
这里需要注意的是, .text节和.cop节都是代码段,两个代码段之间会有很多来回的跳转。没有展开Section的话,需要注意FA和VA的转换。
void AntiObfuscation1()
{
LPBYTE lpStart = 0;
LPBYTE lpEnd = 0;
DWORD dwCount = 0;
//.text节
lpStart = g_lpFileBuffer+0x400;
lpEnd = lpStart + 0x3000;
dwCount = 0;
while (true)
{
//50509CE800000000584835????????48894424109D58C3
LPBYTE lpResult = (LPBYTE)FindHexBytes(lpStart, lpEnd, "50509CE800000000584835????????48894424109D58C3");
if (lpResult == (LPVOID)-1) {
break;
}
else {
dwCount++;
lpStart = lpResult + 1;
QWORD dwFindFA = (QWORD)lpResult;
QWORD dwFindVA = dwFindFA - 0x400 + 0x1000;
QWORD dwCallNextFA = (QWORD)lpResult + 8;
QWORD dwCallNextVA = dwCallNextFA - 0x400 + 0x1000 ;// 修复内存地址
QWORD dwXor = Mem_RDword(lpResult + 11);
QWORD dwNextAddrVA = dwXor ^ dwCallNextVA;
QWORD dwNextAddrFA = 0;
BYTE aryNop[25] = {
0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90,
};
//混淆方案1的opcode size 固定是23
Mem_WBytes2((LPVOID)dwFindFA, aryNop, 23);
BYTE bHook[] = { 0xE9, 00, 00, 00, 00 };
Mem_WDword(bHook + 1, (DWORD)(dwNextAddrVA - dwFindVA - 5));
Mem_WBytes2((LPVOID)dwFindFA, bHook, 5);
}
}
D("patch .text %d addr", dwCount);
//.cop节
lpStart = g_lpFileBuffer + 0x6200;
lpEnd = lpStart + 0xF0000;
dwCount = 0;
while (true)
{
//50509CE800000000584835????????48894424109D58C3
LPBYTE lpResult = (LPBYTE)FindHexBytes(lpStart, lpEnd, "50509CE800000000584835????????48894424109D58C3");
if (lpResult == (LPVOID)-1) {
break;
}
else {
dwCount++;
lpStart = lpResult + 1;
QWORD dwFindFA = (QWORD)lpResult;
QWORD dwFindVA = dwFindFA - 0x6200 + 0xB000;
QWORD dwCallNextFA = (QWORD)lpResult + 8;
QWORD dwCallNextVA = dwCallNextFA - 0x6200 + 0xB000;// 修复内存地址
QWORD dwXor = Mem_RDword(lpResult + 11);
QWORD dwNextAddrVA = dwXor ^ dwCallNextVA;
QWORD dwNextAddrFA = 0;
BYTE aryNop[25] = {
0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90,
};
//混淆方案1的opcode size 固定是23
Mem_WBytes2((LPVOID)dwFindFA, aryNop, 23);
BYTE bHook[] = { 0xE9, 00, 00, 00, 00 };
Mem_WDword(bHook + 1, (DWORD)(dwNextAddrVA - dwFindVA - 5));
Mem_WBytes2((LPVOID)dwFindFA, bHook, 5);
}
}
D("patch .cop %d addr", dwCount);
}
去混淆方案2
混淆方案2等价于jmp指令。
但是混淆方案2的代码在内存中并不是连续的,不好直接搜索特征来去除。
这里想到个办法,使用x64dbg的TraceInto/TraceOver功能先跑出代码来,再针对输出的log文件进行文本分析得到混淆方案2的地址。 x64dbg的TraceInto/TraceOver功能,要输出到log的内容填上0x{p:cip} {i:cip},选择好输出log文件的路径,设置步数500000(只能多不能少,至少要跑完输出错误提示吧),不用设置停止条件,即可开始trace。
一眼望去,代码里全是"去混淆方案1"中patch的jmp和成对出现的垃圾指令"push reg"/"pop reg"。
写个python脚本优化掉(方法见"去混淆方案3")。
去掉jmp和push reg/popreg的log文本文件中,可以见到混淆方案2的代码已经连续出现了。 这里使用notepad++的插件python script写个脚本,输出针对混淆方案2优化需要用到的几个重要参数(起始地址,callnext返回地址,异或运算常量,加法运算常量)。
a = editor.getText()
lst=a.split("\n")
lstdel=[]
for index in range(len(lst)-18):
#for index in range(20):
i1=lst[index][-8:]
i2=lst[index+1][-8:]
i3=lst[index+2][-6:]
i17=lst[index+17][-3:]
#print(i1,i2,i3,i17)
if(i1=="push rax" and i2=="push rax" and i3=="pushfq" and i17=="ret"):
eva=lst[index][:18]
cnva=lst[index+7][:18]
xi=lst[index+8].rfind(" ");
x=lst[index+8][xi:]
ai=lst[index+13].rfind(" ");
a=lst[index+13][ai:]
print(eva, cnva, x, a)
print("finish.")
输出的地址特别多,整理填入C代码中。
LPCSTR g_aryFix[] = {
"0x00000001400B5E68", "0x000000014007FBD8", "0x6665", "0x36F74",
"0x000000014005AF49", "0x000000014002BB97", "0x28376", "0xCFCD7",
"0x00000001400E05AD", "0x0000000140040B81", "0xD7FA9", "0xFFFFFFFFFFFE178B",
"0x000000014000151A", "0x000000014008F26F", "0xDB257", "0x78C45",
"0x0000000140029C72", "0x000000014008F92C", "0xEB504", "0x328B9",
"0x000000014001E0A6", "0x00000001400F1BA1", "0x6545B", "0x28976",
"0x0000000140052153", "0x0000000140012134", "0x485D5", "0xFFFFFFFFFFFF07DE",
"0x0000000140083D5A", "0x000000014000F6CA", "0x43749", "0x46ACE",
"0x000000014002C47E", "0x00000001400C2EFA", "0xA5AD1", "0x89CB0",
"0x00000001400E7AEC", "0x00000001400370EC", "0xFEA16", "0xFFFFFFFFFFFD4BAC",
"0x00000001400C0C09", "0x00000001400F44A9", "0x2F746", "0xFFFFFFFFFFF323E7",
"0x000000014001533A", "0x0000000140020B70", "0x15CD7", "0x5E0A2",
"0x00000001400572A6", "0x0000000140059016", "0xFBD1", "0xFFFFFFFFFFFDF0F0",
"0x0000000140022F8A", "0x00000001400767A4", "0x4BE4C", "0x88F4",
//... ...
};
修复混淆方案2的代码(注意FA和RVA的转换):
void AntiObfuscation2()
{
for (int i = 0; i < sizeof(g_aryFix) / sizeof(g_aryFix[0]) / 4 ;i++)
{
QWORD dwFindRVA = strtoull(g_aryFix[i * 4 ], 0, 16) - 0x140000000;
QWORD dwFindFA = 0;
if (dwFindRVA < 0x3400)
{
dwFindFA = (QWORD)g_lpFileBuffer + dwFindRVA - 0x1000 + 0x400;
}
else
{
dwFindFA = (QWORD)g_lpFileBuffer + dwFindRVA - 0xB000 + 0x6200;
}
QWORD dwCallNextRVA= strtoull(g_aryFix[i * 4+1], 0, 16) - 0x140000000;
QWORD dwXor = strtoull(g_aryFix[i * 4 + 2], 0, 16);
QWORD dwAdd = strtoull(g_aryFix[i * 4 + 3], 0, 16);
QWORD dwNextAddrRVA = (dwXor ^ dwCallNextRVA) + dwAdd;
BYTE bHook[] = { 0xE9, 00, 00, 00, 00 };
Mem_WDword(bHook + 1, (DWORD)(dwNextAddrRVA - dwFindRVA - 5));
Mem_WBytes2((LPVOID)dwFindFA, bHook, 5);
}
}
去混淆方案3
混淆方案3是垃圾指令,成对的push reg和pop reg。
写python脚本在log文本中搜索几次,即可清理干净。
这里内存中就不patch了,不是很影响看代码。
对python脚本输出的文件可以进行"去混淆方案2"和"去混淆方案4"操作。
a = editor.getText()
lst=a.split("\n")
print(len(lst));
lstdel=[]
for index in range(len(lst)):
if(lst[index][-22:-19] == "jmp"):
lstdel.append(index)
lstdel.reverse()
print(len(lstdel))
for index in range(len(lstdel)):
del lst[lstdel[index]]
for fxcktime in range(10):
lstdel=[]
for index in range(len(lst)-1):
i1=lst[index][-8:-4]
i2=lst[index+1][-7:-4]
reg1=lst[index][-3:]
reg2=lst[index+1][-3:]
if(i1=="push" and i2=="pop" and reg1==reg2):
#print(i1,reg1,i2,reg2)
lstdel.append(index)
lstdel.append(index+1)
lstdel.reverse()
for index in range(len(lstdel)):
del lst[lstdel[index]]
fo=open("D:\\Projects\\KCTF2021Spring\\CrackNote\\6\\crack\\crack\\pyoutput.txt","w")
for index in range(len(lst)):
fo.write(lst[index]+"\n")
fo.close()
print("finish.");
去混淆方案4
混淆方案4等价于call指令。
混淆方案4和混淆方案2的差别,就是最后一行一个是ret,一个是jmp。
在对trace的log进行完"去混淆指令3"和"去混淆指令2"操作后,混淆方案4的代码就会"浮出水面",在文本中连续。push addr+ret = jmp addr
push addr1+jmp addr2= call addr2 (addr1是call的返回地址)
继续写python脚本提取输出要处理的信息。
a = editor.getText()
lst=a.split("\n")
lstdel=[]
for index in range(len(lst)-18):
i1=lst[index][-8:]
i2=lst[index+1][-8:]
i3=lst[index+2][-6:]
i17=lst[index+16][-7:]
#print(i1,i2,i3,i17)
if(i1=="push rax" and i2=="push rax" and i3=="pushfq" and i17=="pop rax"):
eva=lst[index][:18]
cnva=lst[index+7][:18]
callva=lst[index+17][:18]
xi=lst[index+8].rfind(" ");
x=lst[index+8][xi:]
ai=lst[index+13].rfind(" ");
a=lst[index+13][ai:]
print(eva, cnva, x, a, callva)
print("finish.")
输出的数据填入C代码中,进行修复。
LPCSTR g_aryFixC[] = {
"0x000000014007A4C4", "0x0000000140089E51", "0xDC85F", "0x51438", "0x00000001400979D8",
"0x00000001400ADE90", "0x00000001400D4AEB", "0x4E2ED", "0xFFFFFFFFFFFD199D", "0x0000000140060882",
"0x00000001400C4678", "0x0000000140020DE5", "0xD2FE3", "0xFFFFFFFFFFFBE04E", "0x000000014005CC6E",
"0x00000001400C673D", "0x00000001400E7B10", "0xEB97C", "0x6DDA4", "0x000000014003F7D0",
"0x000000014003AF3E", "0x00000001400DDF75", "0x9704D", "0xFFFFFFFFFFFF5B08", "0x000000014003F7D0",
"0x00000001400839E3", "0x000000014007BECA", "0x36372", "0xA777F", "0x00000001400F90F3",
"0x00000001400543F0", "0x00000001400A164B", "0xAF7C5", "0x5CDEC", "0x0000000140078E09",
"0x00000001400998CB", "0x00000001400B928A", "0x2C92F", "0xE4AC", "0x00000001400D031B",
"0x000000014007C0AC", "0x000000014009B414", "0xC2AD8", "0xFFFFFFFFFFFC7598", "0x00000001400D42F1",
"0x000000014008FBE2", "0x0000000140047E12", "0x56408", "0x65F02", "0x00000001400728A0",
"0x000000014006B5D5", "0x00000001400194CA", "0x78E10", "0x3996", "0x0000000140078E09",
"0x0000000140077920", "0x00000001400A2374", "0x4FE21", "0xFFFFFFFFFFFCEA1A", "0x0000000140039068",
"0x00000001400943F5", "0x000000014008595F", "0xF3ADF", "0xFFFFFFFFFFFA3AAE", "0x00000001400DFCB3",
"0x000000014003768E", "0x000000014005087F", "0x45B11", "0xAB445", "0x000000014002650B",
//... ...
};
优化混淆方案4的方法:
1. 起始地址jmp到返回地址-5
void AntiObfuscation3()
{
for (int i = 0; i < sizeof(g_aryFixC) / sizeof(g_aryFixC[0]) / 5; i++)
{
QWORD dwFindRVA = strtoull(g_aryFixC[i * 5], 0, 16) - 0x140000000;
QWORD dwFindFA = 0;
if (dwFindRVA < 0x3400)
{
dwFindFA = (QWORD)g_lpFileBuffer + dwFindRVA - 0x1000 + 0x400;
}
else
{
dwFindFA = (QWORD)g_lpFileBuffer + dwFindRVA - 0xB000 + 0x6200;
}
QWORD dwCallNextRVA = strtoull(g_aryFixC[i * 5 + 1], 0, 16) - 0x140000000;
QWORD dwXor = strtoull(g_aryFixC[i * 5 + 2], 0, 16);
QWORD dwAdd = strtoull(g_aryFixC[i * 5 + 3], 0, 16);
QWORD dwReturnRVA = (dwXor ^ dwCallNextRVA) + dwAdd;
QWORD dwCallRVA= strtoull(g_aryFixC[i * 5+4], 0, 16) - 0x140000000;
QWORD dwHookRVA = dwReturnRVA - 5 ;
QWORD dwHookFA = 0;
if (dwHookRVA < 0x3400)
{
dwHookFA = (QWORD)g_lpFileBuffer + dwHookRVA - 0x1000 + 0x400;
}
else
{
dwHookFA = (QWORD)g_lpFileBuffer + dwHookRVA - 0xB000 + 0x6200;
}
//BYTE nop[] = { 0x90,0x90,0x90,0x90,0x90 };
//if (memcmp((LPBYTE)dwHookFA, nop, 5) != 0)
//{
// printf("5 nops is not found.\r\n");
//}
BYTE bHook[] = { 0xE9, 00, 00, 00, 00 };
Mem_WDword(bHook + 1, (DWORD)(dwHookRVA - dwFindRVA - 5));
Mem_WBytes2((LPVOID)dwFindFA, bHook, 5);
BYTE bHook2[] = { 0xE8, 00, 00, 00, 00 };
Mem_WDword(bHook2 + 1, (DWORD)(dwCallRVA - dwHookRVA - 5));
Mem_WBytes2((LPVOID)dwHookFA, bHook2, 5);
}
}
0x02 分析程序的验证逻辑
去混淆过后的代码
经过优化代码混淆后,看一下EntryPoint处代码。
0x0000000140071DE0 sub rsp, 0x28
0x00000001400A6A41 call 0x00000001400979D8
0x000000014000B2EA add rsp, 0x28
0x0000000140066D1D jmp 0x00000001400F4594
0x00000001400F4596 mov qword ptr ss:[rsp+0x8], rbx
0x00000001400574EC mov qword ptr ss:[rsp+0x10], rsi
0x000000014003AE64 push rdi
0x00000001400339F9 sub rsp, 0x30
0x00000001400B5EDB mov ecx, 0x1
0x000000014006C19E call 0x0000000140060882
0x0000000140052C16 test al, al
0x000000014002C7AC je 0x000000014002C7C5
0x00000001400FA123 xor sil, sil
0x00000001400503B8 mov byte ptr ss:[rsp+0x20], sil
0x00000001400F5532 call 0x00000001400F90F3
0x000000014005EB9A mov bl, al
0x000000014003B118 mov ecx, dword ptr ds:[0x00000001400066A0]
0x0000000140082181 cmp ecx, 0x1
0x00000001400F4B13 je 0x00000001400F4B2C
0x0000000140044567 test ecx, ecx
0x000000014000E80D jne 0x000000014000E826
0x000000014001938F mov dword ptr ds:[0x00000001400066A0], 0x1
0x00000001400768A6 lea rdx, ds:[0x0000000140004658]
0x00000001400D8E77 lea rcx, ds:[0x0000000140004640]
0x00000001400A404C call 0x00000001400D031B
0x000000014001A610 test eax, eax
0x00000001400BF926 je 0x00000001400BF93F
0x00000001400BBCE9 lea rdx, ds:[0x0000000140004638]
0x000000014003E227 lea rcx, ds:[0x0000000140004620]
0x000000014002145F call 0x00000001400D42F1
0x000000014007CF86 mov dword ptr ds:[0x00000001400066A0], 0x2
0x000000014004C6F2 mov cl, bl
0x0000000140077917 call 0x00000001400728A0
0x00000001400BC76A call 0x0000000140039068
0x0000000140046F51 mov rbx, rax
0x00000001400E073E cmp qword ptr ds:[rax], 0x0
0x00000001400A2DD5 je 0x00000001400A2DEE
0x0000000140019E29 call 0x00000001400DFCB3
0x0000000140019E30 mov rbx, rax
0x00000001400C7DA6 cmp qword ptr ds:[rax], 0x0
0x000000014007A883 je 0x000000014007A89C
0x00000001400C07AE call 0x000000014002650B ;_get_initial_narrow_environment
0x000000014008E432 mov rdi, rax
0x0000000140041D1A call 0x00000001400EED90 ;__p___argv
0x0000000140041D23 mov rbx, qword ptr ds:[rax]
0x0000000140088748 call 0x0000000140068129 ;__p___argc
0x0000000140092400 mov r8, rdi
0x000000014003D3AC mov rdx, rbx
0x0000000140059887 mov ecx, dword ptr ds:[rax]
0x00000001400A313B call 0x0000000140083814 ;main
这代码看着就眼熟多了,一股浓浓的VS201X编译器的味道。 main函数给定位到了:0x0000000140083814
下次trace就可以从main函数开始。 暂时想不到什么很好的办法能把代码全部按顺序修复好让ida能F5,不过这反汇编代码已经看起来非常舒适了。 又多再看了几眼,程序编译的时候没有开优化,反汇编代码看起来又2更亲切了。 多亏笔者在武汉科锐培训三阶段的时候,练就了看汇编代码还原C代码的"神功",就当成是培训时间一次正常的作业吧。
main函数里程序的验证逻辑
首先是输出字符串到控制台和输入flag。
输入输出使用的std::cout和std::cin。
输入之后首先判断了输入字符串的长度(0x54=84)。
0x0000000140083333 cmp rax, 0x54
0x0000000140035698 je 0x00000001400356B1
;输出错误提示
0x0000000140048E00 lea rcx, ds:[0x000000014000449C]
0x0000000140089A5C call 0x00000001400CD482
0x00000001400356B1 ....
接着为一块大小为0x150的局部变量数组初始化数值为0
0x0000000140064B92 xor edx, edx
0x0000000140013BE4 lea rax, ss:[rsp+0xEC0]
0x000000014002469D mov rcx, rax
0x000000014004DA5D mov r8d, 0x150
0x000000014007C4C8 call 0x00000001400C2EA1 ;memset
观察对改数组的访问得到数组成员size为8字节,算出总共42个成员,数组定义如下:
QWORD ary[42] = {0};
循环往数组内写入数据。
写入数值=取一个字符转数字*0x2A+再取一个字符转数字。
;循环遍历输入的flag
0x00000001400462B9 mov qword ptr ss:[rsp+0xA8], 0x0 ;循环变量
0x0000000140054381 cmp qword ptr ss:[rsp+0xA8], 0x2A ;循环次数=0x2A
0x000000014001603F jge 0x0000000140016058
0x0000000140036738 lea rax, ss:[rsp+0x1010]
0x00000001400B433E mov rcx, qword ptr ss:[rsp+0xA8]
0x00000001400F7366 shl rcx, 0x1
0x00000001400F9958 add rax, rcx
0x00000001400F5555 mov rcx, rax
0x0000000140014AC7 call 0x0000000140018FA3 ;计算数值
0x0000000140014ACC mov rcx, qword ptr ss:[rsp+0xA8]
0x00000001400787FE mov qword ptr ss:[rsp+rcx*8+0xEC0], rax ;写入数组
0x00000001400AFE42 cmp qword ptr ss:[rsp+0xA8], 0x0
0x0000000140022C16 je 0x0000000140022C2F
0x00000001400297BA mov rax, qword ptr ss:[rsp+0xA8]
0x00000001400BCBD3 add rax, 0x1 ;循环变量+=1
0x000000014007F965 mov qword ptr ss:[rsp+0xA8], rax
0x0000000140054381 cmp qword ptr ss:[rsp+0xA8], 0x2A
0x000000014001603F jge 0x0000000140016058
;...进入下一次循环
;计算数值函数
0x000000014005828F sub rsp, 0x38
0x00000001400AB750 mov qword ptr ss:[rsp+0x30], rcx
0x00000001400466C5 mov rax, qword ptr ss:[rsp+0x30]
0x000000014003423A mov cl, byte ptr ds:[rax]
0x0000000140048352 call 0x00000001400706B9 ;字符转数字
0x00000001400BA991 imul rax, rax, 0x2A
0x00000001400EA00D mov rdx, qword ptr ss:[rsp+0x30]
0x0000000140039BC9 mov cl, byte ptr ds:[rdx+0x1]
0x000000014005C2D0 mov qword ptr ss:[rsp+0x28], rax
0x000000014008807A call 0x0000000140087B21 ;字符转数字
0x0000000140037CF1 mov rdx, qword ptr ss:[rsp+0x28]
0x00000001400A7DF8 add rdx, rax
0x00000001400D4395 mov rax, rdx
0x0000000140057C71 add rsp, 0x38
0x00000001400DA66E ret
;字符转数字函数(部分)
0x00000001400D5E04 sub rsp, 0x10
0x00000001400731D7 mov byte ptr ss:[rsp+0x7], cl
0x00000001400882CA movsx eax, byte ptr ss:[rsp+0x7]
0x00000001400938D7 cmp eax, 0x30
0x000000014009A8BF jl 0x000000014009A8D8
0x00000001400B54F4 movsx eax, byte ptr ss:[rsp+0x7]
0x000000014001A372 cmp eax, 0x39 ;'9'
0x00000001400794C2 jg 0x00000001400794DB
0x0000000140043279 movsx rax, byte ptr ss:[rsp+0x7]
0x0000000140069CAB sub rax, 0x30 ;'0'
0x000000014002E028 mov qword ptr ss:[rsp+0x8], rax
0x000000014008BBCC mov rax, qword ptr ss:[rsp+0x8]
0x00000001400831F7 add rsp, 0x10
0x000000014002974F ret
仔细分析字符转数字函数,得出输入总共有42个字符,分别是:
下标就是这个字符转换出来的数值: 接着循环判断数组QWORD ary[42]中以从小到大顺序排序。0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-*/%=
0x0000000140014ACC mov rcx, qword ptr ss:[rsp+0xA8]
0x00000001400787FE mov qword ptr ss:[rsp+rcx*8+0xEC0], rax
0x00000001400AFE42 cmp qword ptr ss:[rsp+0xA8], 0x0
0x0000000140022C16 je 0x0000000140022C2F
0x00000001400CD021 mov rax, qword ptr ss:[rsp+0xA8]
0x00000001400EA72C mov rax, qword ptr ss:[rsp+rax*8+0xEC0]
0x00000001400ABE1C mov rcx, qword ptr ss:[rsp+0xA8]
0x00000001400E81E3 sub rcx, 0x1
0x00000001400906DB cmp rax, qword ptr ss:[rsp+rcx*8+0xEC0]
0x00000001400A9B93 jg 0x00000001400A9BAC
0x00000001400297BA mov rax, qword ptr ss:[rsp+0xA8]
0x00000001400BCBD3 add rax, 0x1
0x000000014007F965 mov qword ptr ss:[rsp+0xA8], rax
0x0000000140054381 cmp qword ptr ss:[rsp+0xA8], 0x2A
0x000000014001603F jge 0x0000000140016058
这里说明flag偶数下标的数值是限制死的。
只能是"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-*/%="依次排序。 接着一个check,循环判断了输入的偶数位和奇数位字符都不一样。
;初始化两个数组char ary[42],分别记录偶数位和奇数位每个字符是否出现
0x0000000140001A0A xor eax, eax
0x00000001400CCBF8 lea rcx, ss:[rsp+0xE90]
0x000000014004662E mov edx, eax
0x000000014004A3B8 mov r8d, 0x2A
0x000000014006EA24 mov qword ptr ss:[rsp+0x48], r8
0x0000000140098B05 mov dword ptr ss:[rsp+0x44], eax
;call memset
0x00000001400C9BCF jmp qword ptr ds:[0x0000000140004BD8]
0x00000001400930C5 lea rcx, ss:[rsp+0xE60]
0x00000001400EE6C6 mov edx, dword ptr ss:[rsp+0x44]
0x00000001400450F9 mov r8, qword ptr ss:[rsp+0x48]
;call memset
0x00000001400C9BCF jmp qword ptr ds:[0x0000000140004BD8]
;循环check
0x000000014007D9D5 mov qword ptr ss:[rsp+0xA0], 0x0 ;循环变量
0x0000000140002310 cmp qword ptr ss:[rsp+0xA0], 0x2A ;循环次数=2A
0x0000000140003980 jge 0x0000000140003999
;对0x2A一个求模一个整除, 等于是拿出了偶数位和奇数位数值
0x0000000140001F98 mov rax, qword ptr ss:[rsp+0xA0]
0x000000014002D5AC mov rax, qword ptr ss:[rsp+rax*8+0xEC0]
0x0000000140046C25 cqo
0x0000000140073E00 mov ecx, 0x2A
0x000000014009EA4F idiv rcx
0x000000014006EC35 mov qword ptr ss:[rsp+0x98], rdx
0x0000000140060863 mov rdx, qword ptr ss:[rsp+0xA0]
0x0000000140063300 mov rdx, qword ptr ss:[rsp+rdx*8+0xEC0]
0x0000000140017083 mov rax, rdx
0x00000001400C5CFF cqo
0x00000001400BCF6C idiv rcx
0x000000014008CCE3 mov qword ptr ss:[rsp+0x90], rax
0x00000001400BF3EF mov rax, qword ptr ss:[rsp+0x98]
;判断这个数值是否已经存在
0x000000014004B8F8 test byte ptr ss:[rsp+rax*1+0xE90], 0x1
0x00000001400ECE6A jne 0x00000001400ECE83
0x000000014005224C mov rax, qword ptr ss:[rsp+0x90]
0x0000000140015F89 test byte ptr ss:[rsp+rax*1+0xE60], 0x1
0x000000014006633F je 0x0000000140066358
;填入两个数组,记录当前已出现的数值
0x0000000140027ACE mov rax, qword ptr ss:[rsp+0x98]
0x00000001400E4C50 mov byte ptr ss:[rsp+rax*1+0xE90], 0x1
0x00000001400AC622 mov rax, qword ptr ss:[rsp+0x90]
0x00000001400B5248 mov byte ptr ss:[rsp+rax*1+0xE60], 0x1
0x0000000140037CD0 mov rax, qword ptr ss:[rsp+0xA0]
0x0000000140026ABB add rax, 0x1
0x00000001400258E6 mov qword ptr ss:[rsp+0xA0], rax
0x0000000140002310 cmp qword ptr ss:[rsp+0xA0], 0x2A
0x0000000140003980 jge 0x0000000140003999
;进入下一次循环
这里可以得到信息,奇数位的中每个字符都只出现一次。 接着下一个check,嵌套的循环。
大概逻辑是,相邻的偶数位和奇数位为一组,对于每两组数据差得到的一个坐标都不能相同(这里形容得不太准确,没有太理解,直接把反汇编代码翻译成C代码写个check函数即可)。
;初始化数组, size=1743
;@@@这里发现个问题, 后面给这个数组填数值的时候越界了
;@@@在这里纠结了很久, 照着代码抄下, 假装没看到数组访问越界就过了
0x000000014006B00F xor edx, edx
0x0000000140088714 lea rax, ss:[rsp+0xC0]
0x00000001400CA071 mov rcx, rax
0x0000000140062801 mov r8d, 0xD9E ;1743
call memset
0x00000001400C9BCF jmp qword ptr ds:[0x0000000140004BD8]
;嵌套的循环开始, 两两比较
;for (int i = 0; i < 42; i++)
0x00000001400C86F5 mov qword ptr ss:[rsp+0x88], 0x0
0x00000001400D320E cmp qword ptr ss:[rsp+0x88], 0x2A
0x0000000140056B2F jge 0x0000000140056B48
;for (int j = i+1; j < 42;j++)
0x00000001400CBE10 mov rax, qword ptr ss:[rsp+0x88]
0x0000000140052E9D add rax, 0x1
0x0000000140078F7F mov qword ptr ss:[rsp+0x80], rax
0x00000001400286DB cmp qword ptr ss:[rsp+0x80], 0x2A
0x000000014000D91A jge 0x000000014000D933
0x00000001400F755C mov rax, qword ptr ss:[rsp+0x88]
0x0000000140098656 mov rax, qword ptr ss:[rsp+rax*8+0xEC0]
0x0000000140001584 cqo
0x00000001400A4ADC mov ecx, 0x2A
0x000000014001F9E4 idiv rcx
0x000000014009D9EA mov r8, qword ptr ss:[rsp+0x80]
0x00000001400EDA1E mov r8, qword ptr ss:[rsp+r8*8+0xEC0]
0x000000014000EA19 mov rax, r8
0x00000001400A04C3 mov qword ptr ss:[rsp+0x38], rdx ;rsp_38 = ary[i]%42
0x0000000140037898 cqo
0x000000014008A550 idiv rcx
0x00000001400399F2 mov r8, qword ptr ss:[rsp+0x38] ;r8 = ary[j]%42
0x00000001400D4106 sub r8, rdx ;r8 = ary[j]%42- ary[i]%42
0x00000001400901F7 mov qword ptr ss:[rsp+0x78], r8
0x000000014009DAAD mov rdx, qword ptr ss:[rsp+0x88]
0x000000014000E4E3 mov rdx, qword ptr ss:[rsp+rdx*8+0xEC0]
0x00000001400EE409 mov rax, rdx
0x00000001400C5EE9 cqo
0x0000000140020832 idiv rcx
0x000000014008C074 mov r8, qword ptr ss:[rsp+0x80]
0x000000014006079F mov r8, qword ptr ss:[rsp+r8*8+0xEC0]
0x0000000140013395 mov qword ptr ss:[rsp+0x30], rax
0x00000001400C5CE1 mov rax, r8
0x000000014002FFF8 cqo
0x000000014007539E idiv rcx
0x000000014006BF8A mov rcx, qword ptr ss:[rsp+0x30]
0x00000001400DB4A6 sub rcx, rax ;rcx = ary[j]/42- ary[i]/42
0x00000001400E127D mov qword ptr ss:[rsp+0x70], rcx
;if(ary[j]%42- ary[i]%42 > 0)
0x0000000140073B88 cmp qword ptr ss:[rsp+0x78], 0x0
0x000000014006437E jge 0x0000000140064397
; 负数 则 取反
0x00000001400BFF7E xor eax, eax
0x00000001400EACB4 mov ecx, eax
0x0000000140080A4A mov rdx, rcx ;
0x00000001400A7967 sub rdx, qword ptr ss:[rsp+0x78]
0x00000001400517AC mov qword ptr ss:[rsp+0x78], rdx
0x000000014000B3C6 sub rcx, qword ptr ss:[rsp+0x70]
0x000000014008DA3A mov qword ptr ss:[rsp+0x70], rcx
0x0000000140064397:
;ary[j]/42 -ary[i]/42 +0x29
0x00000001400B368C mov rax, qword ptr ss:[rsp+0x70]
0x0000000140079E70 add rax, 0x29
0x00000001400DE855 mov qword ptr ss:[rsp+0x70], rax
;这里看出是个二维数组访问
;说明上面初始化的数组定义为char ary[?][42]
0x000000014007C0C8 imul rax, qword ptr ss:[rsp+0x70], 0x2A
0x00000001400DC9B8 lea rcx, ss:[rsp+0xC0]
0x0000000140038FE8 add rcx, rax
0x0000000140067202 mov rax, qword ptr ss:[rsp+0x78]
; 判断不为1
0x000000014008B8FD test byte ptr ds:[rcx+rax*1], 0x1
0x0000000140063B11 je 0x0000000140063B2A
; 填1
0x00000001400F9BFC imul rax, qword ptr ss:[rsp+0x70], 0x2A
0x000000014007F148 lea rcx, ss:[rsp+0xC0]
0x00000001400EB582 add rcx, rax
0x0000000140065E6F mov rax, qword ptr ss:[rsp+0x78]
0x00000001400F535F mov byte ptr ds:[rcx+rax*1], 0x1
; j++
0x0000000140013FC1 mov rax, qword ptr ss:[rsp+0x80]
0x0000000140073E8D add rax, 0x1
0x00000001400B9738 mov qword ptr ss:[rsp+0x80], rax
0x00000001400286DB cmp qword ptr ss:[rsp+0x80], 0x2A
0x000000014000D91A jge 0x000000014000D933
;进入下一次循环
这里check逻辑虽然看得不是太懂,但是突然产生个脑洞。
有点像n皇后问题,使用回溯法写代码可以解决。
但是看这规模,跑出flag来估计也要个很长时间,不太现实。
继续看后面有没有check可以缩小穷举范围。 一看果然有发现:
;strncmp
0x0000000140039540 lea rcx, ss:[rsp+0x1010]
0x00000001400CE591 lea rdx, ds:[0x0000000140004521]
0x000000014004CACF mov r8d, 0x1C
0x000000014004FED9 call qword ptr ds:[0x0000000140004C98]
[rsp+0x1010]是输入的flag。
[0x0000000140004521]是"02152S3X4Z5Q6C7T819/ADB%C*DL"。 也就是说,flag的前面28个字符已经给定。
范围已经缩到最小,可以写代码进行穷举了。
0x03 回溯法穷举flag
百度抄下个n皇后回溯法求解的代码,修改下check函数和输入输出。
int n = 0;
#define BORDER 42
char board[BORDER] = { 0 };
char tb[200][42] = { 0 };
char x[2000] = { 0 };
char y[2000] = { 0 };
bool check(char row, char col)
{
for (char i = 0; i < row; i++)
{
if (board[i] == col)
{
return false;
}
}
short sum = 0;
for (char i = 0; i < row; i++)
{
for (char j = i+1; j < row; j++)
{
int tmp1 = j - i;
int tmp2 = board[j] - board[i];
if (tmp2 < 0)
{
tmp1 = 0 - tmp1;
tmp2 = 0 - tmp2;
}
tmp1 = tmp1 + 0x29;
x[sum] = tmp1;
y[sum] = tmp2;
sum++;
}
}
memset(tb, 0, sizeof(tb));
for (short j = 0; j < sum; j++)
{
tb[x[j]][y[j]] = 1;
}
for (char i = 0; i < row; i++)
{
//(i, board[i])
//(row, col)
int tmp1 = row - i;
int tmp2 = col - board[i];
if (tmp2 < 0)
{
tmp1 = 0 - tmp1;
tmp2 = 0 - tmp2;
}
tmp1 = tmp1 + 0x29;
if (tb[tmp1][tmp2] == 1)
{
return false;
}
}
return true;
}
void fxck(char row)
{
if (row == BORDER)
{
//output
n++;
printf("%d: 02152S3X4Z5Q6C7T819/ADB%cC*DL", n, '%');
for (int i = 14; i < BORDER; i++)
{
printf("%c%c", getccc(i), getccc(board[i]));
}
printf("\r\n");
return;
}
for (char col = 0; col < BORDER; col++)
{
if (check(row, col))
{
board[row] = col;
fxck(row + 1);
}
}
}
int main(int argc, const char** argv, const char** envp)
{
//"02152S3X4Z5Q6C7T819/ADB%C*DL"
LPCSTR yz = "25SXZQCT1/D%*L";
for(int i = 0; i< 14;i++)
{
auto idx= getidx(yz[i]);
board[i] = idx;
}
fxck(14);
printf("n = %d \r\n", n);
return 0;
}
运行出结果需要一段时间,一两分钟吧,不算太久。
Microsoft Windows [版本 10.0.19042.985]
(c) Microsoft Corporation。保留所有权利。
C:\Users\KuCha\Desktop\crack_login\x64\Release>crack_login.exe
1: 02152S3X4Z5Q6C7T819/ADB%C*DLEIFUG3HRIHJ6K7L0MBNKOJPPQ=RNS+TEUOVWWGXYYMZ9+4-8*F/-%V=A
n = 1
C:\Users\KuCha\Desktop\crack_login\x64\Release>
02152S3X4Z5Q6C7T819/ADB%CDLEIFUG3HRIHJ6K7L0MBNKOJPPQ=RNS+TEUOVWWGXYYMZ9+4-8F/-%V=A
0x04 总结
题目主打还是代码流,很合笔者胃口。
题目算法验证部分最后一个循环check中,有数组越界访问,这个很坑,不按题目算法这样写代码得不到flag。
混淆方法太少,使得攻击者要恢复代码工作量不算太大。 附件文件:
crack_login.cpp是修复混淆和最后回溯穷举的代码
KCTF_patch8.exe是优化部分混淆过后的题目可执行文件
python script.rar是notepad++插件的python脚本
往期解析
1. 看雪·深信服 2021 KCTF 春季赛 | 第二题设计思路及解析
2. 看雪·深信服 2021 KCTF 春季赛 | 第三题设计思路及解析3. 看雪·深信服 2021 KCTF 春季赛 | 第四题设计思路及解析4. 看雪·深信服 2021 KCTF 春季赛 | 第五题设计思路及解析主办方
看雪CTF(简称KCTF)是圈内知名度最高的技术竞技之一,从原CrackMe攻防大赛中发展而来,采取线上PK的方式,规则设置严格周全,题目涵盖Windows、Android、iOS、Pwn、智能设备、Web等众多领域。
看雪CTF比赛历史悠久、影响广泛。自2007年以来,看雪已经举办十多个比赛,与包括金山、360、腾讯、阿里等在内的各大公司共同合作举办赛事。比赛吸引了国内一大批安全人士的广泛关注,历年来CTF中人才辈出,汇聚了来自国内众多安全人才,高手对决,精彩异常,成为安全圈的一次比赛盛宴,突出了看雪论坛复合型人才多的优势,成为企业挑选人才的重要途径,在社会安全事业发展中产生了巨大的影响力。
合作伙伴
深信服科技股份有限公司成立于2000年,是一家专注于企业级安全、云计算及基础架构的产品和服务供应商,致力于让用户的IT更简单、更安全、更有价值。目前深信服在全球设有50余个分支机构,员工规模超过7000名。
- End -
球分享
球点赞
球在看