不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

本文主要是介绍不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在上一节中,研究人员已经研究了Emotet通过运行Microsoft Word文档中混淆VBA宏来删除有效载荷的能力。

本文中,研究人员会举一个Emotet示例的二进制进行分析,探索恶意软件是如何工作的。其中研究人员演示了如何对主要有效载荷进行解压缩,并在调试器中解压缩后遵循其执行进程。 Emotet的执行包括多个阶段的混淆可执行文档,这些可执行文档已加载到内存中,并且在不同的内存区域都具有复杂的代码流。这些设计使分析人员,静态分析引擎和反汇编程序更难以分析木马。

另外,研究人员还描述了如何在Emotet的解压代码中检查注册表项的存在。如果无法访问注册表项,则有效载荷不会解压并运行,研究人员发现阻止对密钥的读取访问是防止Emotet有效载荷运行的有效方法。其中研究人员还会讨论Emotet如何充当其他恶意软件家族传播器,以及这如何表明其运营商已采用了“恶意软件即服务”业务模型。在上一篇文章中,研究人员对名为15.exe的Emotet可执行文档的分析并最终删除了另一个银行木马Trickbot。在安装了Bromium的计算机上,Emotet可以在被隔离的微型虚拟机中运行,而不会损害系统的完整性。由于恶意软件是动态运行的,因此它会产生危害指标(IOC),安全团队可以使用这些指标来防御网络的相同攻击。

EMOTET的封装

封装程序的主要目的是压缩和加密便携式可执行(PE)文档。奇热加密的有效载荷在运行时被解压缩,然后解压缩代码将执行传递给新解压缩的代码。对于恶意软件的开发者来说,封装程序通过使对二进制文档的静态分析更加困难来帮助逃避检测。 Emotet的封装程序是变化的,这使得基于签名的检测工具根据封装程序的足迹对样本进行分析变得更加棘手。

文档名:15.exe

大小:428808字节

MD5:322F9CA84DFA866CB719B7AECC249905

SHA1:147DDEB14BFCC1FF2EE7EF6470CA9A720E61AEAA

SHA256:AF2F82ADF716209CD5BA1C98D0DCD2D9A171BB0963648BD8BD962EDB52761241

该PE文档,其资源(.rsrc)部分占文档总大小的很大一部分(51%),这表明该恶意软件可能已经被封装。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

资源部分消耗了二进制文档大小的一半以上

查看资源部分会发现两个异常资源,调用EXCEPT和CALIBRATE。 EXCEPT的文件量表明这可能是加密的有效载荷。转储资源将确认它包含加密的数据。在一些示例中,研究人员发现.data部分中的解密PE文档被删除了。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

异常资源调用EXCEPT和CALIBRATE

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

EXCEPT中的加密数据

Emotet是模块化的,并使用诸如沙箱检测之类的反分析技术来逃避分析。一个解压后的Emotet二进制文档包含数百个函数,当在诸如Ghidra的反汇编器中打开可疑的封装样品时,只有少数函数可以识别。以下是二进制文档已封装的另一个迹象。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

Ghidra在封装的Emotet示例中识别的函数列表

注册表检查

在分析封装程序代码期间,研究人员注意到一个函数,该函数生成一个char数组并具有条件while(true)无限循环。这一发现使研究人员感到好奇,是否可以触发无限循环来停止执行解压缩代码,从而阻止主要Emotet有效载荷运行。该函数通过调用RegOpenKeyA读取Windows注册表项来工作。如果找不到密钥,则恶意软件会进入无限循环(图5)。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

检查注册表中是否存在 “interface\{aa5b6a80-b834-11d0-932f-00a0c90dcaa9}”函数

函数FUN_00401a90解码值为 “interface\{aa5b6a80-b834-11d0-932f-00a0c90dcaa9}”的字符串,该字符串作为参数传递给RegOpenKeyA。要使Windows脚本引擎接口IActiveScriptParseProcedure32起作用,此注册表项是必需的。具体地说,接口解析给定的代码进程并将该进程添加到名称空间。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

RegOpenKeyA参数

研究人员回顾了Emotet的其他示例以获取类似函数,有趣的是,在执行时,所有示例要么退出主线程,要么在缺少注册表项的情况下进入无限循环。

文档名:891.exe

MD5:BD3B9E60EA96C2A0F7838E1362BBF266

SHA1:62C1BEFA98D925C7D65F8DC89504B7FBB82A6FE3

SHA256:28E3736F37222E7FBC4CDE3E0CC31F88E3BFC16CC5C889B326A2F74F46E415AC

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

在没有注册表项的情况下主线程陷入无限循环

