物联网设备常见的web服务器——uhttpd源码分析(一)

本文详细介绍了物联网设备常用的轻量级Web服务器uHTTPd,包括其主要功能、源码分析,如主函数解析、配置文件处理、插件初始化等,旨在帮助读者理解uHTTPd的运行机制及其在物联网安全中的重要性。

目录

0x00 前言

0x01 简介

0x02 主函数main

0x03 signal函数

0x04 uh_config_parse函数

0x05 init_defaults_post函数

0x00 uh_index_add函数

0x06 uh_plugin_init函数

0x00 dlopen函数

0x01 dlsym函数

0x07 run_server函数


0x00 前言

       uHTTPd 是一个 OpenWrt/LUCI 开发者从头编写的 Web 服务器。 它着力于实现一个稳定高效的服务器,能够满足嵌入式设备的轻量级任务需求,且能够与 OpenWrt 的配置框架 (UCI) 整合。默认情况下它被用于 OpenWrt 的 Web 管理接口 LuCI。当然,uHTTPd 也能提供一个常规 Web 服务器所需要的所有功能。

0x01 简介

讲解uhttpd的主要原因是:uhttpd是物联网设备很常见的一个web服务器,在物联网设备漏洞挖掘的过程中,最常见的漏洞都是出现在web服务器上,如果能熟悉各个开源的web服务器的开发流程,更容易理解其他厂商的开发者是如何开发自己的web服务器,那么对物联网漏洞挖掘将事倍功半。

下载地址:https://git.openwrt.org/?p=project/uhttpd.git;a=summary  点击 snapshot 即可下载

0x02 主函数main

