Microsoft MVP성태의 닷넷 이야기
Linux: 126. eBPF (bpf2go) - tcp_sendmsg 예제 [링크 복사], [링크+제목 복사],
조회: 666
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 

eBPF (bpf2go) - tcp_sendmsg 예제

지난 글에서는 소켓 모니터링을 위한 별도의 BPF_PROG_TYPE_SOCKET_FILTER 유형을 살펴봤고,

eBPF (bpf2go) - BPF_PROG_TYPE_SOCKET_FILTER 예제 - SEC("socket")
; https://www.sysnet.pe.kr/2/0/14017

이번에는 TCP 전용으로 제공하는 tcp_sendmsg, tcp_recvmsg 커널 함수를 가로채는 예제를 다뤄볼 텐데요, 아래의 질문에 나온 코드로 시작해 보겠습니다.

TCP Packet Payload Sniffing with eBPF Kprobe Attachment to tcp_sendmsg
; https://stackoverflow.com/questions/79112904/tcp-packet-payload-sniffing-with-ebpf-kprobe-attachment-to-tcp-sendmsg

위의 예제를 참고해 BPF_PROG_TYPE_KPROBE(kprobe/kretprobe) 유형으로는 이렇게 코딩할 수 있는데요,

SEC("kprobe/tcp_sendmsg")
int kprobe__tcp_sendmsg(struct pt_regs *ctx) {
    struct sock *sk = (struct sock *)PT_REGS_PARM1(ctx);
    struct msghdr *msg = (struct msghdr *)PT_REGS_PARM2(ctx);

    if (sk == NULL || msg == NULL) {
        return 0;
    }

    __be16 lport = 0;
    __u16 dport = 0;
    bpf_probe_read_kernel(&lport, sizeof(lport), &sk->__sk_common.skc_num);
    bpf_probe_read_kernel(&dport, sizeof(dport), &sk->__sk_common.skc_dport);

    dport = bpf_ntohs(dport);
    bpf_printk("kprobe__tcp_sendmsg: lport = %d, dport = %d\n", lport, dport);
}

테스트를 위해 C# 소켓 클라이언트 코드를 작성한 후,

var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

socket.Connect("20.249.98.255", 80);

// Prepare HTTP GET request
string request = "GET / HTTP/1.1\r\nHost: www.sysnet.pe.kr\r\nConnection: close\r\n\r\n";
byte[] requestBytes = Encoding.ASCII.GetBytes(request);
Console.WriteLine($"Socket payload size: {requestBytes.Length} bytes");

// Send request
socket.Send(requestBytes);

// Receive response
var buffer = new byte[4096];
int bytesReceived;
var response = new StringBuilder();

do
{
    bytesReceived = socket.Receive(buffer);
    response.Append(Encoding.ASCII.GetString(buffer, 0, bytesReceived));
                    
} while (bytesReceived > 0);

Console.WriteLine($"{socket.LocalEndPoint} <==> {socket.RemoteEndPoint}");
socket.Close();

실행하면 C# App 콘솔에는 대략 이런 출력이 나오고,

$ dotnet run
Socket payload size: 61 bytes
192.168.100.50:59718 <==> 20.249.98.255:80

eBPF 측 출력에서도 위의 포트 번호에 대한 로그가 찍히는 것을 확인할 수 있습니다.

$ sudo cat /sys/kernel/debug/tracing/trace_pipe
testapp-2629028 [002] ...31 3694779.211927: bpf_trace_printk: kprobe__tcp_sendmsg: lport = 59718, dport = 80

물론, BPF_PROG_TYPE_TRACING(fentry/fexit) 유형으로 작성해도 코드를 거의 그대로 활용할 수 있습니다.

