大神论坛

找回密码
快速注册
查看: 73 | 回复: 0

[原创] 对一道CTF题的逆向分析 附生成flag的完整分析流程

digest

主题

帖子

0

积分

初入江湖

UID
675
积分
0
精华
威望
0 点
违规
大神币
68 枚
注册时间
2023-10-14 10:52
发表于 2023-12-23 23:33
本帖最后由 mmortalyi 于 2023-12-23 23:33 编辑

题目

题目名称:decode_me

附件:https://dslt.lanzouv.com/iR2ww1is558b

非常简单粗暴的一题,直接丢个文件过来,要求找出 flag


文件格式判断

拿到文件,先判断一下文件格式,丢入 PE 工具:Detect It Easy 里看一下:

可以看到是 ELF64 的文件格式,可以在 Linux 64 位系统上运行


IDA 解析

把文件直接拖入 IDA Pro 64 得到:

自动定位到了 main 函数入口,直接按 F5 生成伪代码看看:

int __cdecl main(int argc, const char **argv, const char **envp)
{
char v3; // bl
char buf; // [rsp+17h] [rbp-29h] BYREF
ssize_t v6; // [rsp+18h] [rbp-28h]
int fd; // [rsp+24h] [rbp-1Ch]
unsigned int v8; // [rsp+28h] [rbp-18h]
char v9; // [rsp+2Fh] [rbp-11h]

if ( argc != 2 )
exit(1);
fd = open(argv[1], 0, 0LL);
if ( fd == -1 )
exit(1);
v8 = 0;
v9 = 0;
while ( v8 <= 0x224 )
{
v6 = read(fd, &buf, 1uLL);
if ( v6 <= 0 )
break;
v3 = byte_404060[v8];
v9 |= v3 ^ sub_4012D8((unsigned int)buf, v8++);
}
if ( !v9 )
puts("[+] Correct one!");
return 0;
}

伪代码很简洁,感觉很简单.jpg

忽略掉伪代码里不确定的部分,可以大致分析出对应的逻辑

int main(){
//如果参数个数不为2 则自动退出
if(argc !=2){
exit(1);
}
//打开参数对应路径的文件,PS:argv[0] 是运行路径,argv[1] 才是真正的参数
fd = open(argv[1], 0, 0LL);
//如果文件打开失败则退出
if ( fd == -1 )
exit(1);
//这里重命名为 index 是根据下面 while 循环对数组的遍历可以推断出这是下标
int index = 0;
//这里重命名为 flag 是根据最下面输出 Correct one 判断这个是个标记
int flag = 0;
while(index <= 0x224){
//每次从文件里读 1 个字节,如果读不到就终止
ssize_t read_bytes = read(fd, &buf, 1uLL);
if ( read_bytes <= 0 ){
break;
}
//从某个数组里根据下标读取值
key = byte_404060[index];
//计算 flag
flag |= (key ^ sub_4012D8((unsigned int)buf, index++));
}
//如果 flag = 0 才输出正确
if ( !flag )
puts("[+] Correct one!");
return 0;
}

伪代码很简洁,感觉很简单.jpg

忽略掉伪代码里不确定的部分,可以大致分析出对应的逻辑

int main(){
//如果参数个数不为2 则自动退出
if(argc !=2){
exit(1);
}
//打开参数对应路径的文件,PS:argv[0] 是运行路径,argv[1] 才是真正的参数
fd = open(argv[1], 0, 0LL);
//如果文件打开失败则退出
if ( fd == -1 )
exit(1);
//这里重命名为 index 是根据下面 while 循环对数组的遍历可以推断出这是下标
int index = 0;
//这里重命名为 flag 是根据最下面输出 Correct one 判断这个是个标记
int flag = 0;
while(index <= 0x224){
//每次从文件里读 1 个字节,如果读不到就终止
ssize_t read_bytes = read(fd, &buf, 1uLL);
if ( read_bytes <= 0 ){
break;
}
//从某个数组里根据下标读取值
key = byte_404060[index];
//计算 flag
flag |= (key ^ sub_4012D8((unsigned int)buf, index++));
}
//如果 flag = 0 才输出正确
if ( !flag )
puts("[+] Correct one!");
return 0;
}

简单分析可以得出逻辑:

读取传入的参数作为要读取文件的路径

按字节读取文件内容,然后计算 Flag

最终判断 flag 是否为 0,为 0 才是正解


可以传入的参数也就是要读取的文件路径其实并不重要

重要的是要读取的文件里面的内容,得想办法逆推这个内容


算法的核心就在于

//计算 flag
flag |= (key ^ sub_4012D8((unsigned int)buf, index++));

结合最后的判断,flag 必须为 0,可以推断出: (key ^ sub_4012D8((unsigned int)buf, len++)) 必须一直为 0

PS:0 只有与 0 进行 或运算结果才为 0


这里的 key 十分简单,就是个固定的数组 byte_404060 用 IDA 双击查看:

直接把这里的数组先拷出来,拷贝前面 while 循环的长度:0x224 (IDA 这里解析的并不准确,多了 4 字节是 552):

