Linux soft lockup時遠程調試的可能性

曾經寫過一個模塊,當運行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	30

struct sk_buff *eskb[QUOTA];

static int quota = QUOTA - 1;
//module_param(quota, int, 0644);
//MODULE_PARM_DESC(quota, "soft_lockup");

unsigned short _csum(unsigned short* data, int len)
{
   
    
	int pad = 0;
	int i = 0;
	unsigned short ret = 0;
	unsigned int sum = 0;
	if (len % 2 != 0)
		pad = 1;
	len /= 2;
	for ( i = 0; i < len; i++) {
   
    
		sum += data[i];
	}
	if (pad == 1)
		sum += ((unsigned char*)(data + len))[0] ;
	sum = (sum & 0xffff) + (sum >> 16);
	sum += (sum >> 16);
	ret = ~sum;
	return ret;
}

int myproto_rcv(struct sk_buff *skb)
{
   
    
	struct udphdr *uh, *euh;
	struct iphdr *iph, *eiph;
	struct ethhdr *eh, *ethh;
	char esaddr[6];
	unsigned char *pos;

	if (quota < 0) {
   
    
		goto end;
	}
	iph = ip_hdr(skb);
	uh = udp_hdr(skb);
	eh = (struct ethhdr *)(((unsigned char *)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);
	((unsigned short *)(pos - 4))[0] = 0x1100;
	memcpy(pos - 2, &euh->len, sizeof(euh->len));
	euh->check = _csum((unsigned short*)(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((unsigned short*)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);
	return 0;
}

int myproto_rcv_err(struct sk_buff *skb, unsigned int err)
{
   
    
	printk("myproto_rcv is called:%d\n", err);
	kfree_skb(skb);
	return 0;
}

static const struct net_protocol myproto_protocol = {
   
    
	.handler = myproto_rcv,
	.err_handler = myproto_rcv_err,
	.no_policy = 1,
	.netns_ok = 1,
};

int init_module(void)
{
   
    
	int ret = 0, i;

	// 事先分配skb
	for (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");
	return 0;
}

void cleanup_module(void)
{
   
    
	int rc = 0;
	inet_del_protocol(&myproto_protocol, IPPROTO_MYPROTO);
	//for (i = quota; i >=0; i--) {
   
    
		//kfree_skb(eskb[i]);
	//}
	return;
}

int init_module(void);
void cleanup_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 8192

unsigned short csum(unsigned short *buf, int nwords)
{
   
    
  unsigned long sum;
  for(sum=0; nwords>0; nwords--)
    sum += *buf++;
  sum = (sum >> 16) + (sum &0xffff);
  sum += (sum >> 16);
  return (unsigned short)(~sum);
}

int main(int argc, char const *argv[])
{
   
    
	int sd, usd;
	struct iphdr *ip;
	struct udphdr *udp;
	struct sockaddr_in sin, usin, csin;
	u_int16_t src_port, dst_port;
	u_int32_t src_addr, dst_addr;
	int one = 1;
	const int *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((unsigned short *)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);
	return 0;
}


好了,我們在服務端加載內核模塊,製造一個死鎖或者玩一個fork炸彈,SSH已經無法登錄但是能ping通的情況下,執行我們的客戶端程序,可以完美給出結果。

我們只需要把“abcdefghijk123456789”改成當前內核能取到的信息即可,沒意思也不好玩了。

哦,對了,必須補充一段。這個代碼有很多不可行的情況,比如你用了_irq前綴把硬中斷禁用了,比如你的網絡拓撲不是直來直往的,比如你有更好的帶外系統,比如各種其它的不適用。但是至少,在直連的情況下,你SSH都登錄不上了,我這個破爛玩意兒可以帶回一些信息,哪怕只是一雙皮鞋👞。


浙江溫州皮鞋溼,下雨進水不會胖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章