文档名:448.exe

MD5:193643AB7C0B289F5DE3963E4ADC1563

SHA1:B14290BFAE015D37EBA7EDD8F5067AD5E238CC68

SHA256:FD9E5C47F9AEB47F5E720D42DD4B8AD231EE3BA5270E3FBD126FC8C6F399D243

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

在没有注册表项的情况下退出主线程

使用调试器进行二进制分析

为了证实研究人员对15.exe是一个压缩的Emotet二进制文件的怀疑,他们在x64dbg中将其打开并检查其映射的内存区域。在15.exe的可选标头中,禁用了地址空间布局随机化(ASLR),这意味着,如果可能,该模块将以其首选的基地址0x00400000加载到内存中。

第一阶段

在15.exe中导入的函数之一是VirtualAllocEx,此函数用于在远程进程中分配内存,恶意软件通常将其用于进程注入。研究人员将从在VirtualAllocEx的返回地址上放置一个断点。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

15.exe的内存映射部分

如果运行到断点,研究人员将会看到Emotet在0x00220000处创建了内存分配。然后,它将代码存根从映射映像的.data部分中的0x00422200(文档偏移为0x0001FE00)复制到新分配的存储空间,并对其进行控制。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

0x00220000处的内存分配

然后,Emotet对复制到0x00220000的代码中的API和DLL名称进行混淆处理。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

对LoadLibraryExA和kernel32.dll进行混淆处理

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

对VirtualAlloc进行混淆处理

然后,它从kernel32.dll调用GetProcAddress以获取已解码API名称的地址。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

从代码存根0x00220000调用GetProcAddress API,从内核32.dll的映射映像中检索导出API的地址

首先,以这种方式检索LoadLibraryExA的地址。然后,它使用该地址将kernel32.dll加载到地址空间为0x766D0000的位置。然后,它使用加载的模块kernel32.dll的句柄在下面的函数列表中调用GetProcAddress:

LoadLibraryExA
GetProcAddress
VirtualAlloc
SetFilePointer
LstrlenA
LstrcatA
VirtualProtect
UnmapViewOfFile
GetModuleHandleA
WriteFile
CloseHandle
VirtualFree
GetTempPathA
CreateFileA

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

调用GetProcAddress以获取LoadLibraryExA的地址

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

调用LoadLibraryExA将kernel32.dll加载到内存中

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

去混淆后的API名称

要注意的有趣事情之一是Emotet调用了名为 “mknjht34tfserdgfwGetProcAddress”的无效API的GetProcAddress。由于这是无效的,因此该函数返回一个空值,错误代码为0000007F(ERROR_PROC_NOT_FOUND)。在所有Emotet示例中,研究人员检查了对该无效函数名称的调用GetProcAddress。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

为无效的API调用GetProcAddress

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

调用GetProcAddress以获取GetProcAddress的地址

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

堆栈中保存的API的函数地址

一旦代码存根检索到函数地址,就会调用VirtualAlloc分配另一个内存区域,在该区域中,它将从15.exe的.data部分(而不是.rsrc部分)写入解密的PE文档。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

地址0x00240000处的内存分配

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

存根将PE文档写入地址0x00240000

从0x00240000转储的Emotet二进制文档:

文档名:emotet_dumped_240000.exe

MD5:D623BD93618B6BCA25AB259DE21E8E12

SHA1:BBE1BFC57E8279ADDF2183F8E29B90CFA6DD88B4

SHA256:01F86613FD39E5A3EDCF49B101154020A7A3382758F36D875B12A94294FBF0EA

转储可执行文档并进行检查后,发现它是另一个包含主要有效载荷的压缩Emotet二进制文档。研究人员已经在一些Emotet示例中看到,第一个映射的解密可执行文档从内存中转储后无法直接运行,但是该示例可以运行。

Pestudio识别了有关此文档的几个可疑特征,包括没有导入的文档,检测到封装程序签名“Stranik 1.3 Modula/C/Pascal”,并且该文件可能包含另一个文件。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

由pestudio识别的有关emotet_dumped_240000.exe的可疑攻击标识

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

emotet_dumped_240000.exe的Bromium控制器进程交互图,它会自行启动并创建一个称为“ipropmini”的服务,该服务与15.exe所示的行为非常匹配

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

为emotet_dumped_240000.exe检测到的高严重性事件的Bromium控制器视图

第二阶段

在写入和解密0x00240000处的可执行文档后,代码存根使用VirtualAllocEx在地址0x00260000处分配另一个内存区域。分配内存后,它将从内存区域0x00240000读取有效载荷,并将其写入0x00260000。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

调用VirtualAllocEx在0x00260000处分配内存

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