byte_404060 = [0x7b, 0xed, 0x51, 0x57, 0xfd, 0x11, 0x5e, 0x41, 0x6e, 0xab, 0xa2, 0x0c, 0xf0, 0x2a, 0x29, 0x97, 0xd9, 0x67, 0x2a, 0x24, 0x9d, 0x64, 0xbf, 0x74, 0x42, 0x7d, 0x80, 0x8b, 0xea, 0x63, 0x25, 0x4b, 0x0e, 0xab, 0x85, 0x2c, 0x32, 0x67, 0x5a, 0x87, 0x2f, 0xa4, 0x67, 0x25, 0x8d, 0x0c, 0xb5, 0xa4, 0xd9, 0xee, 0xce, 0xbe, 0xa7, 0xb0, 0xf9, 0x19, 0xf1, 0x2d, 0x83, 0x72, 0xf1, 0x1b, 0xb1, 0xf7, 0x01, 0x9a, 0xfa, 0xef, 0xde, 0xc4, 0x9f, 0x98, 0x7d, 0xce, 0x3a, 0x91, 0xf9, 0x84, 0x85, 0xfc, 0x8e, 0x18, 0xb0, 0x4a, 0x75, 0x71, 0x6c, 0x47, 0x81, 0x34, 0xd9, 0xf6, 0x4c, 0x47, 0x8f, 0xdf, 0x1e, 0x37, 0xfa, 0x8f, 0x29, 0x73, 0x0f, 0x9e, 0xbe, 0x0c, 0x4a, 0xaa, 0xd1, 0xfb, 0x09, 0x07, 0x47, 0x22, 0x57, 0x1a, 0xbd, 0x90, 0xbc, 0xe2, 0xcd, 0x33, 0xba, 0xc8, 0x37, 0xfb, 0xa7, 0x7f, 0x0e, 0xec, 0xc7, 0xdc, 0x41, 0x98, 0xf1, 0x49, 0xd5, 0x54, 0xb6, 0x5f, 0x20, 0xfb, 0x59, 0xb1, 0x32, 0xe3, 0xc9, 0xfe, 0x69, 0x30, 0x71, 0xf9, 0xb0, 0xaf, 0xc6, 0x4c, 0x05, 0x61, 0x40, 0x24, 0x41, 0x20, 0xf7, 0x41, 0xde, 0xf5, 0x2b, 0x18, 0x83, 0x02, 0x89, 0x40, 0x9b, 0x04, 0x4b, 0x5d, 0x2e, 0x58, 0x91, 0xca, 0x35, 0x1a, 0x76, 0x20, 0x75, 0xa7, 0xce, 0x91, 0xfa, 0x34, 0x6d, 0x71, 0x79, 0xcd, 0x40, 0x1f, 0xce, 0x46, 0x75, 0xca, 0x76, 0x4f, 0x95, 0xe1, 0x36, 0x1d, 0x9a, 0x17, 0xff, 0x84, 0x17, 0x15, 0x5e, 0x6d, 0x89, 0x6c, 0x33, 0xa8, 0xde, 0x08, 0x66, 0x92, 0xe7, 0x27, 0x1a, 0x95, 0xeb, 0x48, 0xb7, 0xf6, 0xf1, 0xf1, 0x15, 0x37, 0x71, 0x02, 0x70, 0x27, 0x8d, 0x1c, 0x4d, 0xb5, 0x20, 0x9b, 0x1e, 0x0a, 0x7e, 0xcf, 0x18, 0xfb, 0xf2, 0x9e, 0x65, 0xa1, 0x6d, 0xc3, 0x81, 0xaa, 0x6c, 0x77, 0xae, 0xfd, 0xbd, 0x2b, 0xfd, 0xe9, 0xcd, 0x8c, 0xc1, 0x90, 0xb4, 0x68, 0xe9, 0x3f, 0xd2, 0xaf, 0x52, 0x45, 0xce, 0xe9, 0x01, 0xba, 0x21, 0xc5, 0x4e, 0x7d, 0xad, 0xd4, 0x2d, 0xb9, 0x9e, 0x81, 0xbd, 0xc3, 0x92, 0xad, 0x3b, 0x28, 0x05, 0x5b, 0xe5, 0x41, 0xfe, 0x50, 0x85, 0x04, 0xe7, 0xb4, 0x78, 0x83, 0xc3, 0x4c, 0x9a, 0x3d, 0xde, 0xf8, 0xbb, 0x50, 0xce, 0xbd, 0x19, 0x73, 0x5a, 0xcb, 0x52, 0x9b, 0x4e, 0xf3, 0x31, 0xf3, 0x9d, 0xbd, 0x9e, 0x5d, 0x6d, 0x38, 0xb2, 0xea, 0x74, 0xdb, 0xf6, 0x3e, 0x9b, 0xa9, 0xe9, 0x99, 0x81, 0x49, 0x9a, 0xe2, 0x89, 0x17, 0x89, 0x1a, 0x4d, 0x37, 0xde, 0xa4, 0xfd, 0x18, 0xfc, 0x93, 0x2e, 0x61, 0xc9, 0x1e, 0x6e, 0xdd, 0x3d, 0xd3, 0x11, 0x6f, 0x03, 0x0c, 0x76, 0xb4, 0x41, 0x14, 0xfe, 0xb1, 0xe6, 0x07, 0x9d, 0x4c, 0xf9, 0xf3, 0x51, 0x68, 0xe9, 0xca, 0xf5, 0x59, 0x60, 0xdb, 0xa1, 0x6b, 0xab, 0x2a, 0xd8, 0x61, 0xd1, 0x53, 0x1b, 0x81, 0x3a, 0x6d, 0xa2, 0x74, 0xe7, 0xd5, 0xba, 0x4c, 0xa9, 0x64, 0x25, 0x4e, 0xbd, 0xab, 0x31, 0xfb, 0x95, 0xd2, 0x4e, 0x09, 0xaf, 0x6b, 0xf1, 0x41, 0x38, 0xfd, 0x19, 0x1f, 0x2e, 0x99, 0xcc, 0xbc, 0x3a, 0xbe, 0x55, 0xe1, 0xbe, 0xc4, 0x83, 0x4c, 0x45, 0x7b, 0xd5, 0xc4, 0xe2, 0x92, 0xb7, 0xb5, 0x11, 0xc4, 0x27, 0x98, 0xbe, 0x69, 0xcd, 0x0c, 0x41, 0x26, 0x22, 0xa9, 0x86, 0xbf, 0xeb, 0x1c, 0xcd, 0x65, 0xda, 0x37, 0x0f, 0x80, 0xe7, 0xe2, 0x1e, 0xeb, 0x39, 0x8f, 0xfb, 0x92, 0x4c, 0x76, 0x3d, 0x4a, 0x0f, 0x39, 0x98, 0x4d, 0xeb, 0x43, 0x65, 0xd5, 0xa0, 0x80, 0x03, 0x1a, 0x66, 0x8b, 0x80, 0xbc, 0x73, 0x06, 0xa4, 0xbb, 0xa2, 0x79, 0x68, 0x0e, 0x10, 0x9a, 0xbd, 0x01, 0xd3, 0xd0, 0xf2, 0xf4, 0x04, 0x04, 0x17, 0x1c, 0xe8, 0x3d, 0x0e, 0x56, 0x5e, 0xa3, 0x5d, 0x1b, 0x26, 0x7b, 0x5a, 0xb7, 0x3b, 0x59, 0x60, 0x1b, 0x1d, 0x2f, 0xe9, 0x75, 0x14, 0x24, 0x41, 0x8b, 0x7e, 0xe1, 0x3d ]

