GLIBC相关

__environ

libc.so中存在一个符号'__environ'

其存储的是main函数导出的envp参数的指针

因此可以通过其泄露栈地址,并以此获得栈上的各类数据地址

read汇编

在glibc中的read,write,open这些系统调用级调用函数
其汇编代码中都会存在syscall(0x0f05)

在无输出类题中会有妙用

__libc_start_main

__libc_start_main函数

源码在glibc/csu/libc-start.c中

1
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char **argv,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
ElfW(auxv_t) *auxvec,
#endif
__typeof (main) init,
void (*fini) (void),
void (*rtld_fini) (void), void *stack_end)
{
/* Result of the 'main' function. */
int result;

__libc_multiple_libcs = &_dl_starting_up && !_dl_starting_up;

#ifndef SHARED
_dl_relocate_static_pie ();

char **ev = &argv[argc + 1];

__environ = ev;

/* Store the lowest stack address. This is done in ld.so if this is
the code for the DSO. */
__libc_stack_end = stack_end;

# ifdef HAVE_AUX_VECTOR
/* First process the auxiliary vector since we need to find the
program header to locate an eventually present PT_TLS entry. */
# ifndef LIBC_START_MAIN_AUXVEC_ARG
ElfW(auxv_t) *auxvec;
{
char **evp = ev;
while (*evp++ != NULL)
;
auxvec = (ElfW(auxv_t) *) evp;
}
# endif
_dl_aux_init (auxvec);
if (GL(dl_phdr) == NULL)
# endif
{
/* Starting from binutils-2.23, the linker will define the
magic symbol __ehdr_start to point to our own ELF header
if it is visible in a segment that also includes the phdrs.
So we can set up _dl_phdr and _dl_phnum even without any
information from auxv. */

extern const ElfW(Ehdr) __ehdr_start
__attribute__ ((weak, visibility ("hidden")));
if (&__ehdr_start != NULL)
{
assert (__ehdr_start.e_phentsize == sizeof *GL(dl_phdr));
GL(dl_phdr) = (const void *) &__ehdr_start + __ehdr_start.e_phoff;
GL(dl_phnum) = __ehdr_start.e_phnum;
}
}

/* Initialize very early so that tunables can use it. */
__libc_init_secure ();

__tunables_init (__environ);

ARCH_INIT_CPU_FEATURES ();

/* Perform IREL{,A} relocations. */
ARCH_SETUP_IREL ();

/* The stack guard goes into the TCB, so initialize it early. */
ARCH_SETUP_TLS ();

/* In some architectures, IREL{,A} relocations happen after TLS setup in
order to let IFUNC resolvers benefit from TCB information, e.g. powerpc's
hwcap and platform fields available in the TCB. */
ARCH_APPLY_IREL ();

/* Set up the stack checker's canary. */
uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
# ifdef THREAD_SET_STACK_GUARD
THREAD_SET_STACK_GUARD (stack_chk_guard);
# else
__stack_chk_guard = stack_chk_guard;
# endif

# ifdef DL_SYSDEP_OSCHECK
if (!__libc_multiple_libcs)
{
/* This needs to run to initiliaze _dl_osversion before TLS
setup might check it. */
DL_SYSDEP_OSCHECK (__libc_fatal);
}
# endif

/* Initialize libpthread if linked in. */
if (__pthread_initialize_minimal != NULL)
__pthread_initialize_minimal ();

/* Set up the pointer guard value. */
uintptr_t pointer_chk_guard = _dl_setup_pointer_guard (_dl_random,
stack_chk_guard);
# ifdef THREAD_SET_POINTER_GUARD
THREAD_SET_POINTER_GUARD (pointer_chk_guard);
# else
__pointer_chk_guard_local = pointer_chk_guard;
# endif

#endif /* !SHARED */

/* Register the destructor of the dynamic linker if there is any. */
if (__glibc_likely (rtld_fini != NULL))
__cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL);

#ifndef SHARED
/* Call the initializer of the libc. This is only needed here if we
are compiling for the static library in which case we haven't
run the constructors in `_dl_start_user'. */
__libc_init_first (argc, argv, __environ);

/* Register the destructor of the program, if any. */
if (fini)
__cxa_atexit ((void (*) (void *)) fini, NULL, NULL);

