标题: 未知网络服务分析之调试技巧 创建: 2018-12-11 13:22 更新: 2021-04-16 12:55 链接: https://scz.617.cn/unix/201812111322.txt 本文分享一些调试经验,见仁见智。 某IoT设备,已经设法搞掂了调试环境,拥有后台root shell及gdb server。这是个 ARM/Linux,上传ARM版全功能busybox以改善调试环境。netstat看到某个进程(some) 侦听了一堆TCP、UDP端口,显然该程序是该IoT设备的主程序。 定位UDP端口的处理流程,一般这种会调用recvfrom(),分析后发现some!recvfrom() 实际调用libuClibc!recvfrom()。 ssize_t recvfrom ( int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen ); 在IDA中静态分析libuClibc!recvfrom(),在其尾部寻找适当位置,确保有寄存器或 栈变量对应形参buf、src_addr及返回值n,确保此时UDP数据已在buf中。然后在此位 置设置条件断点,比如,当源端口等于0x1314、源IP等于x.x.x.x时显示读取的报文: b *0xhhhhhhhh commands $bpnum silent get_big_endian_2 *(char**)($sp+0x28)+2 set $sport=$ret get_big_endian_4 *(char**)($sp+0x28)+4 set $src=$ret if (($src==0xhhhhhhhh)&&($sport==0x1314)) set scheduler-locking on display i r r7 r8 r10 r4 x/2wx $sp+0x28 db $r8 $r4 else c end end (示例,勿照搬) 为什么不直接拦截libuClibc!recvfrom()?因为入口无法知道源IP、源端口,也不知 道UDP数据,出口知道,可以在出口设条件断点。为什么不在some!recvfrom()的 RetAddr处设断?因为有很多地方调用some!recvfrom(),调试目的就是找出哪个地方 处理所关心的UDP端口。如果能直接找到创建套接字、绑定端口的地方,无需上述方 案。这里介绍的是通用调试思路,在静态分析、交叉引用等手段无法直接定位的情况 下仍然有效。 如果some是单线程进程,可以直接拦截libuClibc!recvfrom(),记录相应形参,设法 继续执行到RetAddr,检查数据后做出相应动作。欲达此目的,可能比你想像的复杂, 参看: 《2.15 GDB断点后处理commands中finish/until/tb带来的问题》 https://scz.617.cn/unix/200901071528.txt 找不到单篇的,看这里: 《Unix编程/应用问答中文版》 https://scz.617.cn/unix/201403241123.txt 此次some是多线程进程,有几十个线程,其中很多会调用libuClibc!recvfrom(),无 法使用上述调试技巧。有人可能想,拦截libuClibc!recvfrom(),命中后锁定在线程 中(set scheduler-locking on),然后使用前述调试技巧。不可行,因为多线程,事 先不知道哪个线程处理哪个端口,如果提前锁定在某一线程中,而该线程并不处理所 关心的端口,条件断点可能这辈子都不会命中,即使命中也不是我们关心的命中。 在libuClibc!recvfrom()出口设置条件断点,稍费点劲的地方是确定哪些寄存器、栈 变量保有入口形参的值,肯定不是原来的r0-r3之流的。一旦条件满足,立即锁定在 当前线程中(set scheduler-locking on),否则单步必飞,线程太多了。 recvmsg()也可以用于读取UDP报文: ssize_t recvmsg ( int sockfd, struct msghdr *msg, int flags ); struct iovec { void *iov_base; /* Starting address */ size_t iov_len; /* Number of bytes to transfer */ }; struct msghdr { void *msg_name; /* optional address */ socklen_t msg_namelen; /* size of address */ struct iovec *msg_iov; /* scatter/gather array */ size_t msg_iovlen; /* # elements in msg_iov */ void *msg_control; /* ancillary data, see below */ size_t msg_controllen; /* ancillary data buffer len */ int msg_flags; /* flags on received message */ }; struct cmsghdr { size_t cmsg_len; /* Data byte count, including header */ int cmsg_level; /* Originating protocol */ int cmsg_type; /* Protocol-specific type */ unsigned char cmsg_data[1]; }; b *0xhhhhhhhh commands $bpnum silent get_big_endian_2 *(char**)$r6+2 set $sport=$ret get_big_endian_4 *(char**)$r6+4 set $src=$ret if (($src==0xhhhhhhhh)&&($sport==0x1314)) set scheduler-locking on display i r r7 r6 r5 db *(*(char***)($r6+8)) $r5 else c end end (示例,勿照搬) 定位TCP端口的处理流程,一般这种会调用recv(): ssize_t recv ( int sockfd, void *buf, size_t len, int flags ); 不同于recvfrom()、recvmsg(),recv()得不到源IP、源端口,断点条件需要做些改 动,比如,当TCP数据区长度等于11,前4字节等于指定值时显示读取的报文: b *0xhhhhhhhh commands $bpnum silent if ($r0==0xb) get_big_endian_4 $r4 set $magic=$ret if ($magic==0xhhhhhhhh) set scheduler-locking on display i r r5 r4 r7 r0 db $r4 $r0 else c end else c end end (示例,勿照搬) $ nsfocus_scan -q tcpdata -k 0x1314 -p 1984 -x "scz@nsfocus" -y "\xff\xff\0\0" -b x.x.x.x -t 3600 向1984/TCP发送"scz@nsfocus",源端口0x1314,等待响应报文,读超时1h。如果前 述条件断点命中,单步回到父函数即可定位1984/TCP的处理流程。发送触发报文时设 置一个较大的读超时是必要的,因为TCP不像UDP,后者发出去就算完事了,TCP得保 持连接不断,否则在服务端的交互式调试不长久。 read()也可以用于读取TCP报文: ssize_t read ( int fd, void *buf, size_t count ); 在IDA中分析这些读取函数时,可能碰上Linux系统调用。参看syscall(2),讲了各种 CPU架构如何传递系统调用号、参数。比如我碰上的这个,"svc 0"相当于x86的 "int 0x80",r7对应系统调用号,r0-r6用于传递参数,r0保存返回值。 前面针对recv()的条件断点有坑,当时假设服务器会一次性读取来自客户端的TCP数 据,对读取到的字节数做了不恰当的约束。312/TCP端口的处理流程是先读4字节长度 域,对长度域进行某些判断之后再继续读取后续数据,结果前述recv()条件断点不会 命中,因为recv()出口TCP数据区长度等于4,不等于11,我发的触发报文不会触发条 件断点。后修改条件断点如下: b *0xhhhhhhhh commands $bpnum silent if ($r0>=4) get_big_endian_4 $r4 set $magic=$ret if ($magic==0xhhhhhhhh) set scheduler-locking on display i r r5 r4 r7 r0 db $r4 $r0 else c end else c end end (示例,勿照搬) 本来在recv()出口对TCP数据区长度进行相等检查,是为了增强约束条件,减少误命 中,适用于1984/TCP,却不适用于312/TCP。起初我忽略了这种可能性,以至于无法 定位312/TCP的处理流程,还很奇怪,难道有什么其他系统调用可以读取网络报文? 修改过的条件断点只是针对312/TCP,如果某TCP端口先读1字节、2字节、3字节等等, 就需要做出相应修改。很难弄一个普适的条件断点,只能基于对系统的理解、长期 积累的经验,在某种条件断点未能如愿命中时做出合理猜测并调整约束条件。 直接拦截对网络报文的读取并设置条件断点是一种比较直白的调试方案,一旦有效命 中,其附近代码马上就是对客户端可控数据的协议解码,如果某端口在处理未久经考 验的私有协议,就开始挖吧。 可以在some中寻找socket()、bind()、listen()、accept()。 对于bind(),由形参可知绑定的端口号。动态拦截bind()有时并不适用,比如因各种 原因只能Attach,不能从some启动之初开始调试,而Attach时已经bind()结束,拦截 读函数不存在这种限制。可以尝试静态分析bind()的主调函数,直接确定绑定的端口 号。即使这样,也不能完全取代拦截读函数,考虑那些复杂网络服务框架,bind()与 读操作相差十万八千里。 对于TCP,动态拦截accept()是一种选择,无论some是否fork(),TCP的accept()是必 经的。假设主调者给accpet()第2、3形参传NULL,此时无法对源IP、源端口设约束条 件。但是,在一段时间内除了你主动发起的TCP连接很可能没有其他新的TCP连接出现, 此时命中accept()本身就具有排他性。 动态拦截网络函数的另一个坑是,假设同时存在liba!read()、libb!read(),来几层 封装、动态加载啥的,很可能只注意到liba!read(),而没有意识到libb!read()的存 在。在用户态使用gdb调试,想直接拦截所有的__NR_read就算了吧。这种时候不要怀 疑人生,不要假设灵异事件存在,要坚信libb!read()的存在,但如何找出它,没有 固定套路,见招拆招吧。 2018-12-14 18:00 scz 周二分享分析未知网络服务的基本套路,当时只是随意小结一下这种反复碰上的需求, 并没有指望它立竿见影什么,周五在某IoT设备上发现个远程洞。这种现世报还是很 令人愉悦的。 2018-12-25 13:49 scz 碰上过某TCP端口调用recv(),它是逐字节读取,形如: i = 0; while ( 1 ) { if ( i >= len ) { break; } recv( s, &c, 1, x ); buf[i++] = c; } 如果已知是逐字节读取,构造条件断点要容易一些,但很可能在调试分析之初并不知 道会面临这种局面,此时前述针对recv()的条件断点不会命中,一度怀疑人生。在共 产主义必将实现的信仰支撑下,我先拦截accept()得到入连接套接字,然后对recv() 的第一形参s设置约束条件,找到上述代码,这是胡说八道。真实情况是,感觉 accept()的主调函数好像是某种对象的成员函数,在IDA中自定义结构以描述该对象, 查看该对象的其他成员函数时,发现其中一个是recv()的主调函数,据此找到上述代 码,然后设断验证recv()第一形参s是accept()返回的入连接套接字。静态分析与动 态调试互相补充,不要思维定势。 就recv()的第二形参buf而言,对读上来的数据直接设置约束条件不太靠谱,单字节 的约束太弱,实在没办法了再说。 不加约束条件直接断recv()?在一个recv()被频繁调用的多线程进程中,这种无约束 条件的断点几乎没有意义。 2018-12-29 14:09 scz 在*nix上涉及网络报文读写,一般只会注意: read/write recv/send recvfrom/sendto recvmsg/sendmsg 但实际上还有其他API可用,比如: ssize_t readv ( int fd, struct iovec *iov, int iovcnt ); ssize_t writev ( int fd, struct iovec *iov, int iovcnt ); 参看socket(7),里面有一段: Seeking, or calling pread(2) or pwrite(2) with a nonzero position is not supported on sockets. 对套接字使用lseek()肯定不行,但可以对套接字使用pread(2)/pwrite(2),只要第 四形参offset指定成0即可。 ssize_t pread ( int fd, void *buf, size_t count, off_t offset ); ssize_t pwrite ( int fd, void *buf, size_t count, off_t offset ); ssize_t preadv ( int fd, struct iovec *iov, int iovcnt, off_t offset ); ssize_t pwritev ( int fd, struct iovec *iov, int iovcnt, off_t offset ); ssize_t preadv2 ( int fd, struct iovec *iov, int iovcnt, off_t offset, int flags ); ssize_t pwritev2 ( int fd, struct iovec *iov, int iovcnt, off_t offset, int flags ); 这些非常规API对于逆向工程来说,真是天坑一般的存在。若你设断点时忽略了这些 可能性,你会以为出现超自然现象。 2018-12-29 阿七也怒了 int recvmmsg ( int sockfd, struct mmsghdr *msgvec, unsigned int vlen, int flags, struct timespec *timeout ); int sendmmsg ( int sockfd, struct mmsghdr *msgvec, unsigned int vlen, int flags ); struct mmsghdr { struct msghdr msg_hdr; /* Message header */ unsigned int msg_len; /* Number of bytes transmitted */ }; 从man手册看,这是系统调用,不是库函数,所以也得拦截。以前从未碰上过,从微 博评论区新学来的。我要是不出来说这个事,也学不到这个新的,真是好心有好报。 2021-04-06 scz 在某Docker环境中用gdb调试某进程,该进程有网络通信操作,想用"catch syscall read/recvfrom"之类的断点寻找读取网络数据的代码所在,始终无法命中,但通过其 他技术手段已经确认网络通信进行中。 目标进程稍显复杂,转用nc进行测试。在该Docker环境中启动"nc -4 -l 12345", gdb attach上去,"catch syscall read/recvfrom",远程访问12345/TCP,前述断点 无法命中。"info proc mappings"确认nc和libc所在,IDA反汇编之,Rebase基址。 在nc中注意到对__recvfrom_chk()、__read_chk()的调用,在libc中反汇编查看 __read_chk(),最终会触发"syscall read"。在__read_chk()中syscall指令之后设 普通断点,能命中,"db $rsi $rax"正是客户端发送的数据。 就该环境中的nc而言,"b *read"并不会命中,该版本nc直接调__read_chk(),没有 调read(),而__read_chk()直接"syscall read",并不会调read(),二者关系如下: read __read_nocancel sys_read // moe eax, 0 __read_chk sys_read // xor eax, eax 本来用"catch syscall read"是想更底层些,避免libc或其他封装导致拦截不全,上 面的read()、__read_chk()就是很好的示例。但前述测试过程表明此环境中gdb有BUG, "catch syscall read"应该命中,但未命中。若gdb无BUG,"catch syscall read"把 这些乱七八糟的封装都覆盖了,谁知gdb有BUG,坑得我死去活来。 更可气的是,"catch syscall accept/write"无此BUG。 未去找BUG源头何在,记录在此以作提醒。就是说,当你碰上不可解释的现象时,还得 考虑调试器BUG这种可能性,别较死劲儿。 对于"catch syscall read"有BUG的情况,若直接断read()、__read_chk()均无命中, 只能手工去找对"syscall read"的其他封装函数,这不太容易。首先要确认目标环境 中sys_read的系统调用号,比如: $ grep -r __NR_read /usr/include ... /usr/include/asm/unistd_64.h:#define __NR_read 0 ... 然后确定将eax置0的代码,可能性太多,比如最直白的: $ rasm2 -a x86 -b 64 -s intel -o 0 "mov eax,0;xor eax,eax;mov rax,0;xor rax,rax;syscall" b80000000031c048c7c0000000004831c00f05 $ rasm2 -a x86 -b 64 -s intel -o 0 -D b80000000031c048c7c0000000004831c00f05 0x00000000 5 b800000000 mov eax, 0 0x00000005 2 31c0 xor eax, eax 0x00000007 7 48c7c000000000 mov rax, 0 0x0000000e 3 4831c0 xor rax, rax 0x00000011 2 0f05 syscall 最后在IDA中Alt-B搜: b8 00 00 00 00 0f 05 31 c0 0f 05 48 c7 c0 00 00 00 00 0f 05 48 31 c0 0f 05 若eax置0与syscall不紧挨着,这样是搜不到的,当然一般会紧挨着。无论如何,事 情到了这一步,全凭人品,没有定数。反过来这也说明"catch syscall"多么有用, 若无BUG应多用之。 关于这个BUG,侯云松有个猜测,"catch syscall"可能是在找"mov eax,n",而 __read_chk()里是"xor eax,eax",导致"catch syscall read"未生效。后来我用nc 之外的some测试,有个read()中"mov eax,0"被经过,但"catch syscall read"未命 中,侯云松的猜测不成立。 网友"早春二月"指出,可能跟read在x86_64架构下是0号系统调用有关,2013年有个 BUG: catch syscall read (syscall 0) doesn't work on Linux/x86_64 https://sourceware.org/bugzilla/show_bug.cgi?id=16297 2021-04-07 scz 若"catch syscall read"因BUG无法命中,就只能对库函数read()或其他封装函数设 断,此时有个新坑需要注意。 (gdb) info functions ^read$ All functions matching regular expression "^read$": Non-debugging symbols: 0x000000000043d7a8 read // read@plt in some 0x00007f4bbbec7800 read // read in libpthread 0x00007f4bbb8d1818 read // read@plt in libcrypto 0x00007f4bbafc3e88 read // read@plt in librt 0x00007f4bbadab668 read // read@plt in libresolv 0x00007f4bba8ec760 read // read in libc 0x00007f4bbc0ed550 read // read in ld-linux-x86-64.so 0x00007f4bba2ff6e8 read // read@plt in libkrb5 0x00007f4bb9eb9f70 read // read@plt in libk5crypto 0x00007f4bb988e1f8 read // read@plt in libselinux 有很多地址对应符号read (gdb) show multiple-symbols How the debugger handles ambiguities in expressions is "all". multiple-symbols缺省就是all,但"b *read"并未在所有动态库提供的read()上同时 设断,只用了其中一个: (gdb) b *read Breakpoint 1 at 0x7f4bbbec7800 (gdb) info symbol 0x7f4bbbec7800 read in section .text of /lib64/libpthread.so.0 (gdb) info symbol read read in section .text of /lib64/libpthread.so.0 (gdb) info address read Symbol "read" is at 0x7f4bbbec7800 in a file compiled without debugging. 本例"b *read"用了libpthread提供的read(),而非libc提供的read()。 How to detect which shared libraries a binary is actually using - Igor Skochinsky [2020-05-02] https://reverseengineering.stackexchange.com/questions/24885/how-to-detect-which-shared-libraries-a-binary-is-actually-using 提问者问了一个问题,如果liba.so/libb.so都提供了符号c,如何知道some用的是哪 个c?Igor Skochinsky在回答中指出 ELF model doesn't bind symbols to a specific library, so the first module providing a specific symbol is used. You can try to check into which address range the symbol's value falls. 意思是,some使用了c,但并未限定由谁提供c,谁先来就用谁。前面"b *read"正是 这种情况,libpthread先于libc提供了read()。 检查目标进程some的PLT[]、GOT[]: (gdb) info address read@plt Symbol "read@plt" is at 0x43d7a8 in a file compiled without debugging. (gdb) x/3i 0x43d7a8 0x43d7a8 : jmp QWORD PTR [rip+0x50c16a] # 0x949918 0x43d7ae : push 0x182 0x43d7b3 : jmp 0x43bf78 (gdb) info address read@got.plt Symbol "read@got.plt" is at 0x949918 in a file compiled without debugging. (gdb) x/1gx 0x949918 0x949918 : 0x00007f4bbbec7800 (gdb) info symbol *(void**)0x949918 read in section .text of /lib64/libpthread.so.0 some中调用read(),一般去调read@plt,而read@got.plt会被重定位,上例重定位到 libpthread提供的read()。 IDA反汇编.got.plt section时有特殊处理。在Segments(Shift-F7)最后几行有个 extern,实际不存在,是IDA歪歪出来的,位于该section中的内容其文件偏移显示 UNKNOWN。extern只是方便IDA在.got.plt中填写一些可读性较好的符号,再直白点, IDA"假装"完成了GOT[]的重定位处理。 gdb doesn't show symbol from a shared library when another library has the same symbol - [2019-08-04] https://stackoverflow.com/questions/57342963/gdb-doesnt-show-symbol-from-a-shared-library-when-another-library-has-the-same 提问者想让"info address read"显示所有的read地址,这是不可能的。"info address read"只会显示被填到read@got.plt中的那个read地址,也就是"b *read"所 用的地址。但前面我演示过,"info functions ^read$"可以显示所有的read地址。 回答者提到 under normal symbol resolution rules, and assuming the symbol is global, all references to Journal will bind to the same symbol, so the fact that the symbol is also present in another library is irrelevant. 意思是,如果some已经使用了liba.so提供的c,libb.so提供的c不会被用到。 windbg可以用"module!func"的形式限定某模块提供的函数,gdb没有类似语法。为了 在libc!read()上设断,只能用16进制地址,"b *0x7f4bba8ec760"。但在本节上下文 里,这样做没有意义,libc提供的read()永远不会被用到。 关于gdb的符号处理,还可以参看: 《3.17 如何知道导入表中某函数由哪个.so提供》 How to get the shared object belonging to an imported function - [2015-04-29] https://stackoverflow.com/questions/29949104/linux-elf-file-how-to-get-the-shared-object-belonging-to-an-imported-function ELF files don't specify which symbols come from which libraries; it just adds a list of shared libraries to link to into the ELF binary, and lets the linker find the symbols in the libraries. 10.2 Ambiguous Expressions https://sourceware.org/gdb/onlinedocs/gdb/Ambiguous-Expressions.html 16 Examining the Symbol Table https://sourceware.org/gdb/current/onlinedocs/gdb/Symbols.html https://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_109.html 前面想强调的是,你在"b *read",未必是你想设的断点,务必用其他手段检查断点 设在哪里。有时你设的断点无命中,并不是灵异事件,要考虑前面介绍的场景。 2021-04-08 scz 演示未知TCP端口的两种调试场景 $ nc -4 -l 12345 $ netstat -natp | grep 12345 tcp 0 0 0.0.0.0:12345 0.0.0.0:* LISTEN 94352/nc $ lsof -lnPR -i 4tcp | grep 12345 nc 94352 98085 0 3u IPv4 31876655 0t0 TCP *:12345 (LISTEN) 主套接字对应3号句柄 $ gdb -q -nx -x /tmp/gdbinit_x64.txt -x "/tmp/ShellPipeCommand.py" -x "/tmp/GetOffset.py" -ex 'display/5i $pc' -p 94352 (gdb) b *accept Breakpoint 1 at 0x7ffff781a750 (gdb) info symbol 0x7ffff781a750 accept in section .text of /lib64/libc.so.6 这次用了libc提供的accept() (gdb) catch syscall accept Catchpoint 2 (syscall 'accept' [43]) (gdb) c Continuing. Catchpoint 2 (call to syscall 'accept'), 0x00007ffff781a760 in __accept_nocancel () from /lib64/libc.so.6 1: x/5i $pc => 0x7ffff781a760 <__accept_nocancel+7>: cmp rax,0xfffffffffffff001 0x7ffff781a766 <__accept_nocancel+13>: jae 0x7ffff781a799 0x7ffff781a768 <__accept_nocancel+15>: ret 0x7ffff781a769 : sub rsp,0x8 0x7ffff781a76d : call 0x7ffff7829360 <__libc_enable_asynccancel> (gdb) bt #0 0x00007ffff781a760 in __accept_nocancel () from /lib64/libc.so.6 #1 0x0000000000402c12 in ?? () #2 0x00007ffff774fd1d in __libc_start_main () from /lib64/libc.so.6 #3 0x0000000000401349 in ?? () (gdb) c 必须两次c,本例中"catch syscall accept"会直接命中第一次,服务端nc已经阻塞 在"syscall accept"中。 从客户端nc访问服务端12345/TCP: $ nc -vv -n 192.168.65.25 12345 Catchpoint 2 (returned from syscall 'accept'), 0x00007ffff781a760 in __accept_nocancel () from /lib64/libc.so.6 1: x/5i $pc => 0x7ffff781a760 <__accept_nocancel+7>: cmp rax,0xfffffffffffff001 0x7ffff781a766 <__accept_nocancel+13>: jae 0x7ffff781a799 0x7ffff781a768 <__accept_nocancel+15>: ret 0x7ffff781a769 : sub rsp,0x8 0x7ffff781a76d : call 0x7ffff7829360 <__libc_enable_asynccancel> (gdb) bt #0 0x00007ffff781a760 in __accept_nocancel () from /lib64/libc.so.6 #1 0x0000000000402c12 in ?? () #2 0x00007ffff774fd1d in __libc_start_main () from /lib64/libc.so.6 #3 0x0000000000401349 in ?? () 注意,"catch syscall accept"命中,"b *accept"未命中。这个很好理解,因为服 务端nc侦听端口时rip已经越过libc!accept()的入口点。 "catch syscall accept"命中时rax对应子套接字: (gdb) i r rax rax 0x4 4 子套接字对应4号句柄,将来read/write等操作用的是4号句柄。 $ netstat -natp | grep 12345 tcp 0 0 0.0.0.0:12345 0.0.0.0:* LISTEN 94352/nc tcp 0 0 192.168.65.25:12345 192.168.65.20:36586 ESTABLISHED 94352/nc $ lsof -lnPR -i 4tcp | grep 12345 nc 94352 98085 0 3u IPv4 31876655 0t0 TCP *:12345 (LISTEN) nc 94352 98085 0 4u IPv4 31880089 0t0 TCP 192.168.65.25:12345->192.168.65.20:36586 (ESTABLISHED) 拦截__read_chk(),句柄为4时断下: b *__read_chk commands $bpnum silent if ($edi==4) display i r rdi rsi rdx else c end end 这个断点会直接命中,因为nc会去调用__read_chk()。 => 0x7ffff7831b80 <__read_chk>: sub rsp,0x8 0x7ffff7831b84 <__read_chk+4>: cmp rdx,rcx 0x7ffff7831b87 <__read_chk+7>: ja 0x7ffff7831b9d <__read_chk+29> 0x7ffff7831b89 <__read_chk+9>: movsxd rdi,edi 0x7ffff7831b8c <__read_chk+12>: xor eax,eax rdi 0x4 4 rsi 0x7fffffff64c0 140737488315584 rdx 0x800 2048 (gdb) bt #0 0x00007ffff7831b80 in __read_chk () from /lib64/libc.so.6 #1 0x0000000000401a62 in ?? () #2 0x0000000000402bca in ?? () #3 0x00007ffff774fd1d in __libc_start_main () from /lib64/libc.so.6 #4 0x0000000000401349 in ?? () (gdb) disas __read_chk Dump of assembler code for function __read_chk: => 0x00007ffff7831b80 <+0>: sub rsp,0x8 0x00007ffff7831b84 <+4>: cmp rdx,rcx 0x00007ffff7831b87 <+7>: ja 0x7ffff7831b9d <__read_chk+29> 0x00007ffff7831b89 <+9>: movsxd rdi,edi 0x00007ffff7831b8c <+12>: xor eax,eax 0x00007ffff7831b8e <+14>: syscall 0x00007ffff7831b90 <+16>: cmp rax,0xfffffffffffff000 0x00007ffff7831b96 <+22>: ja 0x7ffff7831ba2 <__read_chk+34> 0x00007ffff7831b98 <+24>: add rsp,0x8 0x00007ffff7831b9c <+28>: ret 0x00007ffff7831b9d <+29>: call 0x7ffff78316c0 <__chk_fail> 0x00007ffff7831ba2 <+34>: mov rdx,QWORD PTR [rip+0x28d3ff] # 0x7ffff7abefa8 0x00007ffff7831ba9 <+41>: neg eax 0x00007ffff7831bab <+43>: mov DWORD PTR fs:[rdx],eax 0x00007ffff7831bae <+46>: or rax,0xffffffffffffffff 0x00007ffff7831bb2 <+50>: jmp 0x7ffff7831b98 <__read_chk+24> End of assembler dump. 拦截"syscall read"的某个返回点,句柄为4时断下,显示读取的数据: b *0x7ffff7831b90 commands $bpnum silent if ($edi==4) display i r rdi rsi rdx rax db $rsi $rax else c end end 在客户端nc中发送"scz@nsfocus",断点命中: => 0x7ffff7831b90 <__read_chk+16>: cmp rax,0xfffffffffffff000 0x7ffff7831b96 <__read_chk+22>: ja 0x7ffff7831ba2 <__read_chk+34> 0x7ffff7831b98 <__read_chk+24>: add rsp,0x8 0x7ffff7831b9c <__read_chk+28>: ret 0x7ffff7831b9d <__read_chk+29>: call 0x7ffff78316c0 <__chk_fail> rdi 0x4 4 rsi 0x7fffffff64c0 140737488315584 rdx 0x800 2048 rax 0xc 12 00007fffffff64c0: 73 63 7a 40 6e 73 66 6f 63 75 73 0a scz@nsfocus. 虽然我是用nc演示服务端,但前述调试技巧是通用的。小结一下: a) "catch syscall accept"获取子套接字,不要用"b *accept" b) 在适当位置拦截read/write操作,用子套接字做过滤条件 若"catch syscall read"无BUG,直接用这个 若"catch syscall read"有BUG,需要寻找0x7ffff7831b90这种位置 接下来演示另一种调试场景。服务端nc侦听12345/TCP;客户端nc建立TCP连接,但暂 未发送数据。netstat、lsof查看已建立的TCP连接: $ netstat -natp | grep 12345 tcp 0 0 0.0.0.0:12345 0.0.0.0:* LISTEN 115692/nc tcp 0 0 192.168.65.25:12345 192.168.65.20:36588 ESTABLISHED 115692/nc $ lsof -lnPR -i 4tcp | grep 12345 nc 115692 98085 0 3u IPv4 32028398 0t0 TCP *:12345 (LISTEN) nc 115692 98085 0 4u IPv4 32033202 0t0 TCP 192.168.65.25:12345->192.168.65.20:36588 (ESTABLISHED) gdb attach服务端nc,直接用这个断点: b *0x7ffff7831b90 commands $bpnum silent if ($edi==4) display i r rdi rsi rdx rax db $rsi $rax else c end end 小结一下: a) 用客户端nc建立TCP连接,暂未发送数据 b) 用lsof确定TCP连接对应的子套接字,还可以试试"info proc files" c) 在适当位置拦截read/write操作,用子套接字做过滤条件 第二种调试场景看似和第一种调试场景区别不大,只是借助lsof省去对accept的拦截。 但有一些微妙之处,第一种调试场景实际对应服务端阻塞在accpet(),第二种调试场 景实际对应服务端阻塞在read()或类似操作。 假设服务端在TCP连接建立后主动向客户端发送数据,为了拦截这个动作,可以用第 一种调试场景的技巧,从accept开始,结合"catch syscall write"之类的断点。 为什么特意演示第二种调试场景呢?设想这样一些情况,主套接字与子套接字不在同 一进程中使用,甚至不是在父子进程关系中使用,而是完全不同的两个进程;即便主 套接字与子套接字在父子进程关系中使用,但你不想在gdb中处理fork/vfork。 这两种调试场景都利用了TCP长连接的特点,相应技巧不适用于UDP通信。