所以重点就在于 sub_4012D8((unsigned int)buf, index++) 这个函数的算法了

函数的参数很简单,buf :读取进来的文件内容;index:当前下标


双击用 IDA 查看这个函数:

__int64 __fastcall sub_4012D8(__int64 a1, __int64 a2)
{
__int64 v3; // [rsp+0h] [rbp-8h]

v3 = sub_401291();
return v3 ^ sub_401240(a2);
}

根据前面可以得知 这 2 个参数的含义,对应修改下参数类型和参数名称

//buf 从文件中读出来的字符
//index 当前读取文件的对应偏移(下标)
__int64 __fastcall sub_4012D8(char buf, __int64 index)
{
__int64 ret; // rax
//暂时不清楚是干什么的运算
ret = sub_401291();
//把前面返回的结果和另一个函数得到的结果做 xor 运算返回
//这个函数传入当前下标,返回一个结果
return ret ^ sub_401240(index);
}

接下来看第一个函数 sub_401291,依旧直接用 IDA 双击点进去:

// positive sp value has been detected, the output may be wrong!
void sub_401291()
{
__asm { jmp off_404388[rax] }
}

QAQ:好像没办法直接反编译出伪代码,得啃一下汇编了


退出伪代码界面(Pseudocode) 转到 IDA View 界面查看汇编

先看前半部分:

00000000401291 sub_401291 proc near                    ; CODE XREF: sub_4012D8+C↓p
.text:0000000000401291 mov rax, 0FFFFFFFFFFFFFFF8h
.text:0000000000401298 jmp short loc_4012AF

0000000004012AF loc_4012AF: ; CODE XREF: sub_401291+7↑j
.text:00000000004012AF ; sub_401291:loc_4012A0↑j ...
.text:00000000004012AF add rax, 8
.text:00000000004012B3 jmp off_404388[rax]

也就是:

# rax = -8
mov rax, 0FFFFFFFFFFFFFFF8h
# rax = rax + 8 也就是 -8 + 8 = 0
add rax,8
# 跳转到 404388 这个数组里偏移为 rax 的数据对应的地址
# 根据上面的图,可以看出来一共有 5 个地址可跳转(分出来了 5 个分支)
jmp off_404388[rax]

看一下 off_404388 里存储的内容:

根据 rax 每次是 +8 ,调整一下后面数据的类型为 qword (8 字节)

PS:选中对应段按 q 也可以改成 qword 格式


全部调整完得到:

00000000404388 off_404388 dq offset loc_4012AC         ; DATA XREF: sub_401291+22↑r
.data:0000000000404390 dq offset loc_40129A
.data:0000000000404398 dq offset loc_4012A2
.data:00000000004043A0 dq offset loc_40129A
.data:00000000004043A8 dq offset loc_40129A
.data:00000000004043B0 dq offset loc_4012CD
.data:00000000004043B8 dq offset loc_40129A
.data:00000000004043C0 dq offset loc_4012C1
.data:00000000004043C8 off_4043C8 dq offset loc_401283 ; DATA XREF: sub_401240+21↑r
.data:00000000004043D0 dq offset loc_401247
.data:00000000004043D8 dq offset loc_401255
.data:00000000004043E0 dq offset loc_40126C
.data:00000000004043E0 _data ends

结合下半部分的逻辑

只有 loc_4012C1 的逻辑有 retn,其他部分都是跳转回去继续执行

可以推断出这部分的逻辑是按顺序执行 off_404388 这个数组里的存储的函数地址,直到执行完 loc_4012C1  retn 返回

按数组里的顺序依次分析对应的函数:

