Nuclear shell sa ibabaw ng ICMP

Nuclear shell sa ibabaw ng ICMP

Tl; DR: Nagsusulat ako ng kernel module na magbabasa ng mga command mula sa ICMP payload at i-execute ang mga ito sa server kahit na nag-crash ang iyong SSH. Para sa pinaka naiinip, lahat ng code ay github.

Mag-ingat! Ang mga karanasang C programmer ay nanganganib na lumuha ng dugo! Maaaring mali pa nga ako sa terminolohiya, ngunit anumang pagpuna ay malugod na tinatanggap. Ang post ay inilaan para sa mga may isang napaka-magaspang na ideya ng C programming at gustong tingnan ang mga insides ng Linux.

Sa mga komento sa aking una Artikulo binanggit ang SoftEther VPN, na maaaring gayahin ang ilang "regular" na protocol, sa partikular na HTTPS, ICMP at kahit DNS. Maiisip ko lang ang una sa kanila ang gumagana, dahil pamilyar na pamilyar ako sa HTTP(S), at kinailangan kong matuto ng tunneling sa ICMP at DNS.

Nuclear shell sa ibabaw ng ICMP

Oo, noong 2020 nalaman ko na maaari kang magpasok ng arbitrary na payload sa mga ICMP packet. Ngunit mas mahusay na huli kaysa hindi kailanman! At dahil may magagawa tungkol dito, kailangan itong gawin. Dahil sa aking pang-araw-araw na buhay madalas kong ginagamit ang command line, kabilang ang sa pamamagitan ng SSH, ang ideya ng isang ICMP shell ay unang pumasok sa isip ko. At upang makabuo ng isang kumpletong bullshield bingo, nagpasya akong isulat ito bilang isang module ng Linux sa isang wika na mayroon lang akong magaspang na ideya. Ang ganitong shell ay hindi makikita sa listahan ng mga proseso, maaari mong i-load ito sa kernel at hindi ito makikita sa file system, hindi ka makakakita ng anumang kahina-hinala sa listahan ng mga port ng pakikinig. Sa mga tuntunin ng mga kakayahan nito, ito ay isang ganap na rootkit, ngunit umaasa akong pagbutihin ito at gamitin ito bilang isang shell ng huling paraan kapag ang Load Average ay masyadong mataas upang mag-log in sa pamamagitan ng SSH at magsagawa ng hindi bababa sa echo i > /proc/sysrq-triggerupang ibalik ang access nang hindi nagre-reboot.

Kumuha kami ng text editor, mga pangunahing kasanayan sa programming sa Python at C, Google at virtual na hindi mo iniisip na ilagay sa ilalim ng kutsilyo kung masira ang lahat (opsyonal - lokal na VirtualBox/KVM/etc) at tayo na!

panig ng kliyente

Para sa akin, para sa bahagi ng kliyente kailangan kong magsulat ng isang script na may mga 80 linya, ngunit may mga mababait na tao na gumawa nito para sa akin lahat ng gawain. Ang code ay naging hindi inaasahang simple, umaangkop sa 10 makabuluhang linya:

import sys
from scapy.all import sr1, IP, ICMP

if len(sys.argv) < 3:
    print('Usage: {} IP "command"'.format(sys.argv[0]))
    exit(0)

p = sr1(IP(dst=sys.argv[1])/ICMP()/"run:{}".format(sys.argv[2]))
if p:
    p.show()

Ang script ay tumatagal ng dalawang argumento, isang address at isang payload. Bago ipadala, ang payload ay nauunahan ng isang susi run:, kakailanganin namin ito upang ibukod ang mga pakete na may mga random na payload.

Ang kernel ay nangangailangan ng mga pribilehiyo upang gumawa ng mga pakete, kaya ang script ay kailangang patakbuhin bilang superuser. Huwag kalimutang magbigay ng mga pahintulot sa pagpapatupad at i-install ang scapy mismo. Si Debian ay may tinatawag na package python3-scapy. Ngayon ay maaari mong suriin kung paano gumagana ang lahat.

