svenskithesource Posted May 21, 2022 Posted May 21, 2022 View File Python Patchme (Custom VM) Hello, Since the use of vm's is quite popular in .net obfuscation I thought it would be interesting to make one in Python. Hereby I release my first crackme using my VM. It's a very simple VM so it shouldn't be that hard to reverse with the right knowledge. This is purely experimental to see if it would be useful somehow. I also used my own protector that adds cflow, so expect to see a lot of jumps. Goal: Patch it (ultimately devirtualize it, but that's not necessary) Submitter svenskithesource Submitted 05/22/2022 Category CrackMe
Extreme Coders Posted May 22, 2022 Posted May 22, 2022 (edited) Not necessary to devirtualize to get the password. Everything is in the clear. As a result patching was unnecessary towards the end goal. Correct password: Spoiler yougottapatchit Steps Spoiler This is a pyinstaller generated executable in Python 3.9. First step is to extract using pyinstxtractor. The description mentions the crackme uses a custom VM. However we don't want to analyze the VM yet. The idea is to trace the instructions executed by the VM and check if something is compared to the entered password. That something should hopefully be the correct password. Python 3.7 and above support instruction tracing through sys.settrace but we are going to use the x-python instead. x-python is a CPython bytecode interpreter in pure python and useful for cases like this. Lets run the crackme using "tuts4you" as the password. xpython writes the trace log to stderr. Hence we redirect stderr to stdout by 2>&1 to make it a one-liner. Finally, we can grep for "tuts4you" in the log. This gets us the correct password against which the input is compared. Edited May 22, 2022 by Extreme Coders 1 1
svenskithesource Posted May 23, 2022 Author Posted May 23, 2022 10 hours ago, Extreme Coders said: Not necessary to devirtualize to get the password. Everything is in the clear. As a result patching was unnecessary towards the end goal. Correct password: Reveal hidden contents yougottapatchit Steps Reveal hidden contents This is a pyinstaller generated executable in Python 3.9. First step is to extract using pyinstxtractor. The description mentions the crackme uses a custom VM. However we don't want to analyze the VM yet. The idea is to trace the instructions executed by the VM and check if something is compared to the entered password. That something should hopefully be the correct password. Python 3.7 and above support instruction tracing through sys.settrace but we are going to use the x-python instead. x-python is a CPython bytecode interpreter in pure python and useful for cases like this. Lets run the crackme using "tuts4you" as the password. xpython writes the trace log to stderr. Hence we redirect stderr to stdout by 2>&1 to make it a one-liner. Finally, we can grep for "tuts4you" in the log. This gets us the correct password against which the input is compared. Hi, thanks for attempting my patchme, however like you mentioned everything is in the clear because the goal is not to find the password but to patch it so every password is seen as correct. Could you try again and patch it?
Extreme Coders Posted May 23, 2022 Posted May 23, 2022 (edited) 8 hours ago, svenskithesource said: Could you try again and patch it? Patching is nothing difficult either but since you asked. Similar to x86, we can just just reverse the conditional jump so that it will show the success message for every password except the correct one. We can search for "tuts4you" in the trace file. INFO:xpython.vm: @1580: LOAD_FAST OO000O0O0O00O0OO0 INFO:xpython.vm: @1582: LOAD_FAST O0OOOOOO0O00OO0O0 INFO:xpython.vm: @1584: COMPARE_OP ('yougottapatchit', 'tuts4you') == INFO:xpython.vm: @1588: POP_JUMP_IF_FALSE 1594 INFO:xpython.vm: @1594: LOAD_CONST 0 INFO:xpython.vm: @1596: CALL_METHOD 1 The POP_JUMP_IF_FALSE instruction is what that decides to take the success or the failure branch. The instruction jumps to the target address if the comparison fails. We can always patch the instruction to POP_JUMP_IF_TRUE. However ironically in that case the crackme would show the success message for every password except the correct one. Instead we can NOP out the instruction so that it doesn't take the jump irrespective of the comparision. The instruction POP_JUMP_IF_FALSE 1594 actually encodes to 90 06 72 3A Python 3.9 opcodes can be found here: https://github.com/python/cpython/blob/v3.9.6/Include/opcode.h 1594 in hex is 0x63a. Python 3.6 and above uses 2 bytes per instruction consisting of the opcode and the operand both of which are byte sized, In this case the operand 0x63a is too large to fit in 1 byte. Hence it has to be broken down into two instructions - SETUP_EXTENDED 0x6 POP_JUMP_IF_FALSE 0x3a with the final jump address calculated as (0x6<<8) | 0x3a = 0x63a = 1594 Searching for "90 06 72 3A" in the pyc file, we get exactly one hit. Spoiler We want to patch the instruction to POP_TOP NOP which encodes to 01 00 09 00 The POP_TOP instruction is necessary to balance the stack as POP_JUMP_IF_FALSE also does pop the stack before deciding to jump or not. After patching main.pyc 90 06 72 3A => 01 00 09 00 it will show success message for every password. Patched pyc file attached: main obf_patched.pyc Edited May 23, 2022 by Extreme Coders 3 1
svenskithesource Posted May 25, 2022 Author Posted May 25, 2022 On 5/23/2022 at 3:00 PM, Extreme Coders said: Patching is nothing difficult either but since you asked. Similar to x86, we can just just reverse the conditional jump so that it will show the success message for every password except the correct one. We can search for "tuts4you" in the trace file. INFO:xpython.vm: @1580: LOAD_FAST OO000O0O0O00O0OO0 INFO:xpython.vm: @1582: LOAD_FAST O0OOOOOO0O00OO0O0 INFO:xpython.vm: @1584: COMPARE_OP ('yougottapatchit', 'tuts4you') == INFO:xpython.vm: @1588: POP_JUMP_IF_FALSE 1594 INFO:xpython.vm: @1594: LOAD_CONST 0 INFO:xpython.vm: @1596: CALL_METHOD 1 The POP_JUMP_IF_FALSE instruction is what that decides to take the success or the failure branch. The instruction jumps to the target address if the comparison fails. We can always patch the instruction to POP_JUMP_IF_TRUE. However ironically in that case the crackme would show the success message for every password except the correct one. Instead we can NOP out the instruction so that it doesn't take the jump irrespective of the comparision. The instruction POP_JUMP_IF_FALSE 1594 actually encodes to 90 06 72 3A Python 3.9 opcodes can be found here: https://github.com/python/cpython/blob/v3.9.6/Include/opcode.h 1594 in hex is 0x63a. Python 3.6 and above uses 2 bytes per instruction consisting of the opcode and the operand both of which are byte sized, In this case the operand 0x63a is too large to fit in 1 byte. Hence it has to be broken down into two instructions - SETUP_EXTENDED 0x6 POP_JUMP_IF_FALSE 0x3a with the final jump address calculated as (0x6<<8) | 0x3a = 0x63a = 1594 Searching for "90 06 72 3A" in the pyc file, we get exactly one hit. Reveal hidden contents We want to patch the instruction to POP_TOP NOP which encodes to 01 00 09 00 The POP_TOP instruction is necessary to balance the stack as POP_JUMP_IF_FALSE also does pop the stack before deciding to jump or not. After patching main.pyc 90 06 72 3A => 01 00 09 00 it will show success message for every password. Patched pyc file attached: main obf_patched.pyc 6.61 kB · 4 downloads Hello, thanks again for your detailed solution. I released an update and would appreciate if you would try it.
Solution Extreme Coders Posted May 26, 2022 Solution Posted May 26, 2022 This post was recognized by Teddy Rogers! Extreme Coders was awarded the badge 'Great Content' and 1 points. 16 hours ago, svenskithesource said: released an update Nice update. Now the jump or the comparison cannot be nopped easily as the same instruction is also executed for other purpose. The COMPARE_OP instruction is executed three times in total COMPARE_OP ('yougottapatchit', 'tuts4you') == COMPARE_OP (2, 1) == COMPARE_OP (1, 1) == But it can always be patched, details below. The idea is to make changes so that the three comparisons always return true, false and true respectively. Patch 1 ======= In a hex editor search and replace the "yougottapatchit" to 15 null bytes "yougottapatchit" => "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" (15 null bytes) Patch 2 ======= Search for the following sequence of bytes and replace with the corresponding "7C 04 7C 05 6B 02" => "7C 05 7C 04 6B 05" Explanation Patch 1 changes the correct password to a string of 15 null bytes. Patch 2 makes the following change to the comparison as shown. Originally the relevant instructions were: 7C 04 LOAD_FAST 0x4 7C 05 LOAD_FAST 0x5 6B 02 COMPARE_OP 0x2 ('==') 90 06 SETUP_EXTENDED 0x6 72 86 POP_JUMP_IF_FALSE 0x86 In pseudo-code, if (var1 == var2) {...} The patch changes it to 7C 05 LOAD_FAST 0x5 7C 04 LOAD_FAST 0x4 6B 05 COMPARE_OP 0x5 ('>=') 90 06 SETUP_EXTENDED 0x6 72 86 POP_JUMP_IF_FALSE 0x86 In pseudo-code, if (var2 >= var1) {...} As a result of the patches, the 3 COMPARE_OP instructions execute as COMPARE_OP ('tuts4you', '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') >= COMPARE_OP (1, 2) >= COMPARE_OP (1, 1) >= This returns true, false, true exactly in the order we want. One downside of the patch is it will show the success message for every non-empty password. That is have to enter something as the password. main obf v1.2-patched1.pyc --------------- We can go further and patch it in such a way to make it also accept empty passwords. We have to replace "yougottapatchit" with an empty string in the pyc file, however we cant just delete the bytes yet as that will corrupt the file. The string "yougottapatchit" is actually stored within a tuple in the pyc file. 00000A80 29 06 7A 37 ).z7 00000A90 4D 61 64 65 20 62 79 20 73 76 65 6E 73 6B 69 74 Made by svenskit 00000AA0 68 65 73 6F 75 72 63 65 23 32 38 31 35 0A 47 6F hesource#2815.Go 00000AB0 61 6C 3A 20 50 61 74 63 68 20 69 74 0A 50 61 73 al: Patch it.Pas 00000AC0 73 77 6F 72 64 3A 20 DA 0F 79 6F 75 67 6F 74 74 sword: Ú.yougott 00000AD0 61 70 61 74 63 68 69 74 72 35 00 00 00 DA 07 53 apatchitr5...Ú.S 00000AE0 75 63 63 65 73 73 DA 05 57 72 6F 6E 67 72 29 00 uccessÚ.Wrongr). 00000AF0 00 00 .. which when unmarshalled turns out to ('Made by svenskithesource#2815\nGoal: Patch it\nPassword: ', 'yougottapatchit', 1, 'Success', 'Wrong', 2) We can change the above tuple to ('Made by svenskithesource#2815\nGoal: Patch it\nPassword: ', '', 1, 'Success', 'Wrongaaaaaaaaaaaaaaa', 2) "yougottapatchit" is patched to an empty string. To make the final marshalled size of the tuple the same, 15 bytes has been added to the 'Wrong' string. This string won't ever be printed, so that's fine. The patched marshalled tuple now looks like 00000A80 29 06 7A 37 ).z7 00000A90 4D 61 64 65 20 62 79 20 73 76 65 6E 73 6B 69 74 Made by svenskit 00000AA0 68 65 73 6F 75 72 63 65 23 32 38 31 35 0A 47 6F hesource#2815.Go 00000AB0 61 6C 3A 20 50 61 74 63 68 20 69 74 0A 50 61 73 al: Patch it.Pas 00000AC0 73 77 6F 72 64 3A 20 DA 00 72 35 00 00 00 DA 07 sword: Ú.r5...Ú. 00000AD0 53 75 63 63 65 73 73 DA 14 57 72 6F 6E 67 61 61 SuccessÚ.Wrongaa 00000AE0 61 61 61 61 61 61 61 61 61 61 61 61 61 72 29 00 aaaaaaaaaaaaar). 00000AF0 00 00 .. As a result of the changes the first COMPARE_OP execute as COMPARE_OP ('tuts4you', '') >= which is always true. Any string is always >= an empty string Now the patched file will accept any password, even empty ones. main obf v1.2-patched2.pyc 1 1
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now