00000000404388 off_404388 dq offset loc_4012AC         ; DATA XREF: sub_401291+22↑r
.data:0000000000404390 dq offset loc_40129A
.data:0000000000404398 dq offset loc_4012A2
.data:00000000004043A0 dq offset loc_40129A
.data:00000000004043A8 dq offset loc_40129A
.data:00000000004043B0 dq offset loc_4012CD
.data:00000000004043B8 dq offset loc_40129A
.data:00000000004043C0 dq offset loc_4012C1

第一个函数:loc_4012AC  

loc_4012AC:
# 将 rbx 压入堆栈,但这里的 rbx 是什么? 动态调试瞅瞅吧
push rbx
# 跳转回去
jmp short $+2

动态调试环境搭建

前面在文件格式判断里得出了这个文件是 ELF64 且运行在 Linux AMD64上,所以需要搞个 linux AMD64  的环境

我是用 vmware 搞了个 centos 7.9 的 环境:

PS:vmware 安装 centos 的教程网上一抓一大把,这里就不展开了


把要调试的程序 program 传到 centos7.9 上,可以用 Winscp 或者 xftp 等软件

然后赋予权限:

chmod +x program

根据前面的分析得知,调用时需要给一个文件路径

于是在同个目录下随便创建个文件,然后随便给点字符串

#将字符串 "lyl610abc" 写出到 test 文件
echo lyl610abc > test

打开 IDA Pro 的安装目录找到 dbgsrv :

打开得到:

把 linux_server64 拷贝到 centos7.9 上,赋予访问权限,然后运行

chmod +x linux_server64
./linux_server64

为避免可能存在防火墙导致无法连接可以下面命令关闭:

systemctl stop firewalld.service

回到 IDA Pro,选择调试器为 Remote Linux debugger

Debugger → Process options 设置要调试的应用

设置好以后,随便找个地方下个断点验证下是否能正常调试

这里选 main 函数的开头

然后按快捷键 F9 运行

到这里动态调试环境的搭建完毕了


动态调试

继续回到前面要分析的地方,为避免往上翻,再贴一遍:

按数组里的顺序依次分析对应的函数:

00000000404388 off_404388 dq offset loc_4012AC         ; DATA XREF: sub_401291+22↑r
.data:0000000000404390 dq offset loc_40129A
.data:0000000000404398 dq offset loc_4012A2
.data:00000000004043A0 dq offset loc_40129A
.data:00000000004043A8 dq offset loc_40129A
.data:00000000004043B0 dq offset loc_4012CD
.data:00000000004043B8 dq offset loc_40129A
.data:00000000004043C0 dq offset loc_4012C1

第一个函数:loc_4012AC  

在 loc_4012AC   下个断点,然后断下来后查看对应值

可以看到 rbx 的值是 0x7b,这个值是不是似曾相识,这正是前面拷贝数组里第一个下标的值:

byte_404060 = [0x7b, 0xed, 0x51, 0x57, 0xfd, 0x11, 0x5e, 0x41, 0x6e, 0xab, 0xa2, 0x0c, 0xf0, 0x2a, 0x29, 0x97, 0xd9, 0x67, 0x2a, 0x24, 0x9d, 0x64, 0xbf, 0x74, 0x42, 0x7d, 0x80 ......]

避免再往上翻,贴一遍引用到的地方

int main(){
//如果参数个数不为2 则自动退出
if(argc !=2){
exit(1);
}
//打开参数对应路径的文件,PS:argv[0] 是运行路径,argv[1] 才是真正的参数
fd = open(argv[1], 0, 0LL);
//如果文件打开失败则退出
if ( fd == -1 )
exit(1);
//这里重命名为 index 是根据下面 while 循环对数组的遍历可以推断出这是下标
int index = 0;
//这里重命名为 flag 是根据最下面输出 Correct one 判断这个是个标记
int flag = 0;
while(index <= 0x224){
//每次从文件里读 1 个字节,如果读不到就终止
ssize_t read_bytes = read(fd, &buf, 1uLL);
if ( read_bytes <= 0 ){
break;
}
//从某个数组里根据下标读取值
key = byte_404060[index];
//计算 flag
flag |= (key ^ sub_4012D8((unsigned int)buf, index++));
}
//如果 flag = 0 才输出正确
if ( !flag )
puts("[+] Correct one!");
return 0;
}

所以这里的 rbx 就是 byte_404060[index] 也就是 key

先记下,接着看第二个函数:loc_40129A

loc_40129A:
jmp loc_4012A0
loc_4012A0:
jmp short loc_4012AF

这个函数啥都没做,就是直接跳转到下一个函数


第三个函数:loc_4012A2

loc_4012A2:
# rbx = unk_404288
lea rbx, unk_404288
# 跳回去
jmp short loc_4012AF

调试可以看到,执行完 lea     rbx, unk_404288 后 rbx 就等于 unk_404288 了

第四、五个函数:loc_40129A

在第二个函数里分析过了,啥都没做,略过

第六个函数:loc_4012CD

loc_4012CD:
# 把 rax 压入堆栈
push rax
# rax 的低 8 位 等于 rdi 的低 8 位
mov al, dil
# mov al,[(E)BX + unsigned AL], ebx这个地址指向的是一个数组,从数组里取出 al 偏移的值 赋值给 al
xlat
# rbx = rax
mov rbx, rax
# 把 rax 弹出堆栈 还原rax
pop rax
# 跳回去
jmp short loc_4012AF

