Bash Readline 有用的快捷键

Bash Ctrl+x 一族有许多方便快捷的指令,类似 Emacs 的按键,记录一下。

  • clear-display (M-C-l) 🔗 类似 clear 指令,达成普通 Ctrl+l 不消除的 Terminal Scrollbuffer
  • transpose-words (M-t) 🔗 可以在两个单词之间,按下进行单词交换。形成单词整体向后移动的效果
  • complete-filename (M-/) 🔗 触发文件名补全,甚至还有 complete-into-braces (M-{) 能自动计算出最短形式。用 possible-filename-completions (C-x /) 能显示列表。
    相似的还有 complete-variable (M-$)complete-command (M-!) 等,明确指定要补全的种类
  • undo (C-_ or C-x C-u) 🔗 Ctrl+ 下划线容易和窗口缩放冲突,用另一个
  • unix-line-discard (C-u) 🔗 在行尾时清除整行
  • insert-comment (M-#) 🔗 把当前行变成注释,相当于取消执行,但保留在可视范围内。方便复制
  • backward-kill-word (M-DEL)🔗 普通的 unix-word-rubout (C-w) 是删除整个不带空格的单词,但这个可以删除 camel case 部分

程序运行在 GDB 与 Shell 中的栈地址有所不同

pwn_college

在进行 Binary Exploitation 章节的挑战时,经常需要通过 GDB + 关闭 ASLR 等方式获取栈上返回地址。但断点调试中地址总是和实际运行中的存在 16 字节或者更多的差异。

实际是 GDB 会以完整路径启动,并且使用的环境变量有所不同,通过比较,相比终端环境多了 LINESCOLUMNS 两个变量
可以设置初始化脚本来解决

unset env LINES
unset env COLUMNS

在配合上固定环境空间,先导出,然后在另一个环境使用

env > runtime.env
env -i $(cat runtime.env | xargs) gdb ./your_program

如果环境变量带空格?或许得 { env -0; printf "%s\0" "$@"; } | xargs -0 env -i 之类的,没试验过

另一个原因是程序参数,也会占用栈的空间

pwntools 会把 ANSI Escape Sequence 原样输出

pwn_college

用 pwntools 写脚本操作比较 fancy 会输出进度条的程序,会遇到输出中一些 ANSI escape code 没有正确输出,反而以反斜杠形式原样输出了。

$ python test.py
[+] Starting local process '/usr/bin/bash': pid 5992
\x1b[2KWorking... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:01
\x1b[?25h[*] Stopped process '/usr/bin/bash' (pid 5992)
$ bat test.py -p
import pwn
import time
from rich.progress import track
 
with pwn.process(["bash"]) as io:
    for _ in track(range(10)):
        time.sleep(0.1)

这里输出中奇怪的内容是 \x1b[2K\x1b[?25h,如果启动的是 reverse bash shell,它的 readline 还可能有 x1b[?2004h\x1b[200C\x01\x1b[6D\x01 等控制光标的指令,均无法正确渲染。

问题并非出在终端模拟器或者程序输出上,而是目前 pwntools 4.15.0 的 term.put 支持不全,直接或间接触发 pwn.term.init() 后,会替换 sys.stdout,并对对于不认识 ANSI escape sequence 的就原样输出。

不过走另一条路,用 2026-04-05 > pwntools 读取程序输出的同时也记录在日志文件中 的 tee 直接输出到 stdout 就能绕过这层限制。

def tee(process: pwn.process):
	orig_send_raw = process.send_raw
	orig_recv_raw = process.recv_raw
	output = sys.__stdout__.buffer  # type: ignore sys.stdout is replaced by pwn.term
 
	def send_raw(data):
		output.write(data)
		output.flush()
		return orig_send_raw(data)
 
	def recv_raw(numb):
		data = orig_recv_raw(numb) or b""  # orig may return str('')
		output.write(data)
		output.flush()
		return data
 
	process.send_raw = send_raw
	process.recv_raw = recv_raw
 
with pwn.process(["/challenge/run"]) as io:
	tee(io)
	io.recvuntil(b"\r")
	io.info("Starting the hacking script...")
	io.sendline(b"python3 /tmp/pwnsolver.py hack")
	io.recvrepeat(timeout=None) # type: ignore

pwntools 在本地通过 SSH 远程解题

pwn_college

在终端中通过 SSH 连接到 200ms+ 遥远的服务器,在上面使用 tmux + vim 远程编程的实在是延迟较高。
即便有内嵌在网页上的 VSCode ,或者本地 VSCode 安装 SSHfs 插件,体验都差强人意。

一种快速调试的方式是,把 challenge 下载下来本地运行,下面这几种我都采用过,各有各的优势

  1. scp 直接拉取文件
  2. 访问 pwncollege/intro-to-cybersecurity-dojo: Intro to Cybersecurity 在线预览源码
  3. 拉取 pwncollege/challenges: pwn.college challenges 使用 pwnshop 工具在本地 Docker 启动题目容器

今天想到一种更流水线的模式,自动上传文件在远程执行。比如这是解决 intro-to-cybersecurity-dojo / integrated-security / secure-chat-1 的框架:

  • 在开发设备上运行脚本,会 SSH 到 challenge 机器,并 SFTF 把当前文件传上去启动运行
  • 脚本检测当前是 challenge 机器,进行解题
    • 这题有特殊要求,需要在 /challenge/run 启动的全新 shell 中进行操作
    • 所以 boot() 中在 shell 中运行 hack()
  • 这里的 tee 用来解决 pwntools 会把 ANSI Escape Sequence 原样输出 问题
  • 使用 io.recvrepeat() 不断触发 tee 的打印操作
  • 使用 io.wait(), io.shutdown() 来在 EOF 后停止程序
  • 在 Python 3.14 上运行如果抛出 opcode LOAD_SMALL_INT not allowed 错误,需要手动合并 https://github.com/Gallopsled/pwntools/pull/2627 中对 safeeval.py 的变更
import pwn
import os
import sys
 
remote_script = "/tmp/pwnsolver.py"
def tee(process: pwn.tube):
    orig_send_raw = process.send_raw
    orig_recv_raw = process.recv_raw
    output = sys.__stdout__.buffer  # type: ignore sys.stdout is replaced by pwn.term
 
    def send_raw(data, *args, **kwargs):
        output.write(data)
        output.flush()
        return orig_send_raw(data, *args, **kwargs)
 
    def recv_raw(numb, *args, **kwargs):
        data = orig_recv_raw(numb, *args, **kwargs) or b""  # orig may return str('')
        output.write(data)
        output.flush()
        return data
 
    process.send_raw = send_raw
    process.recv_raw = recv_raw
    
def main():
    # raw to skip checksec
    ssh = pwn.ssh(user="hacker", host="dojo.pwn.college", raw=True)  
    local_script = os.path.abspath(__file__)
    ssh.upload(local_script, remote_script)
    argv = ["/run/dojo/bin/python3", remote_script]
    # requires patch from https://github.com/Gallopsled/pwntools/pull/2627 on Python 3.14+
    io: pwn.tubes.ssh.ssh_process
    io = ssh.process(argv, argv[0], aslr=True)  # type: ignore
    tee(io)
    # keep triggering tee's recv_raw until remote process joins
    io.recvrepeat()
    io.wait()
 
 
def boot():
    with pwn.process(["/challenge/run"]) as io:
        tee(io)
        # wait for signal of bootup completion
        io.recvuntil(b"\r")
        io.info("Starting the hacking script...")
        io.sendline(f"python3 {remote_script} hack".encode())
        # keep triggering tee's recv_raw until it receives the stop signal
        io.recvuntil(b"pwn.college{")
        # send EOF to stop the process
        io.shutdown()
        io.info("Done! Check the output above for the flag.")
 
 
def hack():
    pass
 
 
if __name__ == "__main__":
    if sys.argv[1:2] == ["hack"]:
        hack()
    elif os.environ.get("DOJO_AUTH_TOKEN"):
        boot()
    else:
        main()