NCK逆向课后作业writeup
2023-06-15 17:12:06 # Reverse Engineering

前言

个人的一些逆向小练习和解题思路

每天一道

课后小程序下载地址

链接:https://pan.baidu.com/s/1U-LK9lZf4CjVjUCuSSFIlQ
提取码:k5pf

第一课课后作业

软件使用

image-20211203152510850

MessageBoxA API断点

通过信息框,猜测有MessageBox API被调用,ctrl+g找到调用处,直接下断点

image-20211203152727330

点击按钮后让他在此处断下。

看见右下方堆栈顶端显示了CALL的返回地址,右键跟随反汇编窗口。

image-20211203152857114

这里跟随到的是返回地址,这样我们就可以找到这个MessageBoxA的返回地址了。

在此处下断点,继续运行,让他断到这里来。往上看了下,好像并没有看见什么明显的判断条件,继续F8。image-20211203153141019

image-20211203153406827

往上面找,但发现这个我们猜测的成功的call并没有命令跳过它。继续往上看,发现又有2个这样的call,同时有个je跳过了这2个call

image-20211203153609778

基本可以确定是这个跳转的问题了。

直接nop掉。

image-20211203153658090

image-20211203153721601

交叉引用查找法

先是在OD里字符串搜索,发现找不到字符串“注册失败” - OD里的各种插件可能会导致这个问题。于是转用IDA。

视图-》子视图-》字符串, 搜索注册失败

搜索字符串还是搜索不到?

在快捷方式处添加-dCULTURE=all

找到后双击跟过去image-20211203154530079

image-20211203154638521

image-20211203154720897

image-20211203154812809

上面的JE就是关键跳了。NOP掉即可

内存查找法

我们知道软件在运行的时候,函数的数据是会保存在内存里的

如果我们运行软件后将他暂停,再看内存窗口的话,可以查找到字符串

image-20211203155354377

内存断点或硬件断点即可。

注意,如果硬件断点不起作用,可能是OD的问题,它各种反调试插件、汉化等等都可能导致这个问题,我们可以使用x64dbg.

栈回溯 ALT+K暂停法

软件再执行call的时候,会把一些参数都压入堆栈,最后执行完后再pop出来,我们可以在弹出信息框的时候给他暂停,这样参数都会保留在stack中,这时我们再去OD的K窗口(栈回溯窗口),就能看到function的信息了。

image-20211203155835217

GetWindowTextA API断点

GetWindowTextA API进行断点,执行程序后被断下,一直F8。

发现在做比较,正确的字符串已经获取到了。

image-20211203160619957

后续就不分析了。

总结

之前一直以为从断下的地方跳一层就会回到主函数下面,导致这么简单都没搞出来,事实上跳N层都是有可能的。

第二课课后作业

软件使用

image-20211204090025209

栈溯源法 ALT+K暂停法

加载程序后运行,点击按钮让他弹出信息框,然后暂停。

image-20211204090912027

在窗口K中找到堆栈信息,双击

image-20211204090935331

发现有跳转,直接enter进入

image-20211204090953203

在此处断点

再让程序跑一次,找到最终返回的地方。

image-20211204091114188

此处跳过了成功消息,将JNZ改为NOP

交叉引用断点

image-20211204093044610

在OD处修改即可。

如何在IDA修改?

选项-》常规-》操作数字节-》16

编辑-》修补程序-》汇编

image-20211204093940723

nop两次把这两个都填充

编辑-》修补程序-》修补程序到输入文件

EA起始为你的基址

生成即可

image-20211204094300480

补丁使用

image-20211204094354057

第三课课后作业

软件使用

image-20211205103356194

MessageBoxA API断点

image-20211205103426796

发现带壳,之后得用补丁。

先让程序运行起来,下断点MessageBoxA

找到关键跳,需要修改JE为nop

image-20211205103920873

补丁使用

image-20211205104024600

image-20211205104112887

第四课课后作业

E语言写内存补丁

image-20211205104706503

image-20211205104725383

E语言写劫持补丁

检测数据是否被壳还原 -》 检测004010A9地址的数据是否为0x55(85)

image-20211205124112526

C语言写内存补丁

image-20211205124716992

这里主要是用OpenProcess()WriteProcessMemory()来完成内存写入操作的。

第五课课后作业

前言

