REBOL 3.0

Comments on: Hidden variables - cracking under pressure?

Carl Sassenrath, CTO
REBOL Technologies
5-Apr-2009 23:06 GMT

Article #0189
Main page || Index || Prior Article [0188] || Next Article [0190] || 31 Comments || Send feedback

The protect function now includes /hide - a method for hiding private values within an object or module context.

For example, you would write:

protect/hide 'password

to hide the variable password in your local context.

To hide multiple words, you can use:

protect/hide/words [username password email-address]

Quite simply, the /hide refinement causes the word(s) to ignore any future binding operations. All bindings prior to the /hide are valid. Any that follow it will either throw an error or make it appear that the hidden words don't even exist.

An example

Let's take a look at an example. Here's an object that manages a password. Our goal is to be able to keep the password secret.

We want to be able to set the password, but not let any code, other than the manager, access the password, other than getting a secure hash value for it. (That can be used for authentication actions, without giving away the secret password.)

manager: object [

    pass: none

    set-pass: func [pw][
        print "setting password..."
        pass: pw
        exit ; return nothing
    ]

    get-pass: does [
        checksum/secure to-binary pass
    ]

    protect/hide 'pass
]

Keep in mind this is just a simple example, and in real code, we'd probably hash the password within set-pass.

So, we can now set the password:

manager/set-pass "bingo"
setting password...

And, get its hash:

print manager/get-pass
#{E410B808A7F76C6890C9ECACF2B564EC98204FDB}

How secure is it?

Ok, now we are going to try to sneak-a-peek at the actual password. We start with:

probe manager/pass
** Script error: cannot access pass in path manager/pass

Ok. Doesn't seem to recognize it.

But, shouldn't the error say that it is hidden? For good security, no. You don't want to give away any information, even the existence of the name of the field.

Well, what if we try to set it:

manager/pass: "crack"
** Script error: cannot access pass in path manager/pass:

Same result. Makes sense.

Ok, let's up-the-level, and see if we can find other ways to crack into the manager object.

Let's try the standard accessor functions:

probe get in manager 'pass
none
probe select manager 'pass
none

Just a NONE, which doesn't tell us anything, because:

probe select manager 'some-random-word
none

Reflection, of course!

Ok, let's get serious. We know REBOL is a reflective language so let's use that powerful feature to get what we want.

We start with:

probe manager
make object! [
    set-pass: make function! [[pw][
        print "setting password..."
        pass: pw
        exit
    ]]
    get-pass: make function! [[][
        checksum/secure to-binary pass
    ]]
]

The pass field doesn't even show up!

Then:

probe words-of manager
[set-pass get-pass]

No pass.

And similar results for:

probe values-of manager
probe body-of manager

So far, access denied.

Oh.. wait! There's a special shortcut method for getting the values of an object as a block. Let's try it:

probe get manager
** Script error: not allowed - would expose hidden values

There's an interesting error message. Maybe we're making some progress? I can feel we've almost cracked it, right?

The obvious method

We forgot the most obvious approach...

unprotect 'manager/pass
unprotect in manager 'pass
unprotect/hide 'manager/pass

Poof. None of them work.

Up the level...

Let's up-the-level again - say, level 7?

We know we can copy and make objects to inherit values. So, let's use that to clone the object and get to that pass field. (Because, after all we don't care that we use some memory for it.)

mgr: copy manager
probe mgr/pass
** Script error: cannot access pass in path mgr/pass

Nope. What about:

mgr: make manager [pass: none]
probe mgr/pass
** Script error: cannot access pass in path mgr/pass

Time for level 9...

Ok, time to ask: WWBD? (What would BrianH do?)

Maybe we need more than one line, and rebind back into our local context. Yeah, that would work....

pass: none
probe get bind 'pass manager
** Script error: pass has no value
probe pass
none

I... will... not... be... defeated...

probe get bind 'pass in manager 'set-pass
** Script error: pass has no value

Noooo....

probe find body-of get in manager 'set-pass to-set-word 'pass
[
    pass: pw
    exit
]

Ah, now I've got you my pretty...

probe get first find body-of get in manager 'set-pass to-set-word 'pass
** Script error: pass: word is not bound to a context

WHAT!!!!??? Where did that come from?

Danger! Level 9.5.... the ultimate security hole, the old "BrianH uses a functional argument trick..." (Because we notice that set-pass didn't specify its arg type. Yes!)

manager/set-pass does [
    probe get bind 'pass manager
]
** Script error: pass has no value

9.7: append to the context....

append manager reduce bind [
    'get-it does [probe pass]
] manager
manager/get-it
** Script error: pass has no value

Noooo.... I'm melting... melting...

Conclusion

Well, a little dramatic. Sorry. I had to make the article interesting, because it was getting just a bit long.

But... anyone got a 9.9? So, where's Mr. Bindology, Ladislav?

Protect /hide will be in A44. Think: WWLD?

31 Comments

Comments:

Paul
5-Apr-2009 19:18:43
Hehe, one of the best posts ever. Was fun.

Carl Sassenrath
5-Apr-2009 19:46:48
Thanks. Can't wait to release it.
Gregg Irwin
5-Apr-2009 20:34:24
Very cool. Two questions:

1) Doesn't this example expose that it exists?

