This binary encodes our input data and is compared against a buffer. We can use Ghidra to determine how the flag is encrypted and reverse the encryption to get the flag.
This will be a good introduction to reversing obfuscated functions in Ghidra.
Running checksec
on the binary, we notice all security features are enabled:
$ checksec chall [*] '/ironforge/chall' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled Stripped: No
Let's check the raw decompilation of main()
:
undefined4 main(void) { undefined4 uVar1; int in_GS_OFFSET; int local_3c; char local_38 [37] = { 0 }; // Ghidra does it the long way int local_14; undefined *local_10; local_10 = &stack0x00000004; local_14 = *(int *)(in_GS_OFFSET + 0x14); setbuf(_stdin,(char *)0x0); setbuf(_stdout,(char *)0x0); setbuf(_stderr,(char *)0x0); printf("What is the password?\n> "); fgets(local_38,0x25,_stdin); encode(local_38); local_3c = 0; do { if (0x23 < local_3c) { win(); LAB_00011397: uVar1 = 0; if (local_14 != *(int *)(in_GS_OFFSET + 0x14)) { uVar1 = __stack_chk_fail_local(); } return uVar1; } if (enckey[local_3c] != local_38[local_3c]) { puts("You lose!"); goto LAB_00011397; } local_3c = local_3c + 1; } while( true ); }
This code is a mess. There is a label in the middle of the function, a do-while loop, and many wonky variables. Let's discuss how we can clean this up:
param_1
is never used, so we'll remove it. This changes the signature to void main(void)
.After these changes, we get the following code:
int main(void) { int iVar1; int i; char buffer [37] = { 0 }; int local_14; undefined *local_10; setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); printf("What is the password?\n> "); fgets(buffer,0x25,_stdin); encode(buffer); i = 0; do { if (0x23 < i) { win(); return 0; } if (enckey[i] != buffer[i]) { puts("You lose!"); return 0; } i = i + 1; } while( true ); }
This is a lot easier to read. We can simplify this further by switching the do-while loop for a while
loop where the condition is checked.
while (i <= 0x23) { if (enckey[i] != buffer[i]) { puts("You lose!"); return 0; } i = i + 1; } win(); return 0;
This means that we're checking the first 0x28 = 40
bytes of enc_key
against buffer
. If they match, we call win()
. If they don't match, we call puts("You lose!")
and return.
We have a few things to check out:
enc_key
?buffer
?encode()
function do?If we look at enc_key
and buffer
, we notice these variables are colored aqua. This means they are global variables. Local variables are colored yellow. Double-clicking on enc_key
and buffer
takes us to the location where they are defined.
This is the definition of enc_key
:
enckey XREF[4]: Entry Point(*), encode:00011233(R), encode:00011255(R), main:0001134d(R) 00014024 08 20 01 00 addr DAT_00012008 = 31h 1
enckey
holds an address, making it a pointer. We can click on DAT_00012008
to see what's there.
DAT_00012008 XREF[4]: encode:00011240(*), encode:00011255(*), main:00011358(*), 00014024(*) 00012008 31 ?? 31h 1 00012009 c0 ?? C0h 0001200a 50 ?? 50h P 0001200b 68 ?? 68h h 0001200c 62 ?? 62h b 0001200d 6f ?? 6Fh o 0001200e 6f ?? 6Fh o 0001200f 74 ?? 74h t 00012010 68 ?? 68h h 00012011 6e ?? 6Eh n 00012012 2f ?? 2Fh / 00012013 72 ?? 72h r 00012014 65 ?? 65h e 00012015 68 ?? 68h h 00012016 2f ?? 2Fh / 00012017 73 ?? 73h s 00012018 62 ?? 62h b 00012019 69 ?? 69h i 0001201a 89 ?? 89h 0001201b e3 ?? E3h 0001201c 50 ?? 50h P 0001201d 66 ?? 66h f 0001201e 68 ?? 68h h 0001201f 2d ?? 2Dh - 00012020 66 ?? 66h f 00012021 89 ?? 89h 00012022 e6 ?? E6h 00012023 50 ?? 50h P 00012024 56 ?? 56h V 00012025 53 ?? 53h S 00012026 89 ?? 89h 00012027 e1 ?? E1h 00012028 b0 ?? B0h 00012029 0b ?? 0Bh 0001202a cd ?? CDh 0001202b 80 ?? 80h 0001202c 00 ?? 00h
We notice that enc_key
is 36
bytes big and starts at 0x12008
. On the left side, we see this data is initialized. The second column of data is the value at each byte. We can get Ghidra to copy this as a Python List using Right Click -> Copy Special -> Python List
. With no extra effort, here is the value of buffer
:
enckey = [ 0x31, 0xc0, 0x50, 0x68, 0x62, 0x6f, 0x6f, 0x74, 0x68, 0x6e, 0x2f, 0x72, 0x65, 0x68, 0x2f, 0x73, 0x62, 0x69, 0x89, 0xe3, 0x50, 0x66, 0x68, 0x2d, 0x66, 0x89, 0xe6, 0x50, 0x56, 0x53, 0x89, 0xe1, 0xb0, 0x0b, 0xcd, 0x80 ]
Now that we have our data, we can look at encode()
:
undefined * encode(int param_1) { undefined4 local_c; for (local_c = 0; *(char *)(param_1 + local_c) != '\0'; local_c = local_c + 1) { *(byte *)(param_1 + local_c) = (*(byte *)(param_1 + local_c) ^ 0x55) + 8; } return param_1; }
From this, we notice three things:
encode()
takes an int
argument.encode()
must cast the input to an int
, and then cast it again inside the function.From this, we can gather that Ghidra got the parameter type wrong. We can help Ghidra by changing the type to what we think it is. It appears that this function is casting to a char*
and a byte*
. A byte*
is simply a signed char*
. Let's change the type in Ghidra to char*
and see what happens. At the same time, we know that enckey
is a char*
, so we'll change the return type too.
char * encode(char *s) { int i; for (i = 0; s[i] != '\0'; i = i + 1) { s[i] = (s[i] ^ 0x55U) + 8; } return s; }
This is a lot better. This function takes a string, performs byte-wise operations on each byte, and stores it in enc_key
. It performs the following operation:
out = (in ^ 0x55) + 8
Both these operations are undo-able, meaning:
in = (out - 8) ^ 0x55
This is our key to solving this problem! If we take the output buffer
, and perform this operation on each byte, we'll get the correct input.
First, we must write a decode()
function that reverses the input. This will take in the list and return a list with the decoded bytes.
def decode(in_list): out_list = [] for i in in_list: out_list.append((i - 8) ^ 0x55) return out_list
We'll take our buffer
and decode it:
enckey = [ 0x31, 0xc0, 0x50, 0x68, 0x62, 0x6f, 0x6f, 0x74, 0x68, 0x6e, 0x2f, 0x72, 0x65, 0x68, 0x2f, 0x73, 0x62, 0x69, 0x89, 0xe3, 0x50, 0x66, 0x68, 0x2d, 0x66, 0x89, 0xe6, 0x50, 0x56, 0x53, 0x89, 0xe1, 0xb0, 0x0b, 0xcd, 0x80 ] payload = decode(enckey)
Now, we need to convert this list to a string. We can do this with the bytes()
function:
payload = bytes(payload)
Finally, we can send this payload to the server:
proc = process() proc.sendline(payload) proc.interactive()
This gives us the flag:
$ python3 asd.py [*] '/ironforge/chall' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled Stripped: No [+] Starting local process '/ironforge/chall': pid 267985 [*] Switching to interactive mode What is the password? > IFC{PL4C3H0LD3R_FL4G_H3R3!} [*] Got EOF while reading in interactive $ [*] Process '/ironforge/chall' stopped with exit code 0 (pid 267985) [*] Got EOF while sending in interactive