想不到标题,就是一个学破解的菜鸟生活压力、工作压力下发疯前的胡言乱语,算分享吧吧
一、豆包:用 c 语言写一个入门级 reserveme 给我
1.得到了如下的代码:
#include <stdio.h>
#include <string.h>
int check_password(const char* input)
{
// 正确密码:hello123
char real_pwd[] = "hello123";
if (strlen(input) != strlen(real_pwd))
{
return 0;
}
for (int i = 0; input[i] != '\0'; i++)
{
// 简单异或混淆,稍微增加一点逆向难度(入门级别)
if( (input[i] ^ 0x01) != (real_pwd[i] ^ 0x01) )
{
return 0;
}
}
return 1;
}
int main()
{
char user_input[32];
printf("Please input password: ");
scanf("%31s", user_input);
if (check_password(user_input))
{
printf("Success! You cracked this reverseme.\n");
}
else
{
printf("Wrong password!\n");
}
return 0;
}
2.下载一个dev c++工具吧,编译下,选64位release试试,来了一个exe,运行一下,随便输入一个1,输出错误了。
3.让我对加解密感兴趣的似乎就是这样简简单单的代码吧,一段输入,走向不同的分支,得到不一样的结果,便是吸引我的地方吧。哦,似乎不对,我是从脱壳开始的,看到了吾爱破解论坛的入门培训课程,似乎听不懂,最后又去看小生我怕怕的脱壳教程吧,每次跟着教程找到oep特别有成就感,虽然现在看来这样模仿似乎没什么用啦,大家都知道入门最难的了,有一段时间快坚持不下去了,非常感觉论坛的小糊涂虫大佬录了一段脱壳视频给我,让我继续学下去了。后来慢慢的又跟着网上的各种破解教程,攻防世界的CTF解题思路学习,然后思路似乎又偏了,想当黑客,学了一段时间web安全,最后又跟着论坛学了点安卓逆向知识。反正就是啥都学了啥都不会,哈哈。用ida 64位打开exe,所有工具吾爱破解的爱盘都有。
4.ida反编译的程序流程图很简单,c语言逻辑就是清晰明了,主要还是有字符串提示,不需要汇编知识也能看明白,现在有了ai,其实很多时候一段代码丢给ai就知道什么意思了,ida的f5功能挺好用,按下f5后就得到了伪代码。
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4[32]; // [rsp+20h] [rbp-20h] BYREF
_main();
printf("Please input password: ");
scanf("%31s", v4);
if ( (unsigned int)check_password(v4) )
puts("Success! You cracked this reverseme.");
else
puts("Wrong password!");
return 0;
}
5.有c语言基础的看这个就很清晰了,check_password函数返回值决定成功or失败,v4就是我们输入的字符串,双击就能进去了,进去看到const char a1常量指针其实就是v4那个字符串数组了,一个是实参,一个是形参而已,strcpy复制字符串hello123到v3,先判断长度是否相等,老实说a1[v3[3]] != ((_BYTE *)v3 + v3[3])这个看不懂。
__int64 __fastcall check_password(const char *a1)
{
size_t v1; // rbx
_DWORD v3[8]; // [rsp+20h] [rbp-60h] BYREF
strcpy((char *)v3, "hello123");
v1 = strlen(a1);
if ( v1 != strlen((const char *)v3) )
return 0i64;
for ( v3[3] = 0; a1[v3[3]]; ++v3[3] )
{
if ( a1[v3[3]] != *((_BYTE *)v3 + v3[3]) )
return 0i64;
}
return 1i64;
}
6.这一段看不懂,就去看for循环,第一个肯定就是int i=0,第三个基本就是i++,v3[3] = 0,是不是很像?直接替换v3[3]为i,数组和指针本质一样,下标加一和指针加一,如果内存区域存放同样的数据是等价的,这里做了一个强制转换保证都是1字节。
- *
a1[i] 等价 `(_BYTE )v3 + i`**
- 指针是什么类型,+1 就跳几个字节
char* / _BYTE* → +1 偏移 1 字节
int* / _DWORD* → +1 偏移 4 字节
long long* / _QWORD* → +1 偏移 8 字节
for ( i = 0; a1[i]; ++i )
{
if ( a1[i] != *((_BYTE *)v3 + i) )
return 0i64;
}
return 1i64;
}
7.现在好理解了,就是比较每一个字符是否相等,异或似乎被编译优化掉了,小学数学运算大家都知道,同加一个数,同乘一个数,两边相等,这里同时异或一个数,所以去掉异或也是等价的,其实看到这里密码就知道了,hello123没经过任何变化,密码就是hello123,输入验证成功,这就是加解密的魅力,也是吸引我的地方。
8.使用x64dbg调试器加载程序,其实我更多的是用吾爱32位od,64位程序我都不敢碰,感觉很复杂,让豆包整理一下常用功能。
| 快捷键 |
功能 |
简要说明 |
| F7 |
单步步入 StepInto |
遇到 call 进入函数内部(比如进入 check_password) |
| F8 |
单步步过 StepOver |
遇到 call 不进函数,直接执行完这个 call |
| F4 |
运行到光标处 Run to selection |
光标放在目标行,直接运行到此停下,无需手动下断点 |
| F2 |
切换软件断点 |
当前行 添加 / 取消断点 |
| Ctrl+G |
跳转到地址 / 表达式 |
输入内存地址,直接跳转 |
| -(主键盘减号) |
视图后退 |
双击 call 进去浏览代码后,回上一处浏览位置(不回程序执行流) |
| +(主键盘加号) |
视图前进 |
后退之后,再前进 |
9.搜索当前模板,字符串,通过字符串定位过来,就能看到输入提示语,输出提示语,call 项目1.401530这里面只有这一个函数未识别,可能就是check_password函数,打上断点。
00000000004015D7 | 48:8D0D 222A0000 | lea rcx,qword ptr ds:[404000] | 0000000000404000:"Please input password: "
00000000004015DE | E8 E5150000 | call <JMP.&printf> |
00000000004015E3 | 48:8D45 E0 | lea rax,qword ptr ss:[rbp-20] |
00000000004015E7 | 48:89C2 | mov rdx,rax |
00000000004015EA | 48:8D0D 272A0000 | lea rcx,qword ptr ds:[404018] | 0000000000404018:"%31s"
00000000004015F1 | E8 DA150000 | call <JMP.&scanf> |
00000000004015F6 | 48:8D45 E0 | lea rax,qword ptr ss:[rbp-20] |
00000000004015FA | 48:89C1 | mov rcx,rax |
00000000004015FD | E8 2EFFFFFF | call 项目1.401530 |
0000000000401602 | 85C0 | test eax,eax |
0000000000401604 | 0F95C0 | setne al |
0000000000401607 | 84C0 | test al,al |
0000000000401609 | 74 0E | je 项目1.401619 |
000000000040160B | 48:8D0D 0E2A0000 | lea rcx,qword ptr ds:[404020] | 0000000000404020:"Success! You cracked this reverseme."
0000000000401612 | E8 C1150000 | call <JMP.&puts> |
0000000000401617 | EB 0C | jmp 项目1.401625 |
0000000000401619 | 48:8D0D 252A0000 | lea rcx,qword ptr ds:[404045] | 0000000000404045:"Wrong password!"
0000000000401620 | E8 B3150000 | call <JMP.&puts> |
10.这种代码简单的我还能看懂,套一点算法我就懵逼了,看不懂逻辑,ai看得懂逻辑,但不一定全对,经常错一点,找不出原因。运行,随便输入123123,F7进去函数,F8单步调试看看,只要有耐心,多来几遍,应该还是能猜到这里的大致功能。
0000000000401567 | E8 2C160000 | call <JMP.&strlen> |
000000000040156C | 48:39C3 | cmp rbx,rax |
000000000040156F | 74 07 | je 项目1.401578 |
0000000000401571 | B8 00000000 | mov eax,0 |
0000000000401576 | EB 4B | jmp 项目1.4015C3 |
这里长度不相等,跳向失败,改一下右边ZF标志,让他跳转,然后就来到了for循环,逐字节比较是否相等,全部相等就返回1。
0000000000401581 | 8B45 AC | mov eax,dword ptr ss:[rbp-54] |
0000000000401584 | 48:63D0 | movsxd rdx,eax |
0000000000401587 | 48:8B45 D0 | mov rax,qword ptr ss:[rbp-30] | [rbp-30]:"123123"
000000000040158B | 48:01D0 | add rax,rdx |
000000000040158E | 0FB610 | movzx edx,byte ptr ds:[rax] |
0000000000401591 | 8B45 AC | mov eax,dword ptr ss:[rbp-54] |
0000000000401594 | 48:98 | cdqe |
0000000000401596 | 0FB64405 A0 | movzx eax,byte ptr ss:[rbp+rax-60] |
000000000040159B | 38C2 | cmp dl,al |
二、豆包:用 c++ 语言写一个入门级 reserveme 给我
1.得到了如下的代码:
#include <iostream>
bool Verify(const char* in)
{
// 原始密码 reverse123,每个字节异或0x05保存
char secret[] = {0x73,0x62,0x62,0x68,0x69,0x65,0x34,0x37,0x36,0x00};
int idx = 0;
while (true)
{
char a = in[idx];
char b = secret[idx] ^ 0x05;
if (a != b)
return false;
if (a == '\0')
return true;
idx++;
}
}
int main()
{
char buf[128]{};
std::cout << "Password: ";
std::cin >> buf;
std::cout << (Verify(buf) ? "Success\n" : "Fail\n");
return 0;
}
2.编译运行一下,用ida打开看看,这代码函数名就长了,当然了由于都是最基本的判断、输出代码,所以整个程序看上去也不复杂,F5看一下伪代码
int __cdecl main(int argc, const char **argv, const char **envp)
{
char *v3; // rax
char v5[144]; // [rsp+20h] [rbp-60h] BYREF
_main();
memset(v5, 0, 0x80ui64);
std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "Password: ");
std::operator>><char,std::char_traits<char>>(refptr__ZSt3cin, v5);
if ( (unsigned __int8)Verify(v5) )
v3 = "Success\n";
else
v3 = "Fail\n";
std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, v3);
return 0;
}
3.一样的步骤,进去看看,相比c,这个数据类型又变化了,好在逻辑简单,去掉数据类型修饰符还是能看懂赋值语句,和前面一样重命名得到类似代码,这里sbbhie476拷贝给v2,但是HIBYTE(v2[2]看不懂,BYTE2(v2[2]看不懂,想想以前数学未知数设x和y,这里我也设置一下,看作整体可能好理解一点,这样我的脑子终于能理解了,x就是我们输入的字符串,取出sbbhie476每一个字符异或5,然后给y,最后判断x是否不等于y,是的话返回0代表失败,这样一看,加解密的魅力与数学基本知识结合的挺紧密的。
__int64 __fastcall Verify(const char *a1)
{
_DWORD v2[4]; // [rsp+0h] [rbp-10h] BYREF
strcpy((char *)v2, "sbbhie476");
for ( v2[3] = 0; ; ++v2[3] )
{
HIBYTE(v2[2]) = a1[v2[3]];
BYTE2(v2[2]) = *((_BYTE *)v2 + v2[3]) ^ 5;
if ( HIBYTE(v2[2]) != BYTE2(v2[2]) )
return 0i64;
if ( !HIBYTE(v2[2]) )
break;
}
return 1i64;
}
重命名后
__int64 __fastcall Verify(const char *a1)
{
_DWORD v2[4]; // [rsp+0h] [rbp-10h] BYREF
strcpy((char *)v2, "sbbhie476");
for ( i = 0; ; ++i )
{
x = a1[i];
y = *((_BYTE *)v2 + i) ^ 5;
if ( x != y )
return 0i64;
if ( !x )
break;
}
return 1i64;
}
4.以前我就在想,两个不知道的数,为什么可以设x和y呢?现在想一想设啥都行,主要是部分到整体的思维,而我们普通人的脑容量有限,理解不了长难句,但是短句还是一听就懂,探索加密解密,攻防领域使我挺快乐的。打开x64dbg看看吧,搜索当前模块字符串,跳转过来,看看程序流程图,按G键还挺好的,和ida看到的结构差不多,对了,ida是空格键切换汇编和程序流程图的。
5.似乎这里函数名都没识别,其实只看sub开头的就行了,其他的应该都是C++库函数了,C++的程序相当于套了一层外衣,好讨厌,一点也没有C语言看的那么顺,脑子烧穿了。401530这里下个断点,F2,F7,F8走吧,看cmp,al是我输入的1,rbp-6是v,不相等,改zf吧,算了,直接改jmp吧,第二次循环,2是我输入的,g是异或5之后的。
000000000040156B | 8B45 FC | mov eax,dword ptr ss:[rbp-4] |
000000000040156E | 48:63D0 | movsxd rdx,eax |
0000000000401571 | 48:8B45 10 | mov rax,qword ptr ss:[rbp+10] | [rbp+10]:"123321"
0000000000401575 | 48:01D0 | add rax,rdx |
0000000000401578 | 0FB600 | movzx eax,byte ptr ds:[rax] |
000000000040157B | 8845 FB | mov byte ptr ss:[rbp-5],al |
000000000040157E | 8B45 FC | mov eax,dword ptr ss:[rbp-4] |
0000000000401581 | 48:98 | cdqe |
0000000000401583 | 0FB64405 F0 | movzx eax,byte ptr ss:[rbp+rax-10] |
0000000000401588 | 83F0 05 | xor eax,5 |
000000000040158B | 8845 FA | mov byte ptr ss:[rbp-6],al |
000000000040158E | 0FB645 FB | movzx eax,byte ptr ss:[rbp-5] |
0000000000401592 | 3A45 FA | cmp al,byte ptr ss:[rbp-6] |
0000000000401595 | 74 07 | je 项目1.40159E |
0000000000401597 | B8 00000000 | mov eax,0 |
000000000040159C | EB 13 | jmp 项目1.4015B1 |
000000000040159E | 807D FB 00 | cmp byte ptr ss:[rbp-5],0 |
00000000004015A2 | 75 07 | jne 项目1.4015AB |
00000000004015A4 | B8 01000000 | mov eax,1 |
00000000004015A9 | EB 06 | jmp 项目1.4015B1 |
00000000004015AB | 8345 FC 01 | add dword ptr ss:[rbp-4],1 |
00000000004015AF | EB BA | jmp 项目1.40156B |
6.实在是讨厌这种一个字节字节比较,不会在内存中暴漏完整解密字符串,长度不够退出了,重来,输入的字符串改成reverse123试试,这个是豆包给我生成代码时提示的正确密码,0000000000401595这一行按空格,je改jmp强制跳吧,在上面cmp处打上断点,记录每次解密的值,似乎ai给我的密码不对啊,我去看下程序,靠代码字节才9位,你是怎么敢在注释上和我说密码是reverse123的,位数都不对啊,算了,我自己拼接吧:vggml`123。
- r v
- e g
- v g
- e m
- r l
- s '
- e 1
- 1 2
- 2 3
- 3 ?
7.你以为ai就给你挖这个坑,你输入这个vggml`123还是fail,为什么?回去看看给的源代码,先异或了0x5,这还怎么进入return true的那个if,这个判断你不放前面放后面???哪怕结束0了,异或一下又进循环了,那我密码永远不对。
while (true)
{
char a = in[idx];
char b = secret[idx] ^ 0x05;
if (a != b)
return false;
if (a == '\0')
return true;
idx++;
}
8.关键是我问了几次ai,都给我说这段代码是对的,还给我分析的头头是道,看起来有理有据,我以为的ai是自动渗透、自动逆向,这普通网页版ai办不到啊,可能还得是氪金大佬用的ai准确性高吧,不过只是把c代码换成c++代码,难度似乎倍增了。
三、豆包:用 Js 语言写一个入门级 reserveme 给我
1.得到了如下代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>JS ReverseMe</title>
</head>
<body>
<script>
// 混淆数组
const _0x2f9b = ['charCodeAt', 'fromCharCode', 'prompt', 'alert'];
const _0x7c1e = function(_0x1a3c, _0x5d4b) {
return _0x2f9b[_0x1a3c];
};
// 核心校验函数(加密逻辑:原始字节 ^ 5)
function checkFlag(_0xinput) {
// 密钥原始串:sbbhie476
const secretRaw = [0x73,0x62,0x62,0x68,0x69,0x65,0x34,0x37,0x36];
for (let i = 0; i < secretRaw.length; i++) {
const inCode = _0xinput[_0x7c1e(0)](i);
const realCode = secretRaw[i] ^ 5;
if (inCode !== realCode) {
return false;
}
}
return true;
}
const userInput = window[_0x7c1e(2)]("请输入flag:");
if (checkFlag(userInput)) {
window[_0x7c1e(3)]("✅ Correct! You solved JS Reverseme");
} else {
window[_0x7c1e(3)]("❌ Wrong flag");
}
</script>
</body>
</html>
2.运行一下看看效果,输入错误提示❌ Wrong flag,看到弹窗,其实就知道window[_0x7c1e(3)]肯定就是alert函数了。
3.查看代码,js怎么查看源代码就不用说怎么操作了,F12就行了,整个代码checkFlag函数里面_0xinput_0x7c1e(0)就这里有混淆了,逐个向上搜索发现其实就是charCodeAt函数
const _0x7c1e = function(_0x1a3c, _0x5d4b) //_0x1a3c = 0
_0x2f9b[_0x1a3c] =charCodeAt
4.可以看出这个就是取出字符对应的ASCII码,然后和我输入字符的ASCII比较,在比较前先做了异或了5。同样这里也是按位比较,有一位不同就返回false。同样试试断点功能,不过这里可以直接用控制台打印变量,例如打印realCode。
console.log(realCode);
VM245:1 118
5.因为代码逻辑已经读懂,那么我就可以直接在控制台运行解密脚本,将realCode值每一个值异或5输出。
const secretRaw = [0x73,0x62,0x62,0x68,0x69,0x65,0x34,0x37,0x36];
let flag = "";
for(let i=0;i<secretRaw.length;i++){
flag += String.fromCharCode(secretRaw[i]^5);
}
console.log(flag);
VM286:6 vggml`123
6.vggml`123输入进去,验证一下,成功。
四、日记
1.正经人谁写日记,只有像小菜鸟这样不务正业的才会有空乱写一篇文章,说实话,学的时间很久了,想了一下自己其实入门都没有,加解密也许自带魅力,让人欲罢不能,这么多年虽然没进步,但还是发了不少文章。
2.最近没找到工作,进机械厂了,没有手艺,没有技术,再加上武汉这就业市场,工作不好找,只能有啥干啥了,一个月多加点班,到手5000+没问题,属于饿不死自己的那种。
3.这几天感冒了,请假三天回来躺着了,人一闲下来就容易胡思乱想了,为了防止自己抑郁,原想再录点啥视频,仔细想想,自己讲的还没有ai说的全,最后就变成了算是一份日记吧。
4.免费的网页版ai在写代码,分析程序的时候,太容易产生幻觉了,给你一堆看似正确的逻辑却是错误的答案,因为网页版ai执行不了我们的代码,本质还是靠猜想,准确率上不去。