/* Some security at this point. Prevent starting a SUID binary where
the standard file descriptors are not opened. We have to do this
only for statically linked applications since otherwise the dynamic
loader did the work already. */
if (__builtin_expect (__libc_enable_secure, 0))
__libc_check_standard_fds ();
#endif

/* Call the initializer of the program, if any. */
#ifdef SHARED
if (__builtin_expect (GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS, 0))
GLRO(dl_debug_printf) ("\ninitialize program: %s\n\n", argv[0]);
#endif
if (init)
(*init) (argc, argv, __environ MAIN_AUXVEC_PARAM);

#ifdef SHARED
/* Auditing checkpoint: we have a new object. */
if (__glibc_unlikely (GLRO(dl_naudit) > 0))
{
struct audit_ifaces *afct = GLRO(dl_audit);
struct link_map *head = GL(dl_ns)[LM_ID_BASE]._ns_loaded;
for (unsigned int cnt = 0; cnt < GLRO(dl_naudit); ++cnt)
{
if (afct->preinit != NULL)
afct->preinit (&link_map_audit_state (head, cnt)->cookie);

afct = afct->next;
}
}
#endif

#ifdef SHARED
if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS))
GLRO(dl_debug_printf) ("\ntransferring control: %s\n\n", argv[0]);
#endif

#ifndef SHARED
_dl_debug_initialize (0, LM_ID_BASE);
#endif
#ifdef HAVE_CLEANUP_JMP_BUF
/* Memory for the cancellation buffer. */
struct pthread_unwind_buf unwind_buf;

int not_first_call;
not_first_call = setjmp ((struct __jmp_buf_tag *) unwind_buf.cancel_jmp_buf);
if (__glibc_likely (! not_first_call))
{
struct pthread *self = THREAD_SELF;

/* Store old info. */
unwind_buf.priv.data.prev = THREAD_GETMEM (self, cleanup_jmp_buf);
unwind_buf.priv.data.cleanup = THREAD_GETMEM (self, cleanup);

/* Store the new cleanup handler info. */
THREAD_SETMEM (self, cleanup_jmp_buf, &unwind_buf);

/* Run the program. */
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
}
else
{
/* Remove the thread-local data. */
# ifdef SHARED
PTHFCT_CALL (ptr__nptl_deallocate_tsd, ());
# else
extern void __nptl_deallocate_tsd (void) __attribute ((weak));
__nptl_deallocate_tsd ();
# endif

/* One less thread. Decrement the counter. If it is zero we
terminate the entire process. */
result = 0;
# ifdef SHARED
unsigned int *ptr = __libc_pthread_functions.ptr_nthreads;
# ifdef PTR_DEMANGLE
PTR_DEMANGLE (ptr);
# endif
# else
extern unsigned int __nptl_nthreads __attribute ((weak));
unsigned int *const ptr = &__nptl_nthreads;
# endif

if (! atomic_decrement_and_test (ptr))
/* Not much left to do but to exit the thread, not the process. */
__exit_thread ();
}
#else
/* Nothing fancy, just call the function. */
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
#endif

exit (result);
}

源码挺长提几个重要的点

  1. __libc_start_main共有7个参数,不过正常利用下只需要注意前三个就行,分别是main,argc,argv
  2. 函数过程中会调用read从而在栈上留下read的libc地址,一些情况下可以利用其中的syscall
  3. 最后启动main时main的三个参数分别是argc,argv,和__environ

__libc_start_main也可以启动除main以外的函数.甚至都不需要是函数只要是可执行的代码就行,不过需要另作一些布置

scanf

在 glibc中,__isoc99_scanfscanf 实际上是同一个函数的两个不同名称。__isoc99_scanfscanf 函数的 ISO C99 标准兼容版本的别名。ISO C99 是 C 语言的标准之一,它引入了一些新的特性和改进,其中包括一些涉及格式化输入的变化。

scanf与 read 函数相同,可以读取 \ x00 后面的内容,仅将换行符作为输入读取的结束标志。

不过这里要注意的是,%s 参数会以空格作为分隔符,也就是说,如果输入中含有空格,那么空格前后的内容会被分配到不同的 %s 参数中

scanf函数实际调用

1
2
3
4
5
int
_IO_vscanf (const char *format, va_list args)
{
return __vfscanf_internal (stdin, format, args, 0);
}