SEC("fentry/tcp_sendmsg")
int BPF_PROG(fentry_tcp_sendmsg, struct sock *sk, struct msghdr *msg)
{
    __be16 lport = 0;
    __u16 dport = 0;
    bpf_probe_read_kernel(&lport, sizeof(lport), &sk->__sk_common.skc_num);
    bpf_probe_read_kernel(&dport, sizeof(dport), &sk->__sk_common.skc_dport);

    dport = bpf_ntohs(dport);
    bpf_printk("fentry_tcp_sendmsg: lport = %d, dport = %d\n", lport, dport);

    return 0;
}




기왕 알아본 김에, TCP Packet Payload Sniffing with eBPF Kprobe Attachment to tcp_sendmsg 질문에 나온 패킷 페이로드 접근도 시도해 봤는데요, 우선 패킷의 전체 길이는 다음과 같이 구할 수 있었습니다.

struct iov_iter* iter = &(msg->msg_iter);
if (iter == NULL) {
    return 0;
}

__u32 nr_segs = 0;
bpf_probe_read(&nr_segs, sizeof(nr_segs), &iter->nr_segs);
if (nr_segs == 0) {
    return 0;
}

if (iter->iter_type != ITER_IOVEC) {
    return 0;
}

__u64 iov_len;
bpf_probe_read(&iov_len, sizeof(__u64), &(iter->count));
bpf_printk("fentry_tcp_sendmsg: nr_segs = %d, iov_len = %d\n", nr_segs, iov_len);

역시 위에서 다룬 C# 예제 코드와 함께 실행하면 이런 로그가 나오는데요,

testapp-2629028 [002] ...11 3695873.126880: bpf_trace_printk: fentry_tcp_sendmsg: lport = 48428, dport = 80
testapp-2629028 [002] ...11 3695873.126882: bpf_trace_printk: fentry_tcp_sendmsg: nr_segs = 1, iov_len = 61

61 바이트 값이 C# 쪽에서 전송한 페이로드 크기와 정확히 일치합니다.

string request = "GET / HTTP/1.1\r\nHost: www.sysnet.pe.kr\r\nConnection: close\r\n\r\n";
byte[] requestBytes = Encoding.ASCII.GetBytes(request);
Console.WriteLine($"Socket payload size: {requestBytes.Length} bytes"); // requestBytes.Length == 61

그런데, 문제는 패킷 내용을 가져오는 것에는 실패한다는 점입니다. 제가 시도한 코드는 우선 iov 필드를 접근한 후,

const struct iovec *iov = (const struct iovec *)BPF_CORE_READ(iter, iov); // 커널 6.11(~ 6.14) 버전 환경이라면 eBPF 로드 단계에서 오류가 발생합니다.
if (iov == NULL) {
    return 0;
}

bpf_printk("iovec == %p\n", iov);

iovec 구조체의,

struct iovec {
        void *iov_base;
        __kernel_size_t iov_len;
};

필드를 접근해 봤는데요,

void *iovbase;
bpf_probe_read(&iovbase, sizeof(iovbase), &iov[0].iov_base);
__u64 iovlen = 0;
bpf_probe_read(&iovlen, sizeof(iov_len), &iov[0].iov_len);

bpf_printk("iov = %p, iovbase = %p, iov_len = %d\n", iov, iovbase, iovlen); 
// 출력 예: iov = 000000008ecaac37, iovbase = 00000000367bdf95, iov_len = 825184340

출력 결과가 좀 이상합니다. 게다가 여러 번 테스트를 해도, iov 포인터 값은 매번 바뀌는 반면 iovbase와 iov_len 값은 변함없이 저 값으로 나옵니다. iov_len 값 자체도 이상하지만, iovbase는 포인터이기 때문에 매번 다른 값이 나와야 정상입니다.

실제로 iovbase 주소를 통해 데이터를 읽어오려 하면,

char* ptr = (char*)iovbase;
bpf_printk("data: %c\n", ptr[0]);

eBPF 프로그램 로드 단계에서 permission denied 오류가 발생합니다.