存根在0x00260000处写入主要Emotet有效载荷

在0x00260000处写入主要Emotet有效载荷后,代码存根将挂钩和JMP指令插入代码中(图28)。 Emotet这样做是为了使代码分析更加困难,并使反汇编程序代码更混乱。

挂钩放置完成后,有效载荷将取决于要运行的另一个内存区域,这意味着即使在修复了PE文件部分的对齐和原始偏移之后,也不允许将其转储到磁盘。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

从虚拟地址0x00260000转储的PE文档的执行错误

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

代码存根对位于0x00260000的可执行文档的修改

第三阶段

在0x00260000处修改了有效载荷并准备就绪后,存根将调用UnmapViewOfFile以从0x00400000(这是第一个Emotet映像加载到的内存区域)中取消映射15.exe。然后,它在0x00400000处分配一个新的内存区域,该区域与有效载荷在0x00260000(15000字节)处的大小相同。分配新的内存区域后,它将修改后的有效内容复制到0x00400000。这是一种进程注入技术,其中恶意软件修改其内存中的二进制文档,然后进行自我覆盖。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

映像15.exe未映射后的内存映射视图

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

新分配的内存为0x00400000

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

分配到0x00400000后的内存视图

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

将载荷从0x00260000复制到0x00400000

第四阶段

将有效载荷复制到0x00400000后,Emotet解析API名称,然后将执行流转移到有效载荷。在本例中,它会转移到0x0040C730,然后调用一个函数来解析与API名称相对应的哈希。

Emotet的主要有效载荷使分析人员难以遵循代码流,因为了解恶意软件函数的字符串可能被混淆了。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

将API哈希表传递给反混淆函数以进行名称解析

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

解析来自ntdll.dll和kernel32.dll的API名称

在API名称解析之后,将调用GetCurrentProcessId以获取Emotet正在运行的进程的进程ID(PID)。然后,Emotet遍历所有正在运行的进程以查找其模块名称和父PID。一旦找到其父PID,它就创建两个PEM%X格式的互斥锁。其中一个互斥锁使用父进程ID(PEM [PPID])创建,另一个使用其自己的PID(PEM [PID])。

创建这些互斥锁后,它将调用CreateEventW以使用PEE%X格式创建事件,其中%X是其父PID。如果两个互斥锁都成功创建,它将从同一路径再次启动15.exe。启动子进程后,它将在PEE%X事件上调用WaitForSingleObject。

研究人员在一些Emotet示例中已经看到,它通过命令行开关启动子进程。此命令行开关表示Emotet进程已作为子进程启动,并且必须执行指定的任务。

启动的子进程执行所有相同的操作,直到它评估是否创建上述两个互斥锁为止。这次对互斥锁PEM [PPID]的CreateMutex调用失败,并显示错误“ERROR_ALREADY_EXISTS”。子进程中互斥锁创建失败后,它将信号PEE [PPID]发送给父进程15.exe。父进程从等待状态退出,然后终止。

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

控制流程图,显示了基于对CreateMutex和CreateEvent的调用启动子进程的决策

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

Emotet子进程15.exe(1352或0x548)和父PID(3520或0xDC0)的PID

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

对互斥锁名称PEMDC0的CreateMutex调用,其中0xDC0是父PID

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

对互斥锁名称PEM548的CreateMutex调用,其中0x548是Emotet进程15.exe的PID

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

对事件对象名称PEE548的CreateEventW调用,其中0x548是Emotet进程15.exe的PID

然后,启动的子进程将创建一个名为“ipropmini”的服务,并建立命令和控制(C2)通信。

总结

不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)

解析载荷步骤的过程

1.删除的Emotet二进制文档(15.exe)分配具有执行许可权的新内存区域,并在其中写入代码存根(图41,内存区域1)。

2.存根从映像的.data部分解密嵌入的PE文档,并将其写入新的内存区域(图41,内存区域2)。

3.写入内存区域2的文档是有效的PE文档,它是另一个Emotet二进制文档,可以转储并执行而无需修复其重定位。

4.内存区域1中的存根分配了一个具有执行许可的新区域(图31,内存区域3)。

5.存根从内存区域2读取嵌入的有效载荷,并将其写入内存区域3。将有效载荷写入存储区3后,然后通过插入新代码和蹦床对其进行修改。

6.一旦有效载荷在内存区域3中准备就绪,它将取消对15.exe映像的映射。在取消映像映射之后,它使用执行权限分配一个与内存区域3大小相同的新区域,并将7.有效载荷从内存区域3复制到新分配的区域(图41,内存区域4)。