Pagpapatakbo at pag-output ng utos
morq@laptop:~/icmpshell$ sudo ./send.py 45.11.26.232 "Hello, world!"
Begin emission:
.Finished sending 1 packets.
*
Received 2 packets, got 1 answers, remaining 0 packets
###[ IP ]###
version = 4
ihl = 5
tos = 0x0
len = 45
id = 17218
flags =
frag = 0
ttl = 58
proto = icmp
chksum = 0x3403
src = 45.11.26.232
dst = 192.168.0.240
options
###[ ICMP ]###
type = echo-reply
code = 0
chksum = 0xde03
id = 0x0
seq = 0x0
###[ Raw ]###
load = 'run:Hello, world!

Ito ang hitsura sa sniffer
morq@laptop:~/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
Running as user "root" and group "root". This could be dangerous.
Capturing on 'wlp1s0'
Frame 1: 59 bytes on wire (472 bits), 59 bytes captured (472 bits) on interface wlp1s0, id 0
Internet Protocol Version 4, Src: 192.168.0.240, Dst: 45.11.26.232
Internet Control Message Protocol
Type: 8 (Echo (ping) request)
Code: 0
Checksum: 0xd603 [correct] [Checksum Status: Good] Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Sequence number (BE): 0 (0x0000)
Sequence number (LE): 0 (0x0000)
Data (17 bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Length: 17]

Frame 2: 59 bytes on wire (472 bits), 59 bytes captured (472 bits) on interface wlp1s0, id 0
Internet Protocol Version 4, Src: 45.11.26.232, Dst: 192.168.0.240
Internet Control Message Protocol
Type: 0 (Echo (ping) reply)
Code: 0
Checksum: 0xde03 [correct] [Checksum Status: Good] Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Sequence number (BE): 0 (0x0000)
Sequence number (LE): 0 (0x0000)
[Request frame: 1] [Response time: 19.094 ms] Data (17 bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Length: 17]

^C2 packets captured

Ang payload sa response package ay hindi nagbabago.

Module ng kernel

Upang bumuo sa isang Debian virtual machine kakailanganin mo ng hindi bababa sa make ΠΈ linux-headers-amd64, ang natitira ay darating sa anyo ng mga dependencies. Hindi ko ibibigay ang buong code sa artikulo; maaari mo itong i-clone sa Github.

Pag-setup ng hook

Upang magsimula sa, kailangan namin ng dalawang function upang mai-load ang module at i-unload ito. Ang function para sa alwas ay hindi kinakailangan, ngunit pagkatapos rmmod hindi ito gagana, ang module ay ilalabas lamang kapag naka-off.

#include <linux/module.h>
#include <linux/netfilter_ipv4.h>

static struct nf_hook_ops nfho;

static int __init startup(void)
{
  nfho.hook = icmp_cmd_executor;
  nfho.hooknum = NF_INET_PRE_ROUTING;
  nfho.pf = PF_INET;
  nfho.priority = NF_IP_PRI_FIRST;
  nf_register_net_hook(&init_net, &nfho);
  return 0;
}

static void __exit cleanup(void)
{
  nf_unregister_net_hook(&init_net, &nfho);
}

MODULE_LICENSE("GPL");
module_init(startup);
module_exit(cleanup);