mgr: make manager [pass: none]
probe mgr/pass
** Script error: cannot access pass in path mgr/pass
If there was no hidden var, that code would work.

2) Why do you need /words to specify more than one word to protect?

Brian Hawley
5-Apr-2009 20:40:11
A challenge! I accept:
it: none
manager/set-pass does [it: to-word first back stack/block 1 "pony"]
get it
== "pony"

You'll still have to use get-word references or argument type specs to block function arguments, or restrict access to the STACK function, but that is easy enough to do. And other attacks would be much more difficult. A good start :)

Brian Hawley
5-Apr-2009 20:46:41
To block the previous hack, have STACK/block return an unbound copy of the block, rather than the original - then you can't get at these words.
Brian Hawley
5-Apr-2009 20:56:14
Gregg, some answers:

1) No, you'd get the same error if there were no pass field.

2) Because PROTECT can protect blocks now too, so you need /words to specify that you want the words protected, not the block that contains them.

Brian Hawley
5-Apr-2009 21:01:01
Where's that comment delete button? Yes to answer 1, Gregg. Not sure whether knowing the word exists would help though. I wonder if overriding visible functions in derivative objects would be a possible exploit...
Carl Sassenrath
5-Apr-2009 21:53:25
Penalty! Cannot use STACK function (because we know it's insecure and must go away.) But, good one. Didn't take you long! Now, how to do it w/o STACK?
Carl Sassenrath
5-Apr-2009 21:57:36
Gregg: well, that is true (error vs no error). Of course, I knew the word to use. I suppose we could change that case... something to think about. There are may combos.
Brian Hawley
5-Apr-2009 22:36:23
It took me less time to come up with an exploit than it did to read the WWBD sentence, but I agree, penalty. I was just trying to emphasize the problems with the STACK function. The code above might still fail because of the TO-WORD (I'd have to test to be sure), but the TO-WORD is unnecessary anyway.

In the case you give here, it is tricky to do it without STACK because there are no references to the word in question physically after the pw expression in the same code block. I've only come up with two other possible exploits so far:

  • Replace the PRINT function with another that took two additional get-word arguments. This would depend on having access to their reference to the PRINT function.
  • Pass a custom op!, though this is blocked since the OP function is unset in the startup code. If the begin intrinsic is executed before OP is unset the exploit is still valid.

In general is is still a good idea to evaluate potential malicious function values in parens, with get-word expressions or with APPLY. Argument type specs and ASSERT are still your best friends though.

-pekr-
5-Apr-2009 23:18:18
So, how do we use such functionality to protect some module context words? Should we run protect function call at the end of module, so that it gets called during module creation? I would prefer 'protect being a header field, like 'exports is, and later on 'import can come too ...

Btw - rename exports to just export, no? Or we will have to have - imports, protects ...

Brian Hawley
6-Apr-2009 0:05:45
Exports is an adverb, like Needs (which we are using instead of Imports). Export is a verb, and headers don't do anything - they are declarative.

As an alternative to a Protects header, it might be a good idea to just automatically hide all non-exported local words of a module during the import process, and protect any exported words you want to using a function call at the end, or flags in the Exports header.

I'm a little concerned about how autoprotecting words might affect explicit binding by code defined inside the module, after the module has been imported. It might be a "don't do that" situation, but unexpected gotchas are bad.

As long as unexported module-local words don't show up outside the module, or at least have WORDS-OF module! return the exports, I'll be fine.

-pekr-
6-Apr-2009 0:51:55
BrianH - by 'imports I mean different functionality - ability to import some other stuff into my module, and make it local to my module. Dunno if it would be useful, it just came to my mind :-)
Gregg Irwin
6-Apr-2009 2:09:43
So, on /words, the question then is which is going to be more common: protecting a block or using a block of words you want to protect?

My gut instinct is that the latter will be more common.

What about /only (treat a series as a series) as the refinement name?

Brian Hawley
6-Apr-2009 6:38:09
Gregg, the previous two blogs were about the changes to PROTECT. Protecting series from modification allows them to be shared between tasks safely, so my gut feeling is that protecting series will happen more often in user code. YMMV.
Peta
6-Apr-2009 10:50:51
Carl, does this count?

unprotect manager

Edoc
7-Apr-2009 15:51:29
Keep in mind this is just a simple example, and in real code, we'd probably hash the password within set-pass.
It would be great if we could turn this into a short tutorial with a real-world example in R3. Something similar to the article Copy and Checksum Large Files -- I remember how informative that was. Although it's not my area of expertise, I volunteer to draft the copy if an R3 insider can help refine & strengthen the code.
Sunanda
8-Apr-2009 14:51:48
Formally, the challenge is to retrieve a word that has had the protect/hide treatment.

I have no way of doing that. So (in a real world example where I wanted to bypass protect/hide protection) I would put this in my user.r:

protect: func [var /hide /deep /words][]

It may not be elegant, but (if it precedes the creation of Carl's Manager object) it does work :-)

Peta
9-Apr-2009 4:37:11
re Sunanda. My above suggested solution not only allows you to retrieve the contents; in addition to that, it allows you to even change it
Peta
9-Apr-2009 5:04:36
it looks, that Carl would probably classify my solution as "an obvious method", but working, in this case
DideC
9-Apr-2009 9:12:40
I don't see the point of having an 'unprotect function that you can simply use to unprotect words !?

It's like having a secured door but leting the key into the lock outside the house !!

What happens with "save %manager.r manager" ??

Peta
9-Apr-2009 9:38:41
re DideC - just checked the SAVE variant. When protected, the PASS value is not available.
Sunanda
10-Apr-2009 11:58:04
Peta, I cannot see that

unprotect manager

gives access to the manager/pass word.....At least not in Alpha 47.

It does give access to the functions within manager, but not the target word.

Brian Hawley
10-Apr-2009 18:34:06
Peta, that full UNPROTECT call was a good catch. I'm not even going to say penalty, because judging from the comments here, it wasn't obvious that we already intend to provide an option to PROTECT that will make UNPROTECT not work.

My STACK exploit was penalty-worthy because I knew that STACK was already going to be fixed or replaced.

Sunanda, that redefinition of PROTECT in %user.r was another good catch. It is intended that critical system words be irreversibly protected well before %rebol.r or %user.r load, though that doesn't happen yet. Clearly the PROTECT function needs to be protected :)

I look forward to seeing more security input from both of you in R3 chat. Peta, when are you going to sign up?

Brian Hawley
10-Apr-2009 18:48:40
DideC, one of the main purposes of the PROTECT function is to prevent accidental modification of stuff, or to make data available to be shared among multiple tasks (in theory). That's why we have UNPROTECT - for when you really want to modify something.

Preventing unauthorized or malicious modification is also important, but probably going to be needed more rarely. I bet that most calls to a permanent PROTECT option will mostly happen inside wrapper code like the module! functions. That's why it isn't the default.

This may change though - that's why we need feedback now :)

Peta
11-Apr-2009 5:18:18
re Sunanda: the solution I posted worked in A44 (the version in the article and A45). Now it does not work, but for some curious, here is another one working in A47:
manager/set-pass "bingo!"
and now:
hack-pass: has [pass] [make manager [set 'pass set-pass] pass]
hack-pass
Brian Hawley
12-Apr-2009 22:56:53
Nice one Peta. Added a CureCode ticket here.

I look forward to seeing you in R3 chat - we could use your contributions :)

Steeve
24-Apr-2009 1:10:59
I'm late...

I'm waiting for the new implementation of struct! before judging if it will be really secure to protect values.

In R2, structures give direct access to the rebol memory. After that, there is now way to hide or protect anything.

(except by crypting some reserved banks for protected values)

Brian Hawley
27-Apr-2009 11:20:12
Peta, your last reported bug (3 messages back) was fixed in alpha 49. Do you have any others? Exploits using %rebol.r or %user.r don't count, since it has been decided that code will no longer be loaded from those files, for security reasons.
Mark Ingram
28-Apr-2009 15:43:24
Is the idea to be that no one, not even the system's owner, can ever undo or prevent a protect/hide? This is a huge mistake. Never mind destroying the usefulness of reflection, though that is bad enough; the bigger point, as everyone knows, is that security through obscurity is no security at all.

Also, what stops malicious code from hiding the security block and preventing any security tightening?

Sunanda
13-Jul-2009 4:54:47
'resolve gives update access to a hidden word. See CureCoe ticket #1095

http://www.curecode.org/rebol3/ticket.rsp?id=1095

Post a Comment:

You can post a comment here. Keep it on-topic.

Name:

Blog id:

R3-0189


Comment:


 Note: HTML tags allowed for: b i u li ol ul font span div a p br pre tt blockquote
 
 

This is a technical blog related to the above topic. We reserve the right to remove comments that are off-topic, irrelevant links, advertisements, spams, personal attacks, politics, religion, etc.

REBOL 3.0
Updated 26-Apr-2024 - Edit - Copyright REBOL Technologies - REBOL.net