hlrcon.c

拙作のCS1.6鯖を操作する目的で作ったrcon操作ツール。
http://hlds.rying.net/arc/hlrcon.c


作ったのは結構前で、ずっとおいてあるんだけど、
いいモン を作ってくれた人がいたので、
せっかくなのでrcon動作やhlrcon.cの説明など。


あ、ちなみに上と同じ目的で作ってたモノの記録が残ってたので置いてみる。
http://d.hatena.ne.jp/ryousanngata/20091111
ここから色々進んでるんだけど、UIの使い勝手がまだイマイチなのと、
配布するにもライセンスの制約上ファイルの数が多くなってしまうため、そういう意味でも使い勝手が悪い。
実行ファイルのみでも数メガバイトになるので。


rconの仕組み

  • クライアントはサーバへ、"challenge rcon"を送る
  • サーバはクライアントへ、チャレンジコードを送る
  • クライアントはサーバへ、チャレンジコードを含めてコマンドを送る
  • サーバはクライアントへ、実行結果を送る


チャレンジコードというのは、送信元を特定するためのコードと思います。
これがなければ一方的にrconを送りつける事ができてしまうので、セキュリティ的にもよくないです。


また、実行結果がない場合もあります。
これは元々実行結果がないコマンドであることや、
通信にUDPというプロトコルが用いられるのが原因で、
UDPは送った事を保障しないため返答をうまく受け取れない事があります。

ソースコード

LinuxWindowsコンパイラGCC 4.3.4以上でコンパイル・動作を確認しています。
古臭いC言語の記法に則っているので、恐らくBCCでもコンパイルできるかと思います。


学校の授業の合間のわずかな時間を見つけてせっせとこしらえたもので、約11ヶ月前に作ったもので、
未熟なC言語知識で書いたため、冗長的なコードになっていますです。

#include <stdio.h>
#include <string.h>

#ifdef WIN32
	#include <winsock.h>
#else
	#include <sys/socket.h>
	#include <arpa/inet.h>
	#include <netdb.h>
	typedef int SOCKET;
#endif

#define BUFF 32768

必要なヘッダの読み込み。
#ifdefを使い、Windows環境毎、それ以外環境毎に、処理を分けています。今後もちょこちょこ出てきます。
Windows環境ではwinsock.hのみ、そうでない環境では以下を読み込みます。
BUFFは適当なバッファ領域です。多すぎるかもしれないけど、少なくして問題が出るよりはいいのでこの値。

void sock_close(SOCKET sock){
#ifdef WIN32
	closesocket(sock);
	WSACleanup();
#else
	close (sock);
#endif
}

関数しないでマクロ関数でできる処理なのにね。バカだね。
まぁ呼ばれる場面が多いけど、実行中に1回しか呼ばれないので、そこまで厳しく気にする必要もないけども。


以下からmain関数

	SOCKET sock;
	struct sockaddr_in addr;
	char buf[BUFF];
	char command_buf[2048]={};
	char challenge_code[256]={};
	char ip_addr[256]={};
	char *p;
	int i, port;
	int ret=0;
	
#ifdef WIN32
	struct WSAData wsaData;
#else
	socklen_t addr_size = sizeof(struct sockaddr_in);
#endif

必要な変数たち。初期値は特に指定してない。
配列は"多分"ゼロクリアされている。({}ではなく{0}とするべきらしい)


ここでもWindows環境特有の構造体WSADataを定義している。
通信のお決まりみたいなもん。めんどくさい。


Linux環境ではsocklen_tを定義している。sockaddr_inの構造体の大きさを入れているだけ。
なんでいちいち入れるのかというと、通信時にこの値へのポインタをほしがるから。
sockaddr_inの構造体の大きさが通信で変わってたらぱにっくになると思うんだがね。よくわからない仕様。

	if(argc<4){
		printf("Usage: this.exe ADDRESS PORT PASS COMMAND\n");
		return 0;
	}
	
	for(i=4;i<argc;i++){
		strcat(command_buf, argv[i]);
		strcat(command_buf, " ");
	}
	
	strcpy(ip_addr, argv[1]);
	port = atoi(argv[2]);
	
	if(!port){
		printf("error: port.\n");
		return 0;
	}