8.然后,存根将执行传递到内存区域4,该区域将启动主Emotet载荷。

9.综上所述,Emotet使用分层方法来解压缩其主要有效载荷。在修改其映射映像后,它执行自注入技术以在内存中执行有效载荷。由于有效载荷永远不会写入文档系统,并且包含挂钩和JMP指令,因此可以轻松地基于动态代码分析来逃避防病毒和检测。

IOC

可以通过阻止对以下注册表项的读取访问来阻止Emotet的运行。由于下载程序将进入无限循环或结束进程,因此它不会损害系统的完整性,因为条件分支会发生在有效载荷执行之前。

32位系统:

HKEY_CLASSES_ROOT\Interface\{AA5B6A80-B834-11D0-932F-00A0C90DCAA9}

64位系统:

HKEY_CLASSES_ROOT\Wow6432Node\Interface\{AA5B6A80-B834-11D0-932F-00A0C90DCAA9}可以使用以下方式检测到Emotet下载程序:

1.从全局可写目录(例如%USERPROFILE%和%TEMP%)启动的进程监控对注册表项“Interface\{aa5b6a80 – b894 – 11d0 – 932f – 00a0c90dca9}”的读取访问。

2.监控对GetProcAddress的函数名称为“mknjht34tfserdgfwGetProcAddress”的API调用;

3.监控对GetProcAddress的API调用的顺序可以用作启发式算法;

这篇关于不在沉默中爆发就在沉默中死亡,处于沉寂状态的 Emotet 僵尸网络是怎样卷土重来的?(二)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/779015

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

hdu1565(状态压缩)

本人第一道ac的状态压缩dp,这题的数据非常水,很容易过 题意:在n*n的矩阵中选数字使得不存在任意两个数字相邻,求最大值 解题思路: 一、因为在1<<20中有很多状态是无效的,所以第一步是选择有效状态,存到cnt[]数组中 二、dp[i][j]表示到第i行的状态cnt[j]所能得到的最大值,状态转移方程dp[i][j] = max(dp[i][j],dp[i-1][k]) ,其中k满足c

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

ASIO网络调试助手之一:简介

多年前,写过几篇《Boost.Asio C++网络编程》的学习文章,一直没机会实践。最近项目中用到了Asio,于是抽空写了个网络调试助手。 开发环境: Win10 Qt5.12.6 + Asio(standalone) + spdlog 支持协议: UDP + TCP Client + TCP Server 独立的Asio(http://www.think-async.com)只包含了头文件,不依

poj 3181 网络流,建图。

题意: 农夫约翰为他的牛准备了F种食物和D种饮料。 每头牛都有各自喜欢的食物和饮料,而每种食物和饮料都只能分配给一头牛。 问最多能有多少头牛可以同时得到喜欢的食物和饮料。 解析: 由于要同时得到喜欢的食物和饮料,所以网络流建图的时候要把牛拆点了。 如下建图: s -> 食物 -> 牛1 -> 牛2 -> 饮料 -> t 所以分配一下点: s  =  0, 牛1= 1~

poj 3068 有流量限制的最小费用网络流

题意: m条有向边连接了n个仓库,每条边都有一定费用。 将两种危险品从0运到n-1,除了起点和终点外,危险品不能放在一起,也不能走相同的路径。 求最小的费用是多少。 解析: 抽象出一个源点s一个汇点t,源点与0相连,费用为0,容量为2。 汇点与n - 1相连,费用为0,容量为2。 每条边之间也相连,费用为每条边的费用,容量为1。 建图完毕之后,求一条流量为2的最小费用流就行了

poj 2112 网络流+二分

题意: k台挤奶机,c头牛,每台挤奶机可以挤m头牛。 现在给出每只牛到挤奶机的距离矩阵,求最小化牛的最大路程。 解析: 最大值最小化,最小值最大化,用二分来做。 先求出两点之间的最短距离。 然后二分匹配牛到挤奶机的最大路程,匹配中的判断是在这个最大路程下,是否牛的数量达到c只。 如何求牛的数量呢,用网络流来做。 从源点到牛引一条容量为1的边,然后挤奶机到汇点引一条容量为m的边

状态dp总结

zoj 3631  N 个数中选若干数和(只能选一次)<=M 的最大值 const int Max_N = 38 ;int a[1<<16] , b[1<<16] , x[Max_N] , e[Max_N] ;void GetNum(int g[] , int n , int s[] , int &m){ int i , j , t ;m = 0 ;for(i = 0 ;

hdu3006状态dp

给你n个集合。集合中均为数字且数字的范围在[1,m]内。m<=14。现在问用这些集合能组成多少个集合自己本身也算。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.Inp

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动