├── README ├── demos ├── blink.rb ├── cmt.rb ├── gameoflif.rb ├── gdbexec.rb ├── gdbinit ├── lif.rb └── marquee.rb ├── idarub.plw ├── idarub.rb ├── idarutils.rb ├── irb.rb └── plugin ├── idaint.rb ├── idarub.cpp ├── idarub.dsp ├── idarub.dsw ├── idarub.h ├── idarub_server.rb ├── idaswig.cpp ├── idaswig.h ├── idaswig.i ├── inline.rb ├── inline.sh ├── rubwrap.cpp ├── rubwrap.h ├── swig.sh └── swig ├── 4.9-sdk.patch ├── allins.i ├── area.i ├── auto.i ├── bytes.i ├── entry.i ├── frame.i ├── funcs.i ├── gdl.i ├── ida.i ├── idasdk.i ├── kernwin.i ├── lines.i ├── moves.i ├── nalt.i ├── name.i ├── offset.i ├── patch.sh ├── pro.i ├── queue.i ├── search.i ├── segment.i ├── sistack.i ├── srarea.i ├── strlist.i ├── struct.i ├── types.i ├── ua.i └── xref.i /README: -------------------------------------------------------------------------------- 1 | IdaRub, what what 2 | 3 | Alpha 0.8, June 2006 4 | 5 | 6 | -- Introduction 7 | 8 | IdaRub is an IDA plugin that wraps the IDA SDK for remote and local access from 9 | the Ruby programming language. It works on both IDA 4.9 and 5.0, although 5.0 10 | API additions are not accessible from IdaRub. 11 | 12 | 13 | -- Installation 14 | 15 | The IdaRub plugin requires a native win32 (not cygwin) install of Ruby. I 16 | suggest the Ruby "one click installer". The IdaRub client libraries should 17 | work on any Ruby installation (cygwin, native win32, linux, osx, etc). 18 | 19 | After installing the Ruby interpreter, to install the IdaRub plugin, simply 20 | copy the idarub.plw plugin to your IDA plugin directory. No installation 21 | should be necessary for the IdaRub client libraries. 22 | 23 | 24 | -- Usage 25 | 26 | After installing the IdaRub plugin, it should be accessible from the IDA plugin 27 | menu, or from the hotkey ALT-F7. This will present a GUI allowing you to start 28 | IdaRub as a server (for remote access), or to load a IdaRub script locally. 29 | The dialog also presents a few options (which only apply to server mode). The 30 | options allow you to pick which IP address the server will listen on, and the 31 | range of ports to try to listen on. While IdaRub only requires a single port, 32 | the range port option is make it easier to run multiple IdaRub server 33 | instances. The IdaRub plugin will incrementally try the ports in the range, 34 | and will print which port it bound to if successful. 35 | 36 | To use the IdaRub client libraries, you simply instantiate a IdaRub object, 37 | which will be covered below. You should see a message in the IDA message 38 | window when a new client has connected, and when a client disconnects. The 39 | IdaRub plugin supports unlimited simultaneous connections. 40 | 41 | 42 | -- Programming with the IdaRub client libraries 43 | 44 | The first step to accessing the IdaRub plugin remotely, is instantiating an 45 | IdaRub object, and telling it how to connect to the IdaRub plugin. This can 46 | be done two ways. The first way, is to call IdaRub.new_client(host, port), 47 | which will return an IdaRub session object (responsible for managing the 48 | connection). In order to get the IDA front object (which corresponds to the 49 | SDK), you simple call the front method on the session object. So for example: 50 | 51 | require 'idarub' 52 | sess = IdaRub.new_client('127.0.0.1', 1234) 53 | ida = sess.front 54 | 55 | An alternative, and generally easier approach, is to use the auto_client 56 | method. There are two conveniences with this. First, for remote scripts, it 57 | will automatically look at ARGV[0], and parse the host/port in the form of 58 | host:port, where the port is optional. It will then remove this entry from 59 | ARGV, so any additional options passed to your script will start at ARGV[0]. 60 | 61 | Another advantage is that calling auto_client from a locally running script 62 | will detect the script is running locally, and return the IDA SDK object. This 63 | allows simple scripts that use auto_client to work both remotely and locally 64 | with no additional logic. 65 | 66 | The auto_client method returns both the session and the IDA object, returning 67 | the IDA object first. This is convenient for simple scripts, where you don't 68 | need to worry about any of the functionality accessible through the session 69 | object. An example of using auto_client that will work both remotely and 70 | locally: 71 | 72 | require 'idarub' 73 | ida, = IdaRub.auto_client 74 | puts "Hello there! Your current ea: 0x%08x" % ida.get_screen_ea 75 | 76 | If you want a copy of the session object (so you can disconnect from the 77 | server, or other operations accessible through this object), you can simple 78 | do: 79 | 80 | ida, sess = IdaRub.auto_client 81 | 82 | Now, once you have the IDA object, you are going to want to call IDA SDK 83 | functions. This is generally quite simple, although there is a few things 84 | to beware of: 85 | 86 | Generally in Ruby, you would access a constant with code like "Foo::BAR", to 87 | access the BAR constant in the Foo module. However, for several technical 88 | reasons, you need to access IDA constants differently for IdaRub. In order to 89 | access constants, you should treat them as if they were methods. For example, 90 | to access the BADADDR constant defined by the SDK, you would do: 91 | 92 | ida.BADADDR 93 | 94 | This also applies for accessing classes, most likely for creating a new 95 | instance of a class. For example: 96 | 97 | ida.Curloc.new 98 | 99 | will create a new "curloc" class. Notice how the constant names are just the 100 | normal IDA classes, except the first letter is capitalized. This should apply 101 | to all IDA classes (Insn_t, Sistack_t, etc, etc). 102 | 103 | You might encounter an issue trying to call a remote method, but a local 104 | method on RefObject exists by the same name. An example of this would be 105 | listing the methods on a remote object. If you call obj.methods, you'll get 106 | the method listing for the local object (RefObject). If you want the call to 107 | be remoted, you can do it two different ways. You can call send_remote, ie 108 | obj.send_remote(:methods), which works the same as send, but will pass any 109 | calls to the remote object. The more convenient way is to simply prefix 110 | the method with "remote_". If you want to call the "methods" method on an obj, 111 | you can simply call obj.remote_methods, and it will get dispatched as the 112 | "methods" method on the remote object. 113 | 114 | 115 | -- Building the IdaRub IDA plugin 116 | 117 | Note: Building requires VC++ 6.0, Ruby, cygwin, SWIG 1.3.28 (or higher?) 118 | 119 | Unfortunately the build process for the IDA plugin is a bit messy, so try to 120 | stay with me. The first step is to copy the entire "idarub" directory to the 121 | "plugins" directory in the IDA SDK. The way the library and header includes 122 | work require this to be just so. You should end up with a path something like 123 | "sdk/plugins/idarub/plugin/idarub.cpp", etc. 124 | 125 | The next thing to make sure is that you have the Ruby include and library 126 | directories added to Visual Studio (Tools -> Options -> Directories). For 127 | "Include files", I have "c:\ruby\lib\ruby\1.8\i386-mswin32". For "Library 128 | files" I have "c:\ruby\bin" and "c:\ruby\lib". 129 | 130 | Since it would be rude to DataRescue to bundle copies of the modified headers, 131 | the changes are distributed as a patch. Copy the SDK "include" directory (the 132 | directory itself, not just it's contents) to the "swig" directory. Then run 133 | "patch.sh", and this should copy the appropiate headers from the includes and 134 | patch them to work for the SWIG bindings. 135 | 136 | Next, go to the "plugin" directory, and run swig.sh and inline.sh. The source 137 | should now be ready to be built. Open idarub.dsw in Visual Studio. Select the 138 | appropriate build configuration (Build -> Set Active Configuration). Hit F7 139 | and hope everything goes well. If the build failed, check to make sure it's 140 | finding the IDA headers/libraries and the Ruby (one-click) headers/libraries. 141 | 142 | The generated plugin should be located at bin/idarub.plw. Rinse and repeat. 143 | -------------------------------------------------------------------------------- /demos/blink.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # REcon 2006 - Blink demo 5 | # 6 | # Silly demo that just blinks a comment at the current ea. You can run 7 | # multiple of these at once for extra awesomeness. 8 | # 9 | 10 | $:.unshift('..') 11 | require 'idarub' 12 | 13 | ida, = IdaRub.auto_client 14 | 15 | ea = ida.get_screen_ea 16 | 17 | 100.times do |i| 18 | ida.set_cmt(ea, i % 2 == 0 ? "blink!" : "", true); 19 | sleep(0.1) 20 | end 21 | -------------------------------------------------------------------------------- /demos/cmt.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # REcon 2006 - Comment porter demo 5 | # 6 | # Will port ithe comments of the current function (chunk) from two .idb's 7 | # of the same executable. Not very glamorous, but it's just meant to show 8 | # how you could potentially use IdaRub for collaboration. 9 | # 10 | # Ex: ruby ./cmt.rb 127.0.0.1:1234 127.0.0.1:1235 11 | # 12 | 13 | $:.unshift('..') 14 | require 'idarub' 15 | 16 | ida1, = IdaRub.auto_client 17 | ida2, = IdaRub.auto_client 18 | 19 | f = ida1.get_func(ida1.get_screen_ea) 20 | 21 | # sloppy since I ignore instruction boundaries, but well, it works... 22 | (f.startEA .. f.endEA).each { |ea| 23 | if str = ida1.get_cmt(ea, true) 24 | ida2.set_cmt(ea, str, true) 25 | puts "0x%08x %s" % [ ea, str ] 26 | end 27 | } 28 | -------------------------------------------------------------------------------- /demos/gameoflif.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # Game of life yo 5 | # 6 | # circular board 7 | # crappy and sloppy implementation, don't reuse for anything 8 | # 9 | 10 | class GameOfLif 11 | attr_accessor :board 12 | 13 | def self.new_random(height, width) 14 | new((0..height).map { (0..width).map { rand(2) } }) 15 | end 16 | 17 | def initialize(_board = [[]]) 18 | self.board = _board 19 | end 20 | 21 | def num_neighbors(y, x) 22 | h, w = height, width 23 | board[y - 1 ][x - 1 ] + 24 | board[y - 1 ][x ] + 25 | board[y - 1 ][(x+1)%w] + 26 | board[y ][x - 1 ] + 27 | board[y ][(x+1)%w] + 28 | board[(y+1)%h][x-1 ] + 29 | board[(y+1)%h][x ] + 30 | board[(y+1)%h][(x+1)%w] 31 | end 32 | 33 | def alive?(y, x) 34 | board[y][x] == 1 35 | end 36 | 37 | def height 38 | board.length 39 | end 40 | def width 41 | board[0].length 42 | end 43 | 44 | def output 45 | str = '' 46 | board.each do |col| 47 | col.each do |cell| 48 | str << (cell == 0 ? ' ' : '*') 49 | end 50 | str << "\n" 51 | end 52 | return str 53 | end 54 | 55 | def next_board 56 | (0..height-1).map do |y| 57 | (0..width-1).map do |x| 58 | n = num_neighbors(y, x) 59 | if alive?(y, x) 60 | (n == 2 || n == 3) ? 1 : 0 61 | else 62 | (n == 3) ? 1 : 0 63 | end 64 | end 65 | end 66 | end 67 | 68 | def step 69 | self.board = next_board 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /demos/gdbexec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # REcon 2006 - GDB <-> IDA link demo 5 | # 6 | # Allows for the ability to link up GDB on any platform/architecture with IDA. 7 | # 8 | # Load the gdbinit file, and it adds commands like: 9 | # idasi - Step instruction (si) and have IDA follow along 10 | # idafollow - Move IDA to the current GDB $pc 11 | # idabreak - Breakpoint on the current IDA ea 12 | # idacmt - Add an IDA comment and the current GDB $pc 13 | # 14 | # Note: Might need to switch the unpack('V') for non-intel architectures. 15 | # Should probably use 'L' since it will run on the same machine as GDB. 16 | # 17 | 18 | $:.unshift(File.join(File.dirname(__FILE__), '..')) 19 | require 'idarub' 20 | 21 | def pc 22 | File.open('/tmp/idarubgdb', 'r') { |f| f.read.unpack('V')[0] } 23 | end 24 | def out(data) 25 | File.open('/tmp/idarubgdb', 'w') { |f| f.write(data) } 26 | end 27 | 28 | host = ENV['IDARUB_HOST'] || '127.0.0.1' 29 | port = ENV['IDARUB_PORT'] || 1234 30 | 31 | sess = IdaRub.new_client(host, port) 32 | ida = sess.front 33 | 34 | eval(ARGV[0]) 35 | 36 | ida.refresh_idaview_anyway 37 | -------------------------------------------------------------------------------- /demos/gdbinit: -------------------------------------------------------------------------------- 1 | set disassembly-flavor intel 2 | 3 | define idadumppc 4 | dump value /tmp/idarubgdb $pc 5 | end 6 | 7 | define idabreak 8 | shell ~/idarub/demos/gdbexec.rb "out('break *0x%08x' % ida.get_screen_ea)" 9 | source /tmp/idarubgdb 10 | end 11 | 12 | define idafollow 13 | idadumppc 14 | shell ~/idarub/demos/gdbexec.rb "ida.jumpto(pc)" 15 | end 16 | 17 | define idasi 18 | si 19 | idafollow 20 | end 21 | 22 | define idacmt 23 | idadumppc 24 | shell ~/idarub/demos/gdbexec.rb "ida.set_cmt(pc, ARGV[1], false)" $arg0 25 | end 26 | -------------------------------------------------------------------------------- /demos/lif.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # REcon 2006 - Lif demo 5 | # 6 | # Plays a Conway's Game of Life in the function comment of the current ea's 7 | # function. 8 | # 9 | 10 | $:.unshift('..') 11 | require 'gameoflif' 12 | require 'idarub' 13 | 14 | ida, = IdaRub.auto_client 15 | 16 | func = ida.get_func(ida.get_screen_ea) 17 | 18 | b = GameOfLif.new_random(10, 60) 19 | 20 | 300.times do 21 | ida.set_func_cmt(func, b.output, false) 22 | ida.refresh_idaview_anyway 23 | b.step 24 | sleep(0.05) 25 | end 26 | -------------------------------------------------------------------------------- /demos/marquee.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # REcon 2006 - Marquee demo 5 | # 6 | # Silly demo that just marquees a comment at the current ea. You can run 7 | # multiple of these at once for extra awesomeness. 8 | # 9 | 10 | $:.unshift('..') 11 | require 'idarub' 12 | 13 | def rotate(str) 14 | str[0,0] = str[-1, 1] 15 | str[-1, 1] = "" 16 | return str 17 | end 18 | 19 | ida, = IdaRub.auto_client 20 | 21 | str = "yoz!!! " 22 | 23 | ea = ida.get_screen_ea 24 | 25 | 200.times do |i| 26 | ida.set_cmt(ea, rotate(str), true); 27 | ida.refresh_idaview_anyway 28 | sleep(0.05) 29 | end 30 | -------------------------------------------------------------------------------- /idarub.plw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoonm/idarub/89c9863a18d8705a85c958518296d129427b09b3/idarub.plw -------------------------------------------------------------------------------- /idarub.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # IdaRub 5 | # 6 | # Word up. 7 | # 8 | 9 | require 'socket' 10 | require 'thread' 11 | 12 | module IdaRub 13 | 14 | def self.new_client(host = '127.0.0.1', port = 1234) 15 | RemoteRub::ClientTcpSession.new(TCPSocket.new(host, port.to_i)) 16 | end 17 | 18 | def self.auto_client 19 | hostport = ARGV.empty? ? [ ] : ARGV.shift.split(':') 20 | ida = (sess = new_client(*hostport)).front 21 | return ida, sess 22 | end 23 | 24 | def self.remote? 25 | true 26 | end 27 | def self.local? 28 | !remote? 29 | end 30 | 31 | module RemoteRub 32 | 33 | # 34 | # This class represents an object on the server side. 35 | # 36 | # We basically just have a token to retrieve the object back 37 | # on the server side. So we can pass a RefObject as an 38 | # argument, and it will get converted back to the actual 39 | # server-side object. 40 | # 41 | # We can also call methods on RefObjects. This is pretty much 42 | # the only way method invocation happens. Remoted method calls 43 | # will be going through here... 44 | # 45 | class RefObject 46 | 47 | # remove functions that might conflict with remote ones 48 | undef_method :id, :type 49 | 50 | # 51 | # Custom marshaling, don't bother with passing session 52 | # around since it has no meaning on the other side... 53 | # 54 | def self._load(str) 55 | return RefObject.new(str.unpack('V')[0]) 56 | end 57 | def _dump(depth) 58 | return [ ref_id ].pack('V') 59 | end 60 | 61 | attr_accessor :ref_id, :session 62 | 63 | def initialize(_id = nil, _sess = nil) 64 | self.ref_id = _id 65 | end 66 | 67 | # 68 | # Would be nice to do this a better way, ideas? 69 | # 70 | def method_missing(meth, *args) 71 | # redirect remote_xyz to xyz, convenient for 72 | # calling methods that exist on the RefObjet, 73 | # ie obj.remote_object_id 74 | if meth.to_s =~ /^remote_(.*)/ 75 | meth = $1.to_sym 76 | end 77 | send_remote(meth, *args) 78 | end 79 | 80 | def send_remote(meth, *args) 81 | session.send_remote(self, meth, *args) 82 | end 83 | 84 | def lock(&blk) 85 | session.lock(&blk) 86 | end 87 | 88 | def unlock 89 | session.unlock 90 | end 91 | end 92 | 93 | # 94 | # Translates all of the stuff to handle unmarshalable objects, 95 | # represented as references of objects saved on the client side. 96 | # Stored in a table will prevent garbage collection and allow 97 | # objects to be mapped back. 98 | # 99 | class Transformer 100 | 101 | OK_KLASSES = [ 102 | Numeric, 103 | String, 104 | FalseClass, 105 | TrueClass, 106 | NilClass 107 | ] 108 | 109 | # 110 | # Transforms 111 | # 112 | def transform(obj) 113 | return obj.class != Array ? 114 | transform_element(obj) : 115 | ( obj.map { |x| transform_element(x) } ) 116 | end 117 | 118 | def transform_element(x) 119 | OK_KLASSES.each do |ok| 120 | return x if x.class <= ok 121 | end 122 | 123 | # 124 | # A ref object.. 125 | # 126 | if x.class <= RefObject 127 | return transform_from_ref(x) 128 | end 129 | 130 | # 131 | # An object we to make a ref object 132 | # 133 | return transform_to_ref(x) 134 | end 135 | 136 | def transform_from_ref(x) 137 | raise "from_ref" 138 | end 139 | 140 | def transform_to_ref(x) 141 | raise "to_ref" 142 | end 143 | end 144 | 145 | # 146 | # Client arguments/return value transformer 147 | # 148 | # We shouldn't need to do anything to the method arguments 149 | # 150 | # We will need to add the sessions into RefObjects in return values 151 | # 152 | class ClientTransformer < Transformer 153 | 154 | attr_accessor :session 155 | 156 | def initialize(_sess = nil) 157 | self.session = _sess 158 | end 159 | 160 | # 161 | # We will keep the RefObjects, we will just 162 | # add in the session that they're from... 163 | # 164 | # So the client can call methods on it, you know.. 165 | # 166 | def transform_from_ref(x) 167 | x.session = session 168 | return x 169 | end 170 | end 171 | 172 | # 173 | # Client Session Object 174 | # 175 | # This provides the transport to talk to the remote host, 176 | # and methods for communicating with it, etc. This represents 177 | # one connection to the server. Multiple session objects can 178 | # exist at the same time, and can be connected to the same or 179 | # different servers. Of course a RefObject is only associated 180 | # with one session. 181 | # 182 | class ClientSession 183 | 184 | attr_accessor :transformer, :front, :comm_mutex, :lock_ctr, :lock_mutex 185 | 186 | def initialize 187 | self.transformer = ClientTransformer.new(self) 188 | self.comm_mutex = Mutex.new 189 | self.lock_mutex = Mutex.new 190 | self.lock_ctr = 0 191 | 192 | # recv the front object 193 | self.front = transformer.transform(load_remote) 194 | end 195 | 196 | def lock(&blk) 197 | _lock 198 | if blk 199 | blk.call 200 | unlock 201 | end 202 | end 203 | 204 | def _lock 205 | # sloppy mutex usage... 206 | lock_mutex.synchronize do 207 | if lock_ctr == 0 208 | if !send_remote(nil, :lock) 209 | raise "Failed to lock server" 210 | end 211 | end 212 | self.lock_ctr = lock_ctr + 1 213 | end 214 | end 215 | 216 | def unlock 217 | lock_mutex.synchronize do 218 | self.lock_ctr = lock_ctr - 1 219 | if lock_ctr == 0 220 | if send_remote(nil, :unlock) 221 | raise "Failed to unlock server" 222 | end 223 | end 224 | end 225 | end 226 | 227 | def transform_return(ret) 228 | transformer.transform(ret) 229 | end 230 | 231 | def send_remote(obj, meth, *args) 232 | 233 | res = nil 234 | ret = nil 235 | 236 | comm_mutex.synchronize do 237 | dump_remote( [ obj, meth, args ] ) 238 | res, ret = load_remote 239 | end 240 | 241 | # call failed, ret is an exception 242 | if !res 243 | raise ret 244 | end 245 | 246 | return transform_return(ret) 247 | end 248 | 249 | end 250 | 251 | 252 | # 253 | # TCP Transport... 254 | # 255 | module TcpSession 256 | 257 | attr_accessor :sock 258 | 259 | def initialize(_sock, *args) 260 | self.sock = _sock 261 | super(*args) 262 | end 263 | 264 | def dump_remote(obj) 265 | Marshal.dump(obj, sock) 266 | end 267 | 268 | def load_remote 269 | Marshal.load(sock) 270 | end 271 | 272 | def close 273 | sock.close 274 | end 275 | end 276 | 277 | class ClientTcpSession < ClientSession 278 | include TcpSession 279 | end 280 | 281 | end 282 | end 283 | -------------------------------------------------------------------------------- /idarutils.rb: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Return a list of area_t objects from an ea... 4 | # 5 | def function_chunks(ida, ea) 6 | entry = ida.get_func(ea) 7 | 8 | if !entry 9 | raise "No function at #{ea}" 10 | end 11 | if !ida.is_func_entry(entry) 12 | raise "Not an entry chunk at #{ea} #{entry.startEA}" 13 | end 14 | 15 | areas = [ entry ] 16 | it = ida.Func_tail_iterator_t.new(entry) 17 | 18 | ok = it.first 19 | while ok 20 | areas << it.chunk 21 | ok = it.next 22 | end 23 | 24 | return areas 25 | end 26 | 27 | -------------------------------------------------------------------------------- /irb.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift(File.dirname(__FILE__)) 4 | 5 | require 'idarub' 6 | require 'idarutils' 7 | 8 | @ida, @sess = IdaRub.auto_client 9 | 10 | irb 11 | -------------------------------------------------------------------------------- /plugin/idaint.rb: -------------------------------------------------------------------------------- 1 | # hack attack 2 | alias :old_require :require 3 | def require(str) 4 | return false if str == 'idarub' || str == 'idarutils' 5 | return old_require(str) 6 | end 7 | 8 | def puts(*args) 9 | IdaInt::Sdk.puts(*args) 10 | end 11 | def print(*args) 12 | IdaInt::Sdk.print(*args) 13 | end 14 | 15 | module IdaInt 16 | module Sdk 17 | def self.method_missing(meth, *args) 18 | if args.empty? && meth.to_s.between?('A', 'Z') 19 | begin 20 | return const_get(meth.to_s) 21 | rescue NameError 22 | end 23 | end 24 | return super(meth, *args) 25 | end 26 | def self.puts(*args) 27 | args.each do |arg| 28 | msg(arg + "\n") 29 | end 30 | end 31 | def self.print(*args) 32 | args.each do |arg| 33 | msg(arg) 34 | end 35 | end 36 | end 37 | 38 | class <