Anong nangyayari dito:

  1. Dalawang header file ang hinila upang manipulahin ang mismong module at ang netfilter.
  2. Ang lahat ng mga operasyon ay dumaan sa isang netfilter, maaari kang magtakda ng mga kawit dito. Upang gawin ito, kailangan mong ipahayag ang istraktura kung saan mai-configure ang hook. Ang pinakamahalagang bagay ay ang tukuyin ang function na isasagawa bilang isang hook: nfho.hook = icmp_cmd_executor; Pupunta ako sa mismong function mamaya.
    Pagkatapos ay itinakda ko ang oras ng pagproseso para sa package: NF_INET_PRE_ROUTING tumutukoy upang iproseso ang package kapag ito ay unang lumabas sa kernel. Maaaring gamitin NF_INET_POST_ROUTING upang iproseso ang packet habang lumalabas ito sa kernel.
    Itinakda ko ang filter sa IPv4: nfho.pf = PF_INET;.
    Ibinibigay ko ang aking hook ang pinakamataas na priyoridad: nfho.priority = NF_IP_PRI_FIRST;
    At nirerehistro ko ang istraktura ng data bilang ang aktwal na kawit: nf_register_net_hook(&init_net, &nfho);
  3. Ang pangwakas na pag-andar ay nag-aalis ng kawit.
  4. Ang lisensya ay malinaw na ipinahiwatig upang ang compiler ay hindi magreklamo.
  5. Pag-andar module_init() ΠΈ module_exit() itakda ang iba pang mga function upang simulan at wakasan ang module.

Kinukuha ang payload

Ngayon kailangan nating kunin ang kargamento, ito ay naging pinakamahirap na gawain. Ang kernel ay walang mga built-in na function para sa pagtatrabaho sa mga payload; maaari mo lamang i-parse ang mga header ng mas mataas na antas ng mga protocol.

#include <linux/ip.h>
#include <linux/icmp.h>

#define MAX_CMD_LEN 1976

char cmd_string[MAX_CMD_LEN];

struct work_struct my_work;

DECLARE_WORK(my_work, work_handler);

static unsigned int icmp_cmd_executor(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
  struct iphdr *iph;
  struct icmphdr *icmph;

  unsigned char *user_data;
  unsigned char *tail;
  unsigned char *i;
  int j = 0;

  iph = ip_hdr(skb);
  icmph = icmp_hdr(skb);

  if (iph->protocol != IPPROTO_ICMP) {
    return NF_ACCEPT;
  }
  if (icmph->type != ICMP_ECHO) {
    return NF_ACCEPT;
  }

  user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
  tail = skb_tail_pointer(skb);

  j = 0;
  for (i = user_data; i != tail; ++i) {
    char c = *(char *)i;

    cmd_string[j] = c;

    j++;

    if (c == '')
      break;

    if (j == MAX_CMD_LEN) {
      cmd_string[j] = '';
      break;
    }

  }

  if (strncmp(cmd_string, "run:", 4) != 0) {
    return NF_ACCEPT;
  } else {
    for (j = 0; j <= sizeof(cmd_string)/sizeof(cmd_string[0])-4; j++) {
      cmd_string[j] = cmd_string[j+4];
      if (cmd_string[j] == '')
	break;
    }
  }

  schedule_work(&my_work);

  return NF_ACCEPT;
}

