串を通して遊んでみた(proxychains)
串を通して遊んでみた(proxychains)
週末に串を通して遊ぶという、特に利益が出る訳でもなく、ただ単に個人的な知的好奇心を満たすだけの活動を実施したので備忘録として記事に残しておく。
主にproxychains
の話。
"串を通す"とは
この記事のタイトルを見て内容に興味を持ってくれた方々であれば、おそらく説明不要だと思っている。
proxychainsとは
その名の通り、proxy
にchains
してくれるツール。
UNIX系のシステム上で利用可能で、パッケージマネージャーから落としてくるなり、ソースからビルドするなりすれば使えるようになる。
最も簡単な使い方としては、こんな風にproxychains
コマンドに続けて実行したいコマンドを渡してあげるだけ。(curlの例)
$ proxychains curl https://www.google.com/
これだけで、予め設定したプロキシを通して通信を行ってくれる。
(設定に関しては後述)
対応プロトコル
proxychains
は、HTTP
プロキシとSOCKS
プロキシに対応している。
HTTP
の方は一般的なHTTP通信を中継してくれるプロキシ。
SOCKS
の方は聞き慣れない方もいるかもしれない。
超絶ざっくり言うと、TCP/UDP
レイヤの通信を中継してくれる。
Tor
プロキシなんかはSOCKS
を利用して複数ノードに渡って通信を中継している。
靴下とはなんの関係もない。
4
とか4a
とか5
とかバージョンがある。
設定ファイル
proxychains
は、環境変数 or 設定ファイルに基づいて利用するプロキシを決定する。
細かい部分は割愛するが、予め決められた順序で設定情報を探索するようにできている。
(具体的な順序が知りたい方は、公式のREADMEを参照)
特に何も指定されなければ、/etc/proxychains.conf
(バージョンによってはproxychains4.conf
)が参照される。
設定ファイルの中身は、"利用するプロキシサーバーのリスト"、"タイムアウトの秒数"や"DNSによる名前解決もプロキシさせるかどうか"などの設定が含まれる。
以下は利用するプロキシサーバーの設定。[ProxyList]
として複数設定できる。
デフォルトでローカルのTorデーモン(127.0.0.1:9050
)が設定されている。
...
[ProxyList]
# add proxy here ...
# meanwhile
# defaults set to "tor"
socks4 127.0.0.1 9050
他にも個人的に興味深かった箇所が、[ProxyList]
で列挙したプロ棋士達をどう扱うかといった設定。
dynamic_chain
, strict_chain
, random_chain
のいずれかを指定することができ、それぞれの挙動としては以下の通り。
-
dynamic_chain
- 設定したプロキシ一覧を順番に利用する
- ダウンしているプロキシはスキップされる
-
strict_chain
- 設定したプロキシ一覧を順番に利用する
- すべてのプロキシがオンラインでなければならない
-
random_chain
- 設定したプロキシ一覧がランダムで利用される
-
this option is good to test your IDS :)
って書いてある
遊んでみる
せっかくなので(?)、グローバルIPを返してくれるサービスも自分で立てて試してみる。
適当にRustで書いたコードをCloudflare Workersにデプロイしておく。(色々と雑ですが検証用ということでお許しください)
#[event(fetch)]
pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {
...
router
...
.get("/global-ip", |req, ctx| {
let connecting_ip = match req.headers().get("CF-Connecting-IP") {
Ok(value) => {
match value {
Some(connecting_ip) => connecting_ip,
None => "Failed to fetch source IP.".to_string(),
}
},
Err(_) => "Failed to fetch source IP.".to_string(),
};
Response::from_json(&json!({ "globalIP": connecting_ip }))
})
.run(req, env)
.await
}
デプロイして動作確認。
$ wrangler publish
$ curl https://~~~.workers.dev/global-ip
{"globalIP":"~~~.~~~.~~~.~~~"}
適当にsquid
を立てて通すようにしてみる。
$ docker run -d --name squid-container -p 3128:3128 ubuntu/squid:latest
# proxychainsの設定を変更しておく
$ cat /etc/proxychains4.conf
[ProxyList]
# add proxy here ...
# meanwile
# defaults set to "tor"
# socks4 127.0.0.1 9050
http 127.0.0.1 3128 # squidのポートを追加
$ proxychains curl https://~~~.workers.dev/global-ip
{"globalIP":"~~~.~~~.~~~.~~~"}
⬇️ squid
のログにも出てる
...
1677407264.464 119 ~~~.~~~.~~~.~~~ TCP_TUNNEL/200 5842 CONNECT ~~~.workers.dev:443 - HIER_DIRECT/~~~.~~~.~~~.~~~-
ローカルのコンテナを経由させているだけなのでなんの代わり映えもないが、とりあえず自分で立てたプロキシサーバーでも動いていることが確認できた。
もちろんデフォルトの設定で稼働させれば、TorのExitノードのIPが確認できる。
どういう仕組み?
毎度仕組みが気になるので中身を覗いてみる。
そもそも、何故proxychains
というコマンドに続けて本来実行したいコマンドを並べるだけで、そのコマンドがプロキシを経由するようになるのか。(コマンドも改変されてないし、OSの設定も変更されていないのに)
結論から言うと、proxychains
によってconnect
システムコールやgethostbyname
などの呼び出しがフックされ、独自の処理に差し替えられているためということになる。
libproxychains.c
proxychains
コマンドのエントリーポイントはmain.c
に書かれているわけだが、実はここを見てもオプションの処理やexecvp
によるプロセス実行の処理しかかかれていない。
前述のような挙動の本体は、共有ライブラリとしてビルドされるlibproxychains.c
に書かれている。
このファイルのdo_init
関数が肝で、上書きたい各関数のシンボルに独自の処理を紐付け、元々の処理をtrue_
から始まるシンボルに紐付け直している。
connect
システムコールであればその実態はtrue_connect
に紐付けられ、connect
の呼び出しはproxychains
独自の処理にすげ替えられる。
...
// true_から始まるシンボルの定義
extern connect_t true_connect;
extern gethostbyname_t true_gethostbyname;
extern getaddrinfo_t true_getaddrinfo;
extern freeaddrinfo_t true_freeaddrinfo;
...
...
// X = connectの場合、以下のように置換するマクロ
// do { true_connect = load_sym("connect"); } while(0)
#define SETUP_SYM(X) do { true_ ## X = load_sym( # X, X ); } while(0)
static void do_init(void) {
...
proxychains_write_log(LOG_PREFIX "DLL init\n");
SETUP_SYM(connect);
SETUP_SYM(gethostbyname);
SETUP_SYM(getaddrinfo);
SETUP_SYM(freeaddrinfo);
SETUP_SYM(gethostbyaddr);
SETUP_SYM(getnameinfo);
...
}
プロキシサーバーの選択
ちなみにrandom_chain
やらの設定でプロキシサーバーを選択する部分は、connect
→connect_proxy_chain
と来てさらにコールされるselect_proxy
で行われている。
static proxy_data *select_proxy(select_type how, proxy_data * pd, unsigned int proxy_count, unsigned int *offset) {
unsigned int i = 0, k = 0;
if(*offset >= proxy_count)
return NULL;
switch (how) {
case RANDOMLY:
do {
k++;
// この辺でランダムに選んだりしている
i = 0 + get_rand_int(proxy_count);
} while(pd[i].ps != PLAY_STATE && k < proxy_count * 100);
break;
case FIFOLY:
for(i = *offset; i < proxy_count; i++) {
if(pd[i].ps == PLAY_STATE) {
*offset = i;
break;
}
}
default:
break;
}
if(i >= proxy_count)
i = 0;
return (pd[i].ps == PLAY_STATE) ? &pd[i] : NULL;
}
proxychains
から叩かせてみる
自作SOCKS4サーバー(雑)を書いて自作SOCKS4サーバーを雑に書いてみてproxychains
から叩かせてみる。
SOCKS4のRFCが見当たらなかったので、Wikipediaのプロトコル説明を読みながら書く。
まずはクライアントからの最初のリクエストを受けるところ。(細かいところは割愛)
fn handle_client(mut stream: TcpStream) -> std::io::Result<()> {
let mut buf = [0; 8];
stream.read(&mut buf)?;
let mut id = Vec::new();
loop {
let mut b = [0; 1];
stream.read(&mut b)?;
if b[0] == b'\0' {
break;
}
id.extend_from_slice(&b);
}
let req = Request::new(
buf[0],
buf[1],
((buf[2] as u16) << 8) + buf[3] as u16,
((buf[4] as u32) << 24) + ((buf[5] as u32) << 16) + ((buf[6] as u32) << 8) + buf[7] as u32,
id,
);
println!("received: {:02x?}", buf);
println!("req: {:?}", req);
...
以下の構造で送られたバイト列をパースするだけ。
- プロトコルバージョン (1 byte)
- コマンドコード (1 byte)
- 転送先ポート番号 (2 bytes)
- 転送先IPアドレス (4 bytes)
次にサーバーからのレスポンスを返すところ。(細かいところは割愛)
...
let res = Response::new(
0x00,
0x5a,
0x00,
req.dst_addr.into(),
);
let res_bytes = res.to_bytes();
println!("transmit: {:02x?}", res_bytes);
println!("res: {:?}", res);
stream.write(&res_bytes)?;
...
これも以下の構造で構成したバイト列を流すだけ。
- Null 0x00 (1 byte)
- リプライコード (1 byte)
-
0x5a
でリクエスト承諾 -
0x5b
,0x5c
,0x5d
でエラー
-
- 転送先ポート番号 (2 bytes)
- SOCKS4ではここはクライアントで無視されるらしいのでぶっちゃけ値は何でも良い
- 転送先IPアドレス (4 bytes)
- ここも無視される
ここまでくれば、この先クライアントから送られてきたデータは転送先のサーバーに横流しにするフェーズに入る。
ファイルディスクリプタを繋ぎ変えて流せばできるのかもしれないが、細かいことは置いといて雑につなぎこむ。
...
let mut client = TcpStream::connect(format!("{}:{}", req.dst_addr.to_string(), req.dst_port))?;
let mut buf = [0; 256];
stream.read(&mut buf)?;
client.write(&mut buf)?;
let mut buf = [0; 256];
client.read(&mut buf)?;
stream.write(&mut buf)?;
...
あとは実際にproxychains
から叩かせてみる。
先程こしらえたWorkersのレスポンスを取ってきても良いのだが、名前解決まで実装するのがめんどくさい + Workersのサーバーレス環境にIPで直アクセスが許可されていないので、ローカルに適当にサーバーを立ててそこに転送させることにする。
# 転送先のサーバーを起動
$ touch ./proxy-test.html
$ echo 'This is on the super awesome http server.' > ./proxy-test.html
$ python -m http.server 9000
# 自作SOCKSサーバーを起動
$ cargo run
# proxychainsの設定を変更しておく(名前解決をプロキシさせない + プロキシサーバーを追加)
$ cat /etc/proxychains4.conf
...
[ProxyList]
# add proxy here ...
# meanwile
# defaults set to "tor"
# socks4 127.0.0.1 9050
socks4 127.0.0.1 11111 # 自作SOCKS4サーバー
# まずは普通に叩いてみる (さっき作ったテスト用のテキストが返ってくるだけ)
$ curl http://127.0.0.1:9000/proxy-test.html
This is on the super awesome http server.
# proxychains実行
[proxychains] config file found: /etc/proxychains4.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.16
[proxychains] Strict chain ... 127.0.0.1:11111 ... 127.0.0.1:9000 ... OK
This is on the super http server.
通った。
最後に
プロキシおもろい。
これで休みの日はSOCKS4プロキシサーバーもどきを作ってましたということができます。
Discussion