Microsoft MVP성태의 닷넷 이야기
Linux: 126. eBPF (bpf2go) - tcp_sendmsg 예제 [링크 복사], [링크+제목 복사],
조회: 435
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




1  2  3  4  5  6  [7]  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13876정성태1/25/20259467닷넷: 2314. C# - ProcessStartInfo 타입의 Arguments와 ArgumentList파일 다운로드1
13875정성태1/24/20257067스크립트: 69. 파이썬 - multiprocessing 패키지의 spawn 모드로 동작하는 uvicorn의 workers
13874정성태1/24/20259089스크립트: 68. 파이썬 - multiprocessing Pool의 기본 프로세스 시작 모드(spawn, fork)
13873정성태1/23/20256836디버깅 기술: 217. WinDbg - PCI 장치 열거파일 다운로드1
13872정성태1/23/20256519오류 유형: 944. WinDbg - 원격 커널 디버깅이 연결은 되지만 Break (Ctrl + Break) 키를 눌러도 멈추지 않는 현상
13871정성태1/22/20258453Windows: 278. Windows - 윈도우를 다른 모니터 화면으로 이동시키는 단축키 (Window + Shift + 화살표)
13870정성태1/18/20259353개발 환경 구성: 741. WinDbg - 네트워크 커널 디버깅이 가능한 NIC 카드 지원 확대
13869정성태1/18/20257756개발 환경 구성: 740. WinDbg - _NT_SYMBOL_PATH 환경 변수에 설정한 경로로 심벌 파일을 다운로드하지 않는 경우
13868정성태1/17/20257926Windows: 277. Hyper-V - Windows 11 VM의 Enhanced Session 모드로 로그인을 할 수 없는 문제
13867정성태1/17/20259499오류 유형: 943. Hyper-V에 Windows 11 설치 시 "This PC doesn't currently meet Windows 11 system requirements" 오류
13866정성태1/16/202510737개발 환경 구성: 739. Windows 10부터 바뀐 device driver 서명 방법
13865정성태1/15/20258809오류 유형: 942. C# - .NET Framework 4.5.2 이하의 버전에서 HttpWebRequest로 https 호출 시 "System.Net.WebException" 예외 발생
13864정성태1/15/20258992Linux: 114. eBPF를 위해 필요한 SELinux 보안 정책
13863정성태1/14/20256942Linux: 113. Linux - 프로세스를 위한 전용 SELinux 보안 문맥 지정
13862정성태1/13/20258044Linux: 112. Linux - 데몬을 위한 SELinux 보안 정책 설정
13861정성태1/11/20258520Windows: 276. 명령행에서 원격 서비스를 동기/비동기로 시작/중지
13860정성태1/10/20257467디버깅 기술: 216. WinDbg - 2가지 유형의 식 평가 방법(MASM, C++)
13859정성태1/9/20259092디버깅 기술: 215. Windbg - syscall 이후 실행되는 KiSystemCall64 함수 및 SSDT 디버깅
13858정성태1/8/20259684개발 환경 구성: 738. PowerShell - 원격 호출 시 "powershell.exe"가 아닌 "pwsh.exe" 환경으로 명령어를 실행하는 방법
13857정성태1/7/20259649C/C++: 187. Golang - 콘솔 응용 프로그램을 Linux 데몬 서비스를 지원하도록 변경파일 다운로드1
13856정성태1/6/20257301디버깅 기술: 214. Windbg - syscall 단계까지의 Win32 API 호출 (예: Sleep)
13855정성태12/28/20249980오류 유형: 941. Golang - os.StartProcess() 사용 시 오류 정리
13854정성태12/27/20249616C/C++: 186. Golang - 콘솔 응용 프로그램을 NT 서비스를 지원하도록 변경파일 다운로드1
13853정성태12/26/20247748디버깅 기술: 213. Windbg - swapgs 명령어와 (Ring 0 커널 모드의) FS, GS Segment 레지스터
13852정성태12/25/202410634디버깅 기술: 212. Windbg - (Ring 3 사용자 모드의) FS, GS Segment 레지스터파일 다운로드1
13851정성태12/23/20248122디버깅 기술: 211. Windbg - 커널 모드 디버깅 상태에서 사용자 프로그램을 디버깅하는 방법
1  2  3  4  5  6  [7]  8  9  10  11  12  13  14  15  ...