按顺序执行观察寄存器情况:

push rax 执行前

push rax 执行后,mov al,dil执行前

可以看到只有堆栈 RSP RIP变化了,毕竟只是个入栈操作

mov al,dil 执行后,xlat 执行前

可以看到这里 al = 6C,这个 6C 对应的 ascii 码 是 l ,也就是我们给的文件内容("lyl610abc")的第一个字符

前面已经说明了 xlat 的作用:mov al,[(E)BX + unsigned AL], ebx这个地址指向的是一个数组,从数组里取出 al 偏移的值 赋值给 al

此时的 ebx 对应的正是一个数组,从这个数组里取出偏移为输入字符 ascii 码的值

按照 xlat 的计算式子,看一下 ebx+al 对应的内容

404288 + 6c = 4042F4

对应的值为 BA


xlat 执行后,mov rbx,rax 执行前

可以看到 RAX 果然变为了 BA ,和推断的一致

后面的 mov rbx,rax 以及 pop rax 很简单,就不再贴图了

归纳一下 loc_4012CD 这个函数的作用

将 rbx 设置为:unk_404288 这个数组里对应我们输入文件内容的偏移的数,即:rbx = unk_404288[buf]


第七个函数:loc_40129A

在第二个函数里分析过了,啥都没做,略过


第八个函数:loc_4012C1

loc_4012C1:
mov rax, rbx
pop rbx
retn

将 rax 作为返回值返回,在这里下个断点看下此时的 rax

可以发现这里的 rax 就是前面计算得到的 BA


综合上面函数的分析,可以得出这里的逻辑就是从 unk_404288 这个数组里找传入参数偏移的对应的数据返回

可以写出对应的伪代码:

//传入的参数为从文件中读出来的字符
int sub_401291(char buf){
return unk_404288[buf];
}

所以这里再保存一下 unk_404288  这个数组,后面逆推要用到:

PS:这里用的是十进制,上图是十六进制

unk_404288 = [36, 221, 193, 137, 10, 207, 131, 226, 198, 253, 127, 70, 68, 37, 157, 46, 39, 32, 184, 225, 53, 57, 51, 177, 144, 200, 48, 169, 152, 117, 115, 234, 248, 54, 64, 35, 164, 41, 105, 93, 21, 145, 196, 11, 228, 172, 55, 149, 60, 99, 82, 133, 224, 30, 243, 255, 130, 132, 181, 187, 151, 146, 150, 18, 142, 74, 214, 73, 122, 155, 139, 52, 12, 143, 213, 239, 44, 90, 58, 202, 197, 110, 160, 103, 13, 140, 31, 245, 7, 29, 91, 79, 159, 171, 5, 166, 129, 83, 63, 251, 246, 236, 15, 78, 66, 156, 8, 107, 186, 242, 75, 95, 25, 20, 84, 194, 76, 28, 92, 113, 254, 162, 161, 27, 3, 216, 100, 17, 38, 109, 14, 24, 203, 112, 235, 81, 98, 104, 45, 67, 89, 1, 0, 119, 88, 170, 189, 230, 49, 238, 135, 223, 26, 47, 85, 182, 240, 106, 80, 50, 101, 6, 180, 118, 217, 34, 108, 190, 232, 165, 154, 111, 72, 87, 201, 4, 249, 205, 212, 219, 33, 233, 43, 174, 222, 124, 123, 69, 244, 192, 9, 121, 147, 250, 229, 77, 163, 206, 191, 179, 125, 86, 59, 148, 176, 134, 97, 158, 227, 252, 208, 138, 42, 102, 128, 183, 218, 247, 22, 199, 56, 167, 195, 211, 209, 94, 71, 141, 168, 16, 65, 220, 96, 136, 120, 61, 237, 188, 62, 241, 173, 175, 114, 23, 2, 126, 215, 185, 153, 40, 116, 204, 178, 231, 210, 19]

分析完 sub_401291 接着向下分析

避免上翻,再贴一遍:

//buf 从文件中读出来的字符
//index 当前读取文件的对应偏移(下标)
__int64 __fastcall sub_4012D8(char buf, __int64 index)
{
__int64 ret; // rax
//已经分析完的函数,传入参数 buf 返回 unk_404288[buf]
ret = sub_401291(buf);
//把前面返回的结果和另一个函数得到的结果做 xor 运算返回
//这个函数传入当前下标,返回一个结果
return ret ^ sub_401240(index);
}

于是接下来分析 sub_401240 这个函数

__int64 __fastcall sub_401240(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5)
{
__int128 v6; // rax

v6 = 0LL;
BYTE8(v6) = 1;
return off_4043C8(a1, a2, *((_QWORD *)&v6 + 1), a4, a5);
}

看起来很前面分析完的函数有点类似,都是某个地址里存储着函数的地址,然后一个个跳转到对应的函数


如法炮制,先看前半部分:

sub_401240 proc near
# 直接跳转
jmp short loc_40126F

loc_40126F:
# rax = 0,同时 ZF = 1
xor rax, rax
# 将 64 位的有符号整数从 RAX 寄存器扩展到 128 位
# 并存储在 RDX 和 RAX 寄存器中,使 RDX 包含高 64 位,RAX 包含低 64 位
#这样,RDX:RAX 寄存器对可以用于进行 128 位整数运算
cqo
#将CF(Carry Flag,进位标志位)设置为 1
#这个指令的作用是将进位标志位设置为指定的值,通常用于进行二进制加法中的进位操作
stc
#如果 ZF 标志位为 1,则将 DL 寄存器设置为 1 ;否则设置为0
#结合前面 ZF = 1 的操作,这里为 DL = 1
setz dl
# rax = rax - 8 结合前面的 xor eax,eax 运算结果为 -8
sub rax, 8
# 直接跳转
jmp short loc_40125D