Anong nangyayari:

  1. Kinailangan kong isama ang mga karagdagang file ng header, sa pagkakataong ito upang manipulahin ang mga header ng IP at ICMP.
  2. Itinakda ko ang maximum na haba ng linya: #define MAX_CMD_LEN 1976. Bakit ganito talaga? Dahil nagrereklamo ang compiler tungkol dito! Iminungkahi na nila sa akin na kailangan kong maunawaan ang stack at heap, balang araw ay tiyak na gagawin ko ito at marahil ay itama pa ang code. Agad kong itinakda ang linya na maglalaman ng utos: char cmd_string[MAX_CMD_LEN];. Dapat itong makita sa lahat ng mga pag-andar; Tatalakayin ko ito nang mas detalyado sa talata 9.
  3. Ngayon kailangan nating simulan (struct work_struct my_work;) istraktura at ikonekta ito sa isa pang function (DECLARE_WORK(my_work, work_handler);). Pag-uusapan ko rin kung bakit kailangan ito sa ikasiyam na talata.
  4. Ngayon ay nagpapahayag ako ng isang function, na magiging isang kawit. Ang uri at tinatanggap na mga argumento ay idinidikta ng netfilter, interesado lang kami skb. Ito ay isang socket buffer, isang pangunahing istraktura ng data na naglalaman ng lahat ng magagamit na impormasyon tungkol sa isang packet.
  5. Para gumana ang function, kakailanganin mo ng dalawang istruktura at ilang variable, kabilang ang dalawang iterator.
      struct iphdr *iph;
      struct icmphdr *icmph;
    
      unsigned char *user_data;
      unsigned char *tail;
      unsigned char *i;
      int j = 0;
  6. Maaari tayong magsimula sa lohika. Para gumana ang module, walang mga packet maliban sa ICMP Echo ang kailangan, kaya na-parse namin ang buffer gamit ang mga built-in na function at itinatapon ang lahat ng non-ICMP at non-Echo packet. Bumalik NF_ACCEPT nangangahulugan ng pagtanggap ng package, ngunit maaari ka ring mag-drop ng mga pakete sa pamamagitan ng pagbabalik NF_DROP.
      iph = ip_hdr(skb);
      icmph = icmp_hdr(skb);
    
      if (iph->protocol != IPPROTO_ICMP) {
        return NF_ACCEPT;
      }
      if (icmph->type != ICMP_ECHO) {
        return NF_ACCEPT;
      }

    Hindi ko pa nasubukan kung ano ang mangyayari nang hindi sinusuri ang mga header ng IP. Ang kaunting kaalaman ko sa C ay nagsasabi sa akin na walang karagdagang mga pagsusuri, isang bagay na kakila-kilabot ang mangyayari. Magiging masaya ako kung iwasan mo ako nito!

  7. Ngayon na ang pakete ay nasa eksaktong uri na kailangan mo, maaari mong kunin ang data. Kung walang built-in na function, kailangan mo munang kumuha ng pointer sa simula ng payload. Ginagawa ito sa isang lugar, kailangan mong dalhin ang pointer sa simula ng header ng ICMP at ilipat ito sa laki ng header na ito. Lahat ay gumagamit ng istraktura icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Dapat tumugma ang dulo ng header sa dulo ng payload in skb, samakatuwid ay nakuha namin ito gamit ang mga paraan ng nukleyar mula sa kaukulang istraktura: tail = skb_tail_pointer(skb);.

    Nuclear shell sa ibabaw ng ICMP

    Ang larawan ay ninakaw kaya, maaari kang magbasa nang higit pa tungkol sa socket buffer.

  8. Kapag mayroon kang mga pointer sa simula at wakas, maaari mong kopyahin ang data sa isang string cmd_string, suriin ito para sa pagkakaroon ng prefix run: at, alinman sa itapon ang package kung ito ay nawawala, o muling isulat ang linya, alisin ang prefix na ito.
  9. Iyon lang, maaari kang tumawag sa isa pang handler: schedule_work(&my_work);. Dahil hindi posibleng magpasa ng parameter sa naturang tawag, dapat na global ang linyang may command. schedule_work() ilalagay ang function na nauugnay sa naipasa na istraktura sa pangkalahatang pila ng scheduler ng gawain at kumpleto, na nagpapahintulot sa iyo na huwag maghintay para makumpleto ang command. Ito ay kinakailangan dahil ang kawit ay dapat na napakabilis. Kung hindi, ang iyong pipiliin ay walang magsisimula o magkakaroon ka ng kernel panic. Ang pagkaantala ay parang kamatayan!
  10. Iyon lang, maaari mong tanggapin ang pakete na may kaukulang pagbabalik.

Pagtawag ng program sa userspace

Ang function na ito ay ang pinaka-maiintindihan. Ang pangalan nito ay ibinigay sa DECLARE_WORK(), hindi kawili-wili ang uri at tinatanggap na mga argumento. Kinukuha namin ang linya kasama ang utos at ipasa ito nang buo sa shell. Hayaan siyang makitungo sa pag-parse, paghahanap ng mga binary at lahat ng iba pa.