int main(int argc, char **argv)
{
	struct alias *alias;
	bool nofork = false;
	char *port;
	int opt, ch;
	int cur_fd;
	int bound = 0;
#ifdef HAVE_TLS
	int n_tls = 0;
	const char *tls_key = NULL, *tls_crt = NULL, *tls_ciphers = NULL;
#endif
#ifdef HAVE_LUA
	const char *lua_prefix = NULL, *lua_handler = NULL;
#endif
    // 如果没有LUA插件,才会执行
	BUILD_BUG_ON(sizeof(uh_buf) < PATH_MAX);

	uh_dispatch_add(&cgi_dispatch); // 添加cgi_dispatch到dispatch_handlers链表中,后续解析cgi或者lua用
	init_defaults_pre(); //初始化默认参数,如:cgi的前缀名为 /cgi-bin
    /*设置信号屏蔽*/
	signal(SIGPIPE, SIG_IGN);
    /*用户输入的参数*/
	while ((ch = getopt(argc, argv, "A:aC:c:Dd:E:e:fh:H:I:i:K:k:L:l:m:N:n:P:p:qRr:Ss:T:t:U:u:Xx:y:")) != -1) {
		switch(ch) {
#ifdef HAVE_TLS
		case 'C':
			tls_crt = optarg;
			break;

		case 'K':
			tls_key = optarg;
			break;

		case 'P':
			tls_ciphers = optarg;
			break;

		case 'q':
			conf.tls_redirect = 1;
			break;

		case 's':
			n_tls++;
			/* fall through */
#else
		case 'C':
		case 'K':
		case 'P':
		case 'q':
		case 's':
			fprintf(stderr, "uhttpd: TLS support not compiled, "
			                "ignoring -%c\n", ch);
			break;
#endif
		case 'p':
			optarg = strdup(optarg);
			bound += add_listener_arg(optarg, (ch == 's'));
			break;

		case 'h':
			if (!realpath(optarg, uh_buf)) {
				fprintf(stderr, "Error: Invalid directory %s: %s\n",
						optarg, strerror(errno));
				exit(1);
			}
			conf.docroot = strdup(uh_buf);
			break;

		case 'H':
			if (uh_handler_add(optarg)) {
				fprintf(stderr, "Error: Failed to load handler script %s\n",
					optarg);
				exit(1);
			}
			break;

		case 'E':
			if (optarg[0] != '/') {
				fprintf(stderr, "Error: Invalid error handler: %s\n",
						optarg);
				exit(1);
			}
			conf.error_handler = optarg;
			break;

		case 'I':
			if (optarg[0] == '/') {
				fprintf(stderr, "Error: Invalid index page: %s\n",
						optarg);
				exit(1);
			}
			uh_index_add(optarg);
			break;

		case 'S':
			conf.no_symlinks = 1;
			break;

		case 'D':
			conf.no_dirlists = 1;
			break;

		case 'R':
			conf.rfc1918_filter = 1;
			break;

		case 'n':
			conf.max_script_requests = atoi(optarg);
			break;

		case 'N':
			conf.max_connections = atoi(optarg);
			break;
		/*cgi文件路径前缀*/
		case 'x':
			fixup_prefix(optarg);
			conf.cgi_prefix = optarg;
			break;

		case 'y':
			alias = calloc(1, sizeof(*alias));
			if (!alias) {
				fprintf(stderr, "Error: failed to allocate alias\n");
				exit(1);
			}
			alias->alias = strdup(optarg);
			alias->path = strchr(alias->alias, '=');
			if (alias->path)
				*alias->path++ = 0;
			list_add(&alias->list, &conf.cgi_alias);
			break;

		case 'i':
			optarg = strdup(optarg);
			port = strchr(optarg, '=');
			if (optarg[0] != '.' || !port) {
				fprintf(stderr, "Error: Invalid interpreter: %s\n",
						optarg);
				exit(1);
			}

			*port++ = 0;
			uh_interpreter_add(optarg, port);
			break;

		case 't':
			conf.script_timeout = atoi(optarg);
			break;

		case 'T':
			conf.network_timeout = atoi(optarg);
			break;

		case 'k':
			conf.http_keepalive = atoi(optarg);
			break;

		case 'A':
			conf.tcp_keepalive = atoi(optarg);
			break;

		case 'f':
			nofork = 1;
			break;

		case 'd':
			optarg = strdup(optarg);
			port = alloca(strlen(optarg) + 1);
			if (!port)
				return -1;

			/* "decode" plus to space to retain compat */
			for (opt = 0; optarg[opt]; opt++)
				if (optarg[opt] == '+')
					optarg[opt] = ' ';

			/* opt now contains strlen(optarg) -- no need to re-scan */
			if (uh_urldecode(port, opt, optarg, opt) < 0) {
				fprintf(stderr, "uhttpd: invalid encoding\n");
				return -1;
			}

			printf("%s", port);
			return 0;
			break;

		/* basic auth realm */
		case 'r':
			conf.realm = optarg;
			break;

		/* md5 crypt */
		case 'm':
			printf("%s\n", crypt(optarg, "$1$"));
			return 0;
			break;

		/* config file */
		case 'c':
			conf.file = optarg;
			break;

#ifdef HAVE_LUA
		case 'l':
		case 'L':
			if (ch == 'l') {
				if (lua_prefix)
					fprintf(stderr, "uhttpd: Ignoring previous -%c %s\n",
					        ch, lua_prefix);

				lua_prefix = optarg;
			}
			else {
				if (lua_handler)
					fprintf(stderr, "uhttpd: Ignoring previous -%c %s\n",
					        ch, lua_handler);

				lua_handler = optarg;
			}

			if (lua_prefix && lua_handler) {
				add_lua_prefix(lua_prefix, lua_handler);
				lua_prefix = NULL;
				lua_handler = NULL;
			}

			break;
#else
		case 'l':
		case 'L':
			fprintf(stderr, "uhttpd: Lua support not compiled, "
			                "ignoring -%c\n", ch);
			break;
#endif
#ifdef HAVE_UBUS
		case 'a':
			conf.ubus_noauth = 1;
			break;

		case 'u':
			conf.ubus_prefix = optarg;
			break;

		case 'U':
			conf.ubus_socket = optarg;
			break;

		case 'X':
			conf.ubus_cors = 1;
			break;

		case 'e':
			conf.events_retry = atoi(optarg);
			break;
#else
		case 'a':
		case 'u':
		case 'U':
		case 'X':
		case 'e':
			fprintf(stderr, "uhttpd: UBUS support not compiled, "
			                "ignoring -%c\n", ch);
			break;
#endif
		default:
			return usage(argv[0]);
		}
	}
	/*配置文件解析*/
	uh_config_parse();

	if (!conf.docroot) {
		if (!realpath(".", uh_buf)) { //uh_buf为当前工作目录的绝对路径指针
			fprintf(stderr, "Error: Unable to determine work dir\n");
			return 1;
		}
		conf.docroot = strdup(uh_buf); //docroot字段保存了当前工作路径
	}
	/*初始化默认主页和cgi绝对路径*/
	init_defaults_post();

	if (!bound) { //如果没有监听端口成功,则报错退出
		fprintf(stderr, "Error: No sockets bound, unable to continue\n");
		return 1;
	}

#ifdef HAVE_TLS // 如果有TLS插件才会执行,也就是https
	if (n_tls) {
		if (!tls_crt || !tls_key) {//没有公匙或者没有私匙,则报错退出
			fprintf(stderr, "Please specify a certificate and "
					"a key file to enable SSL support\n");
			return 1;
		}

		if (uh_tls_init(tls_key, tls_crt, tls_ciphers))//初始化加密
		    return 1;
	}
#endif

#ifdef HAVE_LUA // 有LUA插件才会执行
	if (lua_handler || lua_prefix) {
		fprintf(stderr, "Need handler and prefix to enable Lua support\n");
		return 1;
	}
    /*uh_plugin_init会初始化lua的插件,也就是会执行uhttpd_lua.so中的初始化函数,然后再判断是否存在lua文件,类似执行 lua luci 命令,使用lua执行自己创建的文件,如果执行错误,或者缺少某些文件则不能继续执行*/
	if (!list_empty(&conf.lua_prefix) && uh_plugin_init("uhttpd_lua.so"))
		return 1;
#endif
#ifdef HAVE_UBUS // 有UBUS插件才会执行
    /*原理和LUA差不多*/
	if (conf.ubus_prefix && uh_plugin_init("uhttpd_ubus.so"))
		return 1;
#endif
	/* 加了-f 则 nofork==1,这样则不会通过fork创建子进程*/
	/* fork (if not disabled) */
	if (!nofork) {
		switch (fork()) {
		case -1:
			perror("fork()");
			exit(1);

		case 0:
			/* daemon setup */
			if (chdir("/"))
				perror("chdir()");

			cur_fd = open("/dev/null", O_WRONLY);
			if (cur_fd > 0) {
				dup2(cur_fd, 0);
				dup2(cur_fd, 1);
				dup2(cur_fd, 2);
			}

			break;

		default:
			exit(0);
		}
	}
    /*运行服务器的主要函数*/
	return run_server();
}

