Microsoft MVP성태의 닷넷 이야기
Linux: 126. eBPF (bpf2go) - tcp_sendmsg 예제 [링크 복사], [링크+제목 복사],
조회: 437
글쓴 사람
정성태 (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)
13851정성태12/23/20248122디버깅 기술: 211. Windbg - 커널 모드 디버깅 상태에서 사용자 프로그램을 디버깅하는 방법
13850정성태12/23/202410440오류 유형: 940. "Application Information" 서비스를 중지한 경우, "This file does not have an app associated with it for performing this action."
13849정성태12/20/202410133디버깅 기술: 210. Windbg - 논리(가상) 주소를 Segmentation을 거쳐 선형 주소로 변경
13848정성태12/18/20249329디버깅 기술: 209. Windbg로 알아보는 Prototype PTE파일 다운로드2
13847정성태12/18/20248818오류 유형: 939. golang - 빌드 시 "unknown directive: toolchain" 오류 빌드 시 이런 오류가 발생한다면?
13846정성태12/17/202410712디버깅 기술: 208. Windbg로 알아보는 Trans/Soft PTE와 2가지 Page Fault 유형파일 다운로드1
13845정성태12/16/20247736디버깅 기술: 207. Windbg로 알아보는 PTE (_MMPTE)
13844정성태12/14/202411246디버깅 기술: 206. Windbg로 알아보는 PFN (_MMPFN)파일 다운로드1
13843정성태12/13/20248415오류 유형: 938. Docker container 내에서 빌드 시 error MSB3021: Unable to copy file "..." to "...". Access to the path '...' is denied.
13842정성태12/12/20249030디버깅 기술: 205. Windbg - KPCR, KPRCB
13841정성태12/11/20249719오류 유형: 937. error MSB4044: The "ValidateValidArchitecture" task was not given a value for the required parameter "RemoteTarget"
13840정성태12/11/20248744오류 유형: 936. msbuild - Your project file doesn't list 'win' as a "RuntimeIdentifier"
13839정성태12/11/202410176오류 유형: 936. msbuild - error CS1617: Invalid option '12.0' for /langversion. Use '/langversion:?' to list supported values.
13838정성태12/4/20249975오류 유형: 935. Windbg - Breakpoint 0's offset expression evaluation failed.
13837정성태12/3/202410857디버깅 기술: 204. Windbg - 윈도우 핸들 테이블 (3) - Windows 10 이상인 경우
13836정성태12/3/20247988디버깅 기술: 203. Windbg - x64 가상 주소를 물리 주소로 변환 (페이지 크기가 2MB인 경우)
13835정성태12/2/202410613오류 유형: 934. Azure - rm: cannot remove '...': Directory not empty
13834정성태11/29/202410678Windows: 275. C# - CUI 애플리케이션과 Console 윈도우 (Windows 10 미만의 Classic Console 모드인 경우) [1]파일 다운로드1
13833정성태11/29/20249761개발 환경 구성: 737. Azure Web App에서 Scale-out으로 늘어난 리눅스 인스턴스에 SSH 접속하는 방법
13832정성태11/27/20249584Windows: 274. Windows 7부터 도입한 conhost.exe
13831정성태11/27/20248421Linux: 111. eBPF - BPF_MAP_TYPE_PERF_EVENT_ARRAY, BPF_MAP_TYPE_RINGBUF에 대한 다양한 용어들
13830정성태11/25/202411451개발 환경 구성: 736. 파이썬 웹 앱을 Azure App Service에 배포하기
13829정성태11/25/202411110스크립트: 67. 파이썬 - Windows 버전에서 함께 설치되는 py.exe
13828정성태11/25/20249060개발 환경 구성: 735. Azure - 압축 파일을 이용한 web app 배포 시 디렉터리 구분이 안 되는 문제파일 다운로드1
13827정성태11/25/202410302Windows: 273. Windows 환경의 파일 압축 방법 (tar, Compress-Archive)
13826정성태11/21/202410217닷넷: 2313. C# - (비밀번호 등의) Console로부터 입력받을 때 문자열 출력 숨기기(echo 끄기)파일 다운로드1
1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...