引数となった情報を記録していく。
それにしても記録する順番が見事にバラバラである。
command_bufあたりの処理が危険・・・
もし配列command_bufが初期化できてなかったら終わるので、もし直す人は、

	command_buf[0]=0; // こいつを追加!!
	for(i=4;i<argc;i++){
		strcat(command_buf, argv[i]);
		strcat(command_buf, " ");
	}

とすると少し安心。

#ifdef WIN32
	WSAStartup(MAKEWORD(2,0), &wsaData);
#endif
	
	
	sock = socket(AF_INET, SOCK_DGRAM, 0);
	
	if( !sock ){
		printf("error: SOCKET_ERROR(socket).\n");
		sock_close(sock);
		return 0;
	}
	
	// host -> ip , ip -> ip
	{
		struct hostent *host;
		host = gethostbyname(argv[1]);
		if(host == NULL){
			printf("error: gethostbyname.\n");
#ifdef WIN32
			WSACleanup();
#endif
			return 0;
		}
		strcpy(ip_addr, inet_ntoa(*(struct in_addr *)(host->h_addr_list[0])));
	}
	
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	
#ifdef WIN32
	addr.sin_addr.S_un.S_addr = inet_addr(ip_addr);
#else
	addr.sin_addr.s_addr = inet_addr(ip_addr);
#endif

	ret = connect(sock,(struct sockaddr *)&addr,sizeof(addr));
	
	if( ret == -1){
		printf("error: SOCKET_ERROR(connect).\n");
		sock_close(sock);
		return 0;
	}

通信前の準備と実際に接続を試みる。
ソケットを作れなかったら、エラー。
アドレスがhostの場合は、DNSの正引きを行い、IPアドレスを取得する。
正引きできなかったらエラー。
後はIPアドレスを文字列から4byteの形へと変換とかする。
WindowsLinuxでは使う構造体が異なるので処理わけ。統一すりゃいいのに。
connectで接続を試す。失敗したらエラー。
実際はエラーかどうかをチェックするとき、-1ではなく、
定義されているdefine値で比較するべき。socketならINVALID_SOCKETとかconnectならSOCKET_ERRORとかで。

	ret = sendto(sock, "\xFF\xFF\xFF\xFF challenge rcon\n", 20, 0, (struct sockaddr *)&addr, sizeof(addr));
	
	if(ret == -1){
		printf("error: SOCKET_ERROR(sendto).\n");
		sock_close(sock);
		return 0;
	}

サーバへデータを送りつける。
sendtoはソケット、送信するデータ、データ長、何か、送信先データ情報、その大きさを引数に取る。
正常に送る操作ができるとデータ長が、問題が起きると-1が返ってくるはず。
ここでいう"正常に送る操作"というのは、
相手にデータが届いたというわけではなく、相手にデータを送った、という事。
郵便ポストにあて先を書いた封筒を送ったようなもんで、
相手に届いたかどうかはわからない。(紛失するかもしれない)
これは使用しているプロトコルTCPではなく、UDPのため。


送りつけるデータフォーマットは決まっていて、
最初に4つの\xFF(255)、半角スペース、"データ"、改行
となっている。
今回のデータはチャレンジコードを要求している。

	memset(buf, 0, sizeof(buf));
	
#ifdef WIN32
	ret = recv(sock, buf, sizeof(buf), 0);
#else
	ret = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr *)&addr, &addr_size);
#endif
	
	if(ret == -1){
		printf("error: SOCKET_ERROR(recv).\n");
		sock_close(sock);
		return 0;
	}

memsetで一時領域を初期化。
正常に送れていた場合、相手はチャレンジコードを返してくれるはずなので、recvで受け取り待ちを行う。
recvはソケット、記録データ先、記録するデータ先の領域長、何かを引数にする。
正常に受け取れると、受け取ったデータの長さがretへ入る。
十分な領域を確保してあるはずなので、あふれる事は多分ない。あふれた場合は知らない。


