吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3660|回复: 12
收起左侧

[CTF] HackINI-2k21-CTF-challenges decode_me

[复制链接]
lyl610abc 发表于 2023-12-23 15:46

题目

题目名称:decode_me

附件:https://610-pic-bed.oss-cn-shenzhen.aliyuncs.com/program.zip

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


文件格式判断

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

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


IDA 解析

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

image-20231222201914081


自动定位到了 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;
}

简单分析可以得出逻辑:

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

按字节读取文件内容,然后计算 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 双击查看:

image-20231222210825599

直接把这里的数组先拷出来,拷贝前面 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 界面查看汇编

image-20231222212619577


先看前半部分:

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 里存储的内容:

image-20231223104256996

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

image-20231223104430189

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

结合下半部分的逻辑

image-20231223104934001


只有 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 的 环境:

image-20231223110606790

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


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

然后赋予权限:

chmod +x program

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

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

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

image-20231223111513138


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

image-20231223111942197


打开得到:

image-20231223112030227

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

chmod +x linux_server64
./linux_server64

image-20231223112333750


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

systemctl stop firewalld.service

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

image-20231223113458048


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

image-20231223113612714


image-20231223114220931


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

这里选 main 函数的开头

image-20231223113736542


然后按快捷键 F9 运行

image-20231223113825376

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


动态调试

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

image-20231223104934001


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

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   下个断点,然后断下来后查看对应值

image-20231223114620062

可以看到 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 了

image-20231223115945914


第四、五个函数: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 执行前

image-20231223120958802


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

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

image-20231223121039722


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

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

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

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

image-20231223121221581

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

404288 + 6c = 4042F4

image-20231223122025712

对应的值为 BA


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

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

image-20231223121406950


后面的 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

image-20231223131341127

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


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

可以写出对应的伪代码:

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

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

image-20231223131554826


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);
}

会发现一个很奇怪的点,生成的伪代码有 5 个参数,但是前面调用的时候却只传了一个参数 index,推测是伪代码生成有问题

接着看汇编:

image-20231223133413531

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


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

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 里存储的内容:

image-20231223134700417


得到:

.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

结合下半部分的逻辑

image-20231223134936203


只有 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

image-20231223152757397


总结

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

CTF 的源代码在:https://github.com/Shellmates/HackINI-2k21-CTF-challenges/tree/main/reverse/decode_me/solution

免费评分

参与人数 8吾爱币 +10 热心值 +7 收起 理由
笙若 + 1 + 1 谢谢@Thanks!
debug_cat + 2 + 1 用心讨论,共获提升!
fengbu401 + 1 我很赞同!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
G690 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
sjclch + 1 + 1 我很赞同!
BonnieRan + 1 + 1 谢谢@Thanks!
Sayon + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

cn2jp 发表于 2023-12-23 18:12
非常敬佩楼主写出这么详细的流程,我就没有这个耐心,自愧不如。
Goven 发表于 2023-12-23 18:58
BonnieRan 发表于 2023-12-23 19:00
好详细的分析过程,楼主太强了,这不来个精华贴~
ALchenxin 发表于 2023-12-24 12:22
真的非常nb,能写一个简单的总结就更好了
月清晖 发表于 2023-12-25 10:28
jmp [rax] 不就是现在的控制流混淆的套路么
Hmily 发表于 2024-1-1 01:43
写的非常详细,动静结合,赞&#128077;。
solly 发表于 2024-1-1 11:43
这个 add rax, 8 / jmp [rax],可以理解为没有 break 的 switch/case语法。
pinla0 发表于 2024-1-1 13:48
楼主很强悍很强大,收益了
w8588997 发表于 2024-1-4 17:51
点赞支持大佬
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-21 20:11

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表