🪡

串を通して遊んでみた(proxychains)

2023/02/27に公開

串を通して遊んでみた(proxychains)

週末に串を通して遊ぶという、特に利益が出る訳でもなく、ただ単に個人的な知的好奇心を満たすだけの活動を実施したので備忘録として記事に残しておく。
主にproxychainsの話。

"串を通す"とは

この記事のタイトルを見て内容に興味を持ってくれた方々であれば、おそらく説明不要だと思っている。

proxychainsとは

その名の通り、proxychainsしてくれるツール。
UNIX系のシステム上で利用可能で、パッケージマネージャーから落としてくるなり、ソースからビルドするなりすれば使えるようになる。
https://github.com/haad/proxychains

最も簡単な使い方としては、こんな風に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やらの設定でプロキシサーバーを選択する部分は、connectconnect_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;
}

自作SOCKS4サーバー(雑)を書いてproxychainsから叩かせてみる

自作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プロキシサーバーもどきを作ってましたということができます。

GitHubで編集を提案

Discussion