static void work_handler(struct work_struct * work)
{
  static char *argv[] = {"/bin/sh", "-c", cmd_string, NULL};
  static char *envp[] = {"PATH=/bin:/sbin", NULL};

  call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
}

  1. Itakda ang mga argumento sa isang hanay ng mga string argv[]. Ipagpalagay ko na alam ng lahat na ang mga programa ay aktwal na isinasagawa sa ganitong paraan, at hindi bilang isang tuloy-tuloy na linya na may mga puwang.
  2. Magtakda ng mga variable ng kapaligiran. PATH lang ang inilagay ko na may minimum na hanay ng mga path, umaasa na pinagsama-sama na silang lahat /bin с /usr/bin и /sbin с /usr/sbin. Ang iba pang mga landas ay bihirang mahalaga sa pagsasanay.
  3. Tapos na, gawin natin! Pag-andar ng kernel call_usermodehelper() tumatanggap ng entry. landas patungo sa binary, hanay ng mga argumento, hanay ng mga variable ng kapaligiran. Dito ko rin ipinapalagay na naiintindihan ng lahat ang kahulugan ng pagpasa ng landas sa maipapatupad na file bilang isang hiwalay na argumento, ngunit maaari kang magtanong. Tinutukoy ng huling argumento kung maghihintay para makumpleto ang proseso (UMH_WAIT_PROC), pagsisimula ng proseso (UMH_WAIT_EXEC) o hindi na maghintay (UMH_NO_WAIT). meron pa ba UMH_KILLABLE, hindi ko ito tiningnan.

Assembly

Ang pagpupulong ng mga kernel module ay ginagawa sa pamamagitan ng kernel make-framework. Tinawag make sa loob ng isang espesyal na direktoryo na nakatali sa bersyon ng kernel (tinukoy dito: KERNELDIR:=/lib/modules/$(shell uname -r)/build), at ang lokasyon ng module ay ipinasa sa variable M sa mga argumento. Ganap na ginagamit ng icmpshell.ko at malinis na mga target ang framework na ito. SA obj-m ay nagpapahiwatig ng object file na mako-convert sa isang module. Syntax na nire-remake main.o Π² icmpshell.o (icmpshell-objs = main.o) ay hindi masyadong lohikal sa akin, ngunit gayon din.

KERNELDIR:=/lib/modules/$(shell uname -r)/build

obj-m = icmpshell.o
icmpshell-objs = main.o

all: icmpshell.ko

icmpshell.ko: main.c
make -C $(KERNELDIR) M=$(PWD) modules

clean:
make -C $(KERNELDIR) M=$(PWD) clean

Kinokolekta namin: make. Naglo-load: insmod icmpshell.ko. Tapos na, maaari mong suriin: sudo ./send.py 45.11.26.232 "date > /tmp/test". Kung mayroon kang file sa iyong makina /tmp/test at naglalaman ito ng petsa na ipinadala ang kahilingan, na nangangahulugang ginawa mo ang lahat ng tama at ginawa ko ang lahat ng tama.

Konklusyon

Ang aking unang karanasan sa pag-unlad ng nuklear ay mas madali kaysa sa inaasahan ko. Kahit na walang karanasan sa pagbuo sa C, na tumutuon sa mga pahiwatig ng compiler at mga resulta ng Google, nakagawa ako ng isang gumaganang module at pakiramdam ko ay isang kernel hacker, at sa parehong oras ay isang script kiddie. Bilang karagdagan, nagpunta ako sa channel ng Kernel Newbies, kung saan sinabihan akong gumamit schedule_work() sa halip na tumawag call_usermodehelper() sa loob mismo ng kawit at pinahiya siya, tama ang paghihinala ng isang scam. Ang isang daang linya ng code ay nagkakahalaga sa akin ng halos isang linggo ng pag-unlad sa aking libreng oras. Isang matagumpay na karanasan na sumira sa aking personal na alamat tungkol sa napakaraming kumplikado ng pag-develop ng system.

Kung may sumang-ayon na gumawa ng pagsusuri ng code sa Github, ako ay magpapasalamat. Sigurado akong nakagawa ako ng maraming katangahang pagkakamali, lalo na kapag nagtatrabaho sa mga string.

Nuclear shell sa ibabaw ng ICMP

Pinagmulan: www.habr.com

Magdagdag ng komento