loc_40125D:
# rax = rax + 8 ,第一次运行时为 -8 + 8 = 0
add rax, 8
# 跳转到 4043C8 这个数组里偏移为 rax 的数据对应的地址
# 根据上面的图,可以看出来一共有 4 个地址可跳转(分出来了 4 个分支)
jmp off_4043C8[rax]

看一下 off_4043C8 里存储的内容:

得到:

.data:00000000004043C8 off_4043C8      dq offset loc_401283    ; DATA XREF: sub_401240+21↑r
.data:00000000004043D0 dq offset loc_401247
.data:00000000004043D8 dq offset loc_401255
.data:00000000004043E0 dq offset loc_40126C
.data:00000000004043E0 _data ends

结合下半部分的逻辑

只有 loc_40126C 的逻辑有 retn,其他部分都是跳转回去继续执行

可以推断出这部分的逻辑是按顺序执行 off_4043C8 这个数组里的存储的函数地址,直到执行完 loc_40126C retn 返回

按数组里的顺序依次分析对应的函数:

.data:00000000004043C8 off_4043C8      dq offset loc_401283    ; DATA XREF: sub_401240+21↑r
.data:00000000004043D0 dq offset loc_401247
.data:00000000004043D8 dq offset loc_401255
.data:00000000004043E0 dq offset loc_40126C
.data:00000000004043E0 _data ends

第一个函数:loc_401283

loc_401283:
# rdi = rdi + rcx
add rdi, rcx
# rcx = rcx - rdi = rcx - (原 rdi + rcx) = - 原 rdi
sub rcx, rdi
# 取反 rcx
#再结合前面的 rcx = - 原 rdi , 取反后就变成 rcx = 原 rdi
neg rcx
# rcx = rcx +1
# 结合前面得到 rcx = 原 rdi + 1
inc rcx
# 跳回去
jmp short loc_40125D
sub_401240 endp

这里可以下断点跟踪一下 rdi 表示的是什么,结合这个函数传入的参数,不难推断出 rdi = index 就是当前下标

这里就是将 rcx = 原 rdi + 1 = index + 1


第二个函数:loc_401247

.text:0000000000401247 loc_401247:
# r8 = 0
.text:0000000000401247 xor r8, r8
# 跳回去
.text:000000000040124A jmp short loc_40125D

第三个函数:loc_401255

.text:0000000000401255
.text:0000000000401255 loc_401255:
# rdx = r8 r8 = 原 rdx + r8
.text:0000000000401255 xadd r8, rdx
# 看 rcx 是否为 0 ,不为 0 则循环, 每次循环 rcx = rcx - 1
# 结合前面的 rcx = index ,这里就是以 index 为次数循环进行 xadd 操作
.text:0000000000401259 loop loc_401255
# 跳回去
.text:000000000040125B jmp short $+2

第四个函数:loc_40126C

.text:000000000040126C loc_40126C:
# 交换 rax 和 r8 的值
# 后面没有再用到 r8,而 rax 是作为返回值返回
# 所以可以看作是 rax = r8
.text:000000000040126C xchg rax, r8
# 返回
.text:000000000040126E retn

结合这前面几个函数的分析,可以推断出这个函数的伪代码:

int sub_401240(int index){
int rdx = 1;
int r8 = 0;
for(int i = 0;i < index + 1;i++){
int temp = rdx
rdx = r8
r8 = temp + r8
}
return r8;
}

尝试还原

至此,所有的算法都已经开朗,总结一下:

key = byte_404060[index];
flag |= (key ^ sub_4012D8((unsigned int)buf, index++));

int sub_4012D8(char buf,int index){
int ret = sub_401291(buf);
return ret ^ sub_401240(index);
}

int sub_401291(char buf){
return unk_404288[buf];
}

int sub_401240(int index){
int rdx = 1;
int r8 = 0;
for(int i = 0;i < index + 1;i++){
int temp = rdx
rdx = r8
r8 = temp + r8
}
return r8;
}

有了算法就可以逆运算尝试还原了

已知 flag == 0

则 key ^ sub_4012D8((unsigned int)buf, index++) == 0

任何数异或本身才为 0,得到:key  == sub_4012D8((unsigned int)buf, index++)

key = byte_404060[index]  可以直接从数组里取,是已知的

因此 sub_4012D8((unsigned int)buf, index++)  == key 也已知了


sub_4012D8((unsigned int)buf, index++) 等价于  ret ^ sub_401240(index)

sub_401240 算法是已知的,所以可以求出 ret

ret = sub_4012D8((unsigned int)buf, index++)  ^ sub_401240(index)


ret 又等于 sub_401291(buf) ,sub_401291算法是已知的,所以可以求出  buf


用 python 编写以下代码:

byte_404060 = [0x7b, 0xed, 0x51, 0x57, 0xfd, 0x11, 0x5e, 0x41, 0x6e, 0xab, 0xa2, 0x0c, 0xf0, 0x2a, 0x29, 0x97, 0xd9, 0x67, 0x2a, 0x24, 0x9d, 0x64, 0xbf, 0x74, 0x42, 0x7d, 0x80, 0x8b, 0xea, 0x63, 0x25, 0x4b, 0x0e, 0xab, 0x85, 0x2c, 0x32, 0x67, 0x5a, 0x87, 0x2f, 0xa4, 0x67, 0x25, 0x8d, 0x0c, 0xb5, 0xa4, 0xd9, 0xee, 0xce, 0xbe, 0xa7, 0xb0, 0xf9, 0x19, 0xf1, 0x2d, 0x83, 0x72, 0xf1, 0x1b, 0xb1, 0xf7, 0x01, 0x9a, 0xfa, 0xef, 0xde, 0xc4, 0x9f, 0x98, 0x7d, 0xce, 0x3a, 0x91, 0xf9, 0x84, 0x85, 0xfc, 0x8e, 0x18, 0xb0, 0x4a, 0x75, 0x71, 0x6c, 0x47, 0x81, 0x34, 0xd9, 0xf6, 0x4c, 0x47, 0x8f, 0xdf, 0x1e, 0x37, 0xfa, 0x8f, 0x29, 0x73, 0x0f, 0x9e, 0xbe, 0x0c, 0x4a, 0xaa, 0xd1, 0xfb, 0x09, 0x07, 0x47, 0x22, 0x57, 0x1a, 0xbd, 0x90, 0xbc, 0xe2, 0xcd, 0x33, 0xba, 0xc8, 0x37, 0xfb, 0xa7, 0x7f, 0x0e, 0xec, 0xc7, 0xdc, 0x41, 0x98, 0xf1, 0x49, 0xd5, 0x54, 0xb6, 0x5f, 0x20, 0xfb, 0x59, 0xb1, 0x32, 0xe3, 0xc9, 0xfe, 0x69, 0x30, 0x71, 0xf9, 0xb0, 0xaf, 0xc6, 0x4c, 0x05, 0x61, 0x40, 0x24, 0x41, 0x20, 0xf7, 0x41, 0xde, 0xf5, 0x2b, 0x18, 0x83, 0x02, 0x89, 0x40, 0x9b, 0x04, 0x4b, 0x5d, 0x2e, 0x58, 0x91, 0xca, 0x35, 0x1a, 0x76, 0x20, 0x75, 0xa7, 0xce, 0x91, 0xfa, 0x34, 0x6d, 0x71, 0x79, 0xcd, 0x40, 0x1f, 0xce, 0x46, 0x75, 0xca, 0x76, 0x4f, 0x95, 0xe1, 0x36, 0x1d, 0x9a, 0x17, 0xff, 0x84, 0x17, 0x15, 0x5e, 0x6d, 0x89, 0x6c, 0x33, 0xa8, 0xde, 0x08, 0x66, 0x92, 0xe7, 0x27, 0x1a, 0x95, 0xeb, 0x48, 0xb7, 0xf6, 0xf1, 0xf1, 0x15, 0x37, 0x71, 0x02, 0x70, 0x27, 0x8d, 0x1c, 0x4d, 0xb5, 0x20, 0x9b, 0x1e, 0x0a, 0x7e, 0xcf, 0x18, 0xfb, 0xf2, 0x9e, 0x65, 0xa1, 0x6d, 0xc3, 0x81, 0xaa, 0x6c, 0x77, 0xae, 0xfd, 0xbd, 0x2b, 0xfd, 0xe9, 0xcd, 0x8c, 0xc1, 0x90, 0xb4, 0x68, 0xe9, 0x3f, 0xd2, 0xaf, 0x52, 0x45, 0xce, 0xe9, 0x01, 0xba, 0x21, 0xc5, 0x4e, 0x7d, 0xad, 0xd4, 0x2d, 0xb9, 0x9e, 0x81, 0xbd, 0xc3, 0x92, 0xad, 0x3b, 0x28, 0x05, 0x5b, 0xe5, 0x41, 0xfe, 0x50, 0x85, 0x04, 0xe7, 0xb4, 0x78, 0x83, 0xc3, 0x4c, 0x9a, 0x3d, 0xde, 0xf8, 0xbb, 0x50, 0xce, 0xbd, 0x19, 0x73, 0x5a, 0xcb, 0x52, 0x9b, 0x4e, 0xf3, 0x31, 0xf3, 0x9d, 0xbd, 0x9e, 0x5d, 0x6d, 0x38, 0xb2, 0xea, 0x74, 0xdb, 0xf6, 0x3e, 0x9b, 0xa9, 0xe9, 0x99, 0x81, 0x49, 0x9a, 0xe2, 0x89, 0x17, 0x89, 0x1a, 0x4d, 0x37, 0xde, 0xa4, 0xfd, 0x18, 0xfc, 0x93, 0x2e, 0x61, 0xc9, 0x1e, 0x6e, 0xdd, 0x3d, 0xd3, 0x11, 0x6f, 0x03, 0x0c, 0x76, 0xb4, 0x41, 0x14, 0xfe, 0xb1, 0xe6, 0x07, 0x9d, 0x4c, 0xf9, 0xf3, 0x51, 0x68, 0xe9, 0xca, 0xf5, 0x59, 0x60, 0xdb, 0xa1, 0x6b, 0xab, 0x2a, 0xd8, 0x61, 0xd1, 0x53, 0x1b, 0x81, 0x3a, 0x6d, 0xa2, 0x74, 0xe7, 0xd5, 0xba, 0x4c, 0xa9, 0x64, 0x25, 0x4e, 0xbd, 0xab, 0x31, 0xfb, 0x95, 0xd2, 0x4e, 0x09, 0xaf, 0x6b, 0xf1, 0x41, 0x38, 0xfd, 0x19, 0x1f, 0x2e, 0x99, 0xcc, 0xbc, 0x3a, 0xbe, 0x55, 0xe1, 0xbe, 0xc4, 0x83, 0x4c, 0x45, 0x7b, 0xd5, 0xc4, 0xe2, 0x92, 0xb7, 0xb5, 0x11, 0xc4, 0x27, 0x98, 0xbe, 0x69, 0xcd, 0x0c, 0x41, 0x26, 0x22, 0xa9, 0x86, 0xbf, 0xeb, 0x1c, 0xcd, 0x65, 0xda, 0x37, 0x0f, 0x80, 0xe7, 0xe2, 0x1e, 0xeb, 0x39, 0x8f, 0xfb, 0x92, 0x4c, 0x76, 0x3d, 0x4a, 0x0f, 0x39, 0x98, 0x4d, 0xeb, 0x43, 0x65, 0xd5, 0xa0, 0x80, 0x03, 0x1a, 0x66, 0x8b, 0x80, 0xbc, 0x73, 0x06, 0xa4, 0xbb, 0xa2, 0x79, 0x68, 0x0e, 0x10, 0x9a, 0xbd, 0x01, 0xd3, 0xd0, 0xf2, 0xf4, 0x04, 0x04, 0x17, 0x1c, 0xe8, 0x3d, 0x0e, 0x56, 0x5e, 0xa3, 0x5d, 0x1b, 0x26, 0x7b, 0x5a, 0xb7, 0x3b, 0x59, 0x60, 0x1b, 0x1d, 0x2f, 0xe9, 0x75, 0x14, 0x24, 0x41, 0x8b, 0x7e, 0xe1, 0x3d ]
unk_404288 = [36, 221, 193, 137, 10, 207, 131, 226, 198, 253, 127, 70, 68, 37, 157, 46, 39, 32, 184, 225, 53, 57, 51, 177, 144, 200, 48, 169, 152, 117, 115, 234, 248, 54, 64, 35, 164, 41, 105, 93, 21, 145, 196, 11, 228, 172, 55, 149, 60, 99, 82, 133, 224, 30, 243, 255, 130, 132, 181, 187, 151, 146, 150, 18, 142, 74, 214, 73, 122, 155, 139, 52, 12, 143, 213, 239, 44, 90, 58, 202, 197, 110, 160, 103, 13, 140, 31, 245, 7, 29, 91, 79, 159, 171, 5, 166, 129, 83, 63, 251, 246, 236, 15, 78, 66, 156, 8, 107, 186, 242, 75, 95, 25, 20, 84, 194, 76, 28, 92, 113, 254, 162, 161, 27, 3, 216, 100, 17, 38, 109, 14, 24, 203, 112, 235, 81, 98, 104, 45, 67, 89, 1, 0, 119, 88, 170, 189, 230, 49, 238, 135, 223, 26, 47, 85, 182, 240, 106, 80, 50, 101, 6, 180, 118, 217, 34, 108, 190, 232, 165, 154, 111, 72, 87, 201, 4, 249, 205, 212, 219, 33, 233, 43, 174, 222, 124, 123, 69, 244, 192, 9, 121, 147, 250, 229, 77, 163, 206, 191, 179, 125, 86, 59, 148, 176, 134, 97, 158, 227, 252, 208, 138, 42, 102, 128, 183, 218, 247, 22, 199, 56, 167, 195, 211, 209, 94, 71, 141, 168, 16, 65, 220, 96, 136, 120, 61, 237, 188, 62, 241, 173, 175, 114, 23, 2, 126, 215, 185, 153, 40, 116, 204, 178, 231, 210, 19]