__vfscanf_internal的代码挺长,不做分析

scanf触发malloc探究

看最终触发malloc的部分

1
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
bool
__libc_scratch_buffer_grow_preserve (struct scratch_buffer *buffer)
{
size_t new_length = 2 * buffer->length;
void *new_ptr;

if (buffer->data == buffer->__space.__c)
{
/* Move buffer to the heap. No overflow is possible because
buffer->length describes a small buffer on the stack. */
new_ptr = malloc (new_length);
if (new_ptr == NULL)
return false;
memcpy (new_ptr, buffer->__space.__c, buffer->length);
}
else
{
/* Buffer was already on the heap. Check for overflow. */
if (__glibc_likely (new_length >= buffer->length))
new_ptr = realloc (buffer->data, new_length);
else
{
__set_errno (ENOMEM);
new_ptr = NULL;
}

if (__glibc_unlikely (new_ptr == NULL))
{
/* Deallocate, but buffer must remain valid to free. */
free (buffer->data);
scratch_buffer_init (buffer);
return false;
}
}

/* Install new heap-based buffer. */
buffer->data = new_ptr;
buffer->length = new_length;
return true;
}
libc_hidden_def (__libc_scratch_buffer_grow_preserve)

这里触发了malloc

不过可以发现,在scanf函数执行完后,并不能找到这个chunk,

显然在后面的操作中还触发了free

可以写一些demo跟进调试发现确实如此

触发条件

  1. scanf的格式化字符串参数应该为数字类(%d,%lld,%lf,%zu)
  2. 发送的字符应该为数字字符,即只能‘0’-‘9’
  3. 发送的长度应该>=1024(不含换行符)

细节问题:

  1. 最终变量被写为多少

如果发送1024个’0’,那么scanf最终的读取大小为0

但如果发送1024个大于’0’的字符则有几种情况:

%d FFFFFFFF(-1)
%lld 7FFFFFFFFFFFFFFF
%lf inf
%zu FFFFFFFFFFFFFFFF
  1. malloc申请多大的内存

malloc申请的大小为标准输入流默认缓冲区大小的两倍,一般是0x800,chunk实际大小0x810,属于largebin

会触发malloc_consolidate,如果有fastbin chunk的话会尝试合并并放入unsorted bin,经过unsorted遍历一般会放置到对应的bin

类似的printf

与scanf类似printf也会触发malloc

例如printf("%65535c",var);

其中65535差不多就是临界了,再往低可能就不触发malloc

这样就会触发申请一个很大的chunk(实际大小0x10030)

之后同样也会被free

还有说gets和puts也有这样的机制,不过我测试时并没有成功

相关利用

在低版本中可以用来触发hook函数

或者更通用的用于触发malloc_consolidate来完成一些利用

hateful dot

scanf在读取数字时能否使其不读取改变原值直接跳过?

例如scanf("%d",&var);,要保证var的值不改变

如果直接输入换行符,scanf因为没有发现数字,会继续等待输入,显然不行

这就需要用到scanf读取数字时的一个特性

当scanf读取数字时,可以用.来绕过其对变量值的写入

当读取整型时:可以用.1234534565或者单独.绕过

当读取浮点数时:只能用.绕过

与之类似的还有+-

其实scanf读数字时,所有的非数字类字符都能使得跳过该次scanf

但是除了+-.

剩下的字符在该次scanf跳过后还会保存在缓冲区中,使得接下来的scanf依然会失效,当然如果有其他io函数可以读取这个字符另作考虑

且如果scanf读取的是+-.以外的非数字字符,那么本次scanf甚至不要求接收换行符

exit&&_exit

在Linux中,exit()_exit()函数都用于终止一个程序,但它们在终止程序时的行为有显著差异:

  1. exit() 函数:
    • exit() 是标准C库函数,定义在 <stdlib.h> 中。
    • 在调用 exit() 时,首先执行所有注册的退出函数(通过 atexit() 注册),清理I/O(如关闭所有标准I/O流,刷新缓冲区等)。
    • 然后,exit() 调用底层的 _exit()_Exit() 函数来结束程序。
  2. _exit() 函数:
    • _exit() 是POSIX系统调用,定义在 <unistd.h> 中。
    • 当调用 _exit() 时,程序会立即终止,不会执行任何清理操作,如不刷新I/O缓冲区、不调用注册的退出函数等。
    • 这个函数通常在需要立即终止程序,而不关心清理资源或数据的完整性时使用,例如,在子进程中避免复制父进程的缓冲区。