load program: permission denied: 84: (71) r3 = *(u8 *)(r7 +0): R7 invalid mem access 'scalar' (112 line(s) omitted)

혹시나 싶어 bpf_probe_read_kernel() 함수로 읽어보면,

char data[4];
long ret = bpf_probe_read_kernel(data, 1, iovbase);
bpf_printk("ret == %d\n", ret); // 출력 결과: -34

/*
$ sudo apt install moreutils
$ errno 34
ERANGE 34 Numerical result out of range
*/

아마도 정상적인 포인터 주소가 아니므로 읽기에 실패해 ERANGE 오류가 나는 것 같습니다. 더 이상, 딱히 제 지식으로는 더 해볼 것이 없군요. ^^;




검색해 보면,

sock_sendmsg(): implicit function calls by Kernel
; https://stackoverflow.com/questions/21623814/sock-sendmsg-implicit-function-calls-by-kernel

Extract packet data using BPF from struct msghr
; https://stackoverflow.com/questions/76483747/extract-packet-data-using-bpf-from-struct-msghr

tcp_sendmsg가 아닌 "sock_sendmsg" 함수를 가로채서 시도한 사례가 있는데요, 이상하게 제 환경(Ubuntu 22.04, 24.04) 환경에서는 sock_sendmsg 함수가 동작하지 않습니다. 대신 이름을 바꾸는 경우,

SEC("fentry/sock_sendmsg_my")
int test_sock_sendmsg(void *ctx) {
    bpf_printk("test_sock_sendmsg\n");
    return 0;
}

eBPF 프로그램 로드 단계에서 "program myf_sock_sendmsg: attach Tracing/TraceFEntry: fentry sock_sendmsg_my not supported" 오류가 발생하는 걸로 봐서 sock_sendmsg 함수가 커널에서 없어진 것은 아닌 듯합니다. 암튼, 정상적인 이름으로 로딩을 해도 "test_sock_sendmsg" 메시지가 전혀 찍히지 않습니다.




참고로 현재 tcp_sendmsg의 커널 소스 코드는 이런데,

// https://github.com/torvalds/linux/blob/master/net/ipv4/tcp.c#L1388
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
	int ret;

	lock_sock(sk);
	ret = tcp_sendmsg_locked(sk, msg, size);
	release_sock(sk);

	return ret;
}
EXPORT_SYMBOL(tcp_sendmsg);

과거에는 kiocb 구조체 포인터가 첫 번째 인자로 들어왔던 적이 있었던 것 같습니다.

// https://www.cubrid.org/blog/3826497
// int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t size)

$ cat vmlinux.h | grep -A 13 "struct kiocb {"
struct kiocb {
        struct file *ki_filp;
        loff_t ki_pos;
        void (*ki_complete)(struct kiocb *, long int, long int);
        void *private;
        int ki_flags;
        u16 ki_hint;
        u16 ki_ioprio;
        union {
                unsigned int ki_cookie;
                struct wait_page_queue *ki_waitq;
        };
};

기타 소켓과 관련된 타입은 아래에 정리해 봤습니다.

$ cat vmlinux.h | grep -A 12 "struct socket {"
struct socket {
        socket_state state;
        short int type;
        long unsigned int flags;
        struct file *file;
        struct sock *sk;
        const struct proto_ops *ops;
        long: 64;
        long: 64;
        long: 64;
        struct socket_wq wq;
};

$ cat vmlinux.h | grep -A 9 "enum sock_type {"
enum sock_type {
        SOCK_STREAM = 1,
        SOCK_DGRAM = 2,
        SOCK_RAW = 3,
        SOCK_RDM = 4,
        SOCK_SEQPACKET = 5,
        SOCK_DCCP = 6,
        SOCK_PACKET = 10,
};