这一课涉及到了很多知识点,我花了两三天的事件才慢慢把各个知识点理清楚。 主要涉及到了硬件断点、硬件HOOK和线程方面的知识,虽然老师只讲了一节课,但是自己真正弄清楚并实践还是得费不少时间。另外我使用的VS2022版本给我带来了不少麻烦,各种不兼容,还好最后解决了,之后可能会出一些解决VS问题的合集。

该课后作业的要求是不修改代码从而实现破解。

软件使用

image-20211208125058597

SetWindowTextA 寻找关键跳

OD载入发现有壳

image-20211208125215342

我们将它运行起来 - 加壳的程序必须运行起来,才会被恢复源码。

image-20211208125611118

注册失败的信息显示在了标题上,那么可以猜测它使用了SetWindowTextA函数,下一个断点。

image-20211208125735454

断下来了,开始F8。现在走到了一个可疑的地方,附近有大跳,我们用Immlabel插件标记一下,发现上面也有一处一样的call,那么猜测可能是成功的call。

image-20211208125913192

观察发现,上面有一个jnz跳转,跳过了成功的call,那么我们可以确定关键跳就在这里了0x401053

image-20211208130101207

破解方法一:进程挂起+修改ZF标志位

在之前得知的关键跳0x401053处下一个CC断点

1

奇怪的是,程序会被终止。CC断点是将地址的首字节改为0xCC,因此我们判断该程序使用了某种校验方法(可能是CRC检测)来检测固定的某段代码段是否在运行过程中被修改。此处我们可以修改JNZ为NOP完成破解,但题目要求是不修改代码破解,所以我们还需另寻他法。

一个软件为了保证整个程序流程正常运行,不太可能将CRC检测(while loop形式)写在主线程(单一线程),不然后面的代码将无法执行,所以猜测可能是有新的线程建立。

在OD的T窗口查看线程情况

image-20211208130947860

发现了一个异常活跃的线程,基本可以确定是用来检测的线程 - 直接将线程挂起。

image-20211208131235695

再次将0X00401053下断,发现程序正常,成功过掉CRC检测。

image-20211208131301886

JNZ在ZF标志位为1的时候不跳转 image-20211208130254590

破解成功

image-20211208131520337

破解方法二:硬件HOOK

已知检测程序是校验代码位数,从而判断代码是否被修改,但硬件断点不会被断下,因为它是通过操纵Dr寄存器来完成的下断。关于硬件断点的讲解见浅析硬件断点和内存断点

硬件断点会触发**STATUS_SINGLE_STEP(单步异常)**,异常会交给程序的异常处理函数管理,若程序的异常处理函数不处理,才会交给调试器。

我们现在可以利用以下思路:

配合DLL劫持,让程序在JNZ跳转0x401053处触发硬件断点,同时给程序写一个异常处理函数 - 异常处理函数里是我们的恶意代码 - 将EIP+6,这样能够直接绕过JNZ判断

image-20211208132929602

WIN10代码(我的环境): winspool.drv,使用第四课的C语言劫持补丁代码进行改写。

注意:win10在设置硬件断点的时候必须先挂起目标线程,否则无法断点,所以我们需要创建一个新的线程,再在这个线程中暂停主线程从而保存修改后的寄存器内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
DWORD g_dwBreakPoint = 0x401053;
DWORD WINAPI ThreadProc(_In_ LPVOID lpMainThreadId)
{

HANDLE hMainThread = OpenThread(THREAD_ALL_ACCESS, TRUE, (DWORD)lpMainThreadId);
SuspendThread(hMainThread);// 暂停这个线程,避免它检测
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_ALL;
GetThreadContext(hMainThread, &ctx);//获取该线程的context=》包含了这个线程的各种参数,其中就包含dr寄存器的内容
ctx.Dr7 = (DWORD)0x1;//将该线程的dr7设为0x1=》代表DR0是有效的(L0=1)
ctx.Dr0 = g_dwBreakPoint;//Dr0=硬件断点的地址
SetThreadContext(hMainThread, &ctx);//把修改的值set到context里
ResumeThread(hMainThread);//恢复线程
return 0;
}
//异常捕捉
DWORD NTAPI ExceptionHandler(EXCEPTION_POINTERS* ExceptionInfo)
{
if ((DWORD)ExceptionInfo->ExceptionRecord->ExceptionAddress == g_dwBreakPoint)//如果发生异常的位置就是我们想要跳过的位置
{
ExceptionInfo->ContextRecord->Eip += 6;//将该地址的EIP+6,从而我们可以跳过检测。
//已经处理了异常,不需要再调用下一个异常处理来处理此异常
return EXCEPTION_CONTINUE_EXECUTION;
}
//调用下一个处理器
return EXCEPTION_CONTINUE_SEARCH;
}


BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
::MessageBoxA(0, "开始破解拉", "能否成功呢?leihehehe.github.io", 0);
DisableThreadLibraryCalls(hModule);
AddVectoredExceptionHandler(1, (PVECTORED_EXCEPTION_HANDLER)ExceptionHandler);//添加一个VEH异常处理的函数,如果有异常,该函数会被call
CreateThread(NULL, NULL, ThreadProc, (LPVOID)GetCurrentThreadId(), NULL, NULL);//创建线程来制造硬件断点,这里会发出异常,VEH异常处理函数能捕获它。
return Load();
}
else if (dwReason == DLL_PROCESS_DETACH)
{
Free();
}

return TRUE;
}

下面是win7代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
DWORD g_dwBreakpoint = 0x401053; // 关键指令,希望跳过这条指令

// 设置硬件断点函数
void SetHardwareBreakPoint()
{
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_ALL;
GetThreadContext(GetCurrentThread(), &ctx);
ctx.Dr7 = (DWORD)0x1; // 启用Dr0
ctx.Dr0 = g_dwBreakpoint; // 设置硬件断点
SetThreadContext(GetCurrentThread(), &ctx);
}

// 异常处理函数
DWORD NTAPI ExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo)
{
if ((DWORD)pExceptionInfo->ExceptionRecord->ExceptionAddress == g_dwBreakpoint)
{
pExceptionInfo->ContextRecord->Eip += 6;
// 已经处理了异常,不需要调用下一个异常处理来处理该异常
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}


// 入口函数
BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(hModule);
AddVectoredExceptionHandler(1, (PVECTORED_EXCEPTION_HANDLER) ExceptionHandler);
SetHardwareBreakPoint(); //设置硬件断点,不需要创建一个线程来执行硬件断点
return Load();
}
else if (dwReason == DLL_PROCESS_DETACH)
{
Free();
}

return TRUE;
}

破解成功。

hw5HardBreakHook

第六课课后作业

软件使用

image-20211209185701029

image-20211209185710786

此处注册码并没有任何用处,随意输入也会提示注册成功

破解方法

运行起来转到代码段401000 - 从模块中删除分析

字符串搜索

image-20211209185906127

我们看见有一个\License.key,双击过去,在段开头断点

image-20211209190001299

我们猜测该程序是在启动的时候判断是否注册,而关键点就在于这个License.key

记录一下下断点的地址004016D6

现在重新运行软件,正常的话,程序应该在这里断下来,但现在并没有断下,发现断点被禁用了。

image-20211209190436336

程序带壳,刚开始运行时,下断的地址没有被恢复,所以断点会被禁止。

因此我们需要在程序代码恢复后再给004016D6地址下断。

转到CreateWindowExW下断 - 大部分加壳程序代码在此处都已还原。

image-20211209191453997

在004016D6下断

image-20211209191547960

运行后被断下,一直F8

image-20211209191725737

在此处修改jnz为nop即可

image-20211209191821736

第七课课后作业

软件使用

image-20211213153606918

作业要求:逆出算法,不能爆破,不能修改代码。

逆向流程

直接载入OD,找关键点

image-20211213153904764

在IDA中发现左侧第一个就是401000的函数,我们点击以后按F5,让它直接转变为代码

image-20211213154209385

分析a1应该是我们输入的key,下面的messageBox乱码,怎么解决呢?双击乱码的地方过去

image-20211213154450306

这种情况通常是因为IDA将其识别为Unicode,而unicode不能正确表示我们的字符串。

菜单栏->选项->字符串文本->修改为UTF-16LE

image-20211213154629467

发现已经改回来了。

再回到之前的代码区,按F5

image-20211213154656969

代码成功恢复

接下来我们对一些变量名做一些修改,方便理解

image-20211213160036921

C语言逆算法

直接把代码从IDA上copy&paste,然后做一些修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <windows.h>
int checkKey(int inputKey)
{
int result; //
int v2; // 这个应该是用来作为BOOL,作为一个FLAG判断

v2 = 0;
if (inputKey * (inputKey - 23) == -102)
v2 = 1;
result = v2;
if (v2)//如果上面的条件成立了,则继续检测下面是否成立
{
result = inputKey * inputKey * inputKey;
if (result == 4913)
return 1;
else
return 0;
}
return 0;
}