so的got表

checksec可以发现libc和ld的RELRO保护都是Partial RELRO

且ida打开可以观察到其中确实有.got.plt表,里面有不少函数

这就意味着如果能够修改so的got表也能做到劫持流

常见IO函数触发vtable调用的位置

有些时候程序不能或者很难满足下列三个条件

  1. 通过exit退出
  2. main能返回
  3. 触发__malloc_assert

那么利用伪造的chunk触发vtable调用,显然并不容易做到,

这种情况下可以考虑通过stdin,stdout来触发vtable调用

这里列出几个常见函数触发vtable调用的位置(只列出现的比较早的)

注意:

一般都有两个要求

  1. 是窄字符模式(_mode<=0)
  2. _IO_USER_LOCK标志位为0

且调用时第一个参数rdi一般都是对应FILE结构体指针

因为各种参数的影响,如果实操与所示结果不同,那么需要另行调试并做构造调整

scanf&&gets

scanf和gets调用vtable几乎一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0x00007ffff7e53c92 <+98>:	mov    rbx,QWORD PTR [rbp+0xd8]
0x00007ffff7e53c99 <+105>: lea rdx,[rip+0x156c00] # 0x7ffff7faa8a0 <_IO_helper_jumps>
0x00007ffff7e53ca0 <+112>: lea rax,[rip+0x157961] # 0x7ffff7fab608
0x00007ffff7e53ca7 <+119>: sub rax,rdx
0x00007ffff7e53caa <+122>: mov rcx,rbx
0x00007ffff7e53cad <+125>: sub rcx,rdx
0x00007ffff7e53cb0 <+128>: cmp rax,rcx
0x00007ffff7e53cb3 <+131>: jbe 0x7ffff7e53de0 <__GI___uflow+432>
0x00007ffff7e53cb9 <+137>: mov rax,QWORD PTR [rbx+0x28]
0x00007ffff7e53cbd <+141>: add rsp,0x8
0x00007ffff7e53cc1 <+145>: mov rdi,rbp
0x00007ffff7e53cc4 <+148>: pop rbx
0x00007ffff7e53cc5 <+149>: pop rbp
0x00007ffff7e53cc6 <+150>: jmp rax

在进入__uflow处,存在一次调用vtable,重点关注

1
2
3
4
5
0x00007ffff7e53c92 <+98>:	mov    rbx,QWORD PTR [rbp+0xd8]
.....
0x00007ffff7e53cb9 <+137>: mov rax,QWORD PTR [rbx+0x28]
.....
0x00007ffff7e53cc6 <+150>: jmp rax

其中rbp是IO_2_1_stdin\,取出了stdin的vtable+0x28并调用


1
2
3
4
5
6
7
   0x00007ffff7e53f6e <+30>:	mov    rbp,QWORD PTR [rdi+0xd8]
0x00007ffff7e53f75 <+37>: mov rcx,rbp
0x00007ffff7e53f78 <+40>: sub rcx,rdx
0x00007ffff7e53f7b <+43>: cmp rax,rcx
0x00007ffff7e53f7e <+46>: jbe 0x7ffff7e53fa8 <__GI__IO_default_uflow+88>
0x00007ffff7e53f80 <+48>: mov rdi,rbx
=> 0x00007ffff7e53f83 <+51>: call QWORD PTR [rbp+0x20]

进入_IO_default_uflow后,取出了stdin的vtable+0x20并调用

puts

1
2
3
4
5
6
7
8
9
10
11
   0x00007ffff7e464c7 <+167>:	mov    r14,QWORD PTR [rdi+0xd8]
0x00007ffff7e464ce <+174>: lea rdx,[rip+0x1643cb] # 0x7ffff7faa8a0 <_IO_helper_jumps>
0x00007ffff7e464d5 <+181>: lea rax,[rip+0x16512c] # 0x7ffff7fab608
0x00007ffff7e464dc <+188>: sub rax,rdx
0x00007ffff7e464df <+191>: mov rcx,r14
0x00007ffff7e464e2 <+194>: sub rcx,rdx
=> 0x00007ffff7e464e5 <+197>: cmp rax,rcx
0x00007ffff7e464e8 <+200>: jbe 0x7ffff7e46580 <__GI__IO_puts+352>
0x00007ffff7e464ee <+206>: mov rdx,rbx
0x00007ffff7e464f1 <+209>: mov rsi,r12
0x00007ffff7e464f4 <+212>: call QWORD PTR [r14+0x38]