また、エラーチェック処理を行っているが、実際にここが行われる事はあまりない。
エラーが起こりうるのは、受取前にsockが消されるか、ぐらい。
なぜなら、相手にデータが届いてなくても受け取り待ち状態になるので、
永遠にくる事がない受取物をずっと待っている状態になる。こうなったら強制終了するしかない。
これはプログラムの欠陥であり、本来ならselect関数などを使って、タイムアウトを設けるべき。
当時noobだった僕はそんな事知らなかった。


	if((p = strstr(buf, "rcon "))==NULL){
		printf("error: challenge. \n");
		sock_close(sock);
		return 0;
	}
	
	memset(challenge_code, 0, sizeof(challenge_code));
	strncpy(challenge_code, p+5, strlen(p+5)-1);

受け取ったデータからチャレンジコードがあるかを探す。
rcon xxxxxという形なので、"rcon "がどこに含まれているかを探し、pへ代入。
無かった場合p==NULLなので、エラーを吐いて終了。
見つかった場合、challenge_codeの領域をゼロクリアし、
そこへ受け取ったデータからチャレンジコード部分をコピーする。
+5としているのは"rcon "が5byteだからそのオフセット値。

	sprintf(buf, "\xFF\xFF\xFF\xFF rcon %s \"%s\" %s", challenge_code, argv[3], command_buf);
	sendto(sock, buf, strlen(buf), 0, (struct sockaddr *)&addr, sizeof(addr));

今度はrconを実際に行う。
送るデータは、
先頭の4byteは\xFF(255),半角スペース,"rcon [チャレンジコード] "rconパスワード" コマンド"
という形になる。
その形になるようにデータをsprintfで成形し、それをsendtoで送る。

	memset(buf, 0, sizeof(buf));
	
#ifdef WIN32
	ret = recv(sock, buf, sizeof(buf), 0);
#else
	ret = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr *)&addr, &addr_size);
#endif
	
	if(ret == -1){
		printf("error: SOCKET_ERROR(recv).\n");
		sock_close(sock);
		return 0;
	}
	
	printf("%s\n", buf+5);
	sock_close(sock);

	return 1;

一時領域をゼロクリアし、コマンドの結果を受け取る。
実行結果が空という理由でrecvで止まることはない。
受け取った情報は、"\xFF\xFF\xFF\xFF 実行結果"となった気がする。
先頭の4byteと半角スペースの5文字分を除くため、+5をして出力。
ソケットを閉じて、正常終了としての1を返してプログラム終了。



こうして見返してみると欠陥だらけでワロタ。これでなんで動くと思ったんだろう。
でも実際に動いてるし、いいよね、別に。

オマケ

配列の最初のゼロクリア部分が怪しいので{}ではクリアされないのか検証してみた。

zero.c
#include <stdio.h>

#define LEN 100000

#define zerocheck(x) { \
	int i; printf("ptr[%p]... ", x); \
	for(i=0; i<LEN && !x[i]; ++i); \
	printf(i==LEN ? "OK\n" : "not zero [%d]\n", i); \
}

void main(void){
	char x[LEN];
	char y[LEN]={};
	char z[LEN]={0};
	
	zerocheck(x);
	zerocheck(y);
	zerocheck(z);
}

実行結果

ubuntu Server 9.10 + gcc version 4.4.1 (Ubuntu 4.4.1-4ubuntu9)

ryozi@ubuntu:~$ ./a.out
ptr[0xbfdd57fc]... not zero [98072]
ptr[0xbfdbd15c]... OK
ptr[0xbfda4abc]... OK

CentOS 5.2 + gcc version 4.3.4 20090511 for GNAT GPL 2009 (20090511) (GCC)

[hlds@localhost ~]$ a.out
ptr[0xbf821978]... not zero [97308]
ptr[0xbf8092d8]... OK
ptr[0xbf7f0c38]... OK

WindowsXP + MinGW gcc version 4.4.0 (GCC)

C:\Documents and Settings\Administrator\My Documents\workspace>zero.exe
ptr[002278C0]... not zero[94612]
ptr[0020F220]... OK
ptr[001F6B80]... OK

結果

gccならゼロクリアに{}を使っても大丈夫!
でも何も代入しないのは不味い!
あとprintfのフォーマットにポインタのアドレスを示す%pがあるけど、
これってLinuxWindowsでは先頭に0xが付いたり付かなかったりするんだね。