$ cat vmlinux.h | grep -B 6 "} socket_state;"
typedef enum {
        SS_FREE = 0,
        SS_UNCONNECTED = 1,
        SS_CONNECTING = 2,
        SS_CONNECTED = 3,
        SS_DISCONNECTING = 4,
} socket_state;

$ cat vmlinux.h | grep -A 105 "struct sock {"
struct sock {
        struct sock_common __sk_common;
        socket_lock_t sk_lock;
        atomic_t sk_drops;
        int sk_rcvlowat;
        struct sk_buff_head sk_error_queue;
        struct sk_buff *sk_rx_skb_cache;
        struct sk_buff_head sk_receive_queue;
        struct {
                atomic_t rmem_alloc;
                int len;
                struct sk_buff *head;
                struct sk_buff *tail;
        } sk_backlog;
        int sk_forward_alloc;
        unsigned int sk_ll_usec;
        unsigned int sk_napi_id;
        int sk_rcvbuf;
        int sk_wait_pending;
        struct sk_filter *sk_filter;
        union {
                struct socket_wq *sk_wq;
                struct socket_wq *sk_wq_raw;
        };
        struct xfrm_policy *sk_policy[2];
        struct dst_entry *sk_rx_dst;
        int sk_rx_dst_ifindex;
        u32 sk_rx_dst_cookie;
        struct dst_entry *sk_dst_cache;
        atomic_t sk_omem_alloc;
        int sk_sndbuf;
        int sk_wmem_queued;
        refcount_t sk_wmem_alloc;
        long unsigned int sk_tsq_flags;
        union {
                struct sk_buff *sk_send_head;
                struct rb_root tcp_rtx_queue;
        };
        struct sk_buff *sk_tx_skb_cache;
        struct sk_buff_head sk_write_queue;
        __s32 sk_peek_off;
        int sk_write_pending;
        __u32 sk_dst_pending_confirm;
        u32 sk_pacing_status;
        long int sk_sndtimeo;
        struct timer_list sk_timer;
        __u32 sk_priority;
        __u32 sk_mark;
        long unsigned int sk_pacing_rate;
        long unsigned int sk_max_pacing_rate;
        struct page_frag sk_frag;
        netdev_features_t sk_route_caps;
        netdev_features_t sk_route_nocaps;
        netdev_features_t sk_route_forced_caps;
        int sk_gso_type;
        unsigned int sk_gso_max_size;
        gfp_t sk_allocation;
        __u32 sk_txhash;
        u8 sk_padding: 1;
        u8 sk_kern_sock: 1;
        u8 sk_no_check_tx: 1;
        u8 sk_no_check_rx: 1;
        u8 sk_userlocks: 4;
        u8 sk_pacing_shift;
        u16 sk_type;
        u16 sk_protocol;
        u16 sk_gso_max_segs;
        long unsigned int sk_lingertime;
        struct proto *sk_prot_creator;
        rwlock_t sk_callback_lock;
        int sk_err;
        int sk_err_soft;
        u32 sk_ack_backlog;
        u32 sk_max_ack_backlog;
        kuid_t sk_uid;
        u8 sk_prefer_busy_poll;
        u16 sk_busy_poll_budget;
        spinlock_t sk_peer_lock;
        struct pid *sk_peer_pid;
        const struct cred *sk_peer_cred;
        long int sk_rcvtimeo;
        ktime_t sk_stamp;
        u16 sk_tsflags;
        int sk_bind_phc;
        u8 sk_shutdown;
        atomic_t sk_tskey;
        atomic_t sk_zckey;
        u8 sk_clockid;
        u8 sk_txtime_deadline_mode: 1;
        u8 sk_txtime_report_errors: 1;
        u8 sk_txtime_unused: 6;
        struct socket *sk_socket;
        void *sk_user_data;
        void *sk_security;
        struct sock_cgroup_data sk_cgrp_data;
        struct mem_cgroup *sk_memcg;
        void (*sk_state_change)(struct sock *);
        void (*sk_data_ready)(struct sock *);
        void (*sk_write_space)(struct sock *);
        void (*sk_error_report)(struct sock *);
        int (*sk_backlog_rcv)(struct sock *, struct sk_buff *);
        void (*sk_destruct)(struct sock *);
        struct sock_reuseport *sk_reuseport_cb;
        struct bpf_local_storage *sk_bpf_storage;
        struct callback_head sk_rcu;
};

