logparse.pl 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370
  1. #!/usr/bin/perl
  2. use Getopt::Long;
  3. use strict;
  4. use warnings;
  5. use FileHandle;
  6. my $dumpchannels = 0;
  7. my $dumpdata = 0;
  8. my $pass_through_events = 0;
  9. my $verbose_all;
  10. my %verbose_packet;
  11. GetOptions("dump-channels|c" => \$dumpchannels,
  12. "dump-data|d" => \$dumpdata,
  13. "verbose|v" => \$verbose_all,
  14. "full|f=s" => sub { $verbose_packet{$_[1]} = 1; },
  15. "events|e" => \$pass_through_events,
  16. "help" => sub { &usage(\*STDOUT, 0); })
  17. or &usage(\*STDERR, 1);
  18. sub usage {
  19. my ($fh, $exitstatus) = @_;
  20. print $fh <<'EOF';
  21. usage: logparse.pl [ options ] [ input-log-file ]
  22. options: --dump-channels, -c dump the final state of every channel
  23. --dump-data, -d save data of every channel to ch0.i, ch0.o, ...
  24. --full=PKT, -f PKT print extra detail for packets of type PKT
  25. --verbose, -v print extra detail for all packets if available
  26. --events, -e copy Event Log messages from input log file
  27. EOF
  28. exit $exitstatus;
  29. }
  30. my @channels = (); # ultimate channel ids are indices in this array
  31. my %chan_by_id = (); # indexed by 'c%d' or 's%d' for client and server ids
  32. my %globalreq = (); # indexed by 'i' or 'o'
  33. my %packets = (
  34. #define SSH2_MSG_DISCONNECT 1 /* 0x1 */
  35. 'SSH2_MSG_DISCONNECT' => sub {
  36. my ($direction, $seq, $data) = @_;
  37. my ($reason, $description, $lang) = &parse("uss", $data);
  38. printf "%s\n", &str($description);
  39. },
  40. #define SSH2_MSG_IGNORE 2 /* 0x2 */
  41. 'SSH2_MSG_IGNORE' => sub {
  42. my ($direction, $seq, $data) = @_;
  43. my ($str) = &parse("s", $data);
  44. printf "(%d bytes)\n", length $str;
  45. },
  46. #define SSH2_MSG_UNIMPLEMENTED 3 /* 0x3 */
  47. 'SSH2_MSG_UNIMPLEMENTED' => sub {
  48. my ($direction, $seq, $data) = @_;
  49. my ($rseq) = &parse("u", $data);
  50. printf "i%d\n", $rseq;
  51. },
  52. #define SSH2_MSG_DEBUG 4 /* 0x4 */
  53. 'SSH2_MSG_DEBUG' => sub {
  54. my ($direction, $seq, $data) = @_;
  55. my ($disp, $message, $lang) = &parse("bss", $data);
  56. printf "%s\n", &str($message);
  57. },
  58. #define SSH2_MSG_SERVICE_REQUEST 5 /* 0x5 */
  59. 'SSH2_MSG_SERVICE_REQUEST' => sub {
  60. my ($direction, $seq, $data) = @_;
  61. my ($service) = &parse("s", $data);
  62. printf "%s\n", &str($service);
  63. },
  64. #define SSH2_MSG_SERVICE_ACCEPT 6 /* 0x6 */
  65. 'SSH2_MSG_SERVICE_ACCEPT' => sub {
  66. my ($direction, $seq, $data) = @_;
  67. my ($service) = &parse("s", $data);
  68. printf "%s\n", &str($service);
  69. },
  70. #define SSH2_MSG_KEXINIT 20 /* 0x14 */
  71. 'SSH2_MSG_KEXINIT' => sub {
  72. my ($direction, $seq, $data) = @_;
  73. print "\n";
  74. },
  75. #define SSH2_MSG_NEWKEYS 21 /* 0x15 */
  76. 'SSH2_MSG_NEWKEYS' => sub {
  77. my ($direction, $seq, $data) = @_;
  78. print "\n";
  79. },
  80. #define SSH2_MSG_KEXDH_INIT 30 /* 0x1e */
  81. 'SSH2_MSG_KEXDH_INIT' => sub {
  82. my ($direction, $seq, $data) = @_;
  83. print "\n";
  84. },
  85. #define SSH2_MSG_KEXDH_REPLY 31 /* 0x1f */
  86. 'SSH2_MSG_KEXDH_REPLY' => sub {
  87. my ($direction, $seq, $data) = @_;
  88. print "\n";
  89. },
  90. #define SSH2_MSG_KEX_DH_GEX_REQUEST 30 /* 0x1e */
  91. 'SSH2_MSG_KEX_DH_GEX_REQUEST' => sub {
  92. my ($direction, $seq, $data) = @_;
  93. print "\n";
  94. },
  95. #define SSH2_MSG_KEX_DH_GEX_GROUP 31 /* 0x1f */
  96. 'SSH2_MSG_KEX_DH_GEX_GROUP' => sub {
  97. my ($direction, $seq, $data) = @_;
  98. print "\n";
  99. },
  100. #define SSH2_MSG_KEX_DH_GEX_INIT 32 /* 0x20 */
  101. 'SSH2_MSG_KEX_DH_GEX_INIT' => sub {
  102. my ($direction, $seq, $data) = @_;
  103. print "\n";
  104. },
  105. #define SSH2_MSG_KEX_DH_GEX_REPLY 33 /* 0x21 */
  106. 'SSH2_MSG_KEX_DH_GEX_REPLY' => sub {
  107. my ($direction, $seq, $data) = @_;
  108. print "\n";
  109. },
  110. #define SSH2_MSG_KEXGSS_INIT 30 /* 0x1e */
  111. 'SSH2_MSG_KEXGSS_INIT' => sub {
  112. my ($direction, $seq, $data) = @_;
  113. print "\n";
  114. },
  115. #define SSH2_MSG_KEXGSS_CONTINUE 31 /* 0x1f */
  116. 'SSH2_MSG_KEXGSS_CONTINUE' => sub {
  117. my ($direction, $seq, $data) = @_;
  118. print "\n";
  119. },
  120. #define SSH2_MSG_KEXGSS_COMPLETE 32 /* 0x20 */
  121. 'SSH2_MSG_KEXGSS_COMPLETE' => sub {
  122. my ($direction, $seq, $data) = @_;
  123. print "\n";
  124. },
  125. #define SSH2_MSG_KEXGSS_HOSTKEY 33 /* 0x21 */
  126. 'SSH2_MSG_KEXGSS_HOSTKEY' => sub {
  127. my ($direction, $seq, $data) = @_;
  128. print "\n";
  129. },
  130. #define SSH2_MSG_KEXGSS_ERROR 34 /* 0x22 */
  131. 'SSH2_MSG_KEXGSS_ERROR' => sub {
  132. my ($direction, $seq, $data) = @_;
  133. print "\n";
  134. },
  135. #define SSH2_MSG_KEXGSS_GROUPREQ 40 /* 0x28 */
  136. 'SSH2_MSG_KEXGSS_GROUPREQ' => sub {
  137. my ($direction, $seq, $data) = @_;
  138. print "\n";
  139. },
  140. #define SSH2_MSG_KEXGSS_GROUP 41 /* 0x29 */
  141. 'SSH2_MSG_KEXGSS_GROUP' => sub {
  142. my ($direction, $seq, $data) = @_;
  143. print "\n";
  144. },
  145. #define SSH2_MSG_KEXRSA_PUBKEY 30 /* 0x1e */
  146. 'SSH2_MSG_KEXRSA_PUBKEY' => sub {
  147. my ($direction, $seq, $data) = @_;
  148. print "\n";
  149. },
  150. #define SSH2_MSG_KEXRSA_SECRET 31 /* 0x1f */
  151. 'SSH2_MSG_KEXRSA_SECRET' => sub {
  152. my ($direction, $seq, $data) = @_;
  153. print "\n";
  154. },
  155. #define SSH2_MSG_KEXRSA_DONE 32 /* 0x20 */
  156. 'SSH2_MSG_KEXRSA_DONE' => sub {
  157. my ($direction, $seq, $data) = @_;
  158. print "\n";
  159. },
  160. #define SSH2_MSG_KEX_ECDH_INIT 30 /* 0x1e */
  161. 'SSH2_MSG_KEX_ECDH_INIT' => sub {
  162. my ($direction, $seq, $data) = @_;
  163. print "\n";
  164. },
  165. #define SSH2_MSG_KEX_ECDH_REPLY 31 /* 0x1f */
  166. 'SSH2_MSG_KEX_ECDH_REPLY' => sub {
  167. my ($direction, $seq, $data) = @_;
  168. print "\n";
  169. },
  170. #define SSH2_MSG_KEX_HYBRID_INIT 30 /* 0x1e */
  171. 'SSH2_MSG_KEX_HYBRID_INIT' => sub {
  172. my ($direction, $seq, $data) = @_;
  173. print "\n";
  174. },
  175. #define SSH2_MSG_KEX_HYBRID_REPLY 31 /* 0x1f */
  176. 'SSH2_MSG_KEX_HYBRID_REPLY' => sub {
  177. my ($direction, $seq, $data) = @_;
  178. print "\n";
  179. },
  180. #define SSH2_MSG_USERAUTH_REQUEST 50 /* 0x32 */
  181. 'SSH2_MSG_USERAUTH_REQUEST' => sub {
  182. my ($direction, $seq, $data) = @_;
  183. my ($user, $service, $method) = &parse("sss", $data);
  184. my $out = sprintf "%s %s %s",
  185. &str($user), &str($service), &str($method);
  186. if ($method eq "publickey") {
  187. my ($real) = &parse("b", $data);
  188. $out .= " real=$real";
  189. } elsif ($method eq "password") {
  190. my ($change) = &parse("b", $data);
  191. $out .= " change=$change";
  192. }
  193. print "$out\n";
  194. },
  195. #define SSH2_MSG_USERAUTH_FAILURE 51 /* 0x33 */
  196. 'SSH2_MSG_USERAUTH_FAILURE' => sub {
  197. my ($direction, $seq, $data) = @_;
  198. my ($options) = &parse("s", $data);
  199. printf "%s\n", &str($options);
  200. },
  201. #define SSH2_MSG_USERAUTH_SUCCESS 52 /* 0x34 */
  202. 'SSH2_MSG_USERAUTH_SUCCESS' => sub {
  203. my ($direction, $seq, $data) = @_;
  204. print "\n";
  205. },
  206. #define SSH2_MSG_USERAUTH_BANNER 53 /* 0x35 */
  207. 'SSH2_MSG_USERAUTH_BANNER' => sub {
  208. my ($direction, $seq, $data) = @_;
  209. print "\n";
  210. },
  211. #define SSH2_MSG_USERAUTH_PK_OK 60 /* 0x3c */
  212. 'SSH2_MSG_USERAUTH_PK_OK' => sub {
  213. my ($direction, $seq, $data) = @_;
  214. print "\n";
  215. },
  216. #define SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ 60 /* 0x3c */
  217. 'SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ' => sub {
  218. my ($direction, $seq, $data) = @_;
  219. print "\n";
  220. },
  221. #define SSH2_MSG_USERAUTH_INFO_REQUEST 60 /* 0x3c */
  222. 'SSH2_MSG_USERAUTH_INFO_REQUEST' => sub {
  223. my ($direction, $seq, $data) = @_;
  224. print "\n";
  225. },
  226. #define SSH2_MSG_USERAUTH_INFO_RESPONSE 61 /* 0x3d */
  227. 'SSH2_MSG_USERAUTH_INFO_RESPONSE' => sub {
  228. my ($direction, $seq, $data) = @_;
  229. print "\n";
  230. },
  231. #define SSH2_MSG_GLOBAL_REQUEST 80 /* 0x50 */
  232. 'SSH2_MSG_GLOBAL_REQUEST' => sub {
  233. my ($direction, $seq, $data) = @_;
  234. my ($type, $wantreply) = &parse("sb", $data);
  235. printf "%s (%s)", $type, $wantreply eq "yes" ? "reply" : "noreply";
  236. my $request = [$seq, $type];
  237. push @{$globalreq{$direction}}, $request if $wantreply eq "yes";
  238. if ($type eq "tcpip-forward" or $type eq "cancel-tcpip-forward") {
  239. my ($addr, $port) = &parse("su", $data);
  240. printf " %s:%s", $addr, $port;
  241. push @$request, $port;
  242. }
  243. print "\n";
  244. },
  245. #define SSH2_MSG_REQUEST_SUCCESS 81 /* 0x51 */
  246. 'SSH2_MSG_REQUEST_SUCCESS' => sub {
  247. my ($direction, $seq, $data) = @_;
  248. my $otherdir = ($direction eq "i" ? "o" : "i");
  249. my $request = shift @{$globalreq{$otherdir}};
  250. if (defined $request) {
  251. printf "to %s", $request->[0];
  252. if ($request->[1] eq "tcpip-forward" and $request->[2] == 0) {
  253. my ($port) = &parse("u", $data);
  254. printf " port=%s", $port;
  255. }
  256. } else {
  257. print "(spurious?)";
  258. }
  259. print "\n";
  260. },
  261. #define SSH2_MSG_REQUEST_FAILURE 82 /* 0x52 */
  262. 'SSH2_MSG_REQUEST_FAILURE' => sub {
  263. my ($direction, $seq, $data) = @_;
  264. my $otherdir = ($direction eq "i" ? "o" : "i");
  265. my $request = shift @{$globalreq{$otherdir}};
  266. if (defined $request) {
  267. printf "to %s", $request->[0];
  268. } else {
  269. print "(spurious?)";
  270. }
  271. print "\n";
  272. },
  273. #define SSH2_MSG_CHANNEL_OPEN 90 /* 0x5a */
  274. 'SSH2_MSG_CHANNEL_OPEN' => sub {
  275. my ($direction, $seq, $data) = @_;
  276. my ($type, $sid, $winsize, $packet) = &parse("suuu", $data);
  277. # CHANNEL_OPEN tells the other side the _sender's_ id for the
  278. # channel, so this choice between "s" and "c" prefixes is
  279. # opposite to every other message in the protocol, which all
  280. # quote the _recipient's_ id of the channel.
  281. $sid = ($direction eq "i" ? "s" : "c") . $sid;
  282. my $chan = {'id'=>$sid, 'state'=>'halfopen',
  283. 'i'=>{'win'=>0, 'seq'=>0},
  284. 'o'=>{'win'=>0, 'seq'=>0}};
  285. $chan->{$direction}{'win'} = $winsize;
  286. push @channels, $chan;
  287. my $index = $#channels;
  288. $chan_by_id{$sid} = $index;
  289. printf "ch%d (%s) %s (--%d)", $index, $chan->{'id'}, $type,
  290. $chan->{$direction}{'win'};
  291. if ($type eq "x11") {
  292. my ($addr, $port) = &parse("su", $data);
  293. printf " from %s:%s", $addr, $port;
  294. } elsif ($type eq "forwarded-tcpip") {
  295. my ($saddr, $sport, $paddr, $pport) = &parse("susu", $data);
  296. printf " to %s:%s from %s:%s", $saddr, $sport, $paddr, $pport;
  297. } elsif ($type eq "direct-tcpip") {
  298. my ($daddr, $dport, $saddr, $sport) = &parse("susu", $data);
  299. printf " to %s:%s from %s:%s", $daddr, $dport, $saddr, $sport;
  300. }
  301. print "\n";
  302. },
  303. #define SSH2_MSG_CHANNEL_OPEN_CONFIRMATION 91 /* 0x5b */
  304. 'SSH2_MSG_CHANNEL_OPEN_CONFIRMATION' => sub {
  305. my ($direction, $seq, $data) = @_;
  306. my ($rid, $sid, $winsize, $packet) = &parse("uuuu", $data);
  307. $rid = ($direction eq "i" ? "c" : "s") . $rid;
  308. my $index = $chan_by_id{$rid};
  309. if (!defined $index) {
  310. printf "UNKNOWN_CHANNEL (%s) (--%d)\n", $rid, $winsize;
  311. return;
  312. }
  313. $sid = ($direction eq "i" ? "s" : "c") . $sid;
  314. $chan_by_id{$sid} = $index;
  315. my $chan = $channels[$index];
  316. $chan->{'id'} = ($direction eq "i" ? "$rid/$sid" : "$sid/$rid");
  317. $chan->{'state'} = 'open';
  318. $chan->{$direction}{'win'} = $winsize;
  319. printf "ch%d (%s) (--%d)\n", $index, $chan->{'id'},
  320. $chan->{$direction}{'win'};
  321. },
  322. #define SSH2_MSG_CHANNEL_OPEN_FAILURE 92 /* 0x5c */
  323. 'SSH2_MSG_CHANNEL_OPEN_FAILURE' => sub {
  324. my ($direction, $seq, $data) = @_;
  325. my ($rid, $reason, $desc, $lang) = &parse("uuss", $data);
  326. $rid = ($direction eq "i" ? "c" : "s") . $rid;
  327. my $index = $chan_by_id{$rid};
  328. if (!defined $index) {
  329. printf "UNKNOWN_CHANNEL (%s) %s\n", $rid, &str($reason);
  330. return;
  331. }
  332. my $chan = $channels[$index];
  333. $chan->{'state'} = 'rejected';
  334. printf "ch%d (%s) %s\n", $index, $chan->{'id'}, &str($reason);
  335. },
  336. #define SSH2_MSG_CHANNEL_WINDOW_ADJUST 93 /* 0x5d */
  337. 'SSH2_MSG_CHANNEL_WINDOW_ADJUST' => sub {
  338. my ($direction, $seq, $data) = @_;
  339. my ($rid, $bytes) = &parse("uu", $data);
  340. $rid = ($direction eq "i" ? "c" : "s") . $rid;
  341. my $index = $chan_by_id{$rid};
  342. if (!defined $index) {
  343. printf "UNKNOWN_CHANNEL (%s) +%d\n", $rid, $bytes;
  344. return;
  345. }
  346. my $chan = $channels[$index];
  347. $chan->{$direction}{'win'} += $bytes;
  348. printf "ch%d (%s) +%d (--%d)\n", $index, $chan->{'id'}, $bytes,
  349. $chan->{$direction}{'win'};
  350. },
  351. #define SSH2_MSG_CHANNEL_DATA 94 /* 0x5e */
  352. 'SSH2_MSG_CHANNEL_DATA' => sub {
  353. my ($direction, $seq, $data) = @_;
  354. my ($rid, $bytes) = &parse("uu", $data);
  355. $rid = ($direction eq "i" ? "c" : "s") . $rid;
  356. my $index = $chan_by_id{$rid};
  357. if (!defined $index) {
  358. printf "UNKNOWN_CHANNEL (%s), %s bytes\n", $rid, $bytes;
  359. return;
  360. }
  361. my $chan = $channels[$index];
  362. $chan->{$direction}{'seq'} += $bytes;
  363. printf "ch%d (%s), %s bytes (%d--%d)\n", $index, $chan->{'id'}, $bytes,
  364. $chan->{$direction}{'seq'}-$bytes, $chan->{$direction}{'seq'};
  365. my @realdata = splice @$data, 0, $bytes;
  366. if ($dumpdata) {
  367. my $filekey = $direction . "file";
  368. if (!defined $chan->{$filekey}) {
  369. my $filename = sprintf "ch%d.%s", $index, $direction;
  370. $chan->{$filekey} = FileHandle->new(">$filename");
  371. if (!defined $chan->{$filekey}) {
  372. die "$filename: $!\n";
  373. }
  374. }
  375. die "channel data not present in $seq\n" if @realdata < $bytes;
  376. my $rawdata = pack "C*", @realdata;
  377. my $fh = $chan->{$filekey};
  378. print $fh $rawdata;
  379. }
  380. if (@realdata == $bytes and defined $chan->{$direction."data"}) {
  381. my $rawdata = pack "C*", @realdata;
  382. $chan->{$direction."data"}->($chan, $index, $direction, $rawdata);
  383. }
  384. },
  385. #define SSH2_MSG_CHANNEL_EXTENDED_DATA 95 /* 0x5f */
  386. 'SSH2_MSG_CHANNEL_EXTENDED_DATA' => sub {
  387. my ($direction, $seq, $data) = @_;
  388. my ($rid, $type, $bytes) = &parse("uuu", $data);
  389. if ($type == 1) {
  390. $type = "SSH_EXTENDED_DATA_STDERR";
  391. }
  392. $rid = ($direction eq "i" ? "c" : "s") . $rid;
  393. my $index = $chan_by_id{$rid};
  394. if (!defined $index) {
  395. printf "UNKNOWN_CHANNEL (%s), type %s, %s bytes\n", $rid,
  396. $type, $bytes;
  397. return;
  398. }
  399. my $chan = $channels[$index];
  400. $chan->{$direction}{'seq'} += $bytes;
  401. printf "ch%d (%s), type %s, %s bytes (%d--%d)\n", $index,$chan->{'id'},
  402. $type, $bytes, $chan->{$direction}{'seq'}-$bytes,
  403. $chan->{$direction}{'seq'};
  404. my @realdata = splice @$data, 0, $bytes;
  405. if ($dumpdata) {
  406. # We treat EXTENDED_DATA as equivalent to DATA, for the
  407. # moment. It's not clear what else would be a better thing
  408. # to do with it, and this at least is the Right Answer if
  409. # the data is going to a terminal and the aim is to debug
  410. # the terminal emulator.
  411. my $filekey = $direction . "file";
  412. if (!defined $chan->{$filekey}) {
  413. my $filename = sprintf "ch%d.%s", $index, $direction;
  414. $chan->{$filekey} = FileHandle->new(">$filename");
  415. if (!defined $chan->{$filekey}) {
  416. die "$filename: $!\n";
  417. }
  418. }
  419. die "channel data not present in $seq\n" if @realdata < $bytes;
  420. my $rawdata = pack "C*", @realdata;
  421. my $fh = $chan->{$filekey};
  422. print $fh $rawdata;
  423. }
  424. if (@realdata == $bytes and defined $chan->{$direction."data"}) {
  425. my $rawdata = pack "C*", @realdata;
  426. $chan->{$direction."data"}->($chan, $index, $direction, $rawdata);
  427. }
  428. },
  429. #define SSH2_MSG_CHANNEL_EOF 96 /* 0x60 */
  430. 'SSH2_MSG_CHANNEL_EOF' => sub {
  431. my ($direction, $seq, $data) = @_;
  432. my ($rid) = &parse("uu", $data);
  433. $rid = ($direction eq "i" ? "c" : "s") . $rid;
  434. my $index = $chan_by_id{$rid};
  435. if (!defined $index) {
  436. printf "UNKNOWN_CHANNEL (%s)\n", $rid;
  437. return;
  438. }
  439. my $chan = $channels[$index];
  440. printf "ch%d (%s)\n", $index, $chan->{'id'};
  441. },
  442. #define SSH2_MSG_CHANNEL_CLOSE 97 /* 0x61 */
  443. 'SSH2_MSG_CHANNEL_CLOSE' => sub {
  444. my ($direction, $seq, $data) = @_;
  445. my ($rid) = &parse("uu", $data);
  446. $rid = ($direction eq "i" ? "c" : "s") . $rid;
  447. my $index = $chan_by_id{$rid};
  448. if (!defined $index) {
  449. printf "UNKNOWN_CHANNEL (%s)\n", $rid;
  450. return;
  451. }
  452. my $chan = $channels[$index];
  453. $chan->{'state'} = ($chan->{'state'} eq "open" ? "halfclosed" :
  454. $chan->{'state'} eq "halfclosed" ? "closed" :
  455. "confused");
  456. if ($chan->{'state'} eq "closed") {
  457. $chan->{'ifile'}->close if defined $chan->{'ifile'};
  458. $chan->{'ofile'}->close if defined $chan->{'ofile'};
  459. }
  460. printf "ch%d (%s)\n", $index, $chan->{'id'};
  461. },
  462. #define SSH2_MSG_CHANNEL_REQUEST 98 /* 0x62 */
  463. 'SSH2_MSG_CHANNEL_REQUEST' => sub {
  464. my ($direction, $seq, $data) = @_;
  465. my ($rid, $type, $wantreply) = &parse("usb", $data);
  466. $rid = ($direction eq "i" ? "c" : "s") . $rid;
  467. my $index = $chan_by_id{$rid};
  468. my $chan;
  469. if (!defined $index) {
  470. printf "UNKNOWN_CHANNEL (%s) %s (%s)", $rid,
  471. $type, $wantreply eq "yes" ? "reply" : "noreply";
  472. } else {
  473. $chan = $channels[$index];
  474. printf "ch%d (%s) %s (%s)", $index, $chan->{'id'},
  475. $type, $wantreply eq "yes" ? "reply" : "noreply";
  476. push @{$chan->{'requests_'.$direction}}, [$seq, $type]
  477. if $wantreply eq "yes";
  478. }
  479. if ($type eq "pty-req") {
  480. my ($term, $w, $h, $pw, $ph, $modes) = &parse("suuuus", $data);
  481. printf " %s %sx%s", &str($term), $w, $h;
  482. } elsif ($type eq "x11-req") {
  483. my ($single, $xprot, $xcookie, $xscreen) = &parse("bssu", $data);
  484. print " one-off" if $single eq "yes";
  485. printf " %s :%s", $xprot, $xscreen;
  486. } elsif ($type eq "exec") {
  487. my ($command) = &parse("s", $data);
  488. printf " %s", &str($command);
  489. } elsif ($type eq "subsystem") {
  490. my ($subsys) = &parse("s", $data);
  491. printf " %s", &str($subsys);
  492. if ($subsys eq "sftp") {
  493. &sftp_setup($index);
  494. }
  495. } elsif ($type eq "window-change") {
  496. my ($w, $h, $pw, $ph) = &parse("uuuu", $data);
  497. printf " %sx%s", $w, $h;
  498. } elsif ($type eq "xon-xoff") {
  499. my ($can) = &parse("b", $data);
  500. printf " %s", $can;
  501. } elsif ($type eq "signal") {
  502. my ($sig) = &parse("s", $data);
  503. printf " %s", &str($sig);
  504. } elsif ($type eq "exit-status") {
  505. my ($status) = &parse("u", $data);
  506. printf " %s", $status;
  507. } elsif ($type eq "exit-signal") {
  508. my ($sig, $core, $error, $lang) = &parse("sbss", $data);
  509. printf " %s", &str($sig);
  510. print " (core dumped)" if $core eq "yes";
  511. }
  512. print "\n";
  513. },
  514. #define SSH2_MSG_CHANNEL_SUCCESS 99 /* 0x63 */
  515. 'SSH2_MSG_CHANNEL_SUCCESS' => sub {
  516. my ($direction, $seq, $data) = @_;
  517. my ($rid) = &parse("uu", $data);
  518. $rid = ($direction eq "i" ? "c" : "s") . $rid;
  519. my $index = $chan_by_id{$rid};
  520. if (!defined $index) {
  521. printf "UNKNOWN_CHANNEL (%s)\n", $rid;
  522. return;
  523. }
  524. my $chan = $channels[$index];
  525. printf "ch%d (%s)", $index, $chan->{'id'};
  526. my $otherdir = ($direction eq "i" ? "o" : "i");
  527. my $request = shift @{$chan->{'requests_' . $otherdir}};
  528. if (defined $request) {
  529. printf " to %s", $request->[0];
  530. } else {
  531. print " (spurious?)";
  532. }
  533. print "\n";
  534. },
  535. #define SSH2_MSG_CHANNEL_FAILURE 100 /* 0x64 */
  536. 'SSH2_MSG_CHANNEL_FAILURE' => sub {
  537. my ($direction, $seq, $data) = @_;
  538. my ($rid) = &parse("uu", $data);
  539. $rid = ($direction eq "i" ? "c" : "s") . $rid;
  540. my $index = $chan_by_id{$rid};
  541. if (!defined $index) {
  542. printf "UNKNOWN_CHANNEL (%s)\n", $rid;
  543. return;
  544. }
  545. my $chan = $channels[$index];
  546. printf "ch%d (%s)", $index, $chan->{'id'};
  547. my $otherdir = ($direction eq "i" ? "o" : "i");
  548. my $request = shift @{$chan->{'requests_' . $otherdir}};
  549. if (defined $request) {
  550. printf " to %s", $request->[0];
  551. } else {
  552. print " (spurious?)";
  553. }
  554. print "\n";
  555. },
  556. #define SSH2_MSG_USERAUTH_GSSAPI_RESPONSE 60
  557. 'SSH2_MSG_USERAUTH_GSSAPI_RESPONSE' => sub {
  558. my ($direction, $seq, $data) = @_;
  559. print "\n";
  560. },
  561. #define SSH2_MSG_USERAUTH_GSSAPI_TOKEN 61
  562. 'SSH2_MSG_USERAUTH_GSSAPI_TOKEN' => sub {
  563. my ($direction, $seq, $data) = @_;
  564. print "\n";
  565. },
  566. #define SSH2_MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE 63
  567. 'SSH2_MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE' => sub {
  568. my ($direction, $seq, $data) = @_;
  569. print "\n";
  570. },
  571. #define SSH2_MSG_USERAUTH_GSSAPI_ERROR 64
  572. 'SSH2_MSG_USERAUTH_GSSAPI_ERROR' => sub {
  573. my ($direction, $seq, $data) = @_;
  574. print "\n";
  575. },
  576. #define SSH2_MSG_USERAUTH_GSSAPI_ERRTOK 65
  577. 'SSH2_MSG_USERAUTH_GSSAPI_ERRTOK' => sub {
  578. my ($direction, $seq, $data) = @_;
  579. print "\n";
  580. },
  581. #define SSH2_MSG_USERAUTH_GSSAPI_MIC 66
  582. 'SSH2_MSG_USERAUTH_GSSAPI_MIC' => sub {
  583. my ($direction, $seq, $data) = @_;
  584. print "\n";
  585. },
  586. );
  587. our %disc_reasons = (
  588. 1 => "SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT",
  589. 2 => "SSH_DISCONNECT_PROTOCOL_ERROR",
  590. 3 => "SSH_DISCONNECT_KEY_EXCHANGE_FAILED",
  591. 4 => "SSH_DISCONNECT_RESERVED",
  592. 5 => "SSH_DISCONNECT_MAC_ERROR",
  593. 6 => "SSH_DISCONNECT_COMPRESSION_ERROR",
  594. 7 => "SSH_DISCONNECT_SERVICE_NOT_AVAILABLE",
  595. 8 => "SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED",
  596. 9 => "SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE",
  597. 10 => "SSH_DISCONNECT_CONNECTION_LOST",
  598. 11 => "SSH_DISCONNECT_BY_APPLICATION",
  599. 12 => "SSH_DISCONNECT_TOO_MANY_CONNECTIONS",
  600. 13 => "SSH_DISCONNECT_AUTH_CANCELLED_BY_USER",
  601. 14 => "SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE",
  602. 15 => "SSH_DISCONNECT_ILLEGAL_USER_NAME",
  603. );
  604. my %verbose_packet_dump_functions = (
  605. 'SSH2_MSG_KEXINIT' => sub {
  606. my ($data) = @_;
  607. my ($cookie0, $cookie1, $cookie2, $cookie3,
  608. $kex, $hostkey, $cscipher, $sccipher, $csmac, $scmac,
  609. $cscompress, $sccompress, $cslang, $sclang, $guess, $reserved) =
  610. &parse("uuuussssssssssbu", $data);
  611. printf(" cookie: %08x%08x%08x%08x\n",
  612. $cookie0, $cookie1, $cookie2, $cookie3);
  613. my $print_namelist = sub {
  614. my @names = split /,/, $_[1];
  615. printf " %s: name-list with %d items%s\n", $_[0], (scalar @names),
  616. join "", map { "\n $_" } @names;
  617. };
  618. $print_namelist->("kex", $kex);
  619. $print_namelist->("host key", $hostkey);
  620. $print_namelist->("client->server cipher", $cscipher);
  621. $print_namelist->("server->client cipher", $sccipher);
  622. $print_namelist->("client->server MAC", $csmac);
  623. $print_namelist->("server->client MAC", $scmac);
  624. $print_namelist->("client->server compression", $cscompress);
  625. $print_namelist->("server->client compression", $sccompress);
  626. $print_namelist->("client->server language", $cslang);
  627. $print_namelist->("server->client language", $sclang);
  628. printf " first kex packet follows: %s\n", $guess;
  629. printf " reserved field: %#x\n", $reserved;
  630. },
  631. 'SSH2_MSG_KEXDH_INIT' => sub {
  632. my ($data) = @_;
  633. my ($e) = &parse("m", $data);
  634. printf " e: %s\n", $e;
  635. },
  636. 'SSH2_MSG_KEX_DH_GEX_REQUEST' => sub {
  637. my ($data) = @_;
  638. my ($min, $pref, $max) = &parse("uuu", $data);
  639. printf " min bits: %d\n", $min;
  640. printf " preferred bits: %d\n", $pref;
  641. printf " max bits: %d\n", $max;
  642. },
  643. 'SSH2_MSG_KEX_DH_GEX_GROUP' => sub {
  644. my ($data) = @_;
  645. my ($p, $g) = &parse("mm", $data);
  646. printf " p: %s\n", $p;
  647. printf " g: %s\n", $g;
  648. },
  649. 'SSH2_MSG_KEX_DH_GEX_INIT' => sub {
  650. my ($data) = @_;
  651. my ($e) = &parse("m", $data);
  652. printf " e: %s\n", $e;
  653. },
  654. 'SSH2_MSG_KEX_ECDH_INIT' => sub {
  655. my ($data) = @_;
  656. my ($cpv) = &parse("s", $data);
  657. # Public values in ECDH depend for their interpretation on the
  658. # selected curve, and this script doesn't cross-analyse the
  659. # two KEXINIT packets to independently figure out what that
  660. # curve is. So the best we can do is just dump the raw data.
  661. printf " client public value: %s\n", (unpack "H*", $cpv);
  662. },
  663. 'SSH2_MSG_KEX_HYBRID_INIT' => sub {
  664. my ($data) = @_;
  665. my ($cpv) = &parse("s", $data);
  666. # Hybrid post-quantum + classical KEX is even more confusing,
  667. # since two separate pieces of data are glomphed together into
  668. # this string without any obvious dividing line. The best we
  669. # can sensibly do is to announce that in the log.
  670. printf " client PQ encryption key + public ECDH value: %s\n",
  671. (unpack "H*", $cpv);
  672. },
  673. 'SSH2_MSG_KEXDH_REPLY' => sub {
  674. my ($data) = @_;
  675. my ($hostkeyblob, $f, $sigblob) = &parse("sms", $data);
  676. my ($hktype, @hostkey) = &parse_public_key($hostkeyblob);
  677. printf " host key: %s\n", $hktype;
  678. while (@hostkey) {
  679. my ($key, $value) = splice @hostkey, 0, 2;
  680. printf " $key: $value\n";
  681. }
  682. printf " f: %s\n", $f;
  683. printf " signature:\n";
  684. my @signature = &parse_signature($sigblob, $hktype);
  685. while (@signature) {
  686. my ($key, $value) = splice @signature, 0, 2;
  687. printf " $key: $value\n";
  688. }
  689. },
  690. 'SSH2_MSG_KEX_DH_GEX_REPLY' => sub {
  691. my ($data) = @_;
  692. my ($hostkeyblob, $f, $sigblob) = &parse("sms", $data);
  693. my ($hktype, @hostkey) = &parse_public_key($hostkeyblob);
  694. printf " host key: %s\n", $hktype;
  695. while (@hostkey) {
  696. my ($key, $value) = splice @hostkey, 0, 2;
  697. printf " $key: $value\n";
  698. }
  699. printf " f: %s\n", $f;
  700. printf " signature:\n";
  701. my @signature = &parse_signature($sigblob, $hktype);
  702. while (@signature) {
  703. my ($key, $value) = splice @signature, 0, 2;
  704. printf " $key: $value\n";
  705. }
  706. },
  707. 'SSH2_MSG_KEX_ECDH_REPLY' => sub {
  708. my ($data) = @_;
  709. my ($hostkeyblob, $spv, $sigblob) = &parse("sss", $data);
  710. my ($hktype, @hostkey) = &parse_public_key($hostkeyblob);
  711. printf " host key: %s\n", $hktype;
  712. while (@hostkey) {
  713. my ($key, $value) = splice @hostkey, 0, 2;
  714. printf " $key: $value\n";
  715. }
  716. printf " server public value: %s\n", (unpack "H*", $spv);
  717. printf " signature:\n";
  718. my @signature = &parse_signature($sigblob, $hktype);
  719. while (@signature) {
  720. my ($key, $value) = splice @signature, 0, 2;
  721. printf " $key: $value\n";
  722. }
  723. },
  724. 'SSH2_MSG_KEX_HYBRID_REPLY' => sub {
  725. my ($data) = @_;
  726. my ($hostkeyblob, $spv, $sigblob) = &parse("sss", $data);
  727. my ($hktype, @hostkey) = &parse_public_key($hostkeyblob);
  728. printf " host key: %s\n", $hktype;
  729. while (@hostkey) {
  730. my ($key, $value) = splice @hostkey, 0, 2;
  731. printf " $key: $value\n";
  732. }
  733. # Similarly to HYBRID_INIT, warn the reader that this string
  734. # contains two separate things glomphed together
  735. printf " server PQ KEM ciphertext + public ECDH value: %s\n",
  736. (unpack "H*", $spv);
  737. printf " signature:\n";
  738. my @signature = &parse_signature($sigblob, $hktype);
  739. while (@signature) {
  740. my ($key, $value) = splice @signature, 0, 2;
  741. printf " $key: $value\n";
  742. }
  743. },
  744. 'SSH2_MSG_NEWKEYS' => sub {},
  745. 'SSH2_MSG_SERVICE_REQUEST' => sub {
  746. my ($data) = @_;
  747. my ($servname) = &parse("s", $data);
  748. printf " service name: %s\n", $servname;
  749. },
  750. 'SSH2_MSG_SERVICE_ACCEPT' => sub {
  751. my ($data) = @_;
  752. my ($servname) = &parse("s", $data);
  753. printf " service name: %s\n", $servname;
  754. },
  755. 'SSH2_MSG_DISCONNECT' => sub {
  756. my ($data) = @_;
  757. my ($reason, $desc, $lang) = &parse("uss", $data);
  758. printf(" reason code: %d%s\n", $reason,
  759. defined $disc_reasons{$reason} ?
  760. " ($disc_reasons{$reason})" : "" );
  761. printf " description: '%s'\n", $desc;
  762. printf " language tag: '%s'\n", $lang;
  763. },
  764. 'SSH2_MSG_DEBUG' => sub {
  765. my ($data) = @_;
  766. my ($display, $desc, $lang) = &parse("bss", $data);
  767. printf " always display: %s\n", $display;
  768. printf " description: '%s'\n", $desc;
  769. printf " language tag: '%s'\n", $lang;
  770. },
  771. 'SSH2_MSG_IGNORE' => sub {
  772. my ($data) = @_;
  773. my ($payload) = &parse("s", $data);
  774. printf " data: %s\n", unpack "H*", $payload;
  775. },
  776. 'SSH2_MSG_UNIMPLEMENTED' => sub {
  777. my ($data) = @_;
  778. my ($seq) = &parse("u", $data);
  779. printf " sequence number: %d\n", $seq;
  780. },
  781. 'SSH2_MSG_KEXGSS_INIT' => sub {
  782. my ($data) = @_;
  783. my ($token, $e) = &parse("sm", $data);
  784. printf " output token: %s\n", unpack "H*", $token;
  785. printf " e: %s\n", $e;
  786. },
  787. 'SSH2_MSG_KEXGSS_CONTINUE' => sub {
  788. my ($data) = @_;
  789. my ($token) = &parse("s", $data);
  790. printf " output token: %s\n", unpack "H*", $token;
  791. },
  792. 'SSH2_MSG_KEXGSS_COMPLETE' => sub {
  793. my ($data) = @_;
  794. my ($f, $permsgtoken, $got_output) = &parse("msb", $data);
  795. printf " f: %s\n", $f;
  796. printf " per-message token: %s\n", unpack "H*", $permsgtoken;
  797. printf " output token present: %s\n", $got_output;
  798. if ($got_output eq "yes") {
  799. my ($token) = &parse("s", $data);
  800. printf " output token: %s\n", unpack "H*", $token;
  801. }
  802. },
  803. 'SSH2_MSG_KEXGSS_HOSTKEY' => sub {
  804. my ($data) = @_;
  805. my ($hostkey) = &parse("s", $data);
  806. printf " host key: %s\n", unpack "H*", $hostkey;
  807. },
  808. 'SSH2_MSG_KEXGSS_ERROR' => sub {
  809. my ($data) = @_;
  810. my ($maj, $min, $msg, $lang) = &parse("uuss", $data);
  811. printf " major status: %d\n", $maj;
  812. printf " minor status: %d\n", $min;
  813. printf " message: '%s'\n", $msg;
  814. printf " language tag: '%s'\n", $lang;
  815. },
  816. 'SSH2_MSG_KEXGSS_GROUPREQ' => sub {
  817. my ($data) = @_;
  818. my ($min, $pref, $max) = &parse("uuu", $data);
  819. printf " min bits: %d\n", $min;
  820. printf " preferred bits: %d\n", $pref;
  821. printf " max bits: %d\n", $max;
  822. },
  823. 'SSH2_MSG_KEXGSS_GROUP' => sub {
  824. my ($data) = @_;
  825. my ($p, $g) = &parse("mm", $data);
  826. printf " p: %s\n", $p;
  827. printf " g: %s\n", $g;
  828. },
  829. );
  830. my %sftp_packets = (
  831. #define SSH_FXP_INIT 1 /* 0x1 */
  832. 0x1 => sub {
  833. my ($chan, $index, $direction, $id, $data) = @_;
  834. my ($ver) = &parse("u", $data);
  835. printf "SSH_FXP_INIT %d\n", $ver;
  836. },
  837. #define SSH_FXP_VERSION 2 /* 0x2 */
  838. 0x2 => sub {
  839. my ($chan, $index, $direction, $id, $data) = @_;
  840. my ($ver) = &parse("u", $data);
  841. printf "SSH_FXP_VERSION %d\n", $ver;
  842. },
  843. #define SSH_FXP_OPEN 3 /* 0x3 */
  844. 0x3 => sub {
  845. my ($chan, $index, $direction, $id, $data) = @_;
  846. my ($reqid, $path, $pflags) = &parse("usu", $data);
  847. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_OPEN");
  848. printf " \"%s\" ", $path;
  849. if ($pflags eq 0) {
  850. print "0";
  851. } else {
  852. my $sep = "";
  853. if ($pflags & 1) { $pflags ^= 1; print "${sep}READ"; $sep = "|"; }
  854. if ($pflags & 2) { $pflags ^= 2; print "${sep}WRITE"; $sep = "|"; }
  855. if ($pflags & 4) { $pflags ^= 4; print "${sep}APPEND"; $sep = "|"; }
  856. if ($pflags & 8) { $pflags ^= 8; print "${sep}CREAT"; $sep = "|"; }
  857. if ($pflags & 16) { $pflags ^= 16; print "${sep}TRUNC"; $sep = "|"; }
  858. if ($pflags & 32) { $pflags ^= 32; print "${sep}EXCL"; $sep = "|"; }
  859. if ($pflags) { print "${sep}${pflags}"; }
  860. }
  861. print "\n";
  862. },
  863. #define SSH_FXP_CLOSE 4 /* 0x4 */
  864. 0x4 => sub {
  865. my ($chan, $index, $direction, $id, $data) = @_;
  866. my ($reqid, $handle) = &parse("us", $data);
  867. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_CLOSE");
  868. printf " \"%s\"", &stringescape($handle);
  869. print "\n";
  870. },
  871. #define SSH_FXP_READ 5 /* 0x5 */
  872. 0x5 => sub {
  873. my ($chan, $index, $direction, $id, $data) = @_;
  874. my ($reqid, $handle, $offset, $len) = &parse("usUu", $data);
  875. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_READ");
  876. printf " \"%s\" %d %d", &stringescape($handle), $offset, $len;
  877. print "\n";
  878. },
  879. #define SSH_FXP_WRITE 6 /* 0x6 */
  880. 0x6 => sub {
  881. my ($chan, $index, $direction, $id, $data) = @_;
  882. my ($reqid, $handle, $offset, $wdata) = &parse("usUs", $data);
  883. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_WRITE");
  884. printf " \"%s\" %d [%d bytes]", &stringescape($handle), $offset, length $wdata;
  885. print "\n";
  886. },
  887. #define SSH_FXP_LSTAT 7 /* 0x7 */
  888. 0x7 => sub {
  889. my ($chan, $index, $direction, $id, $data) = @_;
  890. my ($reqid, $path) = &parse("us", $data);
  891. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_LSTAT");
  892. printf " \"%s\"", $path;
  893. print "\n";
  894. },
  895. #define SSH_FXP_FSTAT 8 /* 0x8 */
  896. 0x8 => sub {
  897. my ($chan, $index, $direction, $id, $data) = @_;
  898. my ($reqid, $handle) = &parse("us", $data);
  899. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_FSTAT");
  900. printf " \"%s\"", &stringescape($handle);
  901. print "\n";
  902. },
  903. #define SSH_FXP_SETSTAT 9 /* 0x9 */
  904. 0x9 => sub {
  905. my ($chan, $index, $direction, $id, $data) = @_;
  906. my ($reqid, $path) = &parse("us", $data);
  907. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_SETSTAT");
  908. my $attrs = &sftp_parse_attrs($data);
  909. printf " \"%s\" %s", $path, $attrs;
  910. print "\n";
  911. },
  912. #define SSH_FXP_FSETSTAT 10 /* 0xa */
  913. 0xa => sub {
  914. my ($chan, $index, $direction, $id, $data) = @_;
  915. my ($reqid, $handle) = &parse("us", $data);
  916. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_FSETSTAT");
  917. my $attrs = &sftp_parse_attrs($data);
  918. printf " \"%s\" %s", &stringescape($handle), $attrs;
  919. print "\n";
  920. },
  921. #define SSH_FXP_OPENDIR 11 /* 0xb */
  922. 0xb => sub {
  923. my ($chan, $index, $direction, $id, $data) = @_;
  924. my ($reqid, $path) = &parse("us", $data);
  925. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_OPENDIR");
  926. printf " \"%s\"", $path;
  927. print "\n";
  928. },
  929. #define SSH_FXP_READDIR 12 /* 0xc */
  930. 0xc => sub {
  931. my ($chan, $index, $direction, $id, $data) = @_;
  932. my ($reqid, $handle) = &parse("us", $data);
  933. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_READDIR");
  934. printf " \"%s\"", &stringescape($handle);
  935. print "\n";
  936. },
  937. #define SSH_FXP_REMOVE 13 /* 0xd */
  938. 0xd => sub {
  939. my ($chan, $index, $direction, $id, $data) = @_;
  940. my ($reqid, $path) = &parse("us", $data);
  941. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_REMOVE");
  942. printf " \"%s\"", $path;
  943. print "\n";
  944. },
  945. #define SSH_FXP_MKDIR 14 /* 0xe */
  946. 0xe => sub {
  947. my ($chan, $index, $direction, $id, $data) = @_;
  948. my ($reqid, $path) = &parse("us", $data);
  949. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_MKDIR");
  950. printf " \"%s\"", $path;
  951. print "\n";
  952. },
  953. #define SSH_FXP_RMDIR 15 /* 0xf */
  954. 0xf => sub {
  955. my ($chan, $index, $direction, $id, $data) = @_;
  956. my ($reqid, $path) = &parse("us", $data);
  957. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_RMDIR");
  958. printf " \"%s\"", $path;
  959. print "\n";
  960. },
  961. #define SSH_FXP_REALPATH 16 /* 0x10 */
  962. 0x10 => sub {
  963. my ($chan, $index, $direction, $id, $data) = @_;
  964. my ($reqid, $path) = &parse("us", $data);
  965. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_REALPATH");
  966. printf " \"%s\"", $path;
  967. print "\n";
  968. },
  969. #define SSH_FXP_STAT 17 /* 0x11 */
  970. 0x11 => sub {
  971. my ($chan, $index, $direction, $id, $data) = @_;
  972. my ($reqid, $path) = &parse("us", $data);
  973. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_STAT");
  974. printf " \"%s\"", $path;
  975. print "\n";
  976. },
  977. #define SSH_FXP_RENAME 18 /* 0x12 */
  978. 0x12 => sub {
  979. my ($chan, $index, $direction, $id, $data) = @_;
  980. my ($reqid, $srcpath, $dstpath) = &parse("uss", $data);
  981. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_RENAME");
  982. printf " \"%s\" \"%s\"", $srcpath, $dstpath;
  983. print "\n";
  984. },
  985. #define SSH_FXP_STATUS 101 /* 0x65 */
  986. 0x65 => sub {
  987. my ($chan, $index, $direction, $id, $data) = @_;
  988. my ($reqid, $status) = &parse("uu", $data);
  989. &sftp_logreply($chan, $direction, $reqid, $id, "SSH_FXP_STATUS");
  990. print " ";
  991. if ($status eq "0") { print "SSH_FX_OK"; }
  992. elsif ($status eq "1") { print "SSH_FX_EOF"; }
  993. elsif ($status eq "2") { print "SSH_FX_NO_SUCH_FILE"; }
  994. elsif ($status eq "3") { print "SSH_FX_PERMISSION_DENIED"; }
  995. elsif ($status eq "4") { print "SSH_FX_FAILURE"; }
  996. elsif ($status eq "5") { print "SSH_FX_BAD_MESSAGE"; }
  997. elsif ($status eq "6") { print "SSH_FX_NO_CONNECTION"; }
  998. elsif ($status eq "7") { print "SSH_FX_CONNECTION_LOST"; }
  999. elsif ($status eq "8") { print "SSH_FX_OP_UNSUPPORTED"; }
  1000. else { printf "[unknown status %d]", $status; }
  1001. print "\n";
  1002. },
  1003. #define SSH_FXP_HANDLE 102 /* 0x66 */
  1004. 0x66 => sub {
  1005. my ($chan, $index, $direction, $id, $data) = @_;
  1006. my ($reqid, $handle) = &parse("us", $data);
  1007. &sftp_logreply($chan, $direction, $reqid, $id, "SSH_FXP_HANDLE");
  1008. printf " \"%s\"", &stringescape($handle);
  1009. print "\n";
  1010. },
  1011. #define SSH_FXP_DATA 103 /* 0x67 */
  1012. 0x67 => sub {
  1013. my ($chan, $index, $direction, $id, $data) = @_;
  1014. my ($reqid, $retdata) = &parse("us", $data);
  1015. &sftp_logreply($chan, $direction, $reqid, $id, "SSH_FXP_DATA");
  1016. printf " [%d bytes]", length $retdata;
  1017. print "\n";
  1018. },
  1019. #define SSH_FXP_NAME 104 /* 0x68 */
  1020. 0x68 => sub {
  1021. my ($chan, $index, $direction, $id, $data) = @_;
  1022. my ($reqid, $count) = &parse("uu", $data);
  1023. &sftp_logreply($chan, $direction, $reqid, $id, "SSH_FXP_NAME");
  1024. for my $i (1..$count) {
  1025. my ($name, $longname) = &parse("ss", $data);
  1026. my $attrs = &sftp_parse_attrs($data);
  1027. print " [name=\"$name\", longname=\"$longname\", attrs=$attrs]";
  1028. }
  1029. print "\n";
  1030. },
  1031. #define SSH_FXP_ATTRS 105 /* 0x69 */
  1032. 0x69 => sub {
  1033. my ($chan, $index, $direction, $id, $data) = @_;
  1034. my ($reqid) = &parse("u", $data);
  1035. &sftp_logreply($chan, $direction, $reqid, $id, "SSH_FXP_ATTRS");
  1036. my $attrs = &sftp_parse_attrs($data);
  1037. printf " %s", $attrs;
  1038. print "\n";
  1039. },
  1040. #define SSH_FXP_EXTENDED 200 /* 0xc8 */
  1041. 0xc8 => sub {
  1042. my ($chan, $index, $direction, $id, $data) = @_;
  1043. my ($reqid, $type) = &parse("us", $data);
  1044. &sftp_logreq($chan, $direction, $reqid, $id, "SSH_FXP_EXTENDED");
  1045. printf " \"%s\"", $type;
  1046. print "\n";
  1047. },
  1048. #define SSH_FXP_EXTENDED_REPLY 201 /* 0xc9 */
  1049. 0xc9 => sub {
  1050. my ($chan, $index, $direction, $id, $data) = @_;
  1051. my ($reqid) = &parse("u", $data);
  1052. print "\n";
  1053. &sftp_logreply($chan, $direction, $reqid,$id,"SSH_FXP_EXTENDED_REPLY");
  1054. },
  1055. );
  1056. for my $type (keys %verbose_packet) {
  1057. if (!defined $verbose_packet_dump_functions{$type}) {
  1058. die "no verbose dump available for packet type $type\n";
  1059. }
  1060. }
  1061. my ($direction, $seq, $ourseq, $type, $data, $recording);
  1062. my %ourseqs = ('i'=>0, 'o'=>0);
  1063. $recording = 0;
  1064. while (<<>>) {
  1065. if ($recording) {
  1066. if (/^ [0-9a-fA-F]{8} ((?:[0-9a-fA-F]{2} )*[0-9a-fA-F]{2})/) {
  1067. push @$data, map { $_ eq "XX" ? -1 : hex $_ } split / /, $1;
  1068. } else {
  1069. $recording = 0;
  1070. my $fullseq = "$direction$ourseq";
  1071. print "$fullseq: $type ";
  1072. my ($verbose_dump, $verbose_data) = undef;
  1073. if (defined $verbose_packet_dump_functions{$type} &&
  1074. ($verbose_all || defined $verbose_packet{$type})) {
  1075. $verbose_dump = $verbose_packet_dump_functions{$type};
  1076. $verbose_data = [ @$data ];
  1077. }
  1078. if (defined $packets{$type}) {
  1079. $packets{$type}->($direction, $fullseq, $data);
  1080. } else {
  1081. printf "raw %s\n", join "", map { sprintf "%02x", $_ } @$data;
  1082. }
  1083. if (defined $verbose_dump) {
  1084. $verbose_dump->($verbose_data);
  1085. if (@$verbose_data) {
  1086. printf(" trailing bytes: %s\n",
  1087. unpack "H*", pack "C*", @$verbose_data);
  1088. }
  1089. }
  1090. }
  1091. }
  1092. if (/^(Incoming|Outgoing) packet #0x([0-9a-fA-F]+), type \d+ \/ 0x[0-9a-fA-F]+ \((.*)\)/) {
  1093. $direction = ($1 eq "Incoming" ? 'i' : 'o');
  1094. # $seq is the sequence number quoted in the log file. $ourseq
  1095. # is our own count of the sequence number, which differs in
  1096. # that it shouldn't wrap at 2^32, should anyone manage to run
  1097. # this script over such a huge log file.
  1098. $seq = hex $2;
  1099. $ourseq = $ourseqs{$direction}++;
  1100. $type = $3;
  1101. $data = [];
  1102. $recording = 1;
  1103. }
  1104. if ($pass_through_events && m/^Event Log: ([^\n]*)$/) {
  1105. printf "event: $1\n";
  1106. }
  1107. }
  1108. if ($dumpchannels) {
  1109. my %stateorder = ('closed'=>0, 'rejected'=>1,
  1110. 'halfclosed'=>2, 'open'=>3, 'halfopen'=>4);
  1111. for my $index (0..$#channels) {
  1112. my $chan = $channels[$index];
  1113. my $so = $stateorder{$chan->{'state'}};
  1114. $so = 1000 unless defined $so; # any state I've missed above comes last
  1115. $chan->{'index'} = sprintf "ch%d", $index;
  1116. $chan->{'order'} = sprintf "%08d %08d", $so, $index;
  1117. }
  1118. my @sortedchannels = sort { $a->{'order'} cmp $b->{'order'} } @channels;
  1119. for my $chan (@sortedchannels) {
  1120. printf "%s (%s): %s\n", $chan->{'index'}, $chan->{'id'}, $chan->{'state'};
  1121. }
  1122. }
  1123. sub format_unsigned_hex_integer {
  1124. my $abs = join "", map { sprintf "%02x", $_ } @_;
  1125. $abs =~ s!^0*!!g;
  1126. $abs = "0" if $abs eq "";
  1127. return "0x" . $abs;
  1128. }
  1129. sub parseone {
  1130. my ($type, $data) = @_;
  1131. if ($type eq "u") { # uint32
  1132. my @bytes = splice @$data, 0, 4;
  1133. return "<missing>" if @bytes < 4 or grep { $_<0 } @bytes;
  1134. return unpack "N", pack "C*", @bytes;
  1135. } elsif ($type eq "U") { # uint64
  1136. my @bytes = splice @$data, 0, 8;
  1137. return "<missing>" if @bytes < 8 or grep { $_<0 } @bytes;
  1138. my @words = unpack "NN", pack "C*", @bytes;
  1139. return ($words[0] << 32) + $words[1];
  1140. } elsif ($type eq "b") { # boolean
  1141. my $byte = shift @$data;
  1142. return "<missing>" if !defined $byte or $byte < 0;
  1143. return $byte ? "yes" : "no";
  1144. } elsif ($type eq "B") { # byte
  1145. my $byte = shift @$data;
  1146. return "<missing>" if !defined $byte or $byte < 0;
  1147. return $byte;
  1148. } elsif ($type eq "s" or $type eq "m") { # string, mpint
  1149. my @bytes = splice @$data, 0, 4;
  1150. return "<missing>" if @bytes < 4 or grep { $_<0 } @bytes;
  1151. my $len = unpack "N", pack "C*", @bytes;
  1152. @bytes = splice @$data, 0, $len;
  1153. return "<missing>" if @bytes < $len or grep { $_<0 } @bytes;
  1154. if ($type eq "m") {
  1155. my $str = "";
  1156. if ($bytes[0] >= 128) {
  1157. # Take two's complement.
  1158. @bytes = map { 0xFF ^ $_ } @bytes;
  1159. for my $i (reverse 0..$#bytes) {
  1160. if ($bytes[$i] < 0xFF) {
  1161. $bytes[$i]++;
  1162. last;
  1163. } else {
  1164. $bytes[$i] = 0;
  1165. }
  1166. }
  1167. $str = "-";
  1168. }
  1169. $str .= &format_unsigned_hex_integer(@bytes);
  1170. return $str;
  1171. } else {
  1172. return pack "C*", @bytes;
  1173. }
  1174. }
  1175. }
  1176. sub parse {
  1177. my ($template, $data) = @_;
  1178. return map { &parseone($_, $data) } split //, $template;
  1179. }
  1180. sub str {
  1181. # Quote as a string. If I get enthusiastic I might arrange for
  1182. # strange characters inside the string to be quoted.
  1183. my $str = shift @_;
  1184. return "'$str'";
  1185. }
  1186. sub sftp_setup {
  1187. my $index = shift @_;
  1188. my $chan = $channels[$index];
  1189. $chan->{'obuf'} = $chan->{'ibuf'} = '';
  1190. $chan->{'ocnt'} = $chan->{'icnt'} = 0;
  1191. $chan->{'odata'} = $chan->{'idata'} = \&sftp_data;
  1192. $chan->{'sftpreqs'} = {};
  1193. }
  1194. sub sftp_data {
  1195. my ($chan, $index, $direction, $data) = @_;
  1196. my $buf = \$chan->{$direction."buf"};
  1197. my $cnt = \$chan->{$direction."cnt"};
  1198. $$buf .= $data;
  1199. while (length $$buf >= 4) {
  1200. my $msglen = unpack "N", $$buf;
  1201. last if length $$buf < 4 + $msglen;
  1202. my $msg = substr $$buf, 4, $msglen;
  1203. $$buf = substr $$buf, 4 + $msglen;
  1204. $msg = [unpack "C*", $msg];
  1205. my $type = shift @$msg;
  1206. my $id = sprintf "ch%d_sftp_%s%d", $index, $direction, ${$cnt}++;
  1207. print "$id: ";
  1208. if (defined $sftp_packets{$type}) {
  1209. $sftp_packets{$type}->($chan, $index, $direction, $id, $msg);
  1210. } else {
  1211. printf "unknown SFTP packet type %d\n", $type;
  1212. }
  1213. }
  1214. }
  1215. sub sftp_logreq {
  1216. my ($chan, $direction, $reqid, $id, $name) = @_;
  1217. print "$name";
  1218. if ($direction eq "o") { # requests coming _in_ are too weird to track
  1219. $chan->{'sftpreqs'}->{$reqid} = $id;
  1220. }
  1221. }
  1222. sub sftp_logreply {
  1223. my ($chan, $direction, $reqid, $id, $name) = @_;
  1224. print "$name";
  1225. if ($direction eq "i") { # replies going _out_ are too weird to track
  1226. if (defined $chan->{'sftpreqs'}->{$reqid}) {
  1227. print " to ", $chan->{'sftpreqs'}->{$reqid};
  1228. $chan->{'sftpreqs'}->{$reqid} = undef;
  1229. }
  1230. }
  1231. }
  1232. sub sftp_parse_attrs {
  1233. my ($data) = @_;
  1234. my ($flags) = &parse("u", $data);
  1235. return $flags if $flags eq "<missing>";
  1236. my $out = "{";
  1237. my $sep = "";
  1238. if ($flags & 0x00000001) { # SSH_FILEXFER_ATTR_SIZE
  1239. $out .= $sep . sprintf "size=%d", &parse("U", $data);
  1240. $sep = ", ";
  1241. }
  1242. if ($flags & 0x00000002) { # SSH_FILEXFER_ATTR_UIDGID
  1243. $out .= $sep . sprintf "uid=%d", &parse("u", $data);
  1244. $out .= $sep . sprintf "gid=%d", &parse("u", $data);
  1245. $sep = ", ";
  1246. }
  1247. if ($flags & 0x00000004) { # SSH_FILEXFER_ATTR_PERMISSIONS
  1248. $out .= $sep . sprintf "perms=%#o", &parse("u", $data);
  1249. $sep = ", ";
  1250. }
  1251. if ($flags & 0x00000008) { # SSH_FILEXFER_ATTR_ACMODTIME
  1252. $out .= $sep . sprintf "atime=%d", &parse("u", $data);
  1253. $out .= $sep . sprintf "mtime=%d", &parse("u", $data);
  1254. $sep = ", ";
  1255. }
  1256. if ($flags & 0x80000000) { # SSH_FILEXFER_ATTR_EXTENDED
  1257. my $extcount = &parse("u", $data);
  1258. while ($extcount-- > 0) {
  1259. $out .= $sep . sprintf "\"%s\"=\"%s\"", &parse("ss", $data);
  1260. $sep = ", ";
  1261. }
  1262. }
  1263. $out .= "}";
  1264. return $out;
  1265. }
  1266. sub parse_public_key {
  1267. my ($blob) = @_;
  1268. my $data = [ unpack "C*", $blob ];
  1269. my @toret;
  1270. my ($type) = &parse("s", $data);
  1271. push @toret, $type;
  1272. if ($type eq "ssh-rsa") {
  1273. my ($e, $n) = &parse("mm", $data);
  1274. push @toret, "e", $e, "n", $n;
  1275. } elsif ($type eq "ssh-dss") {
  1276. my ($p, $q, $g, $y) = &parse("mmmm", $data);
  1277. push @toret, "p", $p, "q", $q, "g", $g, "y", $y;
  1278. } elsif ($type eq "ssh-ed25519") {
  1279. my ($xyblob) = &parse("s", $data);
  1280. my @y = unpack "C*", $xyblob;
  1281. push @toret, "hibit(x)", $y[$#y] & 1;
  1282. $y[$#y] &= ~1;
  1283. push @toret, "y & ~1", &format_unsigned_hex_integer(@y);
  1284. } elsif ($type =~ m!^ecdsa-sha2-nistp(256|384|521)$!) {
  1285. my ($curvename, $blob) = &parse("ss", $data);
  1286. push @toret, "curve name", $curvename;
  1287. my @blobdata = unpack "C*", $blob;
  1288. my ($fmt) = &parse("B", \@blobdata);
  1289. push @toret, "format byte", $fmt;
  1290. if ($fmt == 4) {
  1291. push @toret, "x", &format_unsigned_hex_integer(
  1292. @blobdata[0..($#blobdata+1)/2-1]);
  1293. push @toret, "y", &format_unsigned_hex_integer(
  1294. @blobdata[($#blobdata+1)/2..$#blobdata]);
  1295. }
  1296. } else {
  1297. push @toret, "undecoded data", unpack "H*", pack "C*", @$data;
  1298. }
  1299. return @toret;
  1300. };
  1301. sub parse_signature {
  1302. my ($blob, $keytype) = @_;
  1303. my $data = [ unpack "C*", $blob ];
  1304. my @toret;
  1305. if ($keytype eq "ssh-rsa") {
  1306. my ($type, $s) = &parse("ss", $data);
  1307. push @toret, "sig type", $type;
  1308. push @toret, "s", &format_unsigned_hex_integer(unpack "C*", $s);
  1309. } elsif ($keytype eq "ssh-dss") {
  1310. my ($type, $subblob) = &parse("ss", $data);
  1311. push @toret, "sig type", $type;
  1312. push @toret, "r", &format_unsigned_hex_integer(
  1313. unpack "C*", substr($subblob, 0, 20));
  1314. push @toret, "s", &format_unsigned_hex_integer(
  1315. unpack "C*", substr($subblob, 20, 40));
  1316. } elsif ($keytype eq "ssh-ed25519") {
  1317. my ($type, $rsblob) = &parse("ss", $data);
  1318. push @toret, "sig type", $type;
  1319. my @ry = unpack "C*", $rsblob;
  1320. my @sy = splice @ry, 32, 32;
  1321. push @toret, "hibit(r.x)", $ry[$#ry] & 1;
  1322. $ry[$#ry] &= ~1;
  1323. push @toret, "r.y & ~1", &format_unsigned_hex_integer(@ry);
  1324. push @toret, "hibit(s.x)", $sy[$#sy] & 1;
  1325. $sy[$#sy] &= ~1;
  1326. push @toret, "s.y & ~1", &format_unsigned_hex_integer(@sy);
  1327. } elsif ($keytype =~ m!^ecdsa-sha2-nistp(256|384|521)$!) {
  1328. my ($sigtype, $subblob) = &parse("ss", $data);
  1329. push @toret, "sig type", $sigtype;
  1330. my @sbdata = unpack "C*", $subblob;
  1331. my ($r, $s) = &parse("mm", \@sbdata);
  1332. push @toret, "r", $r, "s", $s;
  1333. } else {
  1334. push @toret, "undecoded data", unpack "H*", pack "C*", @$data;
  1335. }
  1336. return @toret;
  1337. };
  1338. sub stringescape {
  1339. my ($str) = @_;
  1340. $str =~ s!\\!\\\\!g;
  1341. $str =~ s![^ -~]!sprintf "\\x%02X", ord $&!eg;
  1342. return $str;
  1343. }