puts主函数很快便会调用vtable+0x38

printf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   0x00007ffff7e3892a <+202>:	mov    rax,QWORD PTR [r12+0xd8]
0x00007ffff7e38932 <+210>: lea rcx,[rip+0x172ccf] # 0x7ffff7fab608
0x00007ffff7e38939 <+217>: sub rcx,rdx
0x00007ffff7e3893c <+220>: mov QWORD PTR [rbp-0x4d0],rdx
0x00007ffff7e38943 <+227>: mov QWORD PTR [rbp-0x4c8],rcx
0x00007ffff7e3894a <+234>: mov rsi,rcx
0x00007ffff7e3894d <+237>: mov rcx,rax
0x00007ffff7e38950 <+240>: sub rcx,rdx
0x00007ffff7e38953 <+243>: cmp rsi,rcx
0x00007ffff7e38956 <+246>: jbe 0x7ffff7e38d30 <__vfprintf_internal+1232>
0x00007ffff7e3895c <+252>: mov r15,QWORD PTR [rbp-0x4b8]
0x00007ffff7e38963 <+259>: mov rsi,r13
=> 0x00007ffff7e38966 <+262>: mov rdi,r12
0x00007ffff7e38969 <+265>: sub r15,r13
0x00007ffff7e3896c <+268>: mov rdx,r15
0x00007ffff7e3896f <+271>: call QWORD PTR [rax+0x38]

__vfprintf_internal处同样会调用vtable+0x38

更多

更多的类似getchar,putchar等io函数可以用相同的办法调试得到

svcudp_reply

该函数中存在一个有用的gadget

1
2
3
4
5
6
7
0x00007ffff7f16dea <+26>:	mov    rbp,QWORD PTR [rdi+0x48]
0x00007ffff7f16dee <+30>: mov rax,QWORD PTR [rbp+0x18]
0x00007ffff7f16df2 <+34>: lea r13,[rbp+0x10]
0x00007ffff7f16df6 <+38>: mov DWORD PTR [rbp+0x10],0x0
0x00007ffff7f16dfd <+45>: mov rdi,r13
0x00007ffff7f16e00 <+48>: call QWORD PTR [rax+0x28]
0x00007ffff7f16e03 <+51>: mov rax,QWORD PTR [rbp+0x8]

strchr与strdup

strchr()

strchr() 用于查找字符串中的一个字符,并返回该字符在字符串中第一次出现的位置。

1
char *strchr(const char *str, int c)

但如果C是null的话,strchr照样能够正常返回

因为在c语言中字符串都是以’\0’结尾的,所以strchr(“aaaaaa”,’\0’);会返回指向字符串结尾的位置,而这是有可能造成一些危害的

strdup()

strdup()函数用于将字符串复制到新建立的空间。

1
char *strdup(const char*s);

其会根据s的大小调整申请的空间大小

因为在报错的时候会调用该函数,所以不失为一种触发malloc的方法

本地预测随机数

如果种子是固定的常数,那么每次的随机数都是相同的

但经常是使用time函数获得种子,这样种子会因为时间的不同而不同,不过我们可以在本地预测,因为time获得的时间是以秒为单位的

而现代计算机的速度已经十分快了,如果我们在远程连接的同时本地启动一个相同的程序获得种子,那么最后的随机数会是相同的

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>