def sub_401291(buf):
return unk_404288[buf]

def sub_401240(index):
rdx = 1
r8 = 0
for num in range(0,index+1):
temp = rdx
rdx = r8
r8 = temp + r8
return r8;

str = ""
# 和 main 函数一样,遍历下标
for index in range(0,0x224):
# 先求出 key
key = byte_404060[index]
# 再求出 ret
r8 = sub_401240(index)
ret = (r8 % 256) ^ (key % 256)
# 遍历 unk_404288 找到值为 ret 的下标,其下标就是 buf
for index,element in enumerate(unk_404288):
if(element == ret):
str = str + chr(index)
print(str)

最后输出结果:

Dear participant,

Congrats on getting this far, It's really satisfying to see people getting good technical skills.

This challenge was easy, wasn't it? but you still managed to learn one new thing or two, this is typically my goal from every challenge I implement for CTF contests, there is no point in implementing a crackme in Rust and adding packers like Themida or VMProtect, it makes the challenge hard, but not necessarily of good quality.

Oh and I forgot, here is your flag: shellmates{x86_has_WA4AY_Too_M4nY_IN$TRUC710N$}

Best,
Redouane

总结

题目的核心在于 jmp [rax],即把要执行的函数存在一个数组里,然后按顺序依次执行,这导致了 IDA Pro 无法正确生成伪代码,需要动态分析

CTF 的源代码附下方链接中。


下方隐藏内容为本帖所有文件或源码下载链接:

游客你好,如果您要查看本帖隐藏链接需要登录才能查看, 请先登录

返回顶部