TL; DR: ฉันกำลังเขียนโมดูลเคอร์เนลที่จะอ่านคำสั่งจากเพย์โหลด ICMP และดำเนินการคำสั่งเหล่านั้นบนเซิร์ฟเวอร์ แม้ว่า SSH ของคุณจะขัดข้องก็ตาม สำหรับผู้ที่ใจร้อนที่สุดรหัสทั้งหมดก็คือ
ข้อควรระวัง! โปรแกรมเมอร์ C ที่มีประสบการณ์เสี่ยงที่จะน้ำตาไหล! ฉันอาจจะผิดคำศัพท์ด้วยซ้ำ แต่ก็ยินดีรับคำวิจารณ์ โพสต์นี้มีไว้สำหรับผู้ที่มีแนวคิดคร่าวๆ เกี่ยวกับการเขียนโปรแกรม C และต้องการดูข้อมูลภายในของ Linux
ในความคิดเห็นของฉันครั้งแรก
ใช่ ในปี 2020 ฉันได้เรียนรู้ว่าคุณสามารถแทรกเพย์โหลดที่กำหนดเองลงในแพ็กเก็ต ICMP ได้ แต่มาช้ายังดีกว่าไม่มา! และเนื่องจากบางสิ่งบางอย่างสามารถทำได้เกี่ยวกับเรื่องนี้ จึงต้องทำให้เสร็จ เนื่องจากในชีวิตประจำวันของฉันฉันมักจะใช้บรรทัดคำสั่งรวมถึงผ่าน SSH แนวคิดของเชลล์ ICMP จึงเข้ามาในใจฉันเป็นอันดับแรก และเพื่อที่จะประกอบบิงโกแบบบุลชิลด์ที่สมบูรณ์ ฉันตัดสินใจเขียนมันเป็นโมดูล Linux ในภาษาที่ฉันมีเพียงความคิดคร่าวๆ เท่านั้น เชลล์ดังกล่าวจะไม่ปรากฏให้เห็นในรายการกระบวนการ คุณสามารถโหลดลงในเคอร์เนลได้ และจะไม่อยู่ในระบบไฟล์ คุณจะไม่เห็นสิ่งใดที่น่าสงสัยในรายการพอร์ตการฟัง ในแง่ของความสามารถ นี่เป็นรูทคิทที่ครบครัน แต่ฉันหวังว่าจะปรับปรุงมันและใช้เป็นช่องทางสุดท้ายเมื่อค่าเฉลี่ยโหลดสูงเกินกว่าจะเข้าสู่ระบบผ่าน SSH และดำเนินการอย่างน้อย echo i > /proc/sysrq-trigger
เพื่อคืนค่าการเข้าถึงโดยไม่ต้องรีบูตเครื่อง
เราใช้โปรแกรมแก้ไขข้อความ ทักษะการเขียนโปรแกรมขั้นพื้นฐานใน Python และ C, Google และ
ด้านลูกค้า
สำหรับฉันดูเหมือนว่าในส่วนของลูกค้าฉันจะต้องเขียนสคริปต์ประมาณ 80 บรรทัด แต่ก็มีคนใจดีที่ทำเพื่อฉัน
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()
สคริปต์รับสองอาร์กิวเมนต์ ที่อยู่ และเพย์โหลด ก่อนที่จะส่ง เพย์โหลดจะนำหน้าด้วยคีย์ run:
เราจำเป็นต้องใช้มันเพื่อยกเว้นแพ็คเกจที่มีเพย์โหลดแบบสุ่ม
เคอร์เนลต้องการสิทธิ์ในการประดิษฐ์แพ็กเกจ ดังนั้นสคริปต์จะต้องทำงานในฐานะผู้ใช้ระดับสูง อย่าลืมให้สิทธิ์ในการดำเนินการและติดตั้ง scapy เอง Debian มีแพ็คเกจที่เรียกว่า python3-scapy
. ตอนนี้คุณสามารถตรวจสอบวิธีการทำงานทั้งหมดได้แล้ว
การรันและส่งออกคำสั่ง
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!
นี่คือลักษณะที่ปรากฏในการดมกลิ่น
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
เพย์โหลดในแพ็คเกจการตอบกลับไม่เปลี่ยนแปลง
โมดูลเคอร์เนล
ในการสร้างเครื่องเสมือน Debian คุณจะต้องมีอย่างน้อย make
и linux-headers-amd64
ที่เหลือจะมาในรูปแบบของการพึ่งพาอาศัยกัน ฉันจะไม่ระบุรหัสทั้งหมดในบทความ คุณสามารถโคลนมันได้บน Github
การตั้งค่าตะขอ
อันดับแรก เราต้องการสองฟังก์ชันเพื่อโหลดโมดูลและยกเลิกการโหลด ไม่จำเป็นต้องมีฟังก์ชั่นการขนถ่าย แต่แล้ว rmmod
มันจะไม่ทำงาน โมดูลจะถูกยกเลิกการโหลดเมื่อปิดเท่านั้น
#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);
เกิดอะไรขึ้นที่นี่:
- ไฟล์ส่วนหัวสองไฟล์ถูกดึงเข้ามาเพื่อจัดการโมดูลเองและ netfilter
- การดำเนินการทั้งหมดต้องผ่าน netfilter คุณสามารถตั้งค่า hooks ในนั้นได้ ในการดำเนินการนี้ คุณจะต้องประกาศโครงสร้างที่จะกำหนดค่า hook สิ่งที่สำคัญที่สุดคือการระบุฟังก์ชันที่จะถูกดำเนินการเป็น hook:
nfho.hook = icmp_cmd_executor;
ฉันจะไปที่ฟังก์ชันนี้ในภายหลัง
จากนั้นฉันตั้งเวลาดำเนินการสำหรับแพ็คเกจ:NF_INET_PRE_ROUTING
ระบุเพื่อประมวลผลแพ็กเกจเมื่อปรากฏครั้งแรกในเคอร์เนล สามารถใช้ได้NF_INET_POST_ROUTING
เพื่อประมวลผลแพ็กเก็ตเมื่อออกจากเคอร์เนล
ฉันตั้งค่าตัวกรองเป็น IPv4:nfho.pf = PF_INET;
.
ฉันให้ความสำคัญกับตะขอของฉันเป็นอันดับแรก:nfho.priority = NF_IP_PRI_FIRST;
และฉันลงทะเบียนโครงสร้างข้อมูลเป็น hook จริง:nf_register_net_hook(&init_net, &nfho);
- ฟังก์ชั่นสุดท้ายจะถอดตะขอออก
- มีการระบุใบอนุญาตอย่างชัดเจนเพื่อให้คอมไพเลอร์ไม่บ่น
- ฟังก์ชั่น
module_init()
иmodule_exit()
ตั้งค่าฟังก์ชันอื่นๆ เพื่อเริ่มต้นและสิ้นสุดโมดูล
กำลังดึงข้อมูลเพย์โหลด
ตอนนี้เราจำเป็นต้องดึงข้อมูลเพย์โหลดออก ซึ่งกลายเป็นงานที่ยากที่สุด เคอร์เนลไม่มีฟังก์ชันในตัวสำหรับการทำงานกับเพย์โหลด คุณสามารถแยกวิเคราะห์ส่วนหัวของโปรโตคอลระดับที่สูงกว่าได้เท่านั้น
#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;
}
เกิดอะไรขึ้น:
- ฉันต้องรวมไฟล์ส่วนหัวเพิ่มเติม คราวนี้เพื่อจัดการส่วนหัว IP และ ICMP
- ฉันตั้งค่าความยาวบรรทัดสูงสุด:
#define MAX_CMD_LEN 1976
. ทำไมตรงนี้? เพราะคอมไพเลอร์บ่นเรื่องนี้! พวกเขาแนะนำฉันไปแล้วว่าฉันต้องเข้าใจ stack และ heap สักวันหนึ่งฉันจะทำเช่นนี้อย่างแน่นอนและอาจแก้ไขโค้ดด้วยซ้ำ ฉันตั้งค่าบรรทัดที่จะมีคำสั่งทันที:char cmd_string[MAX_CMD_LEN];
. ควรมองเห็นได้ในทุกฟังก์ชั่น ฉันจะพูดถึงเรื่องนี้โดยละเอียดในย่อหน้าที่ 9 - ตอนนี้เราต้องเริ่มต้น (
struct work_struct my_work;
) จัดโครงสร้างและเชื่อมต่อกับฟังก์ชันอื่น (DECLARE_WORK(my_work, work_handler);
). ฉันจะพูดถึงว่าทำไมสิ่งนี้ถึงจำเป็นในย่อหน้าที่เก้า - ตอนนี้ฉันประกาศฟังก์ชั่นซึ่งจะเป็นตะขอ ประเภทและอาร์กิวเมนต์ที่ยอมรับถูกกำหนดโดย netfilter เราสนใจเท่านั้น
skb
. นี่คือซ็อกเก็ตบัฟเฟอร์ ซึ่งเป็นโครงสร้างข้อมูลพื้นฐานที่มีข้อมูลที่มีอยู่ทั้งหมดเกี่ยวกับแพ็กเก็ต - เพื่อให้ฟังก์ชันทำงานได้ คุณจะต้องมีโครงสร้าง XNUMX ตัวและตัวแปรหลายตัว รวมถึงตัววนซ้ำ XNUMX ตัว
struct iphdr *iph; struct icmphdr *icmph; unsigned char *user_data; unsigned char *tail; unsigned char *i; int j = 0;
- เราสามารถเริ่มต้นด้วยตรรกะ เพื่อให้โมดูลทำงานได้ ไม่จำเป็นต้องมีแพ็กเก็ตอื่นนอกจาก ICMP Echo ดังนั้นเราจึงแยกวิเคราะห์บัฟเฟอร์โดยใช้ฟังก์ชันในตัว และโยนแพ็กเก็ตที่ไม่ใช่ ICMP และไม่ใช่ Echo ทั้งหมดออกไป กลับ
NF_ACCEPT
หมายถึงการยอมรับพัสดุ แต่คุณสามารถส่งคืนพัสดุได้ด้วยการส่งคืน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; }
ฉันยังไม่ได้ทดสอบว่าจะเกิดอะไรขึ้นโดยไม่ตรวจสอบส่วนหัว IP ความรู้เพียงเล็กน้อยของฉันเกี่ยวกับ C บอกฉันว่าหากไม่มีการตรวจสอบเพิ่มเติม สิ่งที่เลวร้ายจะเกิดขึ้นอย่างแน่นอน ฉันจะดีใจถ้าคุณห้ามฉันเรื่องนี้!
- เมื่อแพ็คเกจเป็นประเภทที่คุณต้องการแล้ว คุณก็สามารถแยกข้อมูลได้ หากไม่มีฟังก์ชันในตัว คุณต้องได้รับตัวชี้ไปยังจุดเริ่มต้นของเพย์โหลดก่อน เสร็จสิ้นในที่เดียว คุณต้องนำตัวชี้ไปที่จุดเริ่มต้นของส่วนหัว ICMP และย้ายไปยังขนาดของส่วนหัวนี้ ทุกอย่างใช้โครงสร้าง
icmph
:user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
ส่วนท้ายของส่วนหัวจะต้องตรงกับจุดสิ้นสุดของเพย์โหลดเข้าskb
ดังนั้นเราจึงได้มาโดยใช้วิธีการทางนิวเคลียร์จากโครงสร้างที่เกี่ยวข้อง:tail = skb_tail_pointer(skb);
.
รูปภาพถูกขโมยด้วยเหตุนี้ คุณสามารถอ่านเพิ่มเติมเกี่ยวกับบัฟเฟอร์ซ็อกเก็ตได้ - เมื่อคุณมีพอยน์เตอร์ที่จุดเริ่มต้นและจุดสิ้นสุดแล้ว คุณสามารถคัดลอกข้อมูลลงในสตริงได้
cmd_string
ให้ตรวจสอบว่ามีคำนำหน้าอยู่หรือไม่run:
และทิ้งแพ็กเกจหากหายไป หรือเขียนบรรทัดใหม่อีกครั้ง โดยลบคำนำหน้านี้ออก - เพียงเท่านี้คุณก็โทรหาผู้จัดการคนอื่นได้แล้ว:
schedule_work(&my_work);
. เนื่องจากไม่สามารถส่งพารามิเตอร์ไปยังการโทรดังกล่าวได้ บรรทัดที่มีคำสั่งจึงต้องเป็นโกลบอลschedule_work()
จะวางฟังก์ชันที่เกี่ยวข้องกับโครงสร้างที่ส่งผ่านลงในคิวทั่วไปของตัวกำหนดเวลางานและเสร็จสิ้น ทำให้คุณไม่ต้องรอให้คำสั่งเสร็จสิ้น สิ่งนี้จำเป็นเพราะตะขอจะต้องเร็วมาก มิฉะนั้น ตัวเลือกของคุณคือจะไม่มีอะไรเริ่มต้น ไม่เช่นนั้น คุณจะเกิดอาการเคอร์เนลตื่นตระหนก ความล่าช้าก็เหมือนความตาย! - เพียงเท่านี้คุณก็สามารถรับแพ็คเกจพร้อมการคืนสินค้าที่เกี่ยวข้องได้
การเรียกโปรแกรมในพื้นที่ผู้ใช้
ฟังก์ชั่นนี้เป็นที่เข้าใจได้มากที่สุด ชื่อของมันถูกตั้งไว้ใน DECLARE_WORK()
ประเภทและข้อโต้แย้งที่ยอมรับนั้นไม่น่าสนใจ เราใช้บรรทัดคำสั่งและส่งต่อไปยังเชลล์ทั้งหมด ให้เขาจัดการกับการแยกวิเคราะห์ ค้นหาไบนารี่ และทุกอย่างอื่นๆ
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);
}
- ตั้งค่าอาร์กิวเมนต์เป็นอาร์เรย์ของสตริง
argv[]
. ฉันจะถือว่าทุกคนรู้ว่าจริง ๆ แล้วโปรแกรมถูกดำเนินการในลักษณะนี้ และไม่ใช่บรรทัดต่อเนื่องกันที่มีช่องว่าง - ตั้งค่าตัวแปรสภาพแวดล้อม ฉันแทรกเฉพาะ PATH ด้วยชุดเส้นทางขั้นต่ำ โดยหวังว่าเส้นทางทั้งหมดจะรวมกันแล้ว
/bin
с/usr/bin
и/sbin
с/usr/sbin
. เส้นทางอื่นๆ ไม่ค่อยมีความสำคัญในทางปฏิบัติ - เสร็จแล้วมาทำกัน! ฟังก์ชันเคอร์เนล
call_usermodehelper()
ยอมรับการเข้า เส้นทางไปยังไบนารี อาร์เรย์ของอาร์กิวเมนต์ อาร์เรย์ของตัวแปรสภาพแวดล้อม ที่นี่ฉันยังถือว่าทุกคนเข้าใจความหมายของการส่งเส้นทางไปยังไฟล์ปฏิบัติการเป็นอาร์กิวเมนต์แยกต่างหาก แต่คุณสามารถถามได้ อาร์กิวเมนต์สุดท้ายระบุว่าจะรอให้กระบวนการเสร็จสิ้นหรือไม่ (UMH_WAIT_PROC
) การเริ่มต้นกระบวนการ (UMH_WAIT_EXEC
) หรือไม่รอเลย (UMH_NO_WAIT
). มีอีกไหมUMH_KILLABLE
ฉันไม่ได้พิจารณามัน
การชุมนุม
การประกอบโมดูลเคอร์เนลจะดำเนินการผ่านเฟรมเวิร์กการสร้างเคอร์เนล เรียกว่า make
ภายในไดเร็กทอรีพิเศษที่เชื่อมโยงกับเวอร์ชันเคอร์เนล (กำหนดไว้ที่นี่: KERNELDIR:=/lib/modules/$(shell uname -r)/build
) และตำแหน่งของโมดูลจะถูกส่งผ่านไปยังตัวแปร M
ในการโต้แย้ง icmpshell.ko และ clean เป้าหมายใช้เฟรมเวิร์กนี้ทั้งหมด ใน obj-m
ระบุไฟล์อ็อบเจ็กต์ที่จะถูกแปลงเป็นโมดูล ไวยากรณ์ที่สร้างใหม่ main.o
в icmpshell.o
(icmpshell-objs = main.o
) ดูไม่ค่อยสมเหตุสมผลสำหรับฉัน แต่ก็เป็นเช่นนั้น
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
เรารวบรวม: make
. กำลังโหลด: insmod icmpshell.ko
. เสร็จแล้วคุณสามารถตรวจสอบ: sudo ./send.py 45.11.26.232 "date > /tmp/test"
. หากคุณมีไฟล์อยู่ในเครื่องของคุณ /tmp/test
และมีวันที่ส่งคำขอ ซึ่งหมายความว่าคุณทำทุกอย่างถูกต้องแล้ว และฉันก็ทำทุกอย่างถูกต้องแล้ว
ข้อสรุป
ประสบการณ์ครั้งแรกของฉันกับการพัฒนานิวเคลียร์นั้นง่ายกว่าที่ฉันคาดไว้มาก แม้ว่าจะไม่มีประสบการณ์ในการพัฒนาในภาษา C โดยเน้นไปที่คำแนะนำของคอมไพเลอร์และผลลัพธ์ของ Google ฉันก็สามารถเขียนโมดูลที่ใช้งานได้และรู้สึกเหมือนเป็นเคอร์เนลแฮ็กเกอร์ และในขณะเดียวกันก็เป็นสคริปต์ตัวเล็กๆ นอกจากนี้ ฉันไปที่ช่อง Kernel Newbies ซึ่งพวกเขาบอกให้ฉันใช้ schedule_work()
แทนที่จะโทร call_usermodehelper()
เข้าไปในตะขอและทำให้เขาอับอายโดยสงสัยว่าเป็นการหลอกลวง โค้ดหลายร้อยบรรทัดทำให้ฉันเสียเวลาในการพัฒนาประมาณหนึ่งสัปดาห์ในเวลาว่าง ประสบการณ์ที่ประสบความสำเร็จซึ่งทำลายความเชื่อส่วนตัวของฉันเกี่ยวกับความซับซ้อนอย่างท่วมท้นของการพัฒนาระบบ
หากมีคนตกลงที่จะทำการตรวจสอบโค้ดบน Github ฉันจะขอบคุณ ฉันค่อนข้างแน่ใจว่าฉันทำผิดพลาดโง่ๆ มากมาย โดยเฉพาะเมื่อทำงานกับสตริง
ที่มา: will.com