int main(){
setbuf(stdout,0);

int seed=time(0);

srand(seed);

int num=rand();

printf("%d",num);

return 0;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import*

import ctypes

p = process('./3')

print(p.recv())

libc = ctypes.CDLL('/lib/x86_64-linux-gnu/libc.so.6')

sleep(0)

seed = libc.time(0)

libc.srand(seed)

print(libc.rand())

运行结果

1
2
3
4
5
$:python 3.py
[+] Starting local process './3': pid 10599
[*] Process './3' stopped with exit code 0 (pid 10599)
b'1434162493'
1434162493

ELF相关

timeout

elf运行时有时会报错timeout: the monitored command dumped core

一般有两种情况会导致这个报错

一是栈对齐问题,函数执行过程中碰到了movaps指令,其操作数未十六进制对齐

可尝试通过增加ret gadget解决

二是程序未正常退出

一般是使返回地址为exit等让程序正常退出的函数地址

三是超时

标准流符号

大多数elf程序的bss段上

都会存在stdin,stdout,stderr这几个符号中的一两个

这几个符号在程序开始后都会指向各自对应的在libc中的FILE结构体

某些情况下可以用其来获得libc或者无输出爆破libc

elf映射

elf在映射时bss段会被映射两次,可以在gdb中利用search指令查找

vdso

elf运行时会有内存映射vdso

dump下来可以发现其中存在一些有用的gadget,例如sycall….,而且与vsyscall中的syscall不能拆分使用不同,这个是可以使用的

此外在栈上环境变量(environ)之上会保存有vdso的起始地址

一些特殊情况可能用得上

TLS

TLS即Thread Local Storage,线程局部存储

TLS(Thread Local Storage)的结构与TCB(Thread Control Block)以及dtv(dynamic thread vector)密切相关,每一个线程中每一个使用了TLS功能的module都拥有一个TLS Block。这几者的关系如下图所示:

TLS Blocks可以分为两类,一类是程序装载时就已经存在的(位于TCB前),这一部分Block被称为static TLS。右边的Blocks是动态分配的,它们被使用dlopen函数在程序运行时动态装载的模块所使用。 TCB作为线程控制块,保存着dtv数组的入口,dtv数组中的每一项都是TLS Block的入口,它们是指向TLS Blocks的指针。特别的,dtv数组的第一个成员是一个计数器,每当程序使用dlopen函数或者dlfree函数加载或者卸载一个具备TLS变量的module,该计数器的值都会加一,从而保证程序内版本的一致性

在x86_64中,fs寄存器指向tls结构体,其位于一块mmap出来的地址,当线程是主线程时,其与libc的偏移是固定的

当线程不是主线程时,其tls结构与栈是靠近的,若存在栈溢出甚至可以直接覆盖tls结构体

其中tcb的定义如下

1
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
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
unsigned long int unused_vgetcpu_cache[2];
/* Bit 0: X86_FEATURE_1_IBT.
Bit 1: X86_FEATURE_1_SHSTK.
*/
unsigned int feature_1;
int __glibc_unused1;
/* Reservation of some values for the TM ABI. */
void *__private_tm[4];
/* GCC split stack support. */
void *__private_ss;
/* The lowest address of shadow stack, */
unsigned long long int ssp_base;
/* Must be kept even if it is no longer used by glibc since programs,
like AddressSanitizer, depend on the size of tcbhead_t. */
__128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));

void *__padding[8];
} tcbhead_t;

具体攻击路径

  1. 由于tcache是一个TLS变量,且该变量没有任何保护,可以写tcache来劫持整个tcache链表。
  2. 泄露pointer_guard后可以劫持exit函数的流程,可以劫持__exit_funcs数组来执行函数列表,但这种方法只能控制一个函数参数。
  3. 同样是泄露pointer_guard,但之后可以劫持tls_dtor_list(主线程情形需要任意地址写,非主线程需要任意地址写或者栈溢出),进而构造dtor_list结构体控制rdiobj域)和rdxnext域),进而利用setcontext+53来进行SROP。此方法适用于目前所有主流libc版本
  4. 在任意地址写情况下,如果已知一个确切的利用pointer_guard解密指针的位置(如printf函数中就存在这样的调用),可以通过修改pointer_guard来使解密后的函数指针指向one_gadget,进而getshell。
  5. 泄露或写入stack_canary来绕过canary机制(注意主线程的stack_canary和子线程的一样,并且修改主线程的stack_canary之后创建的子线程的canary也会被修改)

Linux相关

open调用

某次比赛因为open参数的原因折腾了不久,浅浅学一下

open调用参数

oflags

oflags是由诸多bit组成的标志位,各标志的宏如下

1
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
/* File access modes for `open' and `fcntl'.  */
#define O_RDONLY 0 /* Open read-only. */
#define O_WRONLY 1 /* Open write-only. */
#define O_RDWR 2 /* Open read/write. */