$ cat vmlinux.h | grep -A 60 "struct sock_common {"
struct sock_common {
        union {
                __addrpair skc_addrpair;
                struct {
                        __be32 skc_daddr;
                        __be32 skc_rcv_saddr;
                };
        };
        union {
                unsigned int skc_hash;
                __u16 skc_u16hashes[2];
        };
        union {
                __portpair skc_portpair;
                struct {
                        __be16 skc_dport;
                        __u16 skc_num;
                };
        };
        short unsigned int skc_family;
        volatile unsigned char skc_state;
        unsigned char skc_reuse: 4;
        unsigned char skc_reuseport: 1;
        unsigned char skc_ipv6only: 1;
        unsigned char skc_net_refcnt: 1;
        int skc_bound_dev_if;
        union {
                struct hlist_node skc_bind_node;
                struct hlist_node skc_portaddr_node;
        };
        struct proto *skc_prot;
        possible_net_t skc_net;
        struct in6_addr skc_v6_daddr;
        struct in6_addr skc_v6_rcv_saddr;
        atomic64_t skc_cookie;
        union {
                long unsigned int skc_flags;
                struct sock *skc_listener;
                struct inet_timewait_death_row *skc_tw_dr;
        };
        int skc_dontcopy_begin[0];
        union {
                struct hlist_node skc_node;
                struct hlist_nulls_node skc_nulls_node;
        };
        short unsigned int skc_tx_queue_mapping;
        short unsigned int skc_rx_queue_mapping;
        union {
                int skc_incoming_cpu;
                u32 skc_rcv_wnd;
                u32 skc_tw_rcv_nxt;
        };
        refcount_t skc_refcnt;
        int skc_dontcopy_end[0];
        union {
                u32 skc_rxhash;
                u32 skc_window_clamp;
                u32 skc_tw_snd_nxt;
        };
};

$ cat vmlinux.h | grep -A 12 "struct msghdr {"
struct msghdr {
        void *msg_name;
        int msg_namelen;
        struct iov_iter msg_iter;
        union {
                void *msg_control;
                void *msg_control_user;
        };
        bool msg_control_is_user: 1;
        __kernel_size_t msg_controllen;
        unsigned int msg_flags;
        struct kiocb *msg_iocb;
};

$ cat vmlinux.h | grep -A 22 "struct iov_iter {"
struct iov_iter {
        u8 iter_type;
        bool nofault;
        bool data_source;
        size_t iov_offset;
        size_t count;
        union {
                const struct iovec *iov;
                const struct kvec *kvec;
                const struct bio_vec *bvec;
                struct xarray *xarray;
                struct pipe_inode_info *pipe;
        };
        union {
                long unsigned int nr_segs;
                struct {
                        unsigned int head;
                        unsigned int start_head;
                };
                loff_t xarray_start;
        };
};