int main()
{
for (int i = 0; i < 0xFFFFFFFF; i++) {//遍历key是否正确
if (checkKey(i)) {
printf("%d", i);
}
}
system("pause");
}

image-20211213160052211

算出来正确的注册码是17

image-20211213160126767

第八课课后作业

软件使用

image-20211213160322310

去花指令

载入OD后在字符串中查找,发现并没有注册失败或成功,跟进请输入注册码,发现后面代码加了花指令

image-20211213162550645

我选择先去除花指令。

花指令一般是由恒成立的跳转组成的,我们只需要nop掉永远不会访问的无效指令,就能去除

例如

image-20211213163002542

在数据窗口中去除 image-20211213163034570

将所有花指令去除后,复制保存到可执行文件

分析算法

因为程序比较小,我们可以直接拖入IDA,在前几个函数中能找到我们的关键位置

image-20211213163536224

我们做一些注释

image-20211213164107383

其中一个call中的**&byte_442578引起了我的注意,我有些困惑这是什么,根据上下文猜测,感觉后面的v4像是我们输入的key,&byte_442578**又是什么呢?

双击进去看看,此时它为数据类型

image-20211213164245856

我们将他转换成字符串,发现它变成了%s

返回刚才的地方F5,显而易见,这是一个**scanf()**方法

image-20211213164406622

我们进入**algrithms()**继续分析算法

image-20211213165156917

一顿分析后,我们发现下方有一个*(_BYTE *),通常这种pointer前方又有一个星号的情况,都是因为IDA识别错误。

我们发现括号后面finalResult,和inputKey的类型是错误的

image-20211213165345317

修改一下参数类型

image-20211213165435708

显示正常。

同样我们再修改一下以下位置

image-20211213165659661

image-20211213165742029

对应的值为**’bcdaren’**

image-20211213165804348

C语言逆算法

将算法从IDA复制到C

观察下面的算法,我们发现再第二个for循环中,i一直没变,不停的再给同一个数重新赋值新的aBacdaren_1[j]^xxx,所以我们可以看出来,其实这两个for循环是在干一件事,就是把最后一个字母n和(key中的每一位+13)做异或运算。

1
2
3
4
5
6
7
8
for ( i = 0; ; ++i )
{
v4 = i;
if ( i >= keyLen ) // 如果循环次数大于了key的长度就退出循环
break;
for ( j = 0; j < strlen("bcdaren"); ++j )
finalResult[i] = aBcdaren_1[j] ^ (inputKey[i] + 13);
}

化简以后:

1
2
3
for (int i = 0; i < keyLen; i++) {
finalResult[i] = 'n' ^ (inputKey[i] + 13);
}

于是我们可以分析出,该程序的算法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
char* algrithms(char* inputKey, char* finalResult, unsigned int maxLen)
{
signed int v4;
signed int keyLen;
signed int i;

if (inputKey && finalResult && maxLen)
{
if (strlen(inputKey) <= maxLen) // 判断长度是否小于maxLen
keyLen = strlen(inputKey); // 如果<=最大长度,取key原本的长度
else
keyLen = maxLen; // 如果大于最大长度,取最大长度

for (int i = 0; i < keyLen; i++) {
finalResult[i] = 'n' ^ (inputKey[i] + 13);
}
}
return finalResult;
}

image-20211213172917906

最后finalResult的值应该等于((++**--,,//..QQPP

我们再写个逆运算的算法,注意,异或运算中任意两个数异或可以得到第三个数,例如:

a^b=c

a^c=b

b^c=a

最终逆向算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
char* getCorrectKey(const char* keyToBeConverte, char* finalResult, unsigned int maxLen) {
signed int keyLen;
if (keyToBeConverte && finalResult && maxLen) {
if (strlen(keyToBeConverte) <= maxLen) // 判断长度是否小于maxLen
keyLen = strlen(keyToBeConverte); // 如果<=最大长度,取key原本的长度
else
keyLen = maxLen;
for (int i = 0; i < keyLen; i++) {
finalResult[i] = ('n' ^ keyToBeConverte[i])-13;
}
}
return finalResult;
}

int main()


{
char finalResult[128] = { 0 };
printf("%s\n", getCorrectKey("((++**--,,//..QQPP", finalResult, 128));

system("pause");
}

image-20211213175431347

image-20211213175444354