/* Bits OR'd into the second argument to open. */
#define O_CREAT 0x0200 /* Create file if it doesn't exist. */
#define O_EXCL 0x0800 /* Fail if file already exists. */
#define O_TRUNC 0x0400 /* Truncate file to zero length. */
#define O_NOCTTY 0x8000 /* Don't assign a controlling terminal. */
#define O_ASYNC 0x0040 /* Send SIGIO to owner when data is ready. */
#define O_FSYNC 0x0080 /* Synchronous writes. */
#define O_SYNC O_FSYNC
#ifdef __USE_MISC
#define O_SHLOCK 0x0010 /* Open with shared file lock. */
#define O_EXLOCK 0x0020 /* Open with shared exclusive lock. */
#endif
#ifdef __USE_XOPEN2K8
# define O_DIRECTORY 0x00200000 /* Must be a directory. */
# define O_NOFOLLOW 0x00000100 /* Do not follow links. */
# define O_CLOEXEC 0x00400000 /* Set close_on_exec. */
#endif
#if defined __USE_POSIX199309 || defined __USE_UNIX98
# define O_DSYNC 0x00010000 /* Synchronize data. */
# define O_RSYNC 0x00020000 /* Synchronize read operations. */
#endif

/* All opens support large file sizes, so there is no flag bit for this. */
#ifdef __USE_LARGEFILE64
# define O_LARGEFILE 0
#endif

/* File status flags for `open' and `fcntl'. */
#define O_APPEND 0x0008 /* Writes append to the file. */
#define O_NONBLOCK 0x0004 /* Non-blocking I/O. */

#ifdef __USE_MISC
# define O_NDELAY O_NONBLOCK
#endif

常用的有

1
2
3
4
5
6
7
8
O_RDONLY:以只读方式打开文件。
O_WRONLY:以只写方式打开文件。
O_RDWR:以读写方式打开文件。
O_CREAT:如果文件不存在,则创建文件。
O_EXCL:与O_CREAT一起使用,用于确保文件的创建是独占的,即如果文件已经存在,则open调用会失败。
O_TRUNC:如果文件已经存在,在打开文件时将其截断为空文件。
O_APPEND:在文件末尾追加数据而不是覆盖已有数据。
O_NONBLOCK:以非阻塞方式打开文件。

mode

open系统调用的第三个参数mode是一个无符号整数,用于指定文件的访问权限(仅在创建文件时生效,即当flags中包含O_CREAT时)。

它定义了文件所有者、所属组和其他用户对文件的读、写和执行权限。

当oflags没有O_CREAT标志时无意义

文件标识符上限

使用open调用打开文件时会分配一个文件标识符

通常一个进程能够打开的文件标识符数量是有限的

可以使用ulimit -n查看,一般是1024

如果超过了这个数字,那么将打开失败,进而导致读取失败

除去进程自动打开的stdin,stdout,stderr三个文件流的标识符,我们还能再打开1021个标识符

再多的话就会打开失败

一个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main (void)
{
for(int i=1;i<=1024;i++)
{
FILE *fd = open("/dev/urandom", 0);
int buf=0;
read(fd, &buf, 4uLL);
printf("%d->%d\n",i,buf);
}
return 0;
}

/proc/self/maps

每个进程在/proc目录下都存在一个记录该进程信息的文件夹

命名是/proc/pid

其中有一个maps文本文件,会记载进程的各个段的映射信息

某些情况下可以利用其泄露一些信息,

在大多数不知道pid的情况下,只需要进程内引用/proc/self/maps,就会自动指向该进程的对应文件夹

read返回0

当read调用成功时(即没有负值),read有没有可能返回0

  • 如果物理文件系统不支持从目录中简单读取,如果用于目录,read() 将返回 0。
  • 如果没有进程打开管道进行写入,则 read() 返回 0 以指示文件结束。
  • 如果流套接字上的连接中断,但没有可用数据,则 read() 函数返回 0 字节作为 EOF。

有些偏门的pwn题会考察让read返回值为0,才能进一步利用

这时就是第三种情况,本地调试的话类似,只不过是关闭管道

pwntools提供了一个接口

1
2
3
4
5
6
sh.shutdown()

#当需要关闭对服务端写,则调用
sh.shutdown("write") #也可以是"out","send"
#当需要关闭对服务端读,则调用
sh.shutdown("read") #也可以是"in","recv"