$ cat vmlinux.h | grep -A 7 "enum iter_type {"
enum iter_type {
        ITER_IOVEC = 0,
        ITER_KVEC = 1,
        ITER_BVEC = 2,
        ITER_PIPE = 3,
        ITER_XARRAY = 4,
        ITER_DISCARD = 5,
};




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 10/14/2025]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 121  122  123  124  125  126  127  128  129  130  [131]  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
2882정성태3/10/201527766.NET Framework: 504. OpenCover 코드 커버리지 도구의 동작 방식을 통해 살펴보는 Calli IL 코드 사용법
2881정성태3/9/201528471개발 환경 구성: 261. OpenCover 오픈 소스를 이용한 .NET 코드 커버리지(Code coverage)
2880정성태3/7/201526349개발 환경 구성: 260. C# Code Coverage 도구 - Semantic Designs 소개
2879정성태3/3/201531610개발 환경 구성: 259. Visual Studio 없이 Visual C++ 컴파일하는 방법
2878정성태2/28/201532355.NET Framework: 503. == 연산자보다는 Equals 메서드의 호출이 더 권장됩니다. [3]파일 다운로드1
2877정성태2/28/201525903.NET Framework: 502. 연산자 재정의(operator overloading)와 메서드 재정의(method overriding)의 다른 점 - 가상 함수 호출 여부 [3]파일 다운로드1
2876정성태2/27/201528070VS.NET IDE: 98. IntegraStudio - Visual Studio에서 Java 프로그램 개발
2875정성태2/26/201527812디버깅 기술: 72. Visual Studio 2013에서의 sos.dll 사용 제한
2874정성태2/26/201523855디버깅 기술: 71. windbg + 닷넷 디버깅 (2) - null 체크 패턴
2873정성태2/25/201542134.NET Framework: 501. FtpWebRequest 타입을 이용해 FTP 파일 업로드 [4]파일 다운로드1
2872정성태2/25/201525855디버깅 기술: 70. windbg + 닷넷 디버깅 (1) - 배열 인덱스 사용 패턴
2871정성태2/24/201529295개발 환경 구성: 258. 윈도우 8.1에서 방화벽과 함께 FTP 서버 여는 (하지만, 권장하지 않는) 방법 [1]
2870정성태2/24/201530929개발 환경 구성: 257. 윈도우 8.1에서 방화벽과 함께 FTP 서버 여는 방법
2869정성태2/23/201524306.NET Framework: 500. struct로 정의한 값 형식(Value Type)의 경우 Equals 재정의를 권장합니다.파일 다운로드1
2868정성태2/23/201529543VS.NET IDE: 97. Visual C++ 프로젝트 디버깅 시에 Step-Into(F11) 동작이 원치 않는 함수로 진입하는 것을 막는 방법 [2]
2867정성태2/23/201522999오류 유형: 273. File History - Failed to initiate user data backup (error 80070005)
2866정성태2/23/201525189오류 유형: 272. WAT080 : Failed to locate the Windows Azure SDK. Please make sure the Windows Azure SDK v2.1 is installed.
1868정성태2/20/201522142오류 유형: 271. The type '...' cannot be used as type parameter 'TContext' in the generic type or method 'System.ServiceModel.DomainServices.EntityFramework.LinqToEntitiesDomainService&lt;T&gt;
1866정성태2/20/201523097오류 유형: 270. "aspnet_regiis -i" 실행 시 0x00000006 오류 해결 방법
1865정성태2/20/201524377.NET Framework: 499. 특정 닷넷 프레임워크 버전 이후부터 제공되는 타입을 사용해야 한다면?
1864정성태2/18/201529213.NET Framework: 498. C#으로 간단하게 만들어 본 ASCII Art 프로그램 [2]파일 다운로드1
1862정성태2/18/201533559.NET Framework: 497. .NET Garbage Collection에 대한 정리 [6]
1861정성태2/18/201528748.NET Framework: 496. 마우스 커서가 놓인 지점의 문자열 얻는 방법 [1]파일 다운로드1
1860정성태2/18/201529017.NET Framework: 495. CorElementType의 요소 값 설명파일 다운로드1
1859정성태2/17/201529840Windows: 106. 컴퓨터를 재부팅하면 절전(Power Saver) 전원 모드로 돌아가는 경우
1858정성태2/16/201539478Windows: 105. 자동으로 로그아웃/잠김 화면 상태로 전환된다면? [2]
... 121  122  123  124  125  126  127  128  129  130  [131]  132  133  134  135  ...