0x03 signal函数

位置:main-->signal

signal(SIGPIPE, SIG_IGN);

根据信号的默认处理规则SIGPIPE信号的默认执行动作是terminate(终止、退出)。简单的理解就是在发送或接受数据时,可能会产生一个SIGPIPE信号,可能会导致进程退出,但是我们却不想进程退出,为了避免进程退出, 可以捕获SIGPIPE信号, 或者忽略它, 给它设置SIG_IGN信号(屏蔽)处理函数。

参考:

1. https://blog.csdn.net/sysleo/article/details/95984946

2. https://www.runoob.com/cprogramming/c-function-signal.html

3. https://my.oschina.net/u/2252538/blog/2993724

0x04 uh_config_parse函数

位置:main-->uh_config_parse

static void uh_config_parse(void)
{
	const char *path = conf.file;
	FILE *c;
	char line[512];
	char *col1;
	char *col2;
	char *eol;
    /*如果conf.file没有赋值,则默认为/etc/httpd.conf*/
	if (!path)
		path = "/etc/httpd.conf";

	c = fopen(path, "r");
	if (!c)
		return;

	memset(line, 0, sizeof(line));
    /*配置文件的逐步解析*/
	while (fgets(line, sizeof(line) - 1, c)) {
		if ((line[0] == '/') && (strchr(line, ':') != NULL)) {
			if (!(col1 = strchr(line, ':')) || (*col1++ = 0) ||
				!(col2 = strchr(col1, ':')) || (*col2++ = 0) ||
				!(eol = strchr(col2, '\n')) || (*eol++  = 0))
				continue;

			uh_auth_add(line, col1, col2);
		} else if (!strncmp(line, "I:", 2)) {
			if (!(col1 = strchr(line, ':')) || (*col1++ = 0) ||
				!(eol = strchr(col1, '\n')) || (*eol++  = 0))
				continue;

			uh_index_add(strdup(col1));
		} else if (!strncmp(line, "E404:", 5)) {
			if (!(col1 = strchr(line, ':')) || (*col1++ = 0) ||
				!(eol = strchr(col1, '\n')) || (*eol++  = 0))
				continue;

			conf.error_handler = strdup(col1);
		}
		else if ((line[0] == '*') && (strchr(line, ':') != NULL)) {
			if (!(col1 = strchr(line, '*')) || (*col1++ = 0) ||
				!(col2 = strchr(col1, ':')) || (*col2++ = 0) ||
				!(eol = strchr(col2, '\n')) || (*eol++  = 0))
				continue;

			uh_interpreter_add(col1, col2);
		}
	}

	fclose(c);
}

