【CSDN 編者按】事件陷入死地無可挽救之際,可能會有人選擇不了了之,有人選擇就此放棄……但換個思路想一想,既然都無可挽回了,那幹嘛不試試弄點有價值的信息回來?
作者 | dog250 責編 | 張文
頭圖 | CSDN 下載自視覺中國
曾經寫過一個模塊,當運行 Linux 內核的機器死機時,SSH肯定無法登錄了,但只要它還響應中斷,就盡力讓它可以通過網絡帶回一些信息。陳年的事了:https://blog.csdn.net/dog250/article/details/43370611
今日重提這件事,不陌生,但糾結。
本文不談 sysrq,也不談別的。
Linux 內核在發生 soft lockup 的時候,是可以 ping 通的,只要沒有關中斷,ping 通一般沒有問題。既然可以 ping 通,何必不帶回一些真正重要的信息而不僅僅是 echo 的 reply?
且慢,你可能會覺得這一切沒有意義,懂 kdump 的人都會這麼抬槓,畢竟如果這個時候讓內核 panic 掉,保留一個 vmcore,事後便可以隨便分析了。
哈哈,我也不是不懂 kdump,我當然懂得如何分析 vmcore,我只是不信任它而已。我不覺得它保留有足夠的信息,相比之下,我只想知道在事故發生的當時,到底發生了什麼。因此,我需要儘可能的去 debug 將死未死的系統,也就是說,我想要獲取已經 soft lockup 的內核的信息。
如果你重啟了內核,保留了一具 vmcore 屍體,如果是攻擊的情況,很可能在系統重啟的過程中,攻擊者就發覺了,暫停了攻擊或者更改了方式…
不要在既有的框架內就事論事,找些沒文化的流氓一起討論會比和經理討論可能更有收穫。有的時候我不想爭論,不是說我不善於爭論,而是我覺得和我爭論的人根本不知道我在說什麼,唉。
SSH 已經不能指望了,怎麼辦?
想法簡單,不足道,僅僅是個 POC,也希望能有人一起討論:
註冊一個新的四層協議,除了 TCP/UDP/ICMP 等熟知協議之外的新協議,這是為了避免每一個數據包都要經過過濾,避免影響性能。事先分配 skb,避免當事故發生時回送信息時分配 skb 失敗。好了,看代碼,先給出載入內核的代碼,這個代碼的大部分都是我從網上抄來的,並不是自己寫的,我只是重組了邏輯:
#include<net/protocol.h>#include<linux/if_ether.h>#include<linux/ip.h>#include<linux/udp.h>#define IPPROTO_MYPROTO 123#define QUOTA 30structsk_buff *eskb[QUOTA];staticint quota = QUOTA - 1;//module_param(quota, int, 0644);//MODULE_PARM_DESC(quota, "soft_lockup");unsignedshort _csum(unsignedshort* data, int len){int pad = 0;int i = 0;unsignedshort ret = 0;unsignedint sum = 0;if (len % 2 != 0) pad = 1; len /= 2;for ( i = 0; i < len; i++) { sum += data[i]; }if (pad == 1) sum += ((unsignedchar*)(data + len))[0] ; sum = (sum & 0xffff) + (sum >> 16); sum += (sum >> 16); ret = ~sum;return ret;}intmyproto_rcv(struct sk_buff *skb){structudphdr *uh, *euh;structiphdr *iph, *eiph;structethhdr *eh, *ethh;char esaddr[6];unsignedchar *pos;if (quota < 0) {goto end; } iph = ip_hdr(skb); uh = udp_hdr(skb); eh = (struct ethhdr *)(((unsignedchar *)iph) - sizeof(struct ethhdr));// 出事的時候,直接構造已經分配的skb eskb[quota]->ip_summed = CHECKSUM_NONE; eskb[quota]->protocol = htons(ETH_P_IP); eskb[quota]->priority = 0; eskb[quota]->dev = skb->dev; eskb[quota]->pkt_type = PACKET_OTHERHOST; skb_reserve(eskb[quota], 1300 + sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr)); pos = skb_push(eskb[quota], 1300);strcpy(pos, "abcdefghijk123456789"); pos = skb_push(eskb[quota], sizeof(struct udphdr)); skb_reset_transport_header(eskb[quota]); euh = (struct udphdr *)pos; euh->source = uh->dest; euh->dest = uh->source; euh->len = htons(1300 + sizeof(struct udphdr)); euh->check = 0;memcpy(pos - 12, &iph->daddr, 4);memcpy(pos - 8, &iph->saddr, 4); ((unsignedshort *)(pos - 4))[0] = 0x1100;memcpy(pos - 2, &euh->len, sizeof(euh->len)); euh->check = _csum((unsignedshort*)(pos - 12), 12 + 1300 + sizeof(struct udphdr)); pos = skb_push(eskb[quota], sizeof(struct iphdr)); skb_reset_network_header(eskb[quota]); eiph = (struct iphdr *)pos; eiph->version = 4; eiph->ihl = 5; eiph->tos = 0; eiph->tot_len = htons(1300 + sizeof(struct udphdr) + sizeof(struct iphdr)); eiph->id = 0x1122; eiph->frag_off = 0; eiph->ttl = 64; eiph->protocol = 0x11; eiph->check = 0; eiph->saddr = iph->daddr; eiph->daddr = iph->saddr; eiph->check = _csum((unsignedshort*)pos, sizeof(struct iphdr)); pos = skb_push(eskb[quota], sizeof(struct ethhdr)); skb_reset_mac_header(eskb[quota]); ethh = (struct ethhdr *)pos;memcpy(esaddr, eh->h_dest, 6);memcpy(ethh->h_dest, eh->h_source, ETH_ALEN);memcpy(ethh->h_source, eh->h_dest, ETH_ALEN); ethh->h_proto = htons(ETH_P_IP); printk("myproto_rcv is called, length:%d %x %x\n", skb->len, esaddr[2], esaddr[3]); dev_queue_xmit(eskb[quota]); quota --;end: kfree_skb(skb);return0;}intmyproto_rcv_err(struct sk_buff *skb, unsignedint err){ printk("myproto_rcv is called:%d\n", err); kfree_skb(skb);return0;}staticconststructnet_protocolmyproto_protocol = { .handler = myproto_rcv, .err_handler = myproto_rcv_err, .no_policy = 1, .netns_ok = 1,};intinit_module(void){int ret = 0, i;// 事先分配skbfor (i = 0; i < QUOTA; i++) { eskb[i] = alloc_skb(1300 + sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr), GFP_ATOMIC);if (eskb[i] == NULL) {//int j;//for () {//} printk("alloc failed\n");return-1; } }// 註冊123協議,它不是TCP,UDP,ICMP ret = inet_add_protocol(&myproto_protocol, IPPROTO_MYPROTO);if (ret) { printk("failed\n");return ret; } printk("successful\n");return0;}voidcleanup_module(void){int rc = 0; inet_del_protocol(&myproto_protocol, IPPROTO_MYPROTO);//for (i = quota; i >=0; i--) {//kfree_skb(eskb[i]);//}return;}intinit_module(void);voidcleanup_module(void);MODULE_LICENSE("GPL");
來來來,看一下客戶端的代碼。
客戶端需要通過 raw 套接字發送一個「請求」,它的傳輸層協議是 123,然而回復的卻是一個標準的 UDP 報文,所以客戶端需要在該 UDP 上接收。
代碼如下:
#include<stdlib.h>#include<unistd.h>#include<stdio.h>#include<string.h>#include<sys/socket.h>#include<netinet/in.h>#include<linux/ip.h>#include<linux/udp.h>#define PCKT_LEN 8192unsignedshortcsum(unsignedshort *buf, int nwords){unsignedlong sum;for(sum=0; nwords>0; nwords--) sum += *buf++; sum = (sum >> 16) + (sum &0xffff); sum += (sum >> 16);return (unsignedshort)(~sum);}intmain(int argc, charconst *argv[]){int sd, usd;structiphdr *ip;structudphdr *udp;structsockaddr_insin, usin, csin;u_int16_t src_port, dst_port;u_int32_t src_addr, dst_addr;int one = 1;constint *val = &one;int dlen, rlen, clen = sizeof(csin);char *data;char buf[1300];if (argc != 6) {printf("Usage: %s <source hostname/IP> <source port> <target hostname/IP> <target port>\n", argv[0]);exit(1); } src_addr = inet_addr(argv[1]); dst_addr = inet_addr(argv[3]); src_port = atoi(argv[2]); dst_port = atoi(argv[4]); dlen = atoi(argv[5]); data = malloc(sizeof(struct iphdr) + sizeof(struct udphdr) + dlen); ip = (struct iphdr *)data; udp = (struct udphdr *)(data + sizeof(struct iphdr)); sd = socket(PF_INET, SOCK_RAW, IPPROTO_UDP);if (sd < 0) { perror("raw error");exit(2); }if(setsockopt(sd, IPPROTO_IP, IP_HDRINCL, val, sizeof(one)) < 0) { perror("setsockopt() error");exit(2); }sin.sin_family = AF_INET;sin.sin_port = htons(dst_port);sin.sin_addr.s_addr = inet_addr(argv[3]); ip->ihl = 5; ip->version = 4; ip->tos = 16; // low delay ip->tot_len = sizeof(struct iphdr) + sizeof(struct udphdr) + dlen; ip->id = htons(54321); ip->ttl = 64; // hops ip->protocol = 123; // UDP ip->saddr = src_addr; ip->daddr = dst_addr; udp->source = htons(src_port); udp->dest = htons(dst_port); udp->len = htons(sizeof(struct udphdr) + dlen); ip->check = csum((unsignedshort *)data, sizeof(struct iphdr) + sizeof(struct udphdr) + dlen); usd = socket(AF_INET, SOCK_DGRAM, 0);if (usd < 0) { perror("usd error");exit(2); } bzero(&usin, sizeof(usin)); usin.sin_family = AF_INET; usin.sin_port = htons(src_port); usin.sin_addr.s_addr = inet_addr(argv[1]);if (bind(usd, (struct sockaddr *)&usin, sizeof(usin))) { perror("bind error");exit(2); }if (sendto(sd, data, ip->tot_len, 0, (struct sockaddr *)&sin, sizeof(sin)) < 0) { perror("sendto()");exit(3); } rlen = recvfrom(usd, buf, sizeof(buf), 0, (struct sockaddr*)&csin, &clen);printf("recv:%s\n", buf); close(sd);return0;}
…
好了,我們在服務端加載內核模塊,製造一個死鎖或者玩一個 fork 炸彈,SSH 已經無法登錄但是能 ping 通的情況下,執行我們的客戶端程序,可以完美給出結果。
我們只需要把「abcdefghijk123456789」改成當前內核能取到的信息即可,沒意思也不好玩了。
哦,對了,必須補充一段。這個代碼有很多不可行的情況,比如你用了_irq 前綴把硬中斷禁用了,比如你的網絡拓撲不是直來直往的,比如你有更好的帶外系統,比如各種其它的不適用。
但是至少,在直連的情況下,你 SSH 都登錄不上了,我這個破爛玩意兒可以帶回一些信息,哪怕只是一雙皮鞋。