0x05 init_defaults_post函数

位置:main-->init_defaults_post

static void init_defaults_post(void)
{
    /*将这四个文件设置为默认主页*/
	uh_index_add("index.html");
	uh_index_add("index.htm");
	uh_index_add("default.html");
	uh_index_add("default.htm");
	/*如果设置有cgi前缀,则将当前工作路径加上cgi的工作路径,默认情况(默认情况cgi是/cgi-bin)如:/xxx/xxx/xxx/cgi-bin(绝对路径加 /cgi-bin)*/
	if (conf.cgi_prefix) {
		char *str = malloc(strlen(conf.docroot) + strlen(conf.cgi_prefix) + 1);

		strcpy(str, conf.docroot);
		strcat(str, conf.cgi_prefix);
		conf.cgi_docroot_path = str;
		conf.cgi_prefix_len = strlen(conf.cgi_prefix);
	};
}

0x00 uh_index_add函数

位置:main-->init_defaults_post-->uh_index_add

void uh_index_add(const char *filename)
{
	struct index_file *idx;

	idx = calloc(1, sizeof(*idx));
	idx->name = filename;
	list_add_tail(&idx->list, &index_files);//将传进来的默认主页添加到index_files双向列表中
}

0x06 uh_plugin_init函数

位置:main-->uh_plugin_init

/*通过so库初始化插件*/
int uh_plugin_init(const char *name)
{
	struct uhttpd_plugin *p;
	const char *sym;
	void *dlh;
    /* 路径默认:/usr/lib/ + name 如果路径不对的话,则会报错*/
	dlh = dlopen(name, RTLD_LAZY | RTLD_GLOBAL);
	if (!dlh) {
		fprintf(stderr, "Could not open plugin %s: %s\n", name, dlerror());
		return -ENOENT;
	}

	sym = "uhttpd_plugin";
	p = dlsym(dlh, sym);
	if (!p) {
		fprintf(stderr, "Could not find symbol '%s' in plugin '%s'\n", sym, name);
		return -ENOENT;
	}

	list_add(&p->list, &plugins);
	return p->init(&ops, &conf);//比如ubus:p->init == uh_ubus_plugin_init
}

0x00 dlopen函数

位置:main-->uh_plugin_init-->dlopen

dlh = dlopen(name, RTLD_LAZY | RTLD_GLOBAL);

相当于打开so动态链接库,并返回一个句柄,如果打开失败则会返回NULL(本人只出现过没有该文件,八成就是没有在/usr/lib/路径下创建该动态链接库),以供后续调用该动态链接库中的函数,或其他操作。

参考:

https://man7.org/linux/man-pages/man3/dlopen.3.html

https://blog.csdn.net/teleger/article/details/80857900

0x01 dlsym函数

位置:main-->uh_plugin_init-->dlsym

sym = "uhttpd_plugin";
p = dlsym(dlh, sym);

dlsym是一个计算机函数,功能是根据动态链接库操作句柄与符号,返回符号对应的地址,不但可以获取函数地址,也可以获取变量地址。uhttpd则是获取一个初始化的函数。如果初始化失败,则会返回NULL。

参考:

https://baike.baidu.com/item/dlsym/6603915?fr=aladdin

0x07 run_server函数

位置